Structured outputs sin sufrimiento: JSON que no falla
En la demo el JSON sale bien. En producción, cuando el input es raro o la respuesta se alarga, sale casi bien. Y "casi" es un bug: un campo que falta a las 3 de la mañana es una página en rojo.
Structured outputs es la diferencia entre pedir JSON en el prompt y obligar al modelo a devolver una estructura válida. Esta pieza es cómo lo hago, con el código y la fuente.
El problema: "casi siempre JSON válido"
Si le pides JSON en texto libre, confías en que el modelo no se equivoque. Y se equivoca: comillas sin escapar, un campo de más, un número como string, un objeto truncado. El parseo falla, y el fallo aparece en el peor momento, no en tus pruebas.
La solución en 3 pasos
1. Define el esquema con zod (o el validador de tu lenguaje):
// Fuente: Structured Outputs with Vercel's AI SDK — AI Hero (adaptado)
import { z } from "zod";
const schema = z.object({
ticket: z.object({
title: z.string().describe("Un título corto y accionable"),
category: z.enum(["billing", "bug", "cuenta", "otro"]),
priority: z.enum(["alta", "media", "baja"]),
summary: z.string().describe("Un resumen de una frase"),
}),
});
2. Deja que el SDK pida la estructura, no el texto:
import { generateObject } from "ai";
const { object } = await generateObject({
model,
schema,
schemaName: "Ticket",
prompt: "Clasifica este mensaje del usuario: ...",
});
return object.ticket; // tipado, validado, listo
3. Usa el resultado con tipos. En TypeScript, object.ticket.priority es un union de tres valores: si el modelo devuelve otra cosa, no compila. - Source: Structured Outputs with Vercel's AI SDK - AI Hero
Las 3 claves que casi nadie usa
.describe()en cada campo. No es decoración: es la instrucción de ese campo. "Un título corto y accionable" rinde más quetitle: z.string(). - Source: Structured Outputs with Vercel's AI SDK - AI Hero- Enums en lugar de strings libres. Donde conoces las opciones, no dejes que el modelo invente:
z.enum([...])elimina categorías fantasma. schemaName. Un nombre ayuda al modelo a entender qué estructura está llenando (y mejora la traza cuando depuras).
Valida y reintenta (el seguro que falta)
Structured outputs reduce los fallos, no los elimina (ni en los modelos ni en las versiones). El patrón que uso: valida contra el esquema y, si falla, reintenta pasándole el error al modelo, con un máximo de 2 intentos y una salida segura si no lo logra.
for (let i = 0; i < 2; i++) {
const { object } = await generateObject({ model, schema, prompt });
const parsed = schema.safeParse(object);
if (parsed.success) return parsed.data;
prompt += `\nTu respuesta anterior falló: ${parsed.error.message}`;
}
return fallback; // nunca dejes al usuario con un crash
No dejes que el formato luche contra el modelo
Hay un detalle que Anthropic explica bien: algunos formatos son mucho más difíciles de escribir para un modelo que otros. Escribir un diff exige contar líneas; escribir código dentro de JSON exige escapar saltos de línea y comillas. Sus recomendaciones: dar suficientes tokens para pensar antes de escribir, mantener el formato cerca de lo que el modelo ve naturalmente en texto, y evitar overhead de formato como escapar o contar líneas. - Source: Building effective agents (Apéndice 2) - Anthropic
Traducción: si tu esquema obliga al modelo a hacer malabares de escape, no es un problema de JSON: es un problema de diseño de la interfaz.
Cuándo NO usar structured outputs
- Texto largo o prosa (un post, una explicación): ahí la estructura es ruido.
- Exploración abierta: cuando no sabes la forma de la respuesta todavía, primero prototipa en texto y luego fija el esquema.
- Como sustituto de validación de negocio: que el JSON cumpla el esquema no significa que los datos sean correctos. Eso se mide con evals.
5 reglas
- Define el esquema antes del prompt. El contrato primero.
.describe()cada campo. Es la instrucción que el modelo sí lee.- Enums donde haya opciones cerradas.
- Valida y reintenta con el error incluido; máximo 2 intentos.
- Diseña el esquema para que sea fácil de escribir, no solo bonito para tu código.
Esto es la pieza de "interfaz" del harness aplicada a la salida; el marco está en Harness Engineering: las 9 piezas y el código en contexto en Agentes con Vercel AI SDK 7.
¿Tu app parsea texto libre o ya devuelve objetos validados? Cuéntame en los comentarios qué campo fue el que te rompió producción.