Cambiar tema

Salida estructurada de LLM: JSON Schema obligatorio y fiabilidad en tool calling

Easton editorial illustration: central JSON Schema gate, three incoming provider-output cards, one validated tool-call object

En producción suena la alarma: fallo en tool calling del Agent, cinco reintentos seguidos, todos por errores de formato de parámetros. Reviso los logs y el campo city debería ser "北京", pero el LLM devolvió {"name": "北京", "id": null}. El analizador se cae y todo el pipeline de datos se detiene.

Fue un agujero grande del año pasado.

Desde entonces empecé a estudiar de forma sistemática la salida estructurada de LLM: desde Structured Outputs de OpenAI hasta Tool Use de Anthropic, desde los reintentos automáticos de Instructor hasta la decodificación restringida de Outlines. Al principio pensé que bastaba con «escribir mejor el prompt», pero no — no es un problema de prompts, es un problema de arquitectura de fiabilidad.

Este artículo comparte esa «arquitectura de fiabilidad en tres capas»: capa de validación de parámetros, capa de reintentos con feedback y capa de decodificación restringida. Al final comparo horizontalmente las soluciones de OpenAI, Claude y Gemini para ayudarte a elegir. También incluyo plantillas de código listas para producción.

1. Por qué la salida estructurada es la base de un Agent

Empiezo con el «format drift» que viví. No es un caso aislado: es la pesadilla de quien desarrolla Agents.

Tres formas de format drift

Primera: campos faltantes. Pides un objeto de usuario con name, age y email, y recibes {"name": "张三"} — faltan los otros dos. No falla siempre, falla a veces. En producción, «a veces» es «inevitable».

Segunda: errores de tipo. La documentación dice claro: user_id es entero. El LLM devuelve "user_id": "12345", una cadena. La validación Pydantic en Python falla y se rompe toda la cadena de llamadas.

Tercera: contenido extra. La más sigilosa. Le pides JSON y te añade al inicio «Here is the response:» y al final «I hope this helps!». El analizador JSON no sabe qué hacer.

5-10%
Tasa de fallo de JSON Mode
<0.1%
Tasa de fallo de Structured Outputs

Los datos oficiales de OpenAI lo dejan claro: JSON Mode (solo garantiza JSON válido) falla entre un 5% y un 10%; Structured Outputs (obliga a seguir el Schema) tiene una tasa de fallo inferior al 0.1%. Dos órdenes de magnitud de diferencia.

Por qué importa tanto

Quizá pienses: «¿No es solo un fallo de parsing? Añade unos reintentos».

El problema es que reintentar no es gratis.

Coste de llamadas API. Una llamada a GPT-4 puede costar unos céntimos; cinco reintentos son varias veces más. Si tu Agent procesa 100.000 peticiones al día con una media de 2 reintentos por petición, haz tú la cuenta.

Latencia acumulada. Una llamada de 2 segundos, tres reintentos: el usuario espera más de 6 segundos. En diálogo en tiempo real, es inaceptable.

Experiencia de usuario rota. El usuario pregunta por el tiempo, tu Agent se queda colgado 10 segundos y acaba con «error del sistema». La próxima vez no vuelve.

La salida estructurada no es un «extra bonito»: es la base para que un Agent funcione de forma estable. A continuación explico cómo resolverlo — no con «mejor prompt», sino con una arquitectura fiable.

2. Arquitectura de fiabilidad en tres capas

Esta arquitectura la resumí después de muchos tropiezos. No es bala de plata, pero baja la probabilidad de errores de formato del 5-10% a casi cero.

L1: capa de validación de parámetros — primera línea de defensa

Aquí la idea es simple: define la estructura de datos esperada con Pydantic, fuerza la conversión de tipos y filtra con listas blancas.

from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from datetime import datetime

class ToolCallParams(BaseModel):
    """Modelo de parámetros para tool calling"""
    city: str = Field(..., min_length=1, max_length=50, description="Nombre de la ciudad")
    date: Optional[datetime] = Field(None, description="Fecha de consulta")
    units: str = Field("metric", pattern="^(metric|imperial)$")

    @field_validator("city")
    @classmethod
    def validate_city(cls, v: str) -> str:
        # Validación por lista blanca
        allowed_cities = {"北京", "上海", "广州", "深圳", "杭州"}
        if v not in allowed_cities:
            raise ValueError(f"Ciudad no soportada: {v}, soportadas: {allowed_cities}")
        return v

