Índice de la sección
Formato de salida: pedir JSON y validarlo
Cómo pedir al modelo una salida en JSON, validarla con un esquema en tu código, reintentar con el error y usar salidas estructuradas garantizadas por la API.
Roadmap- Pedir una salida JSON con un esquema explícito en el prompt.
- Validar la salida con Pydantic o Zod y reintentar una vez con el error de validación.
- Usar salidas estructuradas con output_config y conocer sus limitaciones.
- Distinguir la validación de formato de la validación de reglas de negocio.
Requiere: Ejemplos few-shot: enseñar con casos
Al terminar esta lección sabrás conseguir que el modelo devuelva JSON que tu código puede usar sin miedo: pedido con claridad, validado con un esquema y, cuando la API lo permite, garantizado por construcción. Es el paso que convierte un modelo que escribe texto en un componente que se integra con el resto de tu sistema.
La idea
Tu código no lee prosa: lee estructuras. Si el modelo va a alimentar una base de datos, un enrutador de tickets o una interfaz, necesitas que devuelva siempre la misma forma, con los mismos campos y tipos. Eso se consigue en tres capas, y conviene usar las tres:
- Pedirlo bien: describir el formato en el prompt, con un ejemplo.
- Garantizarlo cuando se pueda: usar salidas estructuradas, que obligan a que la respuesta cumpla un esquema.
- Validarlo siempre: comprobar en tu código tipos, valores permitidos y reglas de negocio.
flowchart LR
P[Prompt con formato] --> M[Modelo]
M --> J[Texto JSON]
J --> V{¿Valida?}
V -- sí --> U[Usar el dato]
V -- no, primer fallo --> R[Reintentar con el error]
R --> M
V -- no, segundo fallo --> E[Error controlado]
Define el esquema en tu código
Empieza por el contrato. Con Pydantic en Python (o Zod en TypeScript) defines los campos una vez y te sirven para validar, documentar y generar el JSON Schema.
from typing import Literal
from pydantic import BaseModel
class Ticket(BaseModel):
categoria: Literal["facturacion", "envio", "cuenta", "producto"]
prioridad: Literal["baja", "media", "alta"]
resumen: strLiteral restringe los valores: si el modelo devuelve "facturación" con tilde o "urgente", la validación falla y te enteras.
Capa 1: pedirlo en el prompt
Describe el formato con precisión, da un ejemplo y pide que no haya nada más alrededor:
import json
import anthropic
from pydantic import ValidationError
from esquema import Ticket
MODEL = "claude-sonnet-5-5"
client = anthropic.Anthropic()
PROMPT = """Clasifica el ticket de soporte de <ticket>.
Devuelve únicamente un objeto JSON, sin texto antes ni después y sin bloque de código,
con estos campos:
- "categoria": una de "facturacion", "envio", "cuenta", "producto"
- "prioridad": una de "baja", "media", "alta" (alta si hay dinero cobrado de más,
amenaza de baja o el servicio no funciona)
- "resumen": una frase de menos de 15 palabras
Ejemplo de salida:
{{"categoria": "envio", "prioridad": "media", "resumen": "Pedido retrasado una semana"}}
<ticket>
{ticket}
</ticket>"""
def pedir(messages: list) -> str:
resp = client.messages.create(model=MODEL, max_tokens=300, messages=messages)
if resp.stop_reason == "max_tokens":
raise RuntimeError("JSON cortado por max_tokens")
return "".join(b.text for b in resp.content if b.type == "text")
def clasificar(ticket: str) -> Ticket:
messages = [{"role": "user", "content": PROMPT.format(ticket=ticket)}]
texto = pedir(messages)
try:
return Ticket.model_validate_json(texto)
except ValidationError as e:
# un reintento: le enseñamos su respuesta y el error concreto
messages += [
{"role": "assistant", "content": texto},
{"role": "user", "content": f"Tu JSON no es válido:\n{e}\nDevuelve solo el JSON corregido."},
]
return Ticket.model_validate_json(pedir(messages)) # si falla otra vez, que suba
print(clasificar("Me habéis cobrado dos veces la suscripción de octubre."))Las llaves dobles {{ }} del ejemplo son para que str.format no las confunda con el hueco {ticket}.
El reintento con el error es muy eficaz porque el mensaje de Pydantic dice exactamente qué campo falló y por qué. Limítalo a uno o dos: si el modelo falla repetidamente, el problema está en el prompt o en el esquema, y reintentar más solo gasta.
Capa 2: salidas estructuradas garantizadas
La API de Mensajes puede obligar a que la respuesta cumpla un JSON Schema. Se activa con output_config, y el JSON llega en el bloque de texto:
import anthropic
from esquema import Ticket
MODEL = "claude-sonnet-5-5"
client = anthropic.Anthropic()
ESQUEMA = {
"type": "object",
"properties": {
"categoria": {"type": "string", "enum": ["facturacion", "envio", "cuenta", "producto"]},
"prioridad": {"type": "string", "enum": ["baja", "media", "alta"]},
"resumen": {"type": "string"},
},
"required": ["categoria", "prioridad", "resumen"],
"additionalProperties": False,
}
resp = client.messages.create(
model=MODEL,
max_tokens=300,
messages=[{"role": "user", "content": "Clasifica este ticket: Me habéis cobrado dos veces octubre."}],
output_config={"format": {"type": "json_schema", "schema": ESQUEMA}},
)
texto = "".join(b.text for b in resp.content if b.type == "text")
ticket = Ticket.model_validate_json(texto) # sigue validandoimport Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const MODEL = "claude-sonnet-5-5";
const client = new Anthropic();
const Ticket = z.object({
categoria: z.enum(["facturacion", "envio", "cuenta", "producto"]),
prioridad: z.enum(["baja", "media", "alta"]),
resumen: z.string(),
});
const resp = await client.messages.create({
model: MODEL,
max_tokens: 300,
messages: [{ role: "user", content: "Clasifica este ticket: Me habéis cobrado dos veces octubre." }],
output_config: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: {
categoria: { type: "string", enum: ["facturacion", "envio", "cuenta", "producto"] },
prioridad: { type: "string", enum: ["baja", "media", "alta"] },
resumen: { type: "string" },
},
required: ["categoria", "prioridad", "resumen"],
additionalProperties: false,
},
},
},
});
const texto = resp.content.map((b) => (b.type === "text" ? b.text : "")).join("");
const ticket = Ticket.parse(JSON.parse(texto)); // sigue validando
console.log(ticket);En Python, el SDK te ahorra escribir el esquema a mano: client.messages.parse(..., output_format=Ticket) lo genera desde el modelo Pydantic y devuelve el objeto ya validado en .parsed_output.
resp = client.messages.parse(
model=MODEL,
max_tokens=300,
messages=[{"role": "user", "content": "Clasifica este ticket: Me habéis cobrado dos veces octubre."}],
output_format=Ticket,
)
ticket = resp.parsed_outputTiene limitaciones que conviene conocer: no admite esquemas recursivos ni restricciones numéricas o de longitud (mínimos, máximos, longitud de cadena), y additionalProperties debe ser false. Comprueba en la documentación oficial qué modelos la soportan.
Capa 3: reglas de negocio en tu código
Lo que el esquema no puede expresar, lo compruebas tú después de validar: que el resumen no supere 15 palabras, que una fecha no sea futura, que un importe cuadre con la suma de líneas. Con Pydantic puedes añadirlo como validadores al modelo que usas para comprobar, aunque no lo envíes como esquema a la API.
def comprobar(t: Ticket) -> list[str]:
problemas = []
if len(t.resumen.split()) > 15:
problemas.append("resumen demasiado largo")
return problemasErrores frecuentes
- Hacer
json.loadssobre la respuesta sin capturar el error. - No comprobar
stop_reason: un JSON cortado pormax_tokensnunca valida. - Aceptar cualquier cadena en campos que deberían ser un conjunto cerrado de valores.
- Reintentar en bucle sin límite cuando la validación falla.
- Confiar en las salidas estructuradas como prueba de que el contenido es correcto.
Ejercicio
EjercicioPistas
Para forzar un fallo de validación y probar el reintento, cambia temporalmente una categoría del prompt por otra que no esté en el Literal. Para el ticket vacío, decide qué debe pasar antes de escribir el código: quizá no merezca la pena llamar al modelo.
Resumen
- Define el esquema una vez en tu código (Pydantic o Zod) y úsalo para validar.
- Pide el formato con precisión y un ejemplo; comprueba
stop_reason. - Si falla la validación, reintenta una vez enseñando el error concreto.
output_configconjson_schemaomessages.parsegarantizan el formato, con limitaciones.- Valida siempre las reglas de negocio que el esquema no cubre.
Para seguir
- El prompt de sistema: rol y reglas fijas: dónde poner las reglas fijas del clasificador.
- Formato y salidas estructuradas y Salida estructurada como referencia.
- Extracción documental a JSON: extracción a JSON a escala, con revisión humana.