Salida estructurada

Primero, la forma del problema. Salida estructurada es la práctica de hacer que un modelo devuelva datos que tu código pueda leer directamente, un campo sobre el que puedas ramificarte en lugar de un párrafo que tengas que analizar manualmente. Este capítulo trata sobre hacer eso confiable.
Hasta ahora el modelo te entrega texto, que está bien cuando una persona lo lee. Pero el software no puede hacer mucho con un párrafo. Para actuar sobre la respuesta de un modelo en código, la necesitas como datos: un campo que puedas leer, un valor sobre el que puedas ramificarte, un registro que puedas guardar.
El cambio es pequeño pero cambia todo lo que viene después. Una vez que el modelo devuelve { "sentiment": "negative" } en lugar de una oración sobre cómo el cliente parece infeliz, tu código puede enrutar el ticket, actualizar un tablero o enviar una alerta. La parte interesante es cómo haces que el modelo produzca datos limpios cuando todo lo que hace es predecir texto, y la respuesta revela una palanca que no sabías que tenías.
Solicitar JSON
El primer instinto es pedir en el prompt: "responde como JSON", el formato de texto plano que el software usa para pasar datos, escrito como campos y valores dentro de llaves. Pruébalo y funciona la mayoría de las veces, que es la trampa. El modelo aún no hace nada más que predecir texto probable, y a veces el texto probable es una cerca de código, o un amable "¡Claro, aquí va!" antes de los datos, o un comentario final después. Cualquiera de esos rompe el código que lee el JSON. Pedir amablemente sesga la predicción hacia JSON; no la fuerza.
Así que los proveedores te dan una palanca real. Activar modo JSON con response_format hace más que pedir: constriñe el modelo para que la sintaxis vuelva como JSON válido.
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": 'Extrae la ciudad y la temperatura. Responde como JSON: { "city": string, "tempC": number }.'},
{"role": "user", "content": "Hace unos 18 grados en Ciudad de México ahora."},
],
response_format={"type": "json_object"}, # constriñe la salida a sintaxis JSON válida
)
data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"]) # "Ciudad de México" 18response_format={"type": "json_object"} hace que la salida sea JSON válido, así que json.loads no se atragantará con prosa errante. Aún describes los campos que quieres en el prompt, porque el modo JSON promete JSON válido, no los campos particulares que tenías en mente. Descarta "no es JSON en absoluto", no "JSON con la forma equivocada". Una cosa que aún no puede prometer: si la respuesta se corta a mitad de camino, puedes recibir medio objeto JSON, así que es sintaxis confiable, no una garantía dura en todos los casos.
response_format en modo JSON constriñe la sintaxis para que vuelva como JSON válido. No promete que los campos coincidan con lo que pediste, y una respuesta cortada a mitad de camino aún puede llegar como medio objeto, así que describe la forma en el prompt y verifica qué llega. Bloquea la forma con un esquema
El modo JSON mantiene la sintaxis válida, pero el modelo aún podría renombrar un campo, dejar caer uno, o anidar cosas diferentemente. Para eliminar ese margen, dale un esquema: una descripción exacta de los campos y tipos que debe tener la salida, un formulario que el modelo está obligado a completar. Con strict: true, la salida está entonces garantizada que coincide con esa forma.
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Extrae los detalles de contacto del mensaje."},
{"role": "user", "content": "Hola, soy María González, contáctame en [email protected]."},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "contact",
"strict": True, # impone el esquema exactamente
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
},
"required": ["name", "email"],
"additionalProperties": False,
},
},
},
)
contact = json.loads(response.choices[0].message.content)
# {"name": "María González", "email": "[email protected]"}¿Cómo puede un esquema ser una garantía dura cuando el modelo solo predice tokens? Porque el proveedor restringe qué tokens se le permite predecir. En cada paso el modelo aún clasifica todos los posibles tokens siguientes, pero el sistema elimina los que romperían tu esquema, y el modelo elige de lo que queda.
Si el esquema dice que el siguiente campo debe ser email, los tokens que iniciarían cualquier otro campo se sacan de la mesa antes de que el modelo elija. Los tokens fuera de forma se eliminan, así que el modelo no puede desviarse de la forma.
Hay un efecto más suave que vale la pena saber. Los nombres de campo que eliges se convierten en parte de lo que el modelo lee al predecir el valor, así que un nombre claro guía una respuesta mejor. Pidiendo llenar tempC, el modelo se inclina hacia un número en Celsius; pidiendo llenar value, tiene mucho menos para ir. Nombrar campos claramente es en parte instrucción, en parte esquema.
strict: true obtiene los campos y tipos exactos, porque el sistema elimina cualquier token que rompería la forma antes de que el modelo elija. Eso es imposición, no una solicitud educada. Nombra tus campos claramente también, ya que el nombre del campo es contexto que el modelo lee cuando predice el valor. Me tomó un tiempo confiar en que un nombre como tempC hace trabajo real. Valida de todas formas
Un esquema constriñe la forma, pero trata la salida como datos del mundo exterior de todas formas. La forma puede ser válida mientras el contenido es incorrecto: un correo electrónico extraído que es en realidad un error tipográfico, un número que el modelo adivinó, un campo en blanco porque la entrada no lo contenía. Y la llamada aún puede fallar de maneras ordinarias, como ser cortada por max_tokens y llegar como JSON truncado. Analiza defensivamente: una forma válida no es un valor correcto.
import json
def parse_contact(raw):
try:
data = json.loads(raw)
if not data.get("name") or not data.get("email"):
return None # presente pero vacío
return data
except json.JSONDecodeError:
return None # no es JSON válido (por ejemplo, una respuesta truncada)
contact = parse_contact(response.choices[0].message.content)
if not contact:
pass # manejar el fallo: reintentar, preguntar de nuevo, o mostrar un error amableEsta es la lección de alucinación de cómo funcionan los LLM en un disfraz nuevo: que la forma sea correcta no hace que los valores sean correctos. Un esquema te garantiza que obtengas un campo name; no garantiza que el nombre sea correcto. Un try/except alrededor del análisis más una comprobación de los valores es un seguro barato contra la respuesta que está bien formada y aún es incorrecta.
try/except y comprueba los valores antes de confiar en ellos. Decide de antemano qué sucede cuando la validación falla, un reintento o un error amable, para que no sea un ajetreo después. Dos patrones: clasificar y extraer
La mayoría del trabajo de salida estructurada es una de dos formas.
Clasificación ordena una entrada en uno de un conjunto fijo de etiquetas. Un enum en el esquema constriñe la salida exactamente a esas etiquetas, usando el mismo truco de poda de tokens: en la posición de la etiqueta, solo los valores permitidos pueden ser predichos, así que el modelo no puede inventar una categoría que no esté en tu lista.
# fragmento de esquema para clasificación
{"type": "string", "enum": ["billing", "technical", "general"]}Extracción extrae campos específicos de texto libre, como el ejemplo de contacto anterior: un nombre, una fecha, un monto, una lista de nombres de producto. Juntas, clasificación y extracción cubren una gran parte del trabajo real de IA: enrutamiento de tickets de soporte, etiquetado de contenido, convertir correos en registros, leer recibos. Ambas convierten un generador de texto difuso en un componente confiable en el que tu código puede construir.
enum así que el modelo no puede inventar una categoría fuera de tu lista. La extracción extrae campos nombrados de texto libre en un registro. Ambas convierten un generador de texto difuso en un componente confiable, y entre ellas cubren una gran parte de características prácticas de IA. En la práctica
Convirtiendo un correo de soporte desordenado en un ticket estructurado, combinando clasificación y extracción en un esquema:
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Convierte el correo de soporte en un ticket."},
{"role": "user", "content": email_text},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "ticket",
"strict": True,
"schema": {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "technical", "general"]},
"urgency": {"type": "string", "enum": ["low", "medium", "high"]},
"summary": {"type": "string"},
},
"required": ["category", "urgency", "summary"],
"additionalProperties": False,
},
},
},
)
ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "Cobrado doble el mes pasado."}Una llamada clasifica el correo dos formas y extrae un resumen, devolviendo un registro que tu código puede enrutar y guardar, con la forma garantizada y los valores aún vale la pena chequear. Hasta ahora todo ha fluido texto dentro y texto afuera. Después das al modelo un sentido completamente diferente: en Incrustaciones, el texto se convierte en números que puedes comparar por significado, la fundación para búsqueda y para trabajar con tus propios documentos.

