Regex vs LLM para Análise de Texto Estruturado
Um framework prático de decisão para analisar texto estruturado (quizzes, formulários, faturas, documentos). A ideia central: regex lida com 95-98% dos casos de forma barata e determinística. Reserve chamadas caras a LLMs apenas para os casos extremos restantes.
Quando Ativar
- Analisar texto estruturado com padrões repetitivos (perguntas, formulários, tabelas)
- Decidir entre regex e LLM para extração de texto
- Construir pipelines híbridos que combinam ambas as abordagens
- Otimizar trade-offs de custo/precisão no processamento de texto
Estrutura de Decisão
O formato do texto é consistente e repetitivo?
├── Sim (>90% segue um padrão) → Comece com Regex
│ ├── Regex lida com 95%+ → Pronto, nenhum LLM necessário
│ └── Regex lida com <95% → Adicione LLM apenas para casos extremos
└── Não (texto livre, altamente variável) → Use LLM diretamente
Padrão de Arquitetura
Texto Fonte
│
▼
[Parser Regex] ─── Extrai estrutura (95-98% de precisão)
│
▼
[Limpador de Texto] ─── Remove ruído (marcadores, números de página, artefatos)
│
▼
[Scorer de Confiança] ─── Sinaliza extrações de baixa confiança
│
├── Alta confiança (≥0.95) → Saída direta
│
└── Baixa confiança (<0.95) → [Validador LLM] → Saída
Implementação
1. Parser Regex (Lida com a Maioria)
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class ItemAnalisado:
id: str
texto: str
opcoes: tuple[str, ...]
resposta: str
confianca: float = 1.0
def analisar_texto_estruturado(conteudo: str) -> list[ItemAnalisado]:
"""Analisa texto estruturado usando padrões regex."""
padrao = re.compile(
r"(?P<id>\d+)\.\s*(?P<texto>.+?)\n"
r"(?P<opcoes>(?:[A-D]\..+?\n)+)"
r"Resposta:\s*(?P<resposta>[A-D])",
re.MULTILINE | re.DOTALL,
)
itens = []
for match in padrao.finditer(conteudo):
opcoes = tuple(
c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", match.group("opcoes"))
)
itens.append(ItemAnalisado(
id=match.group("id"),
texto=match.group("texto").strip(),
opcoes=opcoes,
resposta=match.group("resposta"),
))
return itens
2. Pontuação de Confiança
Sinalize itens que podem precisar de revisão por LLM:
@dataclass(frozen=True)
class SinalizacaoConfianca:
item_id: str
pontuacao: float
motivos: tuple[str, ...]
def pontuar_confianca(item: ItemAnalisado) -> SinalizacaoConfianca:
"""Pontua a confiança da extração e sinaliza problemas."""
motivos = []
pontuacao = 1.0
if len(item.opcoes) < 3:
motivos.append("poucas_opcoes")
pontuacao -= 0.3
if not item.resposta:
motivos.append("resposta_ausente")
pontuacao -= 0.5
if len(item.texto) < 10:
motivos.append("texto_curto")
pontuacao -= 0.2
return SinalizacaoConfianca(
item_id=item.id,
pontuacao=max(0.0, pontuacao),
motivos=tuple(motivos),
)
def identificar_baixa_confianca(
itens: list[ItemAnalisado],
limiar: float = 0.95,
) -> list[SinalizacaoConfianca]:
"""Retorna itens abaixo do limiar de confiança."""
sinalizacoes = [pontuar_confianca(item) for item in itens]
return [s for s in sinalizacoes if s.pontuacao < limiar]
3. Validador LLM (Apenas Casos Extremos)
def validar_com_llm(
item: ItemAnalisado,
texto_original: str,
cliente,
) -> ItemAnalisado:
"""Usa LLM para corrigir extrações de baixa confiança."""
resposta = cliente.messages.create(
model="claude-haiku-4-5-20251001", # Modelo mais barato para validação
max_tokens=500,
messages=[{
"role": "user",
"content": (
f"Extraia a pergunta, as opções e a resposta deste texto.\n\n"
f"Texto: {texto_original}\n\n"
f"Extração atual: {item}\n\n"
f"Retorne o JSON corrigido se necessário, ou 'CORRETO' se estiver preciso."
),
}],
)
# Analisar resposta do LLM e retornar item corrigido...
return item_corrigido
4. Pipeline Híbrido
def processar_documento(
conteudo: str,
*,
cliente_llm=None,
limiar_confianca: float = 0.95,
) -> list[ItemAnalisado]:
"""Pipeline completo: regex -> verificação de confiança -> LLM para casos extremos."""
# Passo 1: Extração com regex (lida com 95-98%)
itens = analisar_texto_estruturado(conteudo)
# Passo 2: Pontuação de confiança
baixa_confianca = identificar_baixa_confianca(itens, limiar_confianca)
if not baixa_confianca or cliente_llm is None:
return itens
# Passo 3: Validação com LLM (apenas para itens sinalizados)
ids_baixa_conf = {s.item_id for s in baixa_confianca}
resultado = []
for item in itens:
if item.id in ids_baixa_conf:
resultado.append(validar_com_llm(item, conteudo, cliente_llm))
else:
resultado.append(item)
return resultado
Métricas do Mundo Real
De um pipeline de produção de análise de quizzes (410 itens):
| Métrica | Valor |
|---|---|
| Taxa de sucesso do regex | 98,0% |
| Itens de baixa confiança | 8 (2,0%) |
| Chamadas LLM necessárias | ~5 |
| Economia de custos vs. tudo-LLM | ~95% |
| Cobertura de testes | 93% |
Melhores Práticas
- Comece com regex — mesmo um regex imperfeito fornece uma base para melhorar
- Use pontuação de confiança para identificar programaticamente o que precisa de ajuda do LLM
- Use o LLM mais barato para validação (modelos da classe Haiku são suficientes)
- Nunca mutile itens analisados — retorne novas instâncias das etapas de limpeza/validação
- TDD funciona bem para parsers — escreva testes para padrões conhecidos primeiro, depois para casos extremos
- Registre métricas (taxa de sucesso do regex, contagem de chamadas LLM) para monitorar a saúde do pipeline
Anti-Padrões a Evitar
- Enviar todo o texto para um LLM quando regex lida com 95%+ dos casos (caro e lento)
- Usar regex para texto livre e altamente variável (LLM é melhor aqui)
- Pular a pontuação de confiança e torcer para o regex "simplesmente funcionar"
- Mutilar objetos analisados durante as etapas de limpeza/validação
- Não testar casos extremos (entrada malformada, campos ausentes, problemas de codificação)
Quando Usar
- Análise de perguntas de quizzes/provas
- Extração de dados de formulários
- Processamento de faturas/recibos
- Análise de estrutura de documentos (cabeçalhos, seções, tabelas)
- Qualquer texto estruturado com padrões repetitivos onde o custo importa