C
Contextología
Prompt Engineering

Structured outputs: cómo obtener JSON fiable de cualquier LLM

21 de mayo de 2026· 6 min read

Uno de los problemas más frecuentes al integrar LLMs en aplicaciones reales es la inconsistencia del formato de respuesta. El modelo responde con el JSON correcto el 95% de las veces — y el 5% restante rompe tu parser y falla en producción.

Esta guía cubre todos los métodos disponibles para obtener salidas estructuradas y cómo elegir el correcto para cada situación.

Por qué los LLMs fallan con JSON sin ayuda

Los LLMs son modelos de lenguaje, no serializadores JSON. Cuando pides "responde en JSON", el modelo intenta generar texto que parezca JSON válido, pero:

  • Puede añadir texto explicativo antes o después del bloque
  • Puede usar comillas simples en lugar de dobles
  • Puede truncar el JSON si alcanza el límite de tokens
  • Puede añadir trailing commas o comentarios
  • Puede cambiar los nombres de las claves

El resultado es un JSON que no parsea, o que parsea pero no tiene la estructura esperada.

Método 1: JSON mode nativo

La forma más sencilla. OpenAI, Anthropic y Gemini ofrecen un parámetro específico para forzar JSON:

# OpenAI
response = client.chat.completions.create(
    model="gpt-4o",
    response_format={"type": "json_object"},
    messages=[{"role": "user", "content": "Lista 3 países con su capital"}]
)

# Anthropic — via instrucción en el prompt
# (Anthropic no tiene JSON mode explícito, pero sí structured outputs vía tool use)

Limitación importante: el JSON mode garantiza que la respuesta sea JSON válido, pero no que tenga las claves que esperas. Si quieres un schema específico, necesitas el siguiente método.

Método 2: Structured outputs con schema (OpenAI)

OpenAI introdujo structured outputs con validación de schema real. El modelo no puede generar JSON que no cumpla el schema definido:

from pydantic import BaseModel
from openai import OpenAI

class Pais(BaseModel):
    nombre: str
    capital: str
    poblacion_millones: float

class ListaPaises(BaseModel):
    paises: list[Pais]

client = OpenAI()
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Dame 3 países europeos con su capital y población"}],
    response_format=ListaPaises,
)

resultado = response.choices[0].message.parsed
# resultado.paises[0].nombre → "Francia"

Esto garantiza no solo JSON válido sino que el objeto tiene exactamente las claves y tipos definidos.

Método 3: Tool use / function calling

Function calling es la forma más robusta para obtener datos estructurados, compatible con todos los proveedores principales:

# Con Anthropic
import anthropic

client = anthropic.Anthropic()

tools = [{
    "name": "extraer_informacion",
    "description": "Extrae información estructurada del texto",
    "input_schema": {
        "type": "object",
        "properties": {
            "entidades": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "nombre": {"type": "string"},
                        "tipo": {"type": "string", "enum": ["persona", "empresa", "lugar"]},
                        "contexto": {"type": "string"}
                    },
                    "required": ["nombre", "tipo", "contexto"]
                }
            }
        },
        "required": ["entidades"]
    }
}]

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "extraer_informacion"},
    messages=[{
        "role": "user",
        "content": "Analiza: 'María García fundó TechCorp en Barcelona en 2019'"
    }]
)

# El modelo SIEMPRE llamará a la herramienta
resultado = response.content[0].input
# resultado["entidades"][0]["nombre"] → "María García"

La clave es tool_choice con el nombre específico de la herramienta — esto fuerza al modelo a llamarla siempre en lugar de responder en texto libre.

Método 4: Prefilling de la respuesta (solo Anthropic)

Anthropic permite un truco único: empezar tú la respuesta del assistant para forzar el formato:

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=500,
    messages=[
        {"role": "user", "content": "Extrae el nombre, email y empresa de este texto: [texto]"},
        {"role": "assistant", "content": "{"}  # Fuerza que continúe con JSON
    ]
)
# El modelo continuará desde "{" generando JSON válido

Es un método sencillo y muy efectivo para casos donde no quieres definir un schema completo.

Validación en producción

El peor error es asumir que el structured output es correcto sin validarlo. Usa siempre una librería de validación:

# Con Pydantic (Python)
from pydantic import BaseModel, ValidationError

class Resultado(BaseModel):
    nombre: str
    puntuacion: float
    categorias: list[str]

try:
    datos = Resultado.model_validate_json(response_text)
except ValidationError as e:
    # Reintentar o usar fallback
    print(f"Schema inválido: {e}")
// Con Zod (TypeScript)
import { z } from "zod";

const ResultadoSchema = z.object({
  nombre: z.string(),
  puntuacion: z.number().min(0).max(100),
  categorias: z.array(z.string()),
});

const resultado = ResultadoSchema.safeParse(jsonResponse);
if (!resultado.success) {
  // Reintentar con prompt más explícito
}

Patrones para producción

Retry con feedback de error

Cuando el schema falla, incluye el error en el reintento:

def extraer_con_retry(texto: str, max_intentos: int = 3) -> Resultado:
    error_previo = None
    for intento in range(max_intentos):
        prompt = f"Extrae información de: {texto}"
        if error_previo:
            prompt += f"\n\nEl intento anterior falló con este error: {error_previo}. Corrige el formato."
        
        respuesta = llamar_llm(prompt)
        try:
            return Resultado.model_validate_json(respuesta)
        except ValidationError as e:
            error_previo = str(e)
    
    raise ValueError(f"No se pudo obtener JSON válido tras {max_intentos} intentos")

Describir el schema en el prompt

Aunque uses JSON mode o tool use, describir el schema en el prompt mejora la calidad:

Responde con un objeto JSON con estas claves exactas:
- "resumen": string (máximo 2 oraciones)
- "puntuacion": número entre 0 y 10
- "aspectos_positivos": array de strings
- "aspectos_mejorables": array de strings (puede ser vacío)

No incluyas texto fuera del JSON.

Few-shot con ejemplos del schema

Para schemas complejos, incluir 1-2 ejemplos es muy efectivo:

Extrae la información en el formato indicado.

Ejemplo de entrada: "El cliente reportó un error en el checkout"
Ejemplo de salida:
{
  "categoria": "bug",
  "prioridad": "alta",
  "modulo": "checkout",
  "resumen": "Error en proceso de pago"
}

Ahora procesa: [texto real]

Cuándo usar cada método

| Situación | Método recomendado | |-----------|-------------------| | Schema simple, cualquier proveedor | JSON mode + validación Pydantic/Zod | | Schema complejo, OpenAI | Structured outputs con Pydantic | | Extracción de datos, cualquier proveedor | Tool use con tool_choice forzado | | Anthropic, schema simple | Prefilling de respuesta | | Alta fiabilidad en producción | Tool use + validación + retry |

Errores frecuentes

No validar el output: el JSON mode garantiza sintaxis válida, no schema válido. Siempre valida con Zod o Pydantic.

Schemas demasiado profundos: los schemas anidados con más de 3-4 niveles aumentan la tasa de error. Simplifica el schema siempre que sea posible.

No describir el schema en el prompt: aunque uses structured outputs, describir las claves y su significado mejora la calidad del contenido generado.

Olvidar el tool_choice: sin forzar la herramienta, el modelo puede decidir responder en texto libre en lugar de llamar a la función.

Pon en práctica lo que has aprendido

Evaluador de System Prompts

Evalúa si tu prompt está bien diseñado para obtener outputs consistentes.

Abrir herramienta gratuita →

Recibe lo mejor de Contextología

Diseño de contexto, agentes y workflows de IA directamente en tu correo.