Pydantic hace tres cosas: conversión forzada de tipos (cadena "123" a entero 123), detección de campos faltantes y validación personalizada. Es la capa más básica y la más importante.

L2: capa de reintentos — autocorrección con feedback

Cuando los datos del LLM no pasan la validación, no basta con reintentar a ciegas: hay que devolver el error para que se corrija. La biblioteca Instructor encapsula muy bien esta lógica.

import instructor
from openai import OpenAI
from pydantic import ValidationError

client = instructor.patch(OpenAI())

def get_weather_with_retry(user_query: str, max_retries: int = 3):
    """Mecanismo de reintento con feedback de errores"""
    messages = [{"role": "user", "content": user_query}]

    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="gpt-4o",
                response_model=ToolCallParams,  # Modelo Pydantic
                messages=messages,
                temperature=0.1  # Temperatura baja para salida estructurada
            )
            return response  # Validación automática superada

        except ValidationError as e:
            # Devolver el error al LLM para que corrija
            error_msg = f"Validación de parámetros fallida: {str(e)}\nCorrige y devuelve JSON con el formato correcto."
            messages.append({"role": "assistant", "content": "Generando parámetros..."})
            messages.append({"role": "user", "content": error_msg})

            if attempt == max_retries - 1:
                raise Exception(f"Sigue fallando tras {max_retries} reintentos: {e}")

# Ejemplo de uso
result = get_weather_with_retry("帮我查一下北京明天的天气")

La idea central: el LLM no adivina a ciegas; sabe qué falló y por qué. Con feedback, puede corregir. En mis pruebas, con este mecanismo la tasa de éxito en reintentos subió del 60% a más del 95%.

L3: capa de decodificación restringida — evitar errores desde el origen

Las dos capas anteriores son «remedio después del hecho»; L3 es «prevención antes».

La decodificación restringida funciona así: al generar cada token, una máquina de estados finitos (FSM) limita las opciones y obliga a producir solo secuencias que cumplen el Schema. Es como un «freno» al LLM: no puede salirse del formato.

Hay dos opciones principales:

Outlines (open source, ideal para modelos locales):

from outlines import models, generate
import json

# Cargar modelo local
model = models.transformers("Qwen/Qwen2.5-7B-Instruct")

# Definir Schema
schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer"}
    },
    "required": ["name", "age"]
}

# Crear generador restringido
generator = generate.json(model, schema)
result = generator("提取用户信息: 张三今年28岁")
# 100% conforme al Schema, sin reintentos

guided_json de vLLM (para desplegar modelos grandes):

from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen2.5-72B-Instruct")
sampling_params = SamplingParams(
    temperature=0.0,
    guided_decoding_backend="outlines",
    guided_json={  # Pasar JSON Schema directamente
        "type": "object",
        "properties": {
            "tool_name": {"type": "string"},
            "arguments": {"type": "object"}
        }
    }
)

L3 tiene coste de compilación extra: la FSM se construye a partir del Schema. Si el Schema cambia a menudo, recompilar añade latencia. En la mayoría de Agents el Schema es estable y el coste es aceptable.

Cómo elegir entre las tres capas

EscenarioSolución recomendada
Llamadas a OpenAI APIL1 + L2 (Pydantic + Instructor)
Llamadas a Claude APIL1 + L2 (Claude no soporta Strict Mode)
Modelos locales desplegadosL1 + L3 (Outlines/vLLM guided_json)
Fiabilidad extremaL1 + L2 + L3 completas

3. Comparativa de proveedores: OpenAI, Claude y Gemini

Aquí entran las diferencias de implementación. Sin comparativa horizontal es fácil tropezar: «salida estructurada» no significa lo mismo en cada proveedor.

OpenAI: Strict Mode, cumplimiento obligatorio

OpenAI lanzó Structured Outputs en agosto de 2024; hoy es la opción más fiable entre APIs comerciales.

