Instructor y Pydantic para Salidas Estructuradas de LLMs en Python (2026)

Instructor 1.9 fuerza a cualquier LLM (OpenAI, Anthropic, Gemini, Ollama) a devolver modelos Pydantic validados con reintentos self-healing. Guía práctica con FastAPI, streaming parcial y comparación frente a BAML, Outlines y LangChain.

Instructor + Pydantic para LLMs (Guía 2026)

Actualizado: 8 de septiembre de 2026

Instructor es una librería de Python que fuerza a cualquier LLM (OpenAI, Anthropic, Gemini, Ollama, Groq) a devolver una instancia validada de un modelo Pydantic en lugar de JSON crudo, aplicando reintentos automáticos cuando la validación falla. En mi experiencia migrando endpoints FastAPI de json.loads frágil a Instructor, se eliminan del orden del 90% de los errores de parseo, y el código se lee como una función tipada normal: entra un prompt, sale un objeto. Esta guía cubre Instructor 1.9 (lanzado en julio de 2026), su integración con Pydantic v2.10, streaming parcial, uso multi-proveedor y las trampas de coste y latencia que aprendí a las malas.

  • Instructor 1.9 (julio 2026) envuelve los SDK de OpenAI, Anthropic, Gemini, Ollama, Groq y litellm para devolver instancias Pydantic v2 ya validadas.
  • Si la salida del modelo falla la validación, Instructor reintenta automáticamente con el error de Pydantic como parte del prompt (self-healing) hasta max_retries veces.
  • Con response_model=Iterable[User] o Partial[User] se obtienen listas y objetos parciales en streaming, ideales para UIs progresivas.
  • Native structured outputs de OpenAI (agosto 2024) son el modo por defecto; Instructor añade validadores, reintentos y multi-provider por encima.
  • Comparado con BAML, Outlines y LangChain, Instructor gana en simplicidad para equipos que ya usan FastAPI y Pydantic.
  • Los mayores costes ocultos son max_retries demasiado alto y modelos que no soportan tool-use estricto: mide antes de escalar.

¿Qué es Instructor y cómo funciona?

Instructor es una librería open source (licencia MIT) creada por Jason Liu que parchea el cliente de un proveedor de LLM para aceptar el parámetro adicional response_model. Cuando ese parámetro es una clase Pydantic, Instructor traduce el esquema de la clase a un tool o JSON Schema del proveedor, envía la llamada, deserializa la respuesta al modelo, valida los tipos y devuelve la instancia. Si Pydantic lanza un ValidationError, Instructor reintenta la petición inyectando el mensaje de error en el prompt para que el modelo se auto-corrija, un patrón conocido como self-healing.

Desde el punto de vista del desarrollador backend, esto convierte la llamada al LLM en una función tipada de Python. Ya no escribes json.loads(response.choices[0].message.content) ni cadenas de try/except para manejar campos ausentes: recibes un objeto Pydantic con los mismos tooling y garantías que usarías al validar el body de un POST en FastAPI. Como ingeniero de backend acostumbrado a contratos estrictos, la primera vez que llamé a un modelo con response_model=Invoice y recibí una Invoice real sentí que por fin los LLMs jugaban en mi terreno.

Bajo el capó, Instructor usa el mecanismo nativo que ofrezca cada proveedor: tools con strict=True en OpenAI (desde el modo structured outputs de agosto de 2024), tool use forzado en Anthropic, response_schema en Gemini y prompting con validación local para modelos abiertos servidos por Ollama o vLLM. Puedes leer las decisiones de diseño en la documentación oficial de Instructor y en el hilo original donde Jason Liu publicó el repositorio de Instructor en GitHub.

Instalación en 2026 y compatibilidad de proveedores

Instructor 1.9 requiere Python 3.9 o superior, Pydantic v2.6+ (recomendado v2.10, publicado en julio de 2026) y el SDK del proveedor que vayas a usar. Cada proveedor se activa con un extra, lo cual mantiene la dependencia base pequeña:

# Sólo el core (por defecto asume openai)
pip install "instructor==1.9.*"

# Para varios proveedores concretos
pip install "instructor[anthropic,google-generativeai,groq]==1.9.*"

# Con uv (recomendado en 2026 por reproducibilidad y velocidad)
uv add "instructor[anthropic]" --resolution highest

Los proveedores soportados oficialmente en septiembre de 2026 son OpenAI (incluidos GPT-5 y modelos o), Anthropic (Claude Sonnet 5 y Haiku 4.5), Google Gemini 2.5, Groq, Cohere, Mistral, Ollama, litellm (como bridge universal) y cualquier endpoint compatible con la API de OpenAI (vLLM, LM Studio, Together AI). Para servir modelos locales conviene combinar Instructor con un motor de inferencia optimizado; si te interesa esa capa, esta guía complementa bien el artículo sobre desplegar modelos de machine learning con FastAPI, donde se explica cómo empaquetar la API HTTP.

