LLMs & Agents
Python
LLMs
Pydantic
salida estructurada
extracción de datos
Instructor

Extracción de Datos Estructurados desde Texto con LLMs

Extrae datos estructurados de texto con Python, Pydantic y LLMs: convierte facturas y ofertas de trabajo en objetos Python tipados y validados.

25 de junio de 2026·7 min read

Los pipelines de datos asumen entradas estructuradas. La realidad entrega PDFs, correos electrónicos, contratos y texto libre. En esa brecha es donde mueren los proyectos de ingeniería de datos.

Los LLMs cambian esta ecuación. Con el prompting correcto y un esquema bien definido, puedes convertir cualquier documento de texto en un objeto Python validado — con la confiabilidad suficiente para usarlo en pipelines de producción.

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:

  1. El LLM devuelve nombres de campo ligeramente diferentes cada vez
  2. Las fechas llegan en formatos inconsistentes (ene 5, 2025-01-05, 05/01/25)
  3. 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:

bash
pip install instructor openai pydantic
python
import instructor
from openai import OpenAI

client = instructor.from_openai(OpenAI())

Si prefieres usar Claude de Anthropic:

bash
pip install instructor anthropic
python
import instructor
import anthropic

client = instructor.from_anthropic(anthropic.Anthropic())

El resto del código es idéntico independientemente del modelo subyacente.

Ejemplo 1: Extrayendo Facturas

python
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

python
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: $15.000.000 – $22.000.000 COP/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:    ${oferta.salario.min_cop:,} – ${oferta.salario.max_cop:,} 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:

python
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:

python
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.

Consideraciones de Costo y Latencia

La extracción estructurada con gpt-4o-mini cuesta aproximadamente 0.00020.0002–0.001 por documento según su longitud. Para pipelines de alto volumen:

  • Usa gpt-4o-mini para la mayoría de las extracciones (rápido, barato, preciso)
  • Reserva gpt-4o para casos complejos que mini no maneja bien
  • Cachea respuestas para documentos idénticos
  • Procesa en batch cuando sea posible (reduce overhead por solicitud)
python
# Estimación aproximada de costos
COSTO_POR_1K_TOKENS_ENTRADA = 0.00015   # gpt-4o-mini entrada
COSTO_POR_1K_TOKENS_SALIDA  = 0.0006    # gpt-4o-mini salida

tokens_entrada_promedio = 500   # factura típica
tokens_salida_promedio  = 200   # JSON de salida

costo_por_doc = (tokens_entrada_promedio / 1000) * COSTO_POR_1K_TOKENS_ENTRADA \
              + (tokens_salida_promedio / 1000) * COSTO_POR_1K_TOKENS_SALIDA

print(f"~${costo_por_doc:.5f} por documento")
print(f"~${costo_por_doc * 10_000:.2f} por 10,000 documentos")
# ~$0.00020 por documento
# ~$1.95 por 10,000 documentos

Conclusión

La combinación de Pydantic + Instructor transforma los LLMs de generadores creativos de texto en motores confiables de extracción de datos. La clave es que definir el esquema no se trata solo de estructura: se trata de codificar tu conocimiento de dominio (cómo se ven las fechas, qué campos son obligatorios, qué invariantes deben cumplirse) en una forma que el LLM está obligado a respetar. El resultado son pipelines de extracción en los que realmente puedes confiar en producción.

Una distinción final: la extracción brilla cuando la salida es un esquema fijo por documento. Si necesitas responder preguntas abiertas sobre una colección de documentos, eso es un problema de RAG; y si necesitas cómputo multi-paso sobre los datos ya estructurados, lo tuyo es un agente con herramientas.


Sobre el autor

Rodrigo Arenas es arquitecto de software e ingeniero de machine learning en Medellín, Colombia. Construye productos, plataformas y software open source para IA, Data Engineering y Machine Learning — es el creador de Ciaren, sklearn-genetic-opt y PyWorkforce.



Rodrigo Arenas — ML Engineer & Open Source Maintainer

¿Estás trabajando en algo similar? Puedo ayudarte a llevarlo a producción.

¿Vas a construir una plataforma de IA o de datos?

Productos, plataformas y software open source para IA, Data Engineering y Machine Learning.

Ver proyectos
Visión

Construyendo un ecosistema de productos y software open source para IA, Data Engineering y Machine Learning.

Proyectos
sklearn-genetic-optPyWorkforceCiaren

Iterixs (En construcción)

© 2026 Rodrigo Arenas