Todos los recursos

Lo difícil de la salida estructurada no es sacar JSON. Es sacar JSON correcto, siempre, bajo condiciones reales. Instructor hace que el modelo te devuelva un objeto Pydantic ya validado y se vuelve a preguntar solo cuando la validación falla, así el texto difuso se convierte en datos tipados en los que el código sí puede confiar. Acá va cuándo usarlo, cómo funciona el bucle de reintentos y las concesiones que nadie menciona hasta que se dispara la factura de tokens.

Instructor: salida estructurada validada de cualquier LLM

En resumen

  • Una librería de Python ligera (**pip install instructor**) que parchea el cliente LLM para que le pases un modelo Pydantic como «response_model» y te devuelva una instancia ya validada. Sin json.loads a mano, sin código de reparación.
  • Cuando la salida no pasa la validación de Pydantic, Instructor le pasa el error al modelo y vuelve a preguntar, hasta un límite de reintentos que defines tú. Los validadores pasan a ser el contrato.
  • No te casa con ningún proveedor. Funciona con Anthropic, OpenAI, Google, Groq y Ollama local mediante «from_provider» o helpers por proveedor como «from_anthropic».
  • Soporta partials por streaming, iterables (sacar una lista de registros a medida que van llegando) y modelos anidados, no solo objetos planos.
  • El costo, sin maquillaje: los reintentos consumen tokens y latencia. Una restricción imposible da vueltas hasta el tope de reintentos y al final lanza error. Por eso conviene hacer los validadores precisos pero alcanzables, poner un tope bajo de reintentos y registrar los fallos.

Le pides JSON a un modelo y la mayoría de las veces te llega JSON. El problema empieza en los casos borde. Un campo que falta. Un score que debería ir de 0 a 100 y regresa en 120. Un valor de enum que el modelo se inventó. Una frase suelta envolviendo las llaves. En un script de una sola vez lo revisas a ojo y sigues. En un pipeline que alimenta una base de datos o una decisión de enrutamiento, esos bordes son justo donde todo se rompe, en silencio, en producción. Instructor es una librería pequeña que cierra esa brecha. Hace que la salida del modelo tenga que responder a un schema de Pydantic y vuelve a preguntarle al modelo, con el error de validación encima, cuando se equivoca. Te llevas objetos tipados en los que puedes confiar y una idea clara de lo que cuesta esa confiabilidad.

01 · Qué es en realidad

Instructor parchea el cliente LLM que ya tienes para que la respuesta tome la forma de un modelo Pydantic en vez de un string pelado. Defines la estructura que quieres como una clase, la pasas como response_model y recibes una instancia validada. Si el modelo no logra cumplir el schema dentro de tu presupuesto de reintentos, recibes una excepción limpia.

Es ligero a propósito. No reemplaza el SDK del proveedor, no te esconde los mensajes ni se inventa su propio DSL de prompts. Envuelve el cliente que ya usas, se apoya por debajo en el tool-calling nativo o el modo JSON del proveedor, y le suma dos cosas encima: traducir el schema a prompt y un bucle de reintentos guiado por la validación.

import instructor
from pydantic import BaseModel, Field

class Lead(BaseModel):
    score: int = Field(ge=0, le=100, description="Intención de compra, 0 (frío) a 100 (caliente)")
    intent: str
    needs_human: bool

client = instructor.from_provider("anthropic/claude-sonnet-4-5")

lead = client.chat.completions.create(
    response_model=Lead,
    messages=[{"role": "user", "content": transcript}],
    max_retries=2,
)

Ese campo score resume todo el asunto en pequeño. «ge=0, le=100» no es una pista, es una restricción dura. Si el modelo devuelve 120, Instructor atrapa el error de Pydantic y vuelve a preguntar adjuntando el fallo. No te entrega un objeto malo.

Nota

«from_provider("anthropic/claude-sonnet-4-5")» es el atajo más nuevo, que arma y parchea el cliente por ti. Todavía puedes usar la forma explícita, «instructor.from_anthropic(Anthropic())», cuando necesitas configurar tú mismo el cliente de abajo: un base URL propio, timeouts, una API key sacada de un gestor de secretos.

02 · Por qué importa