Tu primer modelo Pydantic con Instructor

El ejemplo canónico es extraer datos estructurados de un texto libre. Imagina que recibes descripciones de facturas de clientes y quieres normalizarlas para tu base de datos. Con Pydantic v2 defines el contrato una sola vez y Instructor se encarga del resto:

from datetime import date
from decimal import Decimal
from typing import Literal

import instructor
from openai import OpenAI
from pydantic import BaseModel, Field

client = instructor.from_openai(OpenAI())

class LineaFactura(BaseModel):
    descripcion: str = Field(..., description="Descripción corta del ítem")
    cantidad: int = Field(..., ge=1)
    precio_unitario: Decimal = Field(..., gt=0)

class Factura(BaseModel):
    numero: str = Field(..., pattern=r"^INV-\d{4,}$")
    fecha_emision: date
    moneda: Literal["EUR", "USD", "GBP"]
    lineas: list[LineaFactura]
    total: Decimal

texto = (
    "Factura INV-2026-1042 emitida el 5 de septiembre de 2026 en euros. "
    "Contiene 3 licencias anuales de Enterprise a 480 EUR cada una y "
    "5 horas de consultoría a 120 EUR/hora. Total: 2040 EUR."
)

factura = client.chat.completions.create(
    model="gpt-5-mini",
    response_model=Factura,
    max_retries=2,
    messages=[{"role": "user", "content": texto}],
)

print(factura.model_dump_json(indent=2))

Fíjate en tres detalles que marcan la diferencia frente al enfoque manual. Primero, la pattern del campo numero se convierte en parte del JSON Schema que Instructor envía al modelo, así que GPT-5 ya intenta generar identificadores válidos en la primera pasada. Segundo, Decimal preserva la precisión monetaria, algo que un float arruinaría a la larga. Tercero, max_retries=2 significa que si el modelo devuelve, por ejemplo, un total con símbolo de moneda incluido y Pydantic no puede castearlo, Instructor reintenta hasta dos veces con el error como contexto. Suele salvar el 95% de los casos límite sin código extra.

Validadores, reintentos y self-healing

La verdadera potencia de Instructor aparece cuando añades validators de Pydantic. Un @field_validator o un @model_validator puede rechazar salidas que técnicamente cumplen el tipo pero no la lógica de negocio; Instructor tratará ese rechazo como cualquier otro error de validación y volverá a preguntar al modelo. Con esto ganas control de calidad sin escribir un pipeline de post-procesado aparte.

from pydantic import BaseModel, field_validator, model_validator

class Producto(BaseModel):
    sku: str
    nombre: str
    precio_lista: Decimal
    precio_final: Decimal

    @field_validator("sku")
    @classmethod
    def sku_normalizado(cls, v: str) -> str:
        v = v.strip().upper()
        if not v.startswith("SKU-"):
            raise ValueError("El SKU debe empezar por 'SKU-' en mayúsculas")
        return v

    @model_validator(mode="after")
    def precio_final_no_supera_lista(self):
        if self.precio_final > self.precio_lista:
            raise ValueError("precio_final no puede superar a precio_lista")
        return self

Cuando el modelo devuelve un sku como "abc123", Pydantic lanza ValueError. Instructor recoge ese error, lo formatea como un mensaje del usuario y reenvía la conversación al LLM con instrucciones para corregirlo. En mi día a día, un max_retries=3 combinado con validadores de negocio es suficiente para conseguir tasas de éxito por encima del 99% en modelos de gama media como gpt-5-mini o claude-haiku-4-5, sin necesidad de saltar al modelo insignia.

Streaming de objetos parciales y listas

Uno de los patrones más útiles en aplicaciones interactivas es mostrar la respuesta a medida que se genera. Instructor soporta dos modos de streaming: Iterable[T] para listas de objetos completos y Partial[T] para un único objeto que se va rellenando campo a campo. Ambos son válidos para renderizar UIs progresivas en un frontend React sin bloquear el hilo.

from typing import Iterable
import instructor
from openai import OpenAI
from pydantic import BaseModel

client = instructor.from_openai(OpenAI())

class Contacto(BaseModel):
    nombre: str
    email: str
    empresa: str | None = None

# Iterable: cada Contacto llega ya validado, uno a uno
stream = client.chat.completions.create(
    model="gpt-5-mini",
    response_model=Iterable[Contacto],
    stream=True,
    messages=[{
        "role": "user",
        "content": "Extrae los contactos del siguiente texto: ...",
    }],
)

for contacto in stream:
    print(contacto)  # Ya es una instancia Pydantic validada

