Artículos / LLMs & Agents
Extracción de datos con LLMs, Python y Pydantic
Extrae facturas y ofertas de trabajo con Python, Pydantic e Instructor. Valida tipos, revisa errores y mide la calidad de cada campo.

Una factura en texto necesita convertirse en campos que tu aplicación pueda procesar: proveedor, fecha, moneda e importes. Este tutorial usa Python, Pydantic e Instructor para definir esos campos y validar la respuesta de un LLM.
La validación comprueba la estructura y las reglas que escribas. No demuestra que el importe o la fecha coincidan con el documento: esa comprobación requiere evidencia del texto y ejemplos revisados.
El Problema de la Extracción Ingenua
El primer intento obvio es pedirle a un LLM que "extraiga la fecha de la factura y el monto total" y parsear su respuesta. Esto falla de tres formas:
- El LLM devuelve nombres de campo ligeramente diferentes cada vez
- Las fechas llegan en formatos inconsistentes (
ene 5,2025-01-05,05/01/25) - No hay validación de esquema: un campo puede ser string cuando esperabas un float
La solución es forzar al LLM a devolver JSON que coincida con un esquema Pydantic, y validarlo antes de que entre en tu pipeline.
Instalación
Usaremos la librería instructor, que parchea cualquier cliente compatible con OpenAI para devolver objetos Pydantic validados:
pip install instructor openai pydantic
import instructor
from openai import OpenAI
client = instructor.from_openai(OpenAI())
Si prefieres usar Claude de Anthropic:
pip install instructor anthropic
import instructor
import anthropic
client = instructor.from_anthropic(anthropic.Anthropic())
Los ejemplos siguientes usan el cliente de OpenAI. Para Anthropic, adapta el modelo, el método de creación y los parámetros según la integración de Instructor.
Ejemplo 1: Extrayendo Facturas
from pydantic import BaseModel, Field
from typing import Optional
from datetime import date
from decimal import Decimal
class LineaDetalle(BaseModel):
descripcion: str
cantidad: int
precio_unitario: Decimal
total: Decimal
class Factura(BaseModel):
numero_factura: str
nombre_proveedor: str
fecha_emision: date
fecha_vencimiento: Optional[date] = None
lineas: list[LineaDetalle]
subtotal: Decimal
tasa_impuesto: Optional[float] = Field(None, ge=0, le=1)
total: Decimal
moneda: str = 'COP'
TEXTO_FACTURA = """
FACTURA #FAC-2025-0847
Proveedor: Soluciones TI Andinas S.A.S.
Fecha: 12 de marzo de 2025
Vence: 11 de abril de 2025
Servicios:
- Desarrollo de Pipeline de Datos (40 hrs a $150.000/hr): $6.000.000
- Configuración de Infraestructura Cloud (1 unidad a $800.000): $800.000
- Soporte y Documentación (8 hrs a $100.000/hr): $800.000
Subtotal: $7.600.000
IVA (19%): $1.444.000
TOTAL A PAGAR: $9.044.000
"""
factura = client.chat.completions.create(
model='gpt-4o-mini',
response_model=Factura,
messages=[
{
'role': 'user',
'content': f'Extrae todos los datos de factura del siguiente texto:\n\n{TEXTO_FACTURA}',
}
],
)
print(f"Factura: {factura.numero_factura}")
print(f"Proveedor: {factura.nombre_proveedor}")
print(f"Fecha: {factura.fecha_emision}") # datetime.date(2025, 3, 12): siempre un objeto date
print(f"Total: {factura.total}") # Decimal('9044000'): siempre Decimal
print(f"Líneas: {len(factura.lineas)}")
for linea in factura.lineas:
print(f" - {linea.descripcion}: ${linea.total:,}")
Pydantic valida cada campo: fecha_emision es siempre un objeto Python date, total es siempre un Decimal, y si el LLM alucina un campo que no coincide con el esquema, Instructor reintenta automáticamente.
Ejemplo 2: Ofertas de Trabajo
from enum import Enum
from typing import Optional
class NivelExperiencia(str, Enum):
JUNIOR = 'junior'
MID = 'mid'
SENIOR = 'senior'
LEAD = 'lead'
PRINCIPAL = 'principal'
class RangoSalario(BaseModel):
min_cop: Optional[int] = None
max_cop: Optional[int] = None
es_divulgado: bool
class OfertaTrabajo(BaseModel):
titulo: str
empresa: str
ubicacion: str
modalidad: str # 'remoto', 'híbrido', 'presencial'
nivel_experiencia: NivelExperiencia
habilidades_requeridas: list[str]
habilidades_deseadas: list[str]
salario: RangoSalario
responsabilidades: list[str] = Field(max_length=8)
TEXTO_OFERTA = """
Ingeniero Senior de ML: Startup de IA (Remoto / Medellín)
Buscamos un Ingeniero Senior de Machine Learning para unirse a nuestro equipo.
Serás responsable del ciclo completo de ML: recolección de datos, entrenamiento de modelos,
evaluación y despliegue.
Lo que necesitamos:
5+ años de experiencia en ML/Ciencia de Datos
Python avanzado (scikit-learn, PyTorch o TensorFlow)
Experiencia en producción con servicio de modelos (FastAPI, Docker, Kubernetes)
Familiaridad con MLflow u otras herramientas de seguimiento de experimentos
Deseable:
Experiencia con fine-tuning de LLMs
Rust (nuestro motor de inferencia está en Rust)
Responsabilidades:
Diseñar y entrenar modelos para nuestro sistema de recomendación
Configurar y mantener nuestra infraestructura de ML en AWS
Colaborar con producto para definir features impulsadas por ML
Mentorear a ingenieros junior
Compensación: COP 15.000.000 a COP 22.000.000/mes + equity.
"""
oferta = client.chat.completions.create(
model='gpt-4o-mini',
response_model=OfertaTrabajo,
messages=[{'role': 'user', 'content': f'Extrae los datos de la oferta de trabajo:\n\n{TEXTO_OFERTA}'}],
)
print(f"Cargo: {oferta.titulo} ({oferta.nivel_experiencia.value})")
print(f"Empresa: {oferta.empresa} | {oferta.modalidad}")
print(f"Salario: COP {oferta.salario.min_cop:,} a COP {oferta.salario.max_cop:,}/mes")
print(f"Requerido: {', '.join(oferta.habilidades_requeridas)}")
Ejemplo 3: Procesamiento en Batch con Estadísticas de Validación
En producción procesas cientos de documentos, normalmente como un paso dentro de un pipeline orquestado (cubro ese montaje en orquesta pipelines de ML con Prefect). A esa escala necesitas conocer tu tasa de éxito de extracción:
from pydantic import ValidationError
class ResultadoExtraccion(BaseModel):
exitoso: bool
datos: Optional[Factura] = None
error: Optional[str] = None
preview_texto: str
def extraer_factura_seguro(texto: str) -> ResultadoExtraccion:
try:
factura = client.chat.completions.create(
model='gpt-4o-mini',
response_model=Factura,
max_retries=2,
messages=[{
'role': 'user',
'content': f'Extrae los datos de la factura:\n\n{texto}',
}],
)
return ResultadoExtraccion(exitoso=True, datos=factura, preview_texto=texto[:100])
except Exception as e:
return ResultadoExtraccion(exitoso=False, error=str(e), preview_texto=texto[:100])
# Procesar un batch
textos_facturas = [TEXTO_FACTURA, "Esto no es una factura", TEXTO_FACTURA]
resultados = [extraer_factura_seguro(t) for t in textos_facturas]
exitosos = [r for r in resultados if r.exitoso]
fallidos = [r for r in resultados if not r.exitoso]
print(f"Tasa de éxito: {len(exitosos)}/{len(resultados)} ({len(exitosos)/len(resultados)*100:.0f}%)")
for f in fallidos:
print(f" Falló: {f.error}")
Manejo de Estructuras Anidadas Complejas
Para documentos con estructuras condicionales o complejas, agrega lógica de validación directamente en Pydantic:
from pydantic import field_validator, model_validator
from datetime import datetime
class Factura(BaseModel):
# ... campos como antes ...
@field_validator('fecha_emision', 'fecha_vencimiento', mode='before')
@classmethod
def parsear_fecha_flexible(cls, v):
if v is None:
return v
if isinstance(v, date):
return v
# Manejar formatos de fecha comunes de LLMs
for fmt in ('%Y-%m-%d', '%d de %B de %Y', '%d/%m/%Y', '%B %d, %Y'):
try:
return datetime.strptime(str(v), fmt).date()
except ValueError:
continue
raise ValueError(f"No se puede parsear la fecha: {v}")
@model_validator(mode='after')
def validar_totales(self) -> 'Factura':
computado = sum(linea.total for linea in self.lineas)
if abs(computado - self.subtotal) > Decimal('1.00'):
raise ValueError(
f"La suma de líneas ({computado}) no coincide con el subtotal ({self.subtotal})"
)
return self
El model_validator detecta casos donde el LLM extrae líneas individuales cuya suma no coincide con el subtotal declarado: una fuente común de errores en datos reales de facturas.
Medir costo y calidad antes de automatizar
Calcula el costo con los tokens de entrada y salida de cada intento y las tarifas vigentes del proveedor. Incluye reintentos, OCR, almacenamiento y revisión humana. El costo por documento depende de su longitud, del modelo y del porcentaje de fallos; una tarifa fija sin esas condiciones no sirve para presupuestar.
Para comparar modelos, prepara documentos con campos revisados y mide aciertos por campo, valores omitidos, valores inventados y proporción enviada a revisión. Mantén el mismo conjunto de evaluación cuando cambies el prompt o el esquema. Versiona también el modelo y las dependencias.
Qué hacer cuando falta información
Distingue un campo ausente de un cero real o una cadena vacía. Si el documento no contiene una fecha, permite un valor ausente en el esquema y conserva el motivo. No le pidas al modelo que complete datos por plausibilidad. Para importes, compara moneda, unidades y totales con el texto original antes de escribir en el sistema de destino.
Los ejemplos muestran patrones de integración, no una garantía de precisión ni un pipeline listo para cualquier factura. Las llamadas necesitan una clave del proveedor y generan consumo. Prueba primero documentos sintéticos o con los datos sensibles eliminados.
Para consultar una colección documental, continúa con RAG con LangChain. Para ejecutar herramientas sobre datos ya extraídos, consulta agentes con LangGraph.
Referencias
¿Vas a construir una plataforma de IA o de datos?
Si necesitas adaptar este enfoque a tu sistema, cuéntame qué datos tienes y qué decisión técnica necesitas resolver.