La forma ingenua de sacar salida estructurada es pedir JSON en el prompt, llamar a json.loads y escribir código defensivo para cada forma en que puede salir mal. Ese código defensivo crece sin parar. Quitar los fences de markdown. Lidiar con el modelo disculpándose antes del JSON. Forzar tipos. Validar rangos. Decidir qué hacer ante un fallo. Acabas manteniendo una capa frágil de parsear-y-reparar que en el fondo es un validador disfrazado.

Instructor le da la vuelta. Tus validadores son el contrato, escritos una sola vez en Pydantic:

  • Tipos y rangos. «int», «Field(ge=0, le=100)», «datetime», atrapados sin esfuerzo.
  • Enums. Un campo «Literal["low", "medium", "high"]» significa que el modelo no puede inventarse un cuarto valor y colártelo.
  • Chequeos a tu medida. Un «@field_validator» puede rechazar un número de teléfono que no sea E.164, o una cita que no aparece en el texto fuente.

Cuando cualquiera de esos falla, el mensaje de error vuelve al modelo en el siguiente intento. Así el reintento va con información, "el campo score debe ser ≤ 100", no es una tirada a ciegas. Ese único bucle reemplaza casi todo el código de parsear-y-rezar que la gente escribe a mano.

Consejo

Pon tus reglas de dominio en «@field_validator» / «@model_validator», no en código que corre después de la llamada. Un fallo de validador dispara un reintento correctivo. Un chequeo que corres por fuera solo lanza error y desperdicia la llamada que ya pagaste.

03 · Más allá de objetos planos: anidación, listas y streaming

La extracción real casi nunca es un solo registro plano. Instructor maneja las formas con las que de verdad te topas.

Modelos anidados

Compones modelos de Pydantic y el schema se anida junto con ellos. Una «Meeting» con una lista de hijos «ActionItem» regresa completamente tipada, con cada hijo validado.

from typing import Literal

class ActionItem(BaseModel):
    owner: str
    task: str
    priority: Literal["low", "medium", "high"]

class Meeting(BaseModel):
    summary: str
    action_items: list[ActionItem]

notes = client.chat.completions.create(
    response_model=Meeting,
    messages=[{"role": "user", "content": transcript}],
)

Partials por streaming e iterables

Dos funciones distintas que la gente suele confundir:

  1. Partial[Model] con «stream=True» transmite un solo objeto que va creciendo. Útil para ir pintando un formulario o una card a medida que se llenan los campos, para que la UI no quede en blanco tres segundos.
  2. Iterable[Model] transmite una secuencia de objetos completos. Sacas una lista de registros y procesas cada uno según va llegando, en vez de esperar el lote entero.

Para una extracción tipo feed, como sacar cada línea de factura de un documento, la forma iterable deja que el trabajo siguiente arranque de inmediato y mantiene el pico de memoria bajo.

Importante

Hacer streaming de un partial significa que puedes pintar un objeto que todavía no terminó de validarse. Trata los campos parciales como provisionales en la UI y actúa solo sobre la instancia final validada para cualquier cosa que escriba en una base de datos o dispare un efecto secundario.

04 · Cuándo usarlo (y cuándo no)

Usa Instructor cuando el código que viene después necesita una forma garantizada y estás en Python:

  • Extracción: convertir un correo, una transcripción o una página de PDF en registros tipados.
  • Clasificación y enrutamiento: un modelo decide una categoría o un score en el que luego un switch confía.
  • Enriquecimiento: llenar un schema conocido a partir de entrada desordenada antes de que llegue a la base de datos.

Sáltalo, o usa otra cosa, cuando:

  • Estás en TypeScript. «generateObject» del Vercel AI SDK con un schema de Zod cubre el mismo terreno de forma nativa, así que no metas una dependencia de Python solo por eso.
  • Necesitas generación libre: un ensayo, una respuesta de chat, código. Meter prosa a la fuerza por un schema pelea contra el modelo sin ningún beneficio.
  • La forma es trivial y el volumen es enorme. Un solo booleano sobre millones de filas puede salir más barato con un prompt restringido más un parse de una línea que con un round-trip completo de validación.

Instructor es fácil de arrancar y da un retorno serio, pero no es gratis. La pregunta de fondo es: ¿necesito una forma validada, garantizada? Si la respuesta es sí, se gana su lugar. Si es no, es puro adorno.