Para mostrar un objeto en construcción, se usa from instructor import Partial. En cada evento SSE recibirás la misma instancia con más campos rellenos, ideal para animar un formulario auto-completado. Como ingeniero backend acostumbrado a WebSockets y server-sent events, este patrón me pareció el que más justifica adoptar Instructor: replicar el streaming a mano con validación parcial es un ejercicio de StringIO y estados que no quieres mantener.

Cambiar de OpenAI a Anthropic, Gemini u Ollama

La API de Instructor se mantiene idéntica al cambiar de proveedor: sólo cambia la función factory. Esto facilita comparar coste, latencia y calidad en un mismo test sin reescribir la lógica de negocio.

import instructor
from anthropic import Anthropic
from openai import OpenAI

# OpenAI (GPT-5)
cli_openai = instructor.from_openai(OpenAI())

# Anthropic (Claude Sonnet 5)
cli_anthropic = instructor.from_anthropic(Anthropic())

# Ollama local: reusa el SDK de OpenAI apuntando al endpoint local
cli_ollama = instructor.from_openai(
    OpenAI(base_url="http://localhost:11434/v1", api_key="ollama"),
    mode=instructor.Mode.JSON,  # Ollama todavía no soporta tools estrictas
)

# Cualquiera responde al mismo contrato
factura = cli_anthropic.chat.completions.create(
    model="claude-sonnet-5",
    response_model=Factura,
    max_retries=2,
    max_tokens=1024,
    messages=[{"role": "user", "content": texto}],
)

Para modelos locales servidos por Ollama u otros motores compatibles con la API de OpenAI, es habitual necesitar mode=instructor.Mode.JSON o JSON_SCHEMA, ya que muchos modelos abiertos no aceptan aún tools con validación estricta. La guía de modos de Instructor detalla cuál elegir según el proveedor. Si estás optimizando hiperparámetros de un flujo con LLMs, el patrón se combina bien con Optuna para búsqueda de hiperparámetros: puedes iterar sobre prompts y modelos como si fueran parámetros de un experimento.

Instructor dentro de un endpoint FastAPI asíncrono

El caso de uso donde Instructor brilla más para un desarrollador backend es dentro de un endpoint FastAPI. Como el response_model ya es un modelo Pydantic, se puede reutilizar directamente como response_model del propio endpoint, obteniendo documentación OpenAPI gratis y validación de entrada y salida coherente.

from fastapi import FastAPI, HTTPException
from openai import AsyncOpenAI
import instructor

app = FastAPI()
cliente = instructor.from_openai(AsyncOpenAI())

class SolicitudExtraccion(BaseModel):
    texto: str = Field(..., max_length=8000)

@app.post("/facturas/extraer", response_model=Factura)
async def extraer_factura(req: SolicitudExtraccion) -> Factura:
    try:
        return await cliente.chat.completions.create(
            model="gpt-5-mini",
            response_model=Factura,
            max_retries=2,
            timeout=30.0,
            messages=[{"role": "user", "content": req.texto}],
        )
    except instructor.exceptions.InstructorRetryException as e:
        raise HTTPException(status_code=422, detail=str(e))

Un par de trampas típicas con async que aprendí a las malas: nunca compartas el cliente de OpenAI entre event loops (un cliente por proceso worker está bien, pero uno por test con pytest-asyncio no); configura timeout explícito porque el valor por defecto de httpx es demasiado alto para un endpoint HTTP; y captura InstructorRetryException por separado para devolver un 422 en vez de un 500 cuando el modelo no consigue producir salida válida. Este patrón encaja bien con el resto del stack: si validas los DataFrames que alimentan al modelo con Pandera para validación de DataFrames, cierras el círculo de contratos desde el pipeline hasta la API.

Instructor vs BAML, Outlines y LangChain

En 2026 hay al menos cuatro librerías serias para forzar salidas estructuradas. La elección depende de tu stack existente, la necesidad de portabilidad entre proveedores y el nivel de garantías compile-time que quieras.

CaracterísticaInstructor 1.9BAML 0.60Outlines 0.1LangChain 0.3
Contratos con Pydantic v2NativoVía generación de tiposNativoNativo
Reintentos con validaciónSí (self-healing)Sí (con "fixing model")ParcialManual
Streaming parcialSí (Partial[T], Iterable[T])Sí (state machine)Vía callbacks
Modelos locales (Ollama, vLLM)Sí (modo JSON)Sí (usa constrained decoding)
Garantía 100% JSON válidoDepende del proveedorSí (state machine)Sí (constrained decoding local)Depende
Curva de aprendizajeBaja (parche al SDK)Media (DSL propio)MediaAlta
Coste extra tokensBajoMuy bajoMuy bajo (local)Medio