El mecanismo clave es strict: true. Activado, la salida del LLM queda restringida al JSON Schema definido con garantía del 100% de cumplimiento. Por debajo usa decodificación restringida basada en gramáticas, similar a Outlines.

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "提取用户信息"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "user_info",
            "strict": True,  # Parámetro clave
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"}
                },
                "required": ["name", "age"]
            }
        }
    }
)
# Salida 100% conforme al Schema

OpenAI reporta tasa de fallo inferior al 0.1% en Strict Mode. En mi uso no he visto errores de formato — con limitaciones: no soporta Schemas recursivos; estructuras anidadas complejas requieren adaptaciones.

Anthropic Claude: Tool Use, sin garantía de cumplimiento

Claude usa otra vía: Tool Use (tool calling).

Defines una herramienta y Claude la invoca con parámetros. Pero hay trampa: el parámetro strict existe pero la documentación oficial dice que se ignora. Claude no garantiza que los parámetros cumplan el Schema.

Texto oficial de Anthropic (actualizado abril 2026):

“The strict parameter is currently ignored for tool definitions. Claude will make a best effort to provide valid arguments, but does not guarantee schema compliance.”

Traducción: lo intentará, pero no lo garantiza. Con Claude en tool calling, añade siempre L1 (validación) y L2 (reintentos).

import anthropic

client = anthropic.Anthropic()

# Definición de herramienta en Claude
tools = [{
    "name": "get_weather",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string"}
        },
        "required": ["city"]
    }
}]

response = client.messages.create(
    model="claude-3.5-sonnet",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "北京天气"}]
)

# Importante: validar manualmente los parámetros de tool_use
for block in response.content:
    if block.type == "tool_use":
        # Validación Pydantic aquí
        validated_params = ToolCallParams.model_validate(block.input)

Google Gemini: Controlled Generation

Gemini usa Controlled Generation con el parámetro response_schema para fijar la estructura de salida.

import google.generativeai as genai

model = genai.GenerativeModel('gemini-1.5-pro')

response = model.generate_content(
    "提取用户信息",
    generation_config={
        "response_mime_type": "application/json",
        "response_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"}
            },
            "required": ["name", "age"]
        }
    }
)

La fiabilidad de Gemini está entre OpenAI y Claude: hay restricción, pero no con la fuerza de «cumplimiento obligatorio» de OpenAI. En pruebas reales la tasa de fallo ronda el 1-2%: mejor que JSON Mode, pero lejos de Strict Mode.

Modelos open source: dependen de Outlines/vLLM

Modelos open source (Qwen, Llama, Mistral) no traen salida estructurada nativa; hace falta tooling externo. Las opciones habituales son Outlines y guided_json de vLLM.

Detalle interesante: un modelo open source con Outlines puede ser más fiable que algunas APIs comerciales, porque la FSM es restricción dura, no «lo intentamos pero no prometemos».

Tabla rápida de elección

NecesidadSolución recomendadaMotivo
Solo API, máxima estabilidadOpenAI + Structured Outputs~0.1% de fallos, la más fiable
Razonamiento complejo + toolsClaude + validación L1/L2Fuerte en razonamiento, hay que validar
Modelo privado desplegadoQwen/Llama + OutlinesCoste controlado, alta fiabilidad
Formato crítico (finanzas, salud)OpenAI Strict u OutlinesCasi cero fallos
Prototipo rápidoInstructor + cualquier APIBien encapsulado, reintentos automáticos

4. Plantillas de código para producción

Aquí van plantillas que he validado en producción. Puedes usarlas directamente.

Plantilla 1: OpenAI Structured Outputs completo

"""
Ejemplo completo de OpenAI Structured Outputs
Para: tool calling, extracción de datos, generación de informes, etc.
"""
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional
import json

# 1. Definir modelo Pydantic
class SearchQuery(BaseModel):
    """Parámetros de búsqueda"""
    keywords: List[str] = Field(
        ...,
        min_length=1,
        max_length=5,
        description="Lista de palabras clave"
    )
    filters: Optional[dict] = Field(
        default=None,
        description="Filtros opcionales"
    )
    limit: int = Field(
        default=10,
        ge=1,
        le=100,
        description="Número de resultados"
    )

