Skip to Content
IAStructured Output

Structured Output — JSON, Schémas & Tool Calling

Forcer un LLM à produire du JSON valide, contraindre sa sortie avec un schéma, ou lui faire exécuter des fonctions : trois mécanismes qui transforment l’IA d’une curiosité en un composant fiable dans une application.

Pourquoi le structured output ?

Sans contrainte, un LLM peut répondre avec du texte, du JSON mal formé, ou du JSON dans un bloc markdown. Dans un pipeline automatisé, chaque sortie doit être parsable. Les APIs OpenAI, Claude et Gemini proposent trois niveaux de contrôle :

MécanismeContrainteCompatibilité
JSON modeForce une réponse JSON valideOpenAI, Claude, Gemini
Schémas de sortieCible une structure JSON préciseOpenAI (response_format + JSON Schema), Claude, Gemini
Tool callingLe modèle choisit des fonctions prédéfiniesOpenAI, Claude, Gemini

Règle n°1 : ne jamais parser une sortie brute. Toujours structurer la demande côté API, puis valider côté client.

JSON mode — Forcer du JSON valide

Activer le JSON mode garantit que la réponse est un JSON lisible, mais ne garantit pas la structure.

OpenAI

response = openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "Tu es un analyseur de texte."}, {"role": "user", "content": "Extrais les entités de : 'Marie travaille chez Acme depuis 2020.'"} ], response_format={"type": "json_schema", "json_schema": {"name": "entites", "schema": { "type": "object", "properties": { "personne": {"type": "string"}, "entreprise": {"type": "string"}, "annee_debut": {"type": "integer"} }, "required": ["personne", "entreprise"] }}}, ) print(response.choices[0].message.content)

Claude (Anthropic)

response = anthropic_client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[ {"role": "user", "content": "Extrait les entités suivantes au format JSON :\n{personne, entreprise, annee_debut}"}, ], tool_choice={"type": "any"}, ) # Claude ne supporte pas de paramètre natif --json ; on forme le schéma via le system prompt.

Gemini (Google)

import google.generativeai as genai response = genai.GenerativeModel("gemini-2.0-flash").generate_content( "Extrait les entités de : 'Marie travaille chez Acme depuis 2020.'", generation_config=genai.GenerationConfig( response_mime_type="application/json", response_schema={ "type": "OBJECT", "properties": { "personne": {"type": "STRING"}, "entreprise": {"type": "STRING"}, "annee_debut": {"type": "INTEGER"} } } ), )

Le JSON mode est votre filet de sécurité minimal. Il élimine le texte parasite mais ne garantit pas la cohérence du schéma.

Schémas de sortie — JSON Schema

Aller au-delà du JSON valide : imposer les types, les champs requis, les enums, et les relations entre propriétés.

Exemple complet avec validation locale

import json, openai, jsonschema # 1. Appeler l'API avec un schéma schema = { "type": "object", "properties": { "utilisateur": {"type": "string", "minLength": 1}, "email": {"type": "string", "format": "email"}, "role": {"type": "string", "enum": ["admin", "dev", "viewer"]}, "permissions": { "type": "array", "items": {"type": "string"} } }, "required": ["utilisateur", "email", "role"] } response = openai.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "Crée un utilisateur : Alice, alice@example.com, rôle dev."}], response_format={"type": "json_schema", "json_schema": {"name": "utilisateur", "schema": schema}} ) # 2. Valider localement data = json.loads(response.choices[0].message.content) jsonschema.validate(instance=data, schema=schema)

Compatibilité schémas par fournisseur

FournisseurJSON SchemaOpenAPI 3.xValidation intégrée
OpenAIPartie du response_format (GPT-4o+)Non direct, utiliser JSON SchemaPar défaut via response_format
ClaudeVia tools ou systemNon supporté nativementVérification manuelle
GeminiVia response_schemaNon supportéFormat strict (STRING/OBJECT/ARRAY)

Claude accepte les schémas surtout via le tool calling (cf. section suivante). En JSON brut, il faut formater explicitement dans le prompt et valider à la réception.

Tool / Function Calling — Le modèle appelle des fonctions

Le tool calling est le mécanisme le plus puissant : le modèle décide d’appeler une fonction, reçoit le résultat, puis produit une réponse finale. Idéal pour les agents, les requêtes à base de données, ou les intégrations.

OpenAI — Tool calling complet

tools = [ { "type": "function", "function": { "name": "recherche_article", "description": "Recherche un article dans la base en fonction du titre ou du sujet.", "parameters": { "type": "object", "properties": { "sujet": { "type": "string", "description": "Le sujet de la recherche" }, "limit": { "type": "integer", "description": "Nombre max de résultats (1-10)", "minimum": 1, "maximum": 10 } }, "required": ["sujet"] } } } ] # Premier appel : le modèle choisit d'appeler la fonction response = openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "user", "content": "Trouve-moi les derniers articles sur le Kubernetes."} ], tools=tools, tool_choice="auto" ) message = response.choices[0].message if message.tool_calls: # Exécuter la fonction côté applicatif tool_call = message.tool_calls[0] args = json.loads(tool_call.function.arguments) result = recherche_article(args["sujet"], args.get("limit", 5)) # Deuxième appel avec le résultat en contexte response2 = openai.chat.completions.create( model="gpt-4o", messages=[ {"role": "user", "content": "Trouve-moi les derniers articles sur le Kubernetes."}, {"role": "assistant", "content": None, "tool_calls": [tool_call]}, {"role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result)}, ], tools=tools ) print(response2.choices[0].message.content)

Claude — Tool use

tools = [ { "name": "recherche_article", "description": "Recherche un article dans la base.", "input_schema": { "type": "object", "properties": { "sujet": {"type": "string"}, "limit": {"type": "integer"} }, "required": ["sujet"] } } ] response = anthropic_client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "Articles sur le machine learning ?"}], tools=tools ) if response.stop_reason == "tool_use": for block in response.content: if block.type == "tool_use": result = recherche_article( block.input["sujet"], block.input.get("limit", 5) ) # Deuxième appel avec le résultat response2 = anthropic_client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[ {"role": "user", "content": "Articles sur le machine learning ?"}, {"role": "assistant", "content": [ {"type": "tool_use", "id": block.id, "name": block.name, "input": block.input} ]}, {"role": "user", "content": [{"type": "tool_result", "tool_use_id": block.id, "content": str(result)}]}, ], tools=tools ) print(response2.content[0].text)

Pièges spécifiques au tool calling

  • Boucle infinie : ne pas renvoyer la même requête outil indéfiniment. Fixer un max d’itérations (3-5 typiquement).
  • Champs manquants : le modèle peut appeler sans tous les champs required. Valider les arguments avant exécution.
  • Format d’arguments : le JSON dans function.arguments peut contenir du texte autour. Toujours faire un json.loads dans un try/except.
  • Fonctions non définies : refuser les appels à des outils qui ne figurent pas dans la liste tools.

Validation côté client — Nettoyage de la sortie

Même avec tout en place, le modèle peut produire une sortie légèrement différente du schéma attendu. Toujours valider côté client.

def clean_json(text: str) -> dict: """Nettoyer et parser un JSON potentiellement corrompu.""" text = text.strip() # Enlever un bloc markdown ```json ... ``` if text.startswith("```"): text = text.split("\n", 1)[1] if "\n" in text else text[5:] text = text.rsplit("```", 1)[0] return json.loads(text) def validate_or_retry(data: dict, schema: dict, max_retries: int = 2) -> dict: """Valide le JSON et relance le modèle si invalide.""" for attempt in range(max_retries + 1): try: jsonschema.validate(instance=data, schema=schema) return data except jsonschema.ValidationError: if attempt == max_retries: raise # Relancer avec l'erreur comme feedback print(f"Tentative {attempt + 1} échouée : validation invalide") # ... rappeler l'API avec l'erreur en contexte

Comparaison rapide des trois mécanismes

CritèreJSON modeSchéma de sortieTool calling
Fiabilité JSONHauteHauteN/A (réponse textuelle)
Contrainte de structureNulleForteForte (via input_schema)
UsageExtraction simpleStructure complexeActions / APIs externes
LatenceBasseMoyenneHaute (multi-tour)
CoûtStandardStandardMultiplié par le nombre d’appels

Bonnes pratiques résumé

  • Toujours valider la sortie avec un schéma JSON, même avec le mode JSON activé.
  • Nettoyer avant parser : enlever les blocs markdown, commentaires, et texte parasite.
  • Tool calling pour les actions : quand le modèle doit interagir avec un système externe, utiliser le tool calling plutôt que du JSON brut.
  • Max d’itérations : limiter le nombre d’appels outil pour éviter les boucles infinies.
  • Schema simple : ne pas surcharger le schéma. Moins de champs = meilleure fiabilité.
  • Tester la robustesse : soumettre des inputs variés et vérifier que la sortie reste parsable dans tous les cas.

Référence : OpenAI JSON mode docs , Anthropic Tool Use , Google Gemini Function Calling