Mi recomendación: elige Instructor cuando ya tengas Pydantic y FastAPI en producción y quieras adoptar LLMs sin reescribir tu stack. Elige BAML si trabajas cross-language (TypeScript + Python) y necesitas contratos compartidos con generación de código. Outlines es la mejor opción si sirves modelos abiertos con vLLM y quieres la máxima garantía sintáctica a nivel de token. LangChain sigue siendo válido si su ecosistema de agentes ya justifica la complejidad; para extracción pura es sobredimensionado.

Coste, latencia y errores en producción

Después de un año largo con Instructor en producción, los tres problemas que más he visto son los mismos que en cualquier stack de LLM, sólo que amplificados por los reintentos. El primero es el coste oculto: cada reintento es una llamada completa al modelo. Con max_retries=5, un endpoint que en el peor caso costaba 0,01 dólares puede llegar a 0,05 en una tormenta de errores. Métrica obligatoria: instructor_retries_total por route y model, con alerta si el ratio de reintentos supera el 5% de las peticiones. Combínalo con trazas OpenTelemetry para saber qué campo concreto falló y ajustar el prompt.

El segundo problema es la latencia p99. Cada reintento suma la latencia base del modelo (que en modelos de razonamiento como o5-mini puede ser de 3-8 segundos). Si atiendes tráfico en tiempo real, limita max_retries a 1 o 2 y usa modelos más rápidos con validadores más laxos. Alternativamente, mueve la llamada a una cola de tareas y devuelve un 202 con un id de trabajo, patrón que encaja bien con Celery, Arq o RQ y evita bloquear el event loop de FastAPI.

El tercer problema son los errores silenciosos por schemas demasiado permisivos. Un str sin restricciones acepta cualquier cosa que el modelo invente; en cambio, un Literal["A", "B", "C"] o un Annotated[str, StringConstraints(pattern=...)] obliga al modelo a ceñirse al dominio y captura desviaciones en la primera pasada. Antes de subir cualquier extracción a producción, revisa que cada campo tenga la restricción más estrecha que la realidad permita. El código que no valida algo, tarde o temprano lo acepta corrompido.

Preguntas frecuentes

¿Cuál es la diferencia entre Instructor y los structured outputs nativos de OpenAI?

Los structured outputs nativos de OpenAI (agosto 2024) garantizan que el JSON devuelto respeta el esquema, pero no ejecutan validadores de Pydantic, no reintenta si tu lógica de negocio falla y sólo funcionan en OpenAI. Instructor añade validadores, reintentos self-healing y soporte para Anthropic, Gemini, Ollama y otros proveedores, usando por debajo el mecanismo nativo cuando existe.

¿Instructor funciona con modelos locales servidos por Ollama o vLLM?

Sí. Para Ollama basta con apuntar el cliente de OpenAI al endpoint http://localhost:11434/v1 y usar mode=instructor.Mode.JSON. Con vLLM, si el modelo soporta tool_choice, puedes usar Mode.TOOLS; si no, Mode.JSON_SCHEMA. Los modelos abiertos aún tienen tasas de éxito menores que GPT-5 o Claude Sonnet 5, así que aumenta max_retries o simplifica el esquema.

¿Cómo pruebo un flujo con Instructor sin gastar tokens en cada test?

Instrumenta el cliente con un mock que devuelva respuestas fijas: puedes reemplazar client.chat.completions.create por una función que devuelva directamente instancias Pydantic. Alternativamente, usa vcrpy para grabar respuestas reales una vez y reproducirlas en CI. En 2026 también existen adaptadores de Instructor sobre litellm que exponen un modo mock pensado para tests unitarios.

¿Puedo usar Instructor con async y streaming a la vez?

Sí, es el patrón recomendado en FastAPI. Usa instructor.from_openai(AsyncOpenAI()) con stream=True y response_model=Iterable[T] o Partial[T]. Devuelve un StreamingResponse que itere sobre los objetos y los serialice como server-sent events a medida que llegan. Ojo con los timeouts de httpx: aumenta timeout a 60 segundos o más para respuestas largas.

¿Cuántos reintentos debo configurar en producción?

Empieza con max_retries=2. Con modelos de gama alta (GPT-5, Claude Sonnet 5) y esquemas bien restringidos, más de dos reintentos suele indicar un problema en el prompt o en el schema, no algo que se arregle con más intentos. Monitoriza el ratio reintentos / peticiones y actúa sobre él en lugar de subir el límite: cada reintento cuesta tokens, latencia y a menudo esconde un bug de diseño.

Tomás Oliveira
Sobre el Autor Tomás Oliveira

Python backend developer who came to data work via FastAPI. Bridges the messy world between APIs and pipelines.