# 2. Pydantic a JSON Schema
def model_to_schema(model: type[BaseModel]) -> dict:
    """Convierte modelo Pydantic a JSON Schema"""
    schema = model.model_json_schema()
    # Limpiar metadatos de Pydantic
    schema.pop("title", None)
    for prop in schema.get("properties", {}).values():
        prop.pop("title", None)
    return schema

# 3. Llamada de salida estructurada
client = OpenAI()

def extract_search_params(user_input: str) -> SearchQuery:
    """Extrae parámetros de búsqueda del input del usuario"""
    schema = model_to_schema(SearchQuery)

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "你是一个搜索助手,帮助用户提取搜索参数。"
            },
            {"role": "user", "content": user_input}
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "search_query",
                "strict": True,
                "schema": schema
            }
        },
        temperature=0.1  # Temperatura baja para salida estructurada
    )

    # 4. Analizar y validar de nuevo
    raw_content = response.choices[0].message.content
    data = json.loads(raw_content)
    return SearchQuery.model_validate(data)

# Ejemplo de uso
if __name__ == "__main__":
    query = extract_search_params(
        "我想找一些关于 Python 异步编程的文章,只要最近一个月的,最多 20 条"
    )
    print(query)
    # SearchQuery(keywords=['Python', '异步编程'], filters={'date_range': 'last_month'}, limit=20)

Plantilla 2: reintentos automáticos con Instructor

"""
Ejemplo de reintentos automáticos con Instructor
Para: Claude API, OpenAI JSON Mode (no Strict), escenarios que toleran fallos
"""
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field, ValidationError

class AgentAction(BaseModel):
    """Decisión de acción del Agent"""
    action_type: str = Field(
        ...,
        pattern="^(search|execute|respond|clarify)$"
    )
    parameters: dict = Field(default_factory=dict)
    reasoning: str = Field(..., min_length=10)

# patch del cliente OpenAI
client = instructor.patch(OpenAI())

def get_agent_decision(
    context: str,
    user_request: str,
    max_retries: int = 3
) -> AgentAction:
    """
    Obtiene la decisión del Agent con reintentos automáticos

    Args:
        context: Contexto actual de la conversación
        user_request: Petición del usuario
        max_retries: Máximo de reintentos

    Returns:
        AgentAction: Decisión validada
    """
    messages = [
        {"role": "system", "content": "你是一个智能助手,分析用户需求并决定下一步行动。"},
        {"role": "user", "content": f"上下文: {context}\n\n用户请求: {user_request}"}
    ]

    try:
        response = client.chat.completions.create(
            model="gpt-4o",
            response_model=AgentAction,  # Validación automática de Instructor
            messages=messages,
            max_retries=max_retries,  # Reintentos integrados
            temperature=0.1
        )
        return response

    except ValidationError as e:
        # Instructor ya reintentó max_retries veces
        raise Exception(f"Error de formato irreparable, revisa la definición del modelo: {e}")

# Ejemplo de uso
decision = get_agent_decision(
    context="用户正在查询天气信息",
    user_request="帮我查北京明天的天气,要是晴天就推荐户外活动"
)
print(f"Tipo de acción: {decision.action_type}")
print(f"Parámetros: {decision.parameters}")
print(f"Razonamiento: {decision.reasoning}")

Plantilla 3: salida estructurada con Outlines en local

"""
Ejemplo de salida estructurada con Outlines en modelos locales
Para: despliegue privado, coste sensible, requisitos de privacidad
"""
from outlines import models, generate
from pydantic import BaseModel
from typing import List
import json

# Definir estructura de datos
class ProductInfo(BaseModel):
    """Información de producto"""
    name: str
    price: float
    category: str
    tags: List[str]

# Cargar modelo (primera carga tarda unos segundos)
model = models.transformers("Qwen/Qwen2.5-7B-Instruct")

# Crear generador estructurado
# Nota: el schema se compila a FSM en la primera llamada (~1-2 s)
schema_str = json.dumps(ProductInfo.model_json_schema())
generator = generate.json(model, schema_str)

