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écanisme | Contrainte | Compatibilité |
|---|---|---|
| JSON mode | Force une réponse JSON valide | OpenAI, Claude, Gemini |
| Schémas de sortie | Cible une structure JSON précise | OpenAI (response_format + JSON Schema), Claude, Gemini |
| Tool calling | Le modèle choisit des fonctions prédéfinies | OpenAI, 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
| Fournisseur | JSON Schema | OpenAPI 3.x | Validation intégrée |
|---|---|---|---|
| OpenAI | Partie du response_format (GPT-4o+) | Non direct, utiliser JSON Schema | Par défaut via response_format |
| Claude | Via tools ou system | Non supporté nativement | Vérification manuelle |
| Gemini | Via response_schema | Non 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.argumentspeut contenir du texte autour. Toujours faire unjson.loadsdans untry/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 contexteComparaison rapide des trois mécanismes
| Critère | JSON mode | Schéma de sortie | Tool calling |
|---|---|---|---|
| Fiabilité JSON | Haute | Haute | N/A (réponse textuelle) |
| Contrainte de structure | Nulle | Forte | Forte (via input_schema) |
| Usage | Extraction simple | Structure complexe | Actions / APIs externes |
| Latence | Basse | Moyenne | Haute (multi-tour) |
| Coût | Standard | Standard | Multiplié 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