Regex vs LLM para Análise de Texto Estruturado
Uma estrutura prática de decisão para analisar texto estruturado (questionários, formulários, faturas, documentos). O insight principal: regex lida com 95-98% dos casos de forma econômica e determinística. Reserve chamadas caras de LLM 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 compensações 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, sem necessidade de LLM
│ └── Regex lida com <95% → Adicione LLM apenas para casos extremos
└── Não (formato livre, altamente variável) → Use LLM diretamente
Padrão de Arquitetura
Texto Fonte
│
▼
[Analisador Regex] ─── Extrai estrutura (95-98% de precisão)
│
▼
[Limpador de Texto] ─── Remove ruído (marcadores, números de página, artefatos)
│
▼
[Pontuador 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. Analisador Regex (Lida com a Maioria)
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class ParsedItem:
id: str
text: str
choices: tuple[str, ...]
answer: str
confidence: float = 1.0
def parse_structured_text(content: str) -> list[ParsedItem]:
"""Analisa texto estruturado usando padrões regex."""
pattern = re.compile(
r"(?P<id>\d+)\.\s*(?P<text>.+?)\n"
r"(?P<choices>(?:[A-D]\..+?\n)+)"
r"Answer:\s*(?P<answer>[A-D])",
re.MULTILINE | re.DOTALL,
)
items = []
for match in pattern.finditer(content):
choices = tuple(
c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", match.group("choices"))
)
items.append(ParsedItem(
id=match.group("id"),
text=match.group("text").strip(),
choices=choices,
answer=match.group("answer"),
))
return items
2. Pontuação de Confiança
Sinalize itens que podem precisar de revisão do LLM:
@dataclass(frozen=True)
class ConfidenceFlag:
item_id: str
score: float
reasons: tuple[str, ...]
def score_confidence(item: ParsedItem) -> ConfidenceFlag:
"""Pontua a confiança da extração e sinaliza problemas."""
reasons = []
score = 1.0
if len(item.choices) < 3:
reasons.append("poucas_opcoes")
score -= 0.3
if not item.answer:
reasons.append("resposta_faltando")
score -= 0.5
if len(item.text) < 10:
reasons.append("texto_curto")
score -= 0.2
return ConfidenceFlag(
item_id=item.id,
score=max(0.0, score),
reasons=tuple(reasons),
)
def identify_low_confidence(
items: list[ParsedItem],
threshold: float = 0.95,
) -> list[ConfidenceFlag]:
"""Retorna itens abaixo do limite de confiança."""
flags = [score_confidence(item) for item in items]
return [f for f in flags if f.score < threshold]
3. Validador LLM (Apenas Casos Extremos)
def validate_with_llm(
item: ParsedItem,
original_text: str,
client,
) -> ParsedItem:
"""Usa LLM para corrigir extrações de baixa confiança."""
response = client.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: {original_text}\n\n"
f"Extração atual: {item}\n\n"
f"Retorne JSON corrigido se necessário, ou 'CORRETO' se estiver preciso."
),
}],
)
# Analisa a resposta do LLM e retorna o item corrigido...
return corrected_item
4. Pipeline Híbrido
def process_document(
content: str,
*,
llm_client=None,
confidence_threshold: float = 0.95,
) -> list[ParsedItem]:
"""Pipeline completo: regex -> verificação de confiança -> LLM para casos extremos."""
# Etapa 1: Extração com regex (lida com 95-98%)
items = parse_structured_text(content)
# Etapa 2: Pontuação de confiança
low_confidence = identify_low_confidence(items, confidence_threshold)
if not low_confidence or llm_client is None:
return items
# Etapa 3: Validação LLM (apenas para itens sinalizados)
low_conf_ids = {f.item_id for f in low_confidence}
result = []
for item in items:
if item.id in low_conf_ids:
result.append(validate_with_llm(item, content, llm_client))
else:
result.append(item)
return result
Métricas do Mundo Real
De um pipeline de análise de questionários em produção (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 regex imperfeito fornece uma linha de 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 mude itens analisados — retorne novas instâncias das etapas de limpeza/validação
- TDD funciona bem para analisadores — escreva testes para padrões conhecidos primeiro, depois casos extremos
- Registre métricas (taxa de sucesso do regex, número de chamadas LLM) para monitorar a saúde do pipeline
Antipadrões a Evitar
- Enviar todo o texto para um LLM quando o regex lida com 95%+ dos casos (caro e lento)
- Usar regex para texto de formato livre, altamente variável (LLM é melhor aqui)
- Pular a pontuação de confiança e torcer para o regex "simplesmente funcionar"
- Modificar 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 questionário/prova
- 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