def extract_product_info(description: str) -> ProductInfo:
    """
    Extrae información estructurada de la descripción del producto

    Args:
        description: Texto de descripción

    Returns:
        ProductInfo: Información estructurada
    """
    prompt = f"从以下商品描述中提取关键信息,以 JSON 格式返回:\n{description}"

    # Resultado 100% conforme al Schema
    result = generator(prompt)

    # Convertir a modelo Pydantic (segunda validación)
    return ProductInfo.model_validate(result)

# Ejemplo de uso
description = """
这款蓝牙耳机采用最新的降噪技术,价格 299 元,
属于数码配件类,适合运动、通勤等场景使用。
"""
product = extract_product_info(description)
print(product)
# ProductInfo(name='蓝牙耳机', price=299.0, category='数码配件', tags=['运动', '通勤'])

Plantilla 4: flujo completo de tool calling

"""
Flujo completo de validación de parámetros en tool calling
Incluye: definición Schema → llamada LLM → validación → reintentos → ejecución
"""
from openai import OpenAI
from pydantic import BaseModel, Field, field_validator, ValidationError
from typing import Callable, Dict, Any
import json

# 1. Modelo de parámetros de herramienta
class WeatherQueryParams(BaseModel):
    """Parámetros de consulta meteorológica"""
    city: str = Field(..., min_length=1, max_length=50)
    date_offset: int = Field(default=0, ge=-7, le=7, description="Desplazamiento de fecha, 0=hoy")

    @field_validator("city")
    @classmethod
    def validate_city(cls, v: str) -> str:
        allowed = {"北京", "上海", "广州", "深圳", "杭州", "成都", "武汉"}
        if v not in allowed:
            raise ValueError(f"Ciudad no soportada, opciones: {allowed}")
        return v

# 2. Gestor de tool calling
class ToolCallManager:
    """Gestiona el flujo completo de tool calling"""

    def __init__(self):
        self.client = OpenAI()
        self.tools: Dict[str, Callable] = {}

    def register_tool(self, name: str, func: Callable, param_model: type[BaseModel]):
        """Registra una herramienta"""
        self.tools[name] = {
            "function": func,
            "param_model": param_model
        }

    def execute_with_retry(
        self,
        tool_name: str,
        user_request: str,
        max_retries: int = 3
    ) -> Any:
        """Ejecuta tool calling con reintentos"""

        tool_config = self.tools[tool_name]
        param_model = tool_config["param_model"]
        schema = param_model.model_json_schema()

        messages = [
            {"role": "system", "content": f"提取工具 '{tool_name}' 的调用参数"},
            {"role": "user", "content": user_request}
        ]

        for attempt in range(max_retries):
            try:
                # Llamar al LLM para obtener parámetros
                response = self.client.chat.completions.create(
                    model="gpt-4o",
                    messages=messages,
                    response_format={
                        "type": "json_schema",
                        "json_schema": {
                            "name": tool_name,
                            "strict": True,
                            "schema": schema
                        }
                    },
                    temperature=0.1
                )

                # Validar parámetros
                params = param_model.model_validate_json(
                    response.choices[0].message.content
                )

                # Ejecutar herramienta
                return tool_config["function"](params)

            except ValidationError as e:
                # Devolver error para corrección
                messages.append({
                    "role": "user",
                    "content": f"Validación fallida: {e}\nCorrige el formato de parámetros."
                })
                continue

        raise Exception(f"Tool calling fallido tras {max_retries} reintentos")

# 3. Ejemplo de uso
def get_weather(params: WeatherQueryParams) -> str:
    """Simula consulta meteorológica"""
    # Aquí iría la lógica real de la API
    return f"{params.city} 未来 {params.date_offset} 天天气晴朗"

manager = ToolCallManager()
manager.register_tool("get_weather", get_weather, WeatherQueryParams)

result = manager.execute_with_retry(
    "get_weather",
    "帮我查一下北京明天的天气"
)
print(result)  # 北京未来 1 天天气晴朗

Estas plantillas cubren los escenarios más habituales. Combínalas y adáptalas según tu caso.

5. Buenas prácticas en producción

El código no lo es todo: en producción hay muchos detalles. Comparto trampas que pisé y cómo las resolví.

Temperature: no la subas demasiado

En salida estructurada, Temperature entre 0.0 y 0.2. OpenAI lo recomienda y en pruebas es lo más estable.

