Cuando la salida de un modelo alimenta tu código, el texto libre es un riesgo. Esta guía te muestra cómo definir un solo esquema, forzar al modelo a respetarlo con las restricciones que ofrece la API, validar en el borde y recuperarte cuando igual se sale del molde, sin pasarte de restrictivo y terminar provocando más fallos.

En resumen
- Un esquema, una sola fuente de verdad: saca del mismo lugar la restricción para el modelo y el validador en runtime para que nunca se desincronicen.
- Usa la garantía más fuerte que ofrezca la API, sea structured outputs estrictos o tool use estricto, antes de conformarte con una forma JSON descrita en prosa.
- Valida siempre en el borde. Parsea la salida cruda y trata un fallo de parseo como algo que va a pasar, no como un bug.
- Un repair loop de un solo intento, donde le devuelves el error de validación y le pides una versión corregida, convierte la mayoría de los fallos en éxitos sin que nadie tenga que intervenir.
- No te pases de restrictivo. Los esquemas muy anidados y rígidos fallan más a menudo por un solo campo, así que mantenlos planos y tolerantes, y normaliza en código.
Cuando la salida de un LLM fluye hacia el resto de tu programa, el texto libre es un riesgo: un campo que falta, un code fence perdido, una oración extra de explicación, y el parseo te revienta en producción. La solución no es un regex más astuto. Es tratar la salida del modelo como cualquier otro input no confiable que cruza un borde. Esta guía recorre un flujo que puedes llevar a producción: define el esquema una vez, fuerza al modelo a respetarlo con la restricción más fuerte que ofrezca la API, valida antes de que la salida toque nada y recupérate con elegancia cuando igual se salga del molde. Al terminar tendrás un patrón que convierte "el modelo volvió a devolver basura" en algo raro, que queda registrado y se cura solo.
Nota
Los ejemplos usan TypeScript con Zod y el SDK de Anthropic, pero el esquema general es el mismo en cualquier lenguaje: una librería de esquemas, un modo de salida estricto y un paso de validar-o-reparar en el borde. Los principios se trasladan directo a Python con Pydantic, a Go con struct tags o a lo que uses.
01 · Requisitos: un esquema, un modo estricto y un borde
Antes de escribir una línea de código, deja tres cosas claras. Saltarte esto es de donde sale, en realidad, la mayoría de las quejas de "el modelo no sigue instrucciones". El contrato nunca se puso por escrito.
- Una librería de esquemas que te dé a la vez un validador en runtime y un tipo. En TypeScript eso es Zod: escribes el esquema una vez y obtienes los tipos estáticos gratis con z.infer.
- Un modelo y un SDK que soporten un modo de salida estricto. En la API de Anthropic, los structured outputs (vía output_config.format) y el tool use estricto (vía strict: true) están soportados en los modelos actuales de Claude: Opus 4.8, Sonnet 4.6 y Haiku 4.5. Si estás en un modelo más viejo o distinto, el plan B es describir la forma del JSON en el prompt y validar con más rigor.
- Un borde claro. Decide la línea exacta de tu código donde la salida no confiable del modelo se vuelve data tipada y confiable. Todo lo que está antes de esa línea trata la salida como un string que podría venir mal. Todo lo que está después puede dar por hecho que el tipo se cumple.
Atención
No des por confiable la salida del modelo solo porque el SDK te la devolvió sin error. Una respuesta 200 significa que el request salió bien, no que el cuerpo cumpla con tu esquema. El paso de validación no es opcional. Es el borde mismo.
Si no puedes nombrar tu esquema, tu modo estricto y tu línea de borde, todavía no estás listo para escribir el código de parseo.
02 · Define el esquema una vez y deriva todo de ahí
La manera más común en que el código de salida estructurada se pudre es la deriva: el prompt dice una cosa, el validador chequea otra y el tipo de TypeScript es una tercera versión escrita a mano. Acaba con las tres haciendo que una sola definición sea la fuente de verdad.
import { z } from "zod"
export const Invoice = z.object({
customer: z.string(),
amountCents: z.number().int().positive(),
dueDate: z.string(), // fecha ISO 8601; se valida flojo y se normaliza después
status: z.enum(["draft", "sent", "paid"]),
})
export type Invoice = z.infer<typeof Invoice>
De este único objeto sacas el validador en runtime (Invoice.safeParse), el tipo estático (z.infer) y, con un helper de esquema a JSON Schema, la restricción que le pasas al modelo. Nunca describes los mismos campos dos veces, así que no hay forma de que se contradigan.
Mantén el esquema plano y tolerante
Aguanta las ganas de meter cada regla de negocio acá. Un esquema que exige un objeto muy anidado con cinco sub-objetos obligatorios le da al modelo cinco lugares más donde resbalar, y un solo resbalón tumba todo el parseo. Valida la forma con holgura, con strings donde vas a aceptar strings, un enum amplio, fechas como strings simples, y después normaliza y haz cumplir las reglas duras en código común, una vez que el parseo ya pasó. La tarea de un esquema en el borde es garantizar que la data tiene la forma correcta, no que sea correcta a nivel de negocio.
03 · Pide el esquema con la garantía más fuerte que tengas disponible
Hay toda una escalera de qué tan fuerte puedes obligar al modelo a cumplir, de lo más débil a lo más fuerte. Sube tan alto como te lo permitan tu modelo y tu SDK.
- Descripción en prosa dentro del prompt. "Devuelve JSON con estos campos…" más un ejemplo. Es el piso. Funciona en cualquier modelo pero no garantiza nada, así que tiene que ir acompañado de validación agresiva.
- Tool use estricto. Define una herramienta cuyo input_schema sea tu JSON Schema y pon strict: true. El modelo queda obligado a emitir argumentos que cumplan. Va bien cuando la data estructurada es, por naturaleza, "una llamada a función" (extrae estos campos y luego actúa).
- Structured outputs. Pasa output_config: {format: {...}} con tu JSON Schema para que la respuesta misma quede restringida a la forma. Con el SDK de Anthropic, client.messages.parse() con un format derivado de Zod valida por ti la respuesta contra el esquema.
import Anthropic from "@anthropic-ai/sdk"
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"
const client = new Anthropic()
const res = await client.messages.parse({
model: "claude-opus-4-8",
max_tokens: 1024,
messages: [{ role: "user", content: rawText }],
output_config: { format: zodOutputFormat(Invoice) },
})
// res.parsed_output es null si el parseo falló — protégete, no asumas.
const invoice = res.parsed_output
Algunas restricciones reales que conviene conocer antes de apoyarte en esto: el JSON Schema estricto soporta los tipos básicos, enum, anyOf y $ref, pero no cotas numéricas (minimum/maximum), límites de largo de strings ni esquemas recursivos. El SDK los quita y los valida del lado del cliente. Los esquemas nuevos pagan un costo de compilación de una sola vez en el primer request y de ahí en adelante quedan en caché. Y los structured outputs no se combinan con el prefilling de la respuesta, así que ni intentes forzar la llave de apertura a mano.
Consejo
Estés en el peldaño que estés, dale al modelo dos o tres ejemplos resueltos de input a output. Los ejemplos few-shot cierran más de la brecha de cumplimiento que cualquier cantidad de "DEBES", y salen baratos. Deja también los modos de fallo bien explícitos. "Si un campo no está en la fuente, usa null en vez de inventar un valor" elimina toda una clase de salidas seguras de sí mismas pero equivocadas.
04 · Valida en el borde, siempre
Aun con un modo estricto activo, valida. El tool use estricto y los structured outputs hacen que la salida malformada sea rara, no imposible. Un rechazo, una respuesta truncada por chocar con max_tokens o una llamada en modo de respaldo todavía pueden entregarte algo fuera de forma. El chequeo en el borde es tu único punto consistente para atrapar todo eso.
function parseInvoice(raw: string):
| { ok: true; value: Invoice }
| { ok: false; error: z.ZodError } {
let json: unknown
try {
json = JSON.parse(raw)
} catch {
return { ok: false, error: new z.ZodError([]) }
}
const result = Invoice.safeParse(json)
return result.success
? { ok: true, value: result.data }
: { ok: false, error: result.error }
}
Fíjate en lo que esto no hace: nunca lanza una excepción hacia quien lo llama y nunca devuelve data validada a medias. El resto de tu sistema solo llega a ver un Invoice completamente tipado o un fallo explícito que puede decidir cómo manejar. Registra los fallos con la salida cruda adjunta. Ese log es la forma en que vas a descubrir si el eslabón débil es tu prompt, tu esquema o el modelo.
05 · Recupérate con un repair loop de un solo intento
Un fallo de validación no es el final del request. El modelo que produjo una salida casi correcta casi siempre puede arreglarla si le muestras exactamente qué estuvo mal. Devuélvele el error de validación y pídele una versión corregida, una vez, quizás dos, y de ahí pásalo a una persona o cae a un valor por defecto seguro.
async function getInvoice(rawText: string, maxRepairs = 1): Promise<Invoice> {
let attempt = await extract(rawText) // primera llamada estructurada
for (let i = 0; i <= maxRepairs; i++) {
const parsed = parseInvoice(attempt)
if (parsed.ok) return parsed.value
attempt = await repair(attempt, parsed.error) // reenvía con el error adjunto
}
throw new Error("la salida estructurada falló tras los intentos de reparación")
}
La llamada repair le manda al modelo su propia salida mala más el mensaje exacto de validación ("amountCents debe ser un entero positivo; llegó '12.50'") y le pide solo un objeto JSON corregido. Es barato, está acotado y resuelve la mayoría de los fallos pasajeros sin que nadie se entere.
Dónde poner el techo
Limita las reparaciones a una o dos. Un loop sin tope sobre un request imposible de raíz, donde la fuente sencillamente no tiene la data que tu esquema exige, quema tokens hasta el infinito. Cuando llegues al techo, falla de forma visible: lanza una excepción, devuelve un error tipado o cae a un valor por defecto que el resto del sistema sepa interpretar. Un loop que se cura solo debe curarse o dar la cara. Lo que nunca debe hacer es quedarse dando vueltas.
Importante
Cuenta y alerta sobre el uso del repair loop. Una tasa de reparación que va subiendo es una señal, casi siempre de que tu esquema se volvió más estricto de lo que aguanta la data de origen, o de que un cambio de modelo movió el comportamiento. El loop va a absorber el costo en silencio mientras te esconde la regresión. Trata una tasa de reparación al alza como un bug que hay que investigar, no como una funcionalidad operando según lo previsto.
La salida estructurada es una de esas áreas donde un poco de disciplina rinde durante años: un esquema, la restricción más fuerte que aguante tu stack, un borde de validación que nada cruza sin chequear y un repair loop acotado. Ajusta bien esos cuatro y la salida del modelo deja de ser fuente de llamadas a las 3 de la mañana y pasa a ser un valor tipado más circulando por tu sistema. La trampa a evitar es sobre-diseñar el esquema persiguiendo garantías que el borde ya te está dando. Mantenlo plano, valida fuerte y deja que tu código se encargue de la lógica de negocio.
Puntos clave
- Un esquema es la fuente de verdad: saca la restricción para el modelo, el validador en runtime y el tipo de la misma definición para que no se desincronicen.
- Sube la escalera de cumplimiento: prosa, luego tool use estricto, luego structured outputs. Usa el más fuerte que soporten tu modelo y tu SDK, y respáldalo con ejemplos few-shot.
- Valida en el borde siempre, aun con un modo estricto activo. Un 200 significa que el request salió bien, no que el cuerpo cumpla.
- Recupérate con un repair loop acotado de un solo intento que devuelve el error. Limítalo a uno o dos intentos y luego falla a lo grande.
- Mantén los esquemas planos y tolerantes y haz la lógica de negocio en código. Pasarte de restrictivo con el esquema te trae fallos, no garantías.
Preguntas frecuentes
Si uso structured outputs estrictos, ¿todavía necesito validar la respuesta?
Sí. Los modos estrictos hacen que la salida fuera de forma sea rara, no imposible. Un rechazo, una respuesta truncada por chocar con max_tokens o una ruta de respaldo todavía pueden entregarte algo que no cumple. El borde de validación es un único punto consistente para atrapar todo eso, y casi no cuesta nada. Trata el modo estricto como la primera línea de defensa y al validador como el contrato.
¿Debería codificar todas mis reglas de negocio en el esquema?
No. La tarea del esquema en el borde es confirmar que la data tiene la forma correcta, no que sea correcta a nivel de negocio. Los esquemas muy anidados y rígidos le dan al modelo más lugares donde resbalar, y un solo campo malo tumba todo el parseo. Valida la forma con holgura y luego haz cumplir las reglas duras como restricciones entre campos, rangos de valores y lookups en código común una vez que el parseo pasó.
Tool use o structured outputs, ¿cuál debería usar?
Usa tool use estricto cuando la data estructurada es, por naturaleza, una llamada a función: extrae estos campos y luego haz algo con ellos. Usa structured outputs (output_config.format) cuando solo quieres que la respuesta misma sea un objeto tipado que parseas. Ambos dan una garantía estricta en los modelos actuales de Claude, así que elige según si el modelo tiene que actuar o solo devolver data.
¿Cuántas veces debería reintentar el repair loop?
Una o dos, y para. La mayoría de los fallos pasajeros se resuelven en la primera reparación cuando adjuntas el error de validación exacto. Un loop sin tope sobre un request imposible de raíz, donde la fuente no tiene la data que tu esquema exige, quema tokens hasta el infinito. Cuando llegues al techo, falla de forma visible: lanza una excepción, devuelve un error tipado o cae a un valor por defecto que el resto del sistema sepa interpretar.
¿Por qué mi parseo sigue fallando aun con un JSON Schema adjunto?
Causas comunes: el esquema usa restricciones que el modo estricto no hace cumplir (minimum/maximum numéricos, largo de strings, recursión), así que el modelo las ignora; max_tokens quedó muy bajo y el JSON se cortó a mitad del objeto; o intentaste hacer prefill de la respuesta, algo que los structured outputs no soportan. Registra la salida cruda en cada fallo. Ese log casi siempre te dice de una sola lectura si el problema es el esquema, el presupuesto de tokens o la data de origen.
¿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 WhatsAppPrimera conversación gratis. Te responde el fundador.
Recursos relacionados

Instructor: salida estructurada validada de cualquier LLM
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.

Agrega caché de prompts a una llamada de la API de Claude
Marca un prefijo estable como cacheable y bajas latencia y costo en prompts grandes que se repiten. El detalle está en el modelo de match por prefijo, que decide si consigues un hit o terminas pagando precio completo sin enterarte.

Arma un índice RAG en Supabase con pgvector
RAG es búsqueda por vecino más cercano sobre texto que convertiste en embeddings, más un modelo que lee los resultados. Esta guía monta todo sobre Postgres pelado (schema, chunking, un índice HNSW, una función de búsqueda detrás de RLS y la llamada de retrieval a Claude) y te dice sin rodeos dónde pgvector se queda corto.