Structured Output & Tool Calling
Forzar al LLM a responder en JSON válido para integrarlo con código real
Los LLMs generan texto libre por naturaleza. Cuando necesitas integrar su salida con código (guardar en una base de datos, llamar una API, renderizar un formulario), necesitas que la respuesta sea predecible y parseable. Structured output resuelve esto forzando al modelo a generar JSON que cumpla un esquema exacto, en lugar de depender de que el modelo 'recuerde' formatear bien.
JSON Schema es el vocabulario estándar para definir la forma que debe tener la respuesta: qué propiedades existen, qué tipos tienen, cuáles son obligatorias, y qué restricciones adicionales aplican (longitud mínima, valores permitidos, patrones regex). Los LLMs modernos — tanto la API de Anthropic como la de OpenAI — permiten pasar un JSON Schema y garantizan (en modo strict) que la respuesta sea válida contra ese esquema.
Tool calling (también llamado function calling) extiende este concepto: en lugar de solo devolver JSON, el modelo puede 'llamar' una función de tu código, indicando cuál y con qué argumentos. Tú ejecutas la función localmente, le pasas el resultado al modelo en el siguiente turno, y el modelo termina la respuesta. Es el mecanismo detrás de los agentes: el modelo no ejecuta código, declara su intención de llamar una herramienta.
Pydantic es el estándar de facto en Python para validar structured output. Defines un modelo Pydantic con los campos y tipos esperados, y el LLM SDK puede derivar automáticamente el JSON Schema desde esa clase. Al recibir la respuesta, la parseas directamente en el modelo Pydantic, que valida los tipos y lanza excepciones si algo no encaja. Nunca confíes en la salida del LLM sin validarla — incluso en modo strict puede haber errores sutiles.
La validación es el paso que más se omite en prototipos y el que más duele en producción. Un campo que debería ser un entero llega como string, un enum recibe un valor fuera de la lista, una fecha viene en formato inesperado — todos estos casos son silenciosos si no validas. Añadir validación Pydantic tarda minutos y evita horas de debugging. Un ejemplo real de esto en un pipeline de producción que procesa documentos complejos con OCR + LLM está documentado en https://docs.benjacode.com.
Cuando el JSON Schema es demasiado complejo o el modelo comete errores, una estrategia de recuperación es reintentar la llamada con el error de validación incluido en el prompt: 'Tu respuesta anterior falló la validación con este error: [error]. Corrige y vuelve a intentarlo.' Esta técnica de self-correction reduce drásticamente la tasa de errores en producción.
# JSON Schema for a structured extraction task
schema = {
'type': 'object',
'properties': {
'invoice_number': {'type': 'string'},
'total_amount': {'type': 'number', 'minimum': 0},
'currency': {'type': 'string', 'enum': ['USD', 'EUR', 'GBP']},
'line_items': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'description': {'type': 'string'},
'quantity': {'type': 'integer', 'minimum': 1},
'unit_price': {'type': 'number'}
},
'required': ['description', 'quantity', 'unit_price']
}
}
},
'required': ['invoice_number', 'total_amount', 'currency', 'line_items']
}
# Anthropic structured output call
response = client.messages.create(
model='claude-opus-4-5',
max_tokens=1024,
tools=[{
'name': 'extract_invoice',
'description': 'Extract structured invoice data from the text',
'input_schema': schema
}],
tool_choice={'type': 'tool', 'name': 'extract_invoice'},
messages=[{'role': 'user', 'content': invoice_text}]
)
# Pydantic validation
from pydantic import BaseModel, field_validator
from typing import List
class LineItem(BaseModel):
description: str
quantity: int
unit_price: float
class Invoice(BaseModel):
invoice_number: str
total_amount: float
currency: str
line_items: List[LineItem]
@field_validator('currency')
def validate_currency(cls, v):
if v not in ('USD', 'EUR', 'GBP'):
raise ValueError(f'Invalid currency: {v}')
return v
raw = response.content[0].input # dict from tool_use block
invoice = Invoice(**raw) # validates and raises on errorDebugging lab
Detecta y corrige el error en el código.
- 4.3.5.1
schema = {'type': 'object', 'properties': {'name': {'type': 'string'}, 'age': {'type': 'string'}}} # age should be a number
- 4.3.5.2
raw_output = response.content[0].text # assumes text, not tool_use data = json.loads(raw_output)
- 4.3.5.3
# Schema for status field 'status': {'type': 'string'} # no constraints
- 4.3.5.4
try: invoice = Invoice(**raw) except Exception: pass # silently ignore validation errors
- 4.3.5.5
# Tool calling setup response = client.messages.create( model='claude-haiku-4-5', tools=[{'name': 'extract', 'input_schema': schema}], messages=[{'role': 'user', 'content': text}] ) # missing tool_choice