Regex vs LLM para análisis de texto estructurado
Un marco práctico de decisión para analizar texto estructurado (cuestionarios, formularios, facturas, documentos). La idea clave: las expresiones regulares manejan el 95-98% de los casos de forma económica y determinista. Reserva las costosas llamadas a LLM solo para los casos límite restantes.
Cuándo activarlo
- Analizar texto estructurado con patrones repetitivos (preguntas, formularios, tablas)
- Decidir entre regex y LLM para extracción de texto
- Construir pipelines híbridos que combinen ambos enfoques
- Optimizar la relación coste/precisión en procesamiento de texto
Marco de decisión
¿El formato del texto es consistente y repetitivo?
├── Sí (>90% sigue un patrón) → Comienza con Regex
│ ├── Regex maneja el 95%+ → Hecho, no necesita LLM
│ └── Regex maneja <95% → Añade LLM solo para casos límite
└── No (texto libre, muy variable) → Usa LLM directamente
Patrón de arquitectura
Texto fuente
│
▼
[Analizador Regex] ─── Extrae estructura (95-98% de precisión)
│
▼
[Limpiador de texto] ─── Elimina ruido (marcadores, números de página, artefactos)
│
▼
[Puntuador de confianza] ─── Marca extracciones de baja confianza
│
├── Alta confianza (≥0.95) → Salida directa
│
└── Baja confianza (<0.95) → [Validador LLM] → Salida
Implementación
1. Analizador Regex (Maneja la mayoría)
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class ItemAnalizado:
id: str
texto: str
opciones: tuple[str, ...]
respuesta: str
confianza: float = 1.0
def analizar_texto_estructurado(contenido: str) -> list[ItemAnalizado]:
"""Analiza texto estructurado usando patrones regex."""
patron = re.compile(
r"(?P<id>\d+)\.\s*(?P<texto>.+?)\n"
r"(?P<opciones>(?:[A-D]\..+?\n)+)"
r"Respuesta:\s*(?P<respuesta>[A-D])",
re.MULTILINE | re.DOTALL,
)
items = []
for coincidencia in patron.finditer(contenido):
opciones = tuple(
c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", coincidencia.group("opciones"))
)
items.append(ItemAnalizado(
id=coincidencia.group("id"),
texto=coincidencia.group("texto").strip(),
opciones=opciones,
respuesta=coincidencia.group("respuesta"),
))
return items
2. Puntuación de confianza
Marca los elementos que pueden necesitar revisión del LLM:
@dataclass(frozen=True)
class IndicadorConfianza:
item_id: str
puntuacion: float
razones: tuple[str, ...]
def puntuar_confianza(item: ItemAnalizado) -> IndicadorConfianza:
"""Puntúa la confianza de la extracción y marca problemas."""
razones = []
puntuacion = 1.0
if len(item.opciones) < 3:
razones.append("pocas_opciones")
puntuacion -= 0.3
if not item.respuesta:
razones.append("falta_respuesta")
puntuacion -= 0.5
if len(item.texto) < 10:
razones.append("texto_corto")
puntuacion -= 0.2
return IndicadorConfianza(
item_id=item.id,
puntuacion=max(0.0, puntuacion),
razones=tuple(razones),
)
def identificar_baja_confianza(
items: list[ItemAnalizado],
umbral: float = 0.95,
) -> list[IndicadorConfianza]:
"""Devuelve los elementos por debajo del umbral de confianza."""
indicadores = [puntuar_confianza(item) for item in items]
return [i for i in indicadores if i.puntuacion < umbral]
3. Validador LLM (Solo casos límite)
def validar_con_llm(
item: ItemAnalizado,
texto_original: str,
cliente,
) -> ItemAnalizado:
"""Usa LLM para corregir extracciones de baja confianza."""
respuesta = cliente.messages.create(
model="claude-haiku-4-5-20251001", # Modelo más barato para validación
max_tokens=500,
messages=[{
"role": "user",
"content": (
f"Extrae la pregunta, las opciones y la respuesta de este texto.\n\n"
f"Texto: {texto_original}\n\n"
f"Extracción actual: {item}\n\n"
f"Devuelve el JSON corregido si es necesario, o 'CORRECTO' si es exacto."
),
}],
)
# Analiza la respuesta del LLM y devuelve el elemento corregido...
return item_corregido
4. Pipeline híbrido
def procesar_documento(
contenido: str,
*,
cliente_llm=None,
umbral_confianza: float = 0.95,
) -> list[ItemAnalizado]:
"""Pipeline completo: regex -> comprobación de confianza -> LLM para casos límite."""
# Paso 1: Extracción con regex (maneja el 95-98%)
items = analizar_texto_estructurado(contenido)
# Paso 2: Puntuación de confianza
baja_confianza = identificar_baja_confianza(items, umbral_confianza)
if not baja_confianza or cliente_llm is None:
return items
# Paso 3: Validación con LLM (solo para elementos marcados)
ids_baja_conf = {i.item_id for i in baja_confianza}
resultado = []
for item in items:
if item.id in ids_baja_conf:
resultado.append(validar_con_llm(item, contenido, cliente_llm))
else:
resultado.append(item)
return resultado
Métricas del mundo real
De un pipeline de análisis de cuestionarios en producción (410 elementos):
| Métrica | Valor |
|---|---|
| Tasa de éxito de regex | 98.0% |
| Elementos de baja confianza | 8 (2.0%) |
| Llamadas a LLM necesarias | ~5 |
| Ahorro de costes frente a usar solo LLM | ~95% |
| Cobertura de pruebas | 93% |
Buenas prácticas
- Comienza con regex: incluso una regex imperfecta te da una línea base para mejorar
- Usa puntuación de confianza para identificar programáticamente qué necesita ayuda del LLM
- Usa el LLM más barato para la validación (los modelos tipo Haiku son suficientes)
- Nunca mutar los elementos analizados: devuelve nuevas instancias en los pasos de limpieza/validación
- TDD funciona bien para analizadores: escribe pruebas para patrones conocidos primero, luego para casos límite
- Registra métricas (tasa de éxito de regex, número de llamadas al LLM) para monitorizar la salud del pipeline
Antipatrones a evitar
- Enviar todo el texto a un LLM cuando regex maneja más del 95% de los casos (caro y lento)
- Usar regex para texto libre y muy variable (el LLM es mejor aquí)
- Saltarse la puntuación de confianza esperando que regex "simplemente funcione"
- Mutar objetos analizados durante los pasos de limpieza/validación
- No probar casos límite (entradas mal formadas, campos faltantes, problemas de codificación)
Cuándo usarlo
- Análisis de preguntas de exámenes/cuestionarios
- Extracción de datos de formularios
- Procesamiento de facturas/recibos
- Análisis de estructura de documentos (encabezados, secciones, tablas)
- Cualquier texto estructurado con patrones repetitivos donde el coste importe