
Structured Output entre providers
JSON Mode, Tool Use, y response_format - Comparados
Obtener JSON estructurado de los LLMs parece sencillo hasta que necesitas que funcione de forma fiable en OpenAI, Anthropic, Google y Mistral. Estos son los modos de fallo que nadie documenta.
El structured output es una de esas funcionalidades que parece resuelta hasta que la pones en producción con múltiples providers. Entonces descubres que el "JSON mode" significa cuatro cosas distintas según a quién llames, y que dos de ellos devolverán silenciosamente JSON inválido bajo carga.
Esto es lo que realmente funciona, y dónde falla cada provider.
Los cuatro enfoques
1. OpenAI - response_format: { type: "json_object" }
El original. Indica al modelo que devuelva JSON válido sin especificar un esquema. Funciona de forma fiable. El modo de fallo: devuelve JSON válido, pero no el esquema que querías. Pediste { "name": string, "age": number } y obtuviste { "result": "John, age 42" }. El modelo interpretó la solicitud de formato pero no tu intención de esquema.
El correctivo: Los structured outputs de OpenAI con strict: true y una definición de esquema JSON. Esto impone la conformidad al esquema a nivel de decodificación - el modelo está restringido a producir tokens que coincidan con tu esquema. Fiabilidad: ~99,7% en nuestros tests.
Implicación para el routing: Los structured outputs estrictos requieren específicamente GPT-4o o GPT-4o-mini. No todos los modelos de OpenAI lo soportan.
2. Anthropic - Tool use
Claude no tiene un equivalente a response_format. El enfoque idiomático es definir una herramienta con tu esquema JSON deseado e indicar al modelo que la llame. Claude devuelve un bloque tool_use con los datos estructurados.
Es más verboso que el enfoque de OpenAI, pero en realidad más fiable - Claude es excelente en el formateo de llamadas a herramientas (94% en nuestro benchmark de seguimiento de instrucciones). La validación del esquema ocurre en la capa de aplicación, no en la del provider.
El modo de fallo: Claude a veces devuelve una respuesta textual explicando por qué no puede llamar a la herramienta, en lugar de llamarla. Esto ocurre cuando el contexto del prompt hace que la llamada a la herramienta sea semánticamente inapropiada. Tasa: ~2% en nuestros tests con prompts ambiguos.
El correctivo: Añadir una instrucción explícita: "You MUST respond by calling the [tool_name] tool. Do not respond with text." Reduce la tasa de fallo a menos del 0,3%.
3. Google Gemini - response_mime_type + esquema
Gemini soporta response_mime_type: "application/json" con un response_schema opcional. La validación del esquema se realiza en el servidor.
El modo de fallo que no esperábamos: Bajo alta carga (solicitudes concurrentes durante nuestra ventana de test pico), Gemini 2.0 Flash devolvió JSON sintácticamente inválido en el 8,2% de las solicitudes - no un código de error, simplemente output malformado. Gemini 2.0 Pro mostraba el mismo problema al 1,4%. Este comportamiento no estaba documentado en ningún lugar que pudiéramos encontrar.
El correctivo: Siempre envuelve las llamadas de structured output de Gemini en un try/catch sobre el parsing JSON y reintenta en caso de fallo de parsing. Con un solo retry, la tasa efectiva de fallos baja a menos del 0,5%.
Implicación para el routing: Si estás usando Gemini por razones de coste en tareas de structured output, presupuesta para la lógica de retry. El ahorro sigue siendo significativo incluso con el coste ocasional de tokens de retry.
4. Mistral - response_format: { type: "json_object" }
Similar al JSON mode original de OpenAI - sin imposición de esquema, solo una garantía de sintaxis JSON válida. Sintaxis fiable (menos del 0,5% de fallo de parsing), pero sin validación de esquema.
El patrón que funciona: Usar el JSON mode de Mistral con descripción explícita del esquema en el system prompt, por ejemplo:
Respond with a JSON object matching this exact schema:
{"name": "string", "confidence": "number between 0 and 1", "tags": "array of strings"}
Do not include any other fields.
Conformidad al esquema: ~93% en nuestros tests. Suficiente para la mayoría de tareas de extracción.
Resumen de fiabilidad cross-provider
| Provider | Enfoque | Fiabilidad sintaxis | Conformidad esquema | Notas |
|---|---|---|---|---|
| OpenAI (strict) | response_format + esquema | 99,7% | 99,5% | Mejor global |
| Anthropic | Tool use | 99,6% | 98,2% | Mejor para esquemas complejos |
| Mistral | JSON mode + prompt | 99,5% | 93,1% | Bueno para esquemas simples |
| Gemini Pro | response_schema | 98,6% | 96,8% | Retry si fallo de parsing |
| Gemini Flash | response_schema | 91,8% | 89,3% | Lógica de retry obligatoria |
La estrategia de routing para structured output
No estás limitado a un solo provider para todo el structured output. El routing correcto depende de la complejidad del esquema y los requisitos de latencia:
Esquemas simples (menos de 5 campos, estructura plana): Gemini Flash + retry. El camino más barato, la conformidad al esquema es suficiente, el coste de los retries es aceptable.
Esquemas complejos (objetos anidados, arrays, enums): Anthropic tool use u OpenAI strict. La validación del esquema es crítica aquí; una tasa de conformidad del 93% significa que el 7% de tus outputs necesitan revisión humana o un retry.
Structured output en tiempo real (menos de 500ms): GPT-4o-mini con strict mode. Gemini Flash es más barato pero el riesgo de retry añade latencia impredecible. OpenAI strict es rápido y predecible.
Extracción estructurada en batch sensible al coste: Gemini Flash con presupuesto de retries. Incluso con una tasa de retry del 8%, el coste blended sigue siendo 4× más barato que OpenAI strict para la misma tarea.
Lo que esto implica para código agnóstico al provider
Si estás construyendo un sistema que enruta solicitudes de structured output entre providers, tu capa de parsing necesita manejar cuatro formas de respuesta distintas:
def extract_structured(response, provider):
if provider == "openai":
return json.loads(response.choices[0].message.content)
elif provider == "anthropic":
tool_block = next(b for b in response.content if b.type == "tool_use")
return tool_block.input
elif provider in ("gemini", "mistral"):
try:
return json.loads(response.text)
except json.JSONDecodeError:
raise RetryableError("Invalid JSON from provider")
HiWay2LLM normaliza esto entre providers - tu aplicación ve un campo structured_output consistente independientemente del provider que haya procesado la solicitud. La lógica de extracción y retry específica de cada provider vive en la capa de routing, no en tu código de aplicación.
La regla absoluta
Valida el output contra tu esquema en tu aplicación, independientemente del provider que uses. La imposición de esquema a nivel provider es una garantía en el mejor de los casos, no un contrato. La única vez que omitas la validación a nivel de aplicación será aquella en que una respuesta de Gemini Flash devuelva {"name": null} cuando esperabas una cadena de texto, y se propague silenciosamente aguas abajo.
La validación de esquema en la frontera de la aplicación cuesta microsegundos. Depurar una base de datos corrompida porque confiaste en la imposición del provider cuesta horas.
Sin tarjeta de crédito
Was this useful?
Comments
Be the first to comment.