05 · Cómo cambia la forma en que armas pipelines

Acá es donde deja de ser una comodidad para parsear y empieza a ser una herramienta de diseño. Una vez que la salida del modelo es un objeto tipado, la frontera entre la IA y el resto del sistema se vuelve nítida. Todo lo que viene después, el worker de la cola, el insert en la base de datos, la lógica de enrutamiento, programa contra el tipo de Pydantic, no contra un prompt. El schema pasa a ser la API entre la parte difusa y la parte determinista.

En mi propio trabajo me apoyo justo en esa costura. En Proyección, la herramienta de práctica de negociación, un turno de diálogo tiene que convertirse en una lectura estructurada (concesiones ofrecidas, posiciones que se mantienen, una postura estimada) antes de que cualquier lógica de deliberación pueda razonar sobre él. Un schema validado es lo que vuelve seguro ese traspaso. La capa de razonamiento nunca ve la prosa cruda del modelo, solo un objeto tipado que ya pasó sus restricciones. El mismo patrón aparece donde sea que un modelo alimente una decisión: extrae a un schema, valida en la frontera y deja que el código normal tome el control.

También vuelve legible el fallo. Como un error sale como un «ValidationError» de Pydantic con el campo y la regla exactos que fallaron, puedes registrar por qué el modelo no pudo cumplir el contrato. Eso es mucho más útil que "el JSON venía mal formado". Con el tiempo esos logs te dicen si el problema es tu prompt, tu schema o entrada que es genuinamente ambigua.

Atención

La validación garantiza forma, nunca verdad. Un «Lead» con «score=95» está bien formado aunque el modelo haya leído mal la transcripción. Instructor frena los datos mal formados, no los datos equivocados. Mantén tus evals y tus revisiones manuales para la corrección. El schema es un piso, no un techo.

06 · Las concesiones, dichas sin rodeos

El bucle de reintentos es la fuente tanto del valor como del costo. Cada reintento es otra llamada completa al modelo: más tokens, más latencia, más dinero. Unas pocas reglas honestas mantienen eso a raya:

  1. Haz tus validadores precisos pero alcanzables. Una restricción que el modelo nunca puede cumplir, como un regex que no coincide con entradas reales o un rango que deja afuera valores válidos, manda cada llamada a dar vueltas hasta el tope de reintentos y luego a lanzar error. Prueba tu schema contra muestras reales antes de confiar en él.
  2. Pon un tope bajo de reintentos. Con dos suele alcanzar. Si un prompt necesita cuatro intentos para pasar, lo que hay que arreglar es el prompt o el schema, no subir el tope.
  3. Registra cada reintento agotado. Un pico de fallos es tu aviso temprano de que la entrada cambió o de que el schema está mal. Trátalo como cualquier otro presupuesto de errores.
  4. Vigila la costura de la abstracción. Instructor se apoya en el modo de salida estructurada nativo del proveedor. Las funciones propias del proveedor que te importan, como prompt caching, extended thinking o semánticas exóticas de tools, quizá necesiten configuración explícita para pasar. Así que verifica que las que usas funcionen de punta a punta en vez de dar por hecho que el wrapper reenvía todo.

Ninguna de estas es razón para descartarlo. Son el costo normal de cambiar el parseo escrito a mano por un bucle de validación, y para la mayoría del trabajo de extracción y clasificación, ese cambio vale claramente la pena.

Instructor es una de esas herramientas pequeñas que se pagan solas la primera vez que un objeto mal formado habría corrompido una fila aguas abajo sin que nadie se diera cuenta. Define el schema que de verdad necesitas, mantén los validadores honestos, pon un tope bajo a los reintentos y vigila los logs. Conviertes "el modelo casi siempre devuelve JSON" en "el modelo devuelve un objeto tipado sobre el que puedo construir". Para extracción y clasificación en Python es un default razonable. Solo recuerda que el schema cuida la forma, y tus evals todavía cuidan la verdad.

