
Structured Output selon les providers
JSON Mode, Tool Use, et response_format - Comparés
Obtenir du JSON structuré depuis des LLMs semble simple jusqu'à ce que vous en ayez besoin de façon fiable sur OpenAI, Anthropic, Google et Mistral. Voici les modes d'échec que personne ne documente.
Le structured output est une de ces fonctionnalités qui semble résolue jusqu'à ce que vous la fassiez tourner en production sur plusieurs providers. Vous découvrez alors que le "JSON mode" signifie quatre choses différentes selon qui vous appelez, et que deux d'entre eux retourneront silencieusement du JSON invalide sous charge.
Voici ce qui fonctionne vraiment, et là où chaque provider échoue.
Les quatre approches
1. OpenAI - response_format: { type: "json_object" }
L'original. Demande au modèle de retourner du JSON valide sans spécifier de schéma. Fonctionne de façon fiable. Le mode d'échec : il retourne du JSON valide, mais pas le schéma voulu. Vous avez demandé { "name": string, "age": number } et vous obtenez { "result": "John, age 42" }. Le modèle a interprété la demande de format mais pas l'intention de schéma.
Le correctif : Les structured outputs OpenAI avec strict: true et une définition de schéma JSON. Cela impose la conformité au schéma au niveau du décodage - le modèle est contraint de produire des tokens qui correspondent à votre schéma. Fiabilité : ~99,7% dans nos tests.
Implication pour le routing : Les structured outputs strict nécessitent spécifiquement GPT-4o ou GPT-4o-mini. Tous les modèles OpenAI ne le supportent pas.
2. Anthropic - Tool use
Claude n'a pas d'équivalent response_format. L'approche idiomatique consiste à définir un outil avec votre schéma JSON souhaité et demander au modèle de l'appeler. Claude retourne un bloc tool_use contenant les données structurées.
C'est plus verbeux que l'approche OpenAI, mais en réalité plus fiable - Claude excelle dans le formatage des appels d'outils (94% sur notre benchmark de suivi d'instructions). La validation du schéma se produit au niveau applicatif, pas au niveau provider.
Le mode d'échec : Claude retourne parfois une réponse textuelle expliquant pourquoi il ne peut pas appeler l'outil, au lieu de l'appeler. Cela se produit quand le contexte du prompt rend l'appel d'outil sémantiquement inapproprié. Taux : ~2% dans nos tests sur des prompts ambigus.
Le correctif : Ajouter une instruction explicite : "You MUST respond by calling the [tool_name] tool. Do not respond with text." Ramène le taux d'échec à moins de 0,3%.
3. Google Gemini - response_mime_type + schéma
Gemini supporte response_mime_type: "application/json" avec un response_schema optionnel. La validation du schéma est effectuée côté serveur.
Le mode d'échec que nous n'attendions pas : Sous haute charge (requêtes concurrentes pendant notre fenêtre de test de pic), Gemini 2.0 Flash a retourné du JSON syntaxiquement invalide dans 8,2% des requêtes - pas un code d'erreur, juste un résultat malformé. Gemini 2.0 Pro montrait le même problème à 1,4%. Ce comportement n'est documenté nulle part que nous avons pu trouver.
Le correctif : Toujours envelopper les appels Gemini structured output dans un try/catch sur le parsing JSON et relancer en cas d'échec de parsing. Avec un seul retry, le taux d'échec effectif tombe à moins de 0,5%.
Implication pour le routing : Si vous utilisez Gemini pour des raisons de coût sur des tâches de structured output, budgétez pour la logique de retry. Les économies restent significatives même avec le coût occasionnel des tokens de retry.
4. Mistral - response_format: { type: "json_object" }
Similaire au JSON mode original d'OpenAI - pas d'imposition de schéma, juste une garantie de syntaxe JSON valide. Syntaxe fiable (moins de 0,5% d'échec de parsing), mais pas de validation de schéma.
Le pattern qui fonctionne : Utiliser le JSON mode de Mistral avec une description de schéma explicite dans le system prompt, par exemple :
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.
Conformité au schéma : ~93% dans nos tests. Suffisant pour la plupart des tâches d'extraction.
Résumé de fiabilité cross-provider
| Provider | Approche | Fiabilité syntaxe | Conformité schéma | Notes |
|---|---|---|---|---|
| OpenAI (strict) | response_format + schéma | 99,7% | 99,5% | Meilleur global |
| Anthropic | Tool use | 99,6% | 98,2% | Meilleur pour schémas complexes |
| Mistral | JSON mode + prompt | 99,5% | 93,1% | Bon pour schémas simples |
| Gemini Pro | response_schema | 98,6% | 96,8% | Retry si échec de parsing |
| Gemini Flash | response_schema | 91,8% | 89,3% | Logique de retry obligatoire |
La stratégie de routing pour le structured output
Vous n'êtes pas obligé d'utiliser un seul provider pour tout le structured output. Le bon routing dépend de la complexité du schéma et des exigences de latence :
Schémas simples (moins de 5 champs, structure plate) : Gemini Flash + retry. Le chemin le moins cher, la conformité au schéma est suffisante, le surcoût des retries est acceptable.
Schémas complexes (objets imbriqués, arrays, enums) : Anthropic tool use ou OpenAI strict. La validation du schéma est critique ici ; un taux de conformité de 93% signifie que 7% de vos résultats nécessitent une revue humaine ou un retry.
Structured output temps réel (moins de 500ms) : GPT-4o-mini avec strict mode. Gemini Flash est moins cher mais le risque de retry ajoute une latence imprévisible. OpenAI strict est rapide et prévisible.
Extraction structurée en batch sensible aux coûts : Gemini Flash avec budget de retries. Même avec 8% de taux de retry, le coût blended reste 4× moins cher qu'OpenAI strict pour la même tâche.
Ce que ça implique pour un code agnostique aux providers
Si vous construisez un système qui route des requêtes de structured output entre plusieurs providers, votre couche de parsing doit gérer quatre formes de réponse différentes :
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 normalise cela entre les providers - votre application voit un champ structured_output cohérent quel que soit le provider qui a traité la requête. La logique d'extraction et de retry spécifique à chaque provider vit dans la couche de routing, pas dans votre code applicatif.
La règle absolue
Validez le résultat contre votre schéma dans votre application, quel que soit le provider utilisé. L'imposition de schéma au niveau provider est une garantie au mieux, pas un contrat. La seule fois où vous sautez la validation applicative sera celle où une réponse Gemini Flash retournera {"name": null} alors que vous attendiez une chaîne de caractères, et où cela se propagera silencieusement en aval.
La validation de schéma à la frontière applicative coûte des microsecondes. Déboguer une base de données corrompue parce que vous avez fait confiance à l'imposition provider coûte des heures.
Aucune carte bancaire requise
Cet article t'a servi ?
Commentaires
Sois le premier à commenter.