¿Problema de temperatura alta? El LLM «dispersa» más y la salida es más aleatoria. La aleatoriedad es enemiga del formato estructurado: buscas determinismo, no creatividad. Con Temperature 0.7 tuve ~15% de errores de formato; con 0.1 casi desaparecieron.

Reintentos: no todo error merece reintento

Antes de reintentar, clasifica el error:

Tipo de error¿Reintentar?Motivo
Formato de parámetros (campo faltante, tipo incorrecto)Sí + feedback de errorEl LLM puede autocorregirse
Error de servicio API (429, 500)Sí + backoffProblema temporal del servidor
Validación de negocio (ciudad fuera de lista blanca)No, devolver errorRequiere confirmación del usuario
Fallo de ejecución de herramienta (resultado vacío)No, usar fallbackProblema de la herramienta

He visto reintentos infinitos para todo: una ciudad fuera de lista blanca, el LLM adivina 10 veces mal y acaba en timeout. Clasificar errores es clave.

Comparativa de overhead de rendimiento

SoluciónLatencia extraCoste extraFiabilidad
Restricción por prompt (sin parámetros especiales)+0 ms+0%5-10% fallos
JSON Mode (solo OpenAI)+50 ms+0%2-5% fallos
Structured Outputs (Strict)+100 ms+0%<0.1% fallos
Reintentos Instructor+200-500 ms/reintentocoste × reintentos~0% fallos
Outlines FSM+1-2 s (primera compilación)+0%100% cumplimiento

Elige según prioridad: máxima estabilidad → Structured Outputs u Outlines; prototipo rápido → Instructor; presupuesto ajustado → JSON Mode + validación manual.

Métricas de monitorización: tres imprescindibles

Tras el despliegue, monitoriza:

  1. Tasa de fallos de formato: porcentaje de peticiones que no pasan validación. Por encima del 1%, investiga.
  2. Reintentos medios: lo normal es 0.5-1.5. Por encima de 2, revisa modelo o Schema.
  3. Latencia media: la salida estructurada añade 50-200 ms respecto a salida libre; mantén un techo aceptable.

Uso Prometheus + Grafana y reviso el informe semanal. Una vez los reintentos medios saltaron de 0.8 a 2.5: el Schema había cambiado pero no el código — la monitorización lo detectó a tiempo.

Conclusión

En resumen, una idea: en 2026 la salida estructurada ya no es un problema difícil — si usas el método correcto.

La arquitectura en tres capas (validación + reintentos + decodificación restringida) cubre de «que funcione» a «que funcione de forma estable». Al elegir proveedor: OpenAI Strict Mode es lo más sólido; Claude exige validación propia; modelos open source con Outlines pueden ser muy fiables.

Las plantillas están en el capítulo 4, listas para adaptar. Si empiezas con Agents, Instructor es buen punto de partida — reintentos y feedback integrados; cuando domines el flujo, valora Outlines para cumplimiento al 100%.

Si tienes dudas, comenta o escríbeme. Es contenido denso; espero que te ahorre algunos tropiezos.

Flujo completo para implementar OpenAI Structured Outputs

Pasos completos desde la definición del modelo Pydantic hasta la llamada de salida estructurada

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Definir el modelo de datos con Pydantic

    Crea clases de modelo Pydantic y usa Field para definir restricciones:

    • Usa `Field(..., min_length=1, max_length=50)` para rangos de longitud de cadenas
    • Usa `Field(default=10, ge=1, le=100)` para rangos numéricos
    • Usa `@field_validator` para lógica de validación personalizada (p. ej. filtrado por lista blanca)
    • Usa `Optional[T]` para campos opcionales
  2. 2

    Step 2: Convertir el modelo Pydantic a JSON Schema

    Usa el método `model.model_json_schema()`:

    ```python
    schema = SearchQuery.model_json_schema()
    schema.pop("title", None) # limpiar metadatos de Pydantic
    ```

    Asegúrate de que el Schema cumpla los requisitos de OpenAI Structured Outputs.
  3. 3

    Step 3: Llamar a la API de OpenAI con Strict Mode activado

    Configura el parámetro `response_format` en la petición:

    • `type: json_schema` — especifica el tipo de salida estructurada
    • `strict: True` — activa el modo de cumplimiento obligatorio
    • `json_schema.name` — nombre del Schema (personalizable)
    • `json_schema.schema` — el JSON Schema convertido en el paso anterior
  4. 4

    Step 4: Analizar la respuesta y validar de nuevo

    Aunque Strict Mode garantiza el 100% de cumplimiento, conviene una segunda validación:

    • Usa `json.loads()` para analizar la cadena de respuesta
    • Usa `model.model_validate(data)` para la validación Pydantic
    • Captura la excepción `ValidationError` y gestiona casos límite
  5. 5

    Step 5: Configurar el parámetro Temperature

    Usa temperatura baja en escenarios de salida estructurada:

    ```python
    temperature=0.1 # recomendado 0.0-0.2
    ```

    Evita temperaturas altas que aumenten la aleatoriedad y afecten la estabilidad del formato.