Puntos clave

  • Instructor convierte «el modelo casi siempre devuelve JSON» en «el modelo devuelve un objeto tipado y validado», volviendo a preguntar con el error de Pydantic cuando la validación falla.
  • Tus validadores son el contrato. Pon rangos, enums y reglas de dominio en «@field_validator» para que los fallos disparen un reintento con información, en vez de excepciones después de la llamada.
  • Maneja formas reales: modelos anidados, partials por streaming para UI progresiva e iterables para extraer listas, no solo objetos planos.
  • Los reintentos cuestan tokens y latencia. Mantén las restricciones alcanzables, pon un tope bajo a «max_retries» y registra los reintentos agotados para detectar temprano un schema malo.
  • La validación cuida la forma, nunca la verdad. Mantén tus evals y revisiones manuales para la corrección, y si estás en TypeScript usa mejor el Vercel AI SDK.

Preguntas frecuentes

¿En qué se diferencia esto del modo de salida estructurada o de JSON del propio proveedor?

Los modos nativos de JSON/tools te dan un objeto JSON válido, pero no uno validado contra tus reglas de negocio: rangos, enums, chequeos entre campos, lógica a tu medida. Instructor se monta encima de esos modos nativos y suma la capa de validación de Pydantic más el bucle de re-pregunta con información. Cuando una restricción falla, le dice al modelo exactamente qué estuvo mal y reintenta. Puedes usar el modo del proveedor directo sin ningún problema. Instructor es la capa de comodidad para cuando tu schema tiene restricciones de verdad y, si no, tendrías que escribir a mano todo el código de validación y reintentos.

¿Los reintentos lo hacen caro o lento?

Pueden serlo, si los dejas. Cada reintento es una llamada completa al modelo, así que una restricción imposible que siempre falla consume el máximo de reintentos y al final lanza error, pagando por cada intento. La solución es disciplina, no evitarlos: pon «max_retries» en uno o dos, prueba tus validadores contra entrada real para que sean alcanzables y registra los reintentos agotados para detectar temprano un schema malo. Con restricciones sensatas, la mayoría de las llamadas pasa al primer intento y el bucle no te cuesta nada.

Estoy en un stack de TypeScript / Next.js. ¿Debería usar Instructor?

Probablemente no. Instructor es una librería de Python. En TypeScript, «generateObject» del Vercel AI SDK con un schema de Zod cubre el mismo terreno de forma nativa, con resultados tipados de punta a punta y su propio manejo de reintentos. No arrastres una dependencia de Python a un proyecto de TS solo por la comodidad de Instructor. Usa la herramienta natural de tu runtime. Instructor es la opción correcta cuando tu lógica de extracción y validación ya vive en Python.

¿Validar significa que los datos extraídos son correctos?

No, y confundir las dos cosas es el error más común. La validación garantiza que los datos tienen la forma correcta y cumplen tus restricciones. No dice nada sobre si el modelo leyó bien la fuente. Un lead con score de 95 está bien formado aunque la transcripción en realidad fuera tibia. Instructor frena los datos mal formados, no los equivocados. Mantén tus evals, tus revisiones manuales y, donde haga falta, un paso con supervisión humana para la corrección. El schema es un piso, no un techo.

¿Dónde conviene poner mis reglas de negocio, en los validadores o en código después de la llamada?

En los validadores, casi siempre. Una regla expresada como un «@field_validator» o «@model_validator» de Pydantic pasa a ser parte del contrato, así que un fallo dispara un reintento correctivo y con información, y el modelo tiene la oportunidad de arreglarlo. Esa misma regla corrida como un chequeo *después* de la llamada solo lanza error y descarta la respuesta que ya pagaste, sin segunda oportunidad. Reserva el código posterior a la llamada para cosas en las que el modelo de verdad no puede influir, como una búsqueda contra tu propia base de datos.

¿Cuál es la diferencia entre hacerle streaming a un Partial y a un Iterable?

«Partial[Model]» transmite un solo objeto a medida que se llenan sus campos: un registro que crece, ideal para ir pintando un formulario o una card de a poco y que la UI no quede en blanco mientras el modelo trabaja. «Iterable[Model]» transmite una secuencia de objetos completos: muchos registros llegando uno tras otro, ideal para extraer una lista (cada línea de factura, cada tarea pendiente) y procesar cada uno según va llegando. Usa Partial para un solo resultado que quieres mostrar cuanto antes. Usa Iterable para una colección que quieres ir procesando sin esperar el lote entero.

Abrir recurso (abre en pestaña nueva)

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados