May 20261 min de lectureJohan Bretonneau

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

ProviderApprocheFiabilité syntaxeConformité schémaNotes
OpenAI (strict)response_format + schéma99,7%99,5%Meilleur global
AnthropicTool use99,6%98,2%Meilleur pour schémas complexes
MistralJSON mode + prompt99,5%93,1%Bon pour schémas simples
Gemini Proresponse_schema98,6%96,8%Retry si échec de parsing
Gemini Flashresponse_schema91,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.

Économiser maintenant →

Aucune carte bancaire requise

Partager

LinkedInXEmail

Cet article t'a servi ?

Commentaires

Sois le premier à commenter.