FAQ

¿Qué hacer si el LLM devuelve JSON con formato incorrecto?
Adopta una arquitectura de fiabilidad en tres capas:

• Capa L1 de validación de parámetros: define modelos de datos con Pydantic, conversión automática de tipos y validación de campos
• Capa L2 de reintentos: usa la biblioteca Instructor para reintentar automáticamente y devolver el error al LLM para autocorrección
• Capa L3 de decodificación restringida: usa Outlines o guided_json de vLLM para garantizar el cumplimiento desde el origen
¿En qué se diferencian la salida estructurada de OpenAI y Claude?
OpenAI Structured Outputs en modo strict garantiza el 100% de cumplimiento de formato, con tasa de fallo &lt;0.1%; Claude Tool Use no garantiza cumplimiento, el parámetro strict es ignorado oficialmente y necesitas añadir capas L1/L2. Si buscas estabilidad extrema, elige OpenAI; si necesitas razonamiento complejo, Claude + validación propia es la mejor opción.
¿Cómo elegir la solución de salida estructurada adecuada?
Elige según el escenario y la necesidad:

• **Llamadas a OpenAI API**: Structured Outputs + Strict Mode (más estable)
• **Llamadas a Claude API**: validación Pydantic + reintentos con Instructor (requiere validación propia)
• **Despliegue de modelos locales**: Outlines o vLLM guided_json (coste controlado, alta fiabilidad)
• **Prototipado rápido**: biblioteca Instructor (bien encapsulada, lista para usar)
• **Escenarios exigentes (finanzas/salud)**: OpenAI Strict o Outlines (casi cero fallos)
¿Cómo configurar el parámetro Temperature?
En salida estructurada se recomienda Temperature entre 0.0 y 0.2. Es el rango recomendado por OpenAI y el más estable en pruebas reales. Temperaturas altas aumentan la aleatoriedad y suben la tasa de errores de formato. En mis pruebas, con Temperature 0.7 la tasa de error llegó al 15%; al cambiar a 0.1 prácticamente desaparecieron los problemas de formato.
¿Cuánto overhead de rendimiento añade la salida estructurada?
El overhead varía mucho según la solución:

• **Restricción por prompt**: +0 ms de latencia, 5-10% de tasa de fallo
• **JSON Mode**: +50 ms de latencia, 2-5% de tasa de fallo
• **Structured Outputs**: +100 ms de latencia, &lt;0.1% de tasa de fallo
• **Reintentos con Instructor**: +200-500 ms por reintento, tasa de fallo cercana a 0%
• **Outlines FSM**: +1-2 s en la primera compilación, 100% de cumplimiento

Elige según la fiabilidad requerida y el presupuesto.
¿Qué errores conviene reintentar y cuáles no?
La estrategia de reintentos debe distinguir tipos de error:

**Reintentar**:
• Errores de formato de parámetros (campos faltantes, tipos incorrectos) — el LLM puede autocorregirse
• Errores de servicio API (429, 500) — problemas temporales del servidor

**No reintentar**:
• Fallos de validación de negocio (ciudad fuera de lista blanca) — requiere confirmación del usuario
• Fallo en ejecución de herramienta (resultado vacío) — problema de la herramienta, usar fallback

Reintentos infinitos provocan timeout; distinguir tipos de error permite un manejo eficiente.

16 min de lectura · Publicado el: 6 may 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog