Regex primero, el modelo para los bordes
No envíes cada línea de texto estructurado a un modelo de lenguaje. Las regex resuelven la mayor parte a una fracción del costo. Para texto de formato consistente — formularios, cuestionarios, facturas, logs — un conjunto de patrones bien ajustado resuelve aproximadamente el 95–98% de los casos de forma determinista, y los equipos suelen reportar un costo por registro de un orden de magnitud (~20×) menor que un pipeline basado solo en LLM. El modelo se gana su lugar en el 2–5% que las regex no pueden manejar.
Por qué "todo al LLM" es el default equivocado
Una llamada al LLM por un campo que podrías capturar con \d{2}/\d{2}/\d{4} te compra tres desventajas a la vez:
- Costo — pagas por token en cada registro, incluso en los trivialmente regulares.
- Latencia — una regex compilada matchea en microsegundos; un viaje de ida y vuelta al modelo toma cientos de milisegundos o más.
- No determinismo — la misma factura puede parsearse de dos maneras distintas en dos corridas. La regex te da la misma respuesta siempre, que es exactamente lo que quieres para el 95%+ de los registros que siguen el formato.
El pipeline híbrido
Parsea primero con regex. Asigna una puntuación de confianza a cada extracción. Envía al modelo solo las de baja confianza. Todo lo demás sale directo de la regex.
import re
AMOUNT = re.compile(r"(?i)total[:\s]*\$?(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)")
def extract(line: str) -> dict:
m = AMOUNT.search(line)
if m:
return {"amount": m.group(1), "confidence": 1.0, "source": "regex"}
# No match, ambiguous match, or failed validation -> queue for the LLM
return {"amount": None, "confidence": 0.0, "source": "llm_queue"}
results = [extract(line) for line in lines]
edge_cases = [r for r in results if r["confidence"] < 0.8]
# Only edge_cases go to the model — typically ~2–5% of records.
Para la parte del LLM, pide una respuesta restringida por un esquema JSON (structured outputs) para que los casos corregidos se fusionen con la misma forma que la salida de tu regex.
Reglas prácticas
| Situación | Ruta |
|---|---|
| El formato se repite de forma consistente (formularios, facturas, logs) | Regex, siempre |
| Campo con match que pasa la validación (checksum, rango, la fecha parsea) | Envíalo — sin llamada al modelo |
| Sin match, matches en conflicto, o falla la validación | Mándalo al LLM |
| Prosa libre, sin estructura repetitiva | LLM desde el inicio — la regex no es la herramienta |
| Casos borde que toleran latencia | Agrúpalos: la Batch API de OpenAI cuesta 50% menos, entrega en ≤24h |
Movidas de usuario avanzado
- Puntúa la confianza con señales baratas. "Match + validado" (la fecha parsea, el checksum pasa, el valor está en el rango esperado) es alta confianza. "Match pero fuera de rango" o "dos patrones en desacuerdo" es baja — enrútalo al modelo.
- Procesa los casos borde en lote. Si la cola de baja confianza no necesita respuestas en tiempo real, envíala por un endpoint batch con 50% de descuento y ventana de 24 horas.
- Deja que los fallos afinen la regex. Registra cada registro que cayó al LLM. Las formas de fallo recurrentes se convierten en patrones nuevos y tu cobertura de regex sube con el tiempo — la factura del modelo se encoge mes a mes.
- Testea los patrones como código. Mantén un archivo de fixtures con líneas reales acertadas y falladas y córrelo en CI. Una edición de regex que baja la cobertura en silencio es el mismo tipo de bug que una función rota.
- Ancla y valida.
re.fullmatch(o^...$) supera a unsearchlaxo para extraer campos completos; un match que luego falla una verificación semántica (fecha imposible, total negativo) igual debe ir al modelo.
Recursos
- Python
re— referencia del módulo - Python Regular Expression HOWTO
- MDN — guía de expresiones regulares
- OpenAI — guía de Structured Outputs
- OpenAI — guía de la Batch API (50% de descuento, ≤24h)
¿Construyendo una funcionalidad con AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.