Contrôler le format de sortie
Une réponse bien formatée se colle directement dans ton code, ton ticket ou ton tableur. Une réponse mal formatée, tu la retapes.
Temps de lecture : 15 min | Niveau : Intermédiaire
Ce que tu sauras faire après
- Imposer un format précis : JSON, tableau markdown, CSV, SQL nu.
- Décrire un schéma de sortie que le modèle respecte.
- Utiliser les sorties structurées plutôt que d'espérer un JSON valide.
- Parser une réponse JSON en Python, avec la gestion d'erreur qui va bien.
- Savoir ce qui a changé récemment : le prefill n'est plus supporté sur les modèles récents.
🧭 Les 4 techniques, de la plus simple à la plus fiable
| # | Technique | Effort | Fiabilité | Quand l'utiliser |
|---|---|---|---|---|
| 1 | Décrire le format en mots | Faible | Moyenne | Usage en chat, one-shot |
| 2 | Donner un exemple de sortie | Faible | Bonne | Format visuel : tableau, CSV |
| 3 | Baliser la sortie | Moyen | Bonne | Extraction par script |
| 4 | Sorties structurées / appel d'outil | Élevé | Garantie par le schéma | Production, pipeline automatisé |
Monte dans le tableau selon l'enjeu. Pour une question ponctuelle, le niveau 1 suffit. Pour un job qui tourne toutes les heures, va au niveau 4.
Technique 1 — Décrire le format
Trois règles données par la documentation Anthropic dans sa section sur le contrôle du format des réponses.
a) Dis ce qu'il faut faire, pas ce qu'il ne faut pas faire
- Au lieu de :
N'utilise pas de markdown dans ta réponse. - Essaie :
Ta réponse doit être composée de paragraphes de prose fluide.
b) Utilise des indicateurs de format XML
- Essaie :
Écris les parties rédigées de ta réponse dans des balises <paragraphes_fluides>.
c) Fais correspondre le style de ton prompt au style de sortie voulu
Le formatage employé dans ton prompt influence celui de la réponse. Si tu écris ton prompt bourré de puces et de gras, attends-toi à une réponse bourrée de puces et de gras. Si tu veux de la prose, écris ton prompt en prose. Retirer le markdown de ton prompt réduit le markdown en sortie.
C'est contre-intuitif et c'est très efficace.
d) Pour un contrôle fin, un bloc de consignes détaillé
Tu veux vraiment réduire le formatage excessif ? La doc Anthropic fournit un bloc de consignes complet, balisé <avoid_excessive_markdown_and_bullet_points>. Il demande trois choses :
- écrire en paragraphes complets ;
- réserver le markdown au code en ligne, aux blocs de code et aux titres simples (
##,###) ; - n'utiliser une liste que pour des éléments réellement distincts, ou si le lecteur en demande une.
Deux mises en garde importantes, directement issues de la doc :
- Certains modèles récents formatent déjà moins que les précédents. Sur ceux-là, un bloc anti-markdown peut supprimer une structure dont le contenu avait besoin.
- Les comportements de verbosité diffèrent d'un modèle à l'autre, y compris entre modèles d'une même famille. La doc Anthropic tient une page par modèle pour ces écarts. Si ta sortie est trop longue ou trop courte, demande-le explicitement plutôt que de supposer.
Technique 2 — Donner un exemple de sortie
C'est la technique la plus rentable pour les formats visuels. Une ligne d'exemple vaut trois phrases de description.
Tableau markdown
Avant
Compare PostgreSQL et DuckDB pour de l'analytique locale.Après
Compare PostgreSQL 16 et DuckDB pour de l'analytique locale sur des fichiers
Parquet de 5 à 50 Go, sur un poste de développeur.
Format : un tableau markdown, exactement ces colonnes et cet ordre :
| Critère | PostgreSQL 16 | DuckDB | Qui gagne |
|---------|---------------|--------|-----------|
| Lecture Parquet directe | Extension requise, à installer | Natif | DuckDB |
Traite exactement ces critères, dans cet ordre : lecture Parquet directe,
installation, concurrence en écriture, empreinte mémoire, écosystème BI,
courbe d'apprentissage.
La colonne « Qui gagne » contient un seul mot : PostgreSQL, DuckDB, ou égalité.
Aucun texte avant ou après le tableau.Pourquoi c'est mieux : la ligne d'exemple fixe le niveau de détail par cellule. La liste des critères empêche le modèle de choisir les siens. La contrainte « un seul mot » rend la colonne exploitable pour trier.
CSV
Sortie : du CSV brut, séparateur point-virgule, encodage UTF-8.
Première ligne = en-tête. Aucun texte avant ni après.
Les champs contenant un point-virgule sont entourés de guillemets doubles.
Les valeurs manquantes sont vides, jamais "NULL" ni "N/A".
En-tête exact :
table;colonne;type_actuel;type_propose;raisonSQL seul, sans commentaire
Cas fréquent, et souvent mal servi : tu veux la requête, rien d'autre.
Sortie : un unique bloc de code SQL.
Pas de phrase d'introduction, pas d'explication après, aucun commentaire SQL
(ni -- ni /* */). Si tu as un avertissement à donner, mets-le dans des balises
<remarque> placées APRÈS le bloc de code, pas dedans.La balise <remarque> est la clé : sans elle, le modèle a tendance à glisser ses avertissements en commentaires SQL, et tu dois les nettoyer avant d'exécuter.
Technique 3 — Baliser la sortie
Demande une réponse en sections balisées, puis extrais chaque section par script. Ça te donne plusieurs livrables en un appel.
Réponds en trois sections balisées, dans cet ordre :
<diagnostic>Trois phrases maximum expliquant la cause probable.</diagnostic>
<sql>La requête corrigée, sans commentaire.</sql>
<tests>Les assertions pandas qui vérifient le résultat, en Python.</tests>L'extraction côté Python tient en quelques lignes, sans dépendance :
import re
def extraire(texte: str, balise: str) -> str | None:
"""Renvoie le contenu de <balise>...</balise>, ou None si absent."""
motif = rf"<{balise}>(.*?)</{balise}>"
trouve = re.search(motif, texte, re.DOTALL)
return trouve.group(1).strip() if trouve else None
sql = extraire(reponse, "sql")
if sql is None:
raise ValueError("Le modèle n'a pas renvoyé de bloc <sql>")Technique 4 — Les sorties structurées
C'est la seule technique qui garantit la forme, au lieu de la demander.
Le principe
Tu fournis un JSON Schema. Le moteur contraint la génération à le respecter. Tu ne reçois pas « du JSON, si tout va bien » : tu reçois du JSON conforme.
Chez Anthropic
La fonctionnalité s'appelle Structured Outputs et couvre deux capacités complémentaires :
- Sorties JSON, via le paramètre
output_config.format. - Appel d'outil strict, via
strict: true, qui garantit la validation du schéma sur les noms d'outils et leurs entrées.
La structure du paramètre :
{
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"categorie": {"type": "string"},
"urgence": {"type": "string"},
"confiance": {"type": "number"}
},
"required": ["categorie", "urgence"],
"additionalProperties": false
}
}
}
}En Python, le SDK propose une méthode dédiée qui accepte directement un modèle Pydantic :
from pydantic import BaseModel
import anthropic
class Ticket(BaseModel):
categorie: str
urgence: str
confiance: float
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-opus-5-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Classe ce ticket : l'export du matin est vide depuis 3 jours"}
],
output_format=Ticket,
)
print(response.parsed_output)Structure reprise de la documentation Anthropic sur les sorties structurées. Vérifie l'identifiant de modèle à jour au moment où tu écris ton code : ces identifiants changent régulièrement.
Les limites à connaître avant de concevoir ton schéma
La documentation Anthropic les liste explicitement :
- Pas de schémas récursifs.
- Pas de types complexes à l'intérieur d'un
enum; unenumse limite à des chaînes, nombres, booléens ou null. - Pas de
$refexterne ('$ref': 'http://...'). - Les contraintes numériques (
minimum,maximum,multipleOf) ne sont pas directement supportées. - Les contraintes de chaîne (
minLength,maxLength) ne sont pas directement supportées.
Conséquence pratique : ce que le schéma ne peut pas contraindre, exprime-le dans le prompt et revalide-le côté Python. Un montant de type number peut sortir négatif : le schéma ne l'interdit pas.
Deux points de performance également documentés : la première requête avec un schéma donné subit une latence supplémentaire le temps de compiler la grammaire, et les grammaires compilées sont mises en cache 24 heures à compter de leur dernière utilisation. Le cache est invalidé si la structure du schéma ou l'ensemble des outils change, mais pas si tu modifies seulement les champs name ou description.
Chez OpenAI et Google
Le mécanisme est le même, les noms de paramètres diffèrent.
OpenAI : paramètre text, avec format imbriqué contenant type: "json_schema", un name, le schema et strict: true.
response = client.responses.create(
model="[IDENTIFIANT_MODELE]",
input=[
{"role": "system", "content": "Extrais les informations du ticket."},
{"role": "user", "content": "[TEXTE_DU_TICKET]"},
],
text={
"format": {
"type": "json_schema",
"name": "ticket",
"schema": {
"type": "object",
"properties": {
"categorie": {"type": "string"},
"urgence": {"type": "string"},
},
"required": ["categorie", "urgence"],
"additionalProperties": False,
},
"strict": True,
},
},
)Google Gemini : la documentation présente un paramètre response_format contenant le type, le mime_type (application/json) et le schema, qu'on peut alimenter avec MonModele.model_json_schema() depuis Pydantic. Google précise par ailleurs, dans sa page de stratégies de prompt, que pour un schéma JSON complexe il vaut mieux passer par la fonctionnalité de sortie structurée plutôt que de décrire le format dans le prompt.
Les API de sorties structurées évoluent vite chez les trois éditeurs. Reprends les noms exacts de paramètres dans la documentation du jour : ce sont eux qui bougent, pas le principe.
⛔ Le prefill : ce qui a changé
Le prefill consistait à amorcer la réponse du modèle en fournissant un début de message assistant, typiquement { pour forcer du JSON, ou Voici le résumé : pour supprimer le préambule.
Cette technique ne fonctionne plus sur les modèles Claude récents. La documentation Anthropic est explicite sur trois points :
- les réponses pré-remplies sur le dernier tour assistant ne sont plus supportées à partir des modèles Claude 4.6 ;
- une requête qui en contient une renvoie une erreur 400 ;
- les modèles antérieurs les acceptent toujours, et un message assistant placé ailleurs dans la conversation n'est pas concerné.
Tu trouveras encore beaucoup de tutoriels et d'articles qui recommandent le prefill. Ils datent d'avant ce changement.
Par quoi le remplacer
| Ancien usage du prefill | Remplacement documenté |
|---|---|
| Forcer du JSON ou du YAML | Sorties structurées (output_config.format) |
| Classification contrainte | Un outil avec un champ enum listant tes labels, ou les sorties structurées |
Supprimer le préambule (Voici...) | Consigne système : Réponds directement sans préambule. Ne commence pas par « Voici... » ou « D'après... ». Ou demander la réponse dans des balises XML. Et nettoyer en post-traitement si un préambule passe |
| Reprendre une réponse coupée | Mettre la suite dans le message utilisateur en incluant le texte final de la réponse interrompue, ou simplement relancer la requête |
Le cas « supprimer le préambule » mérite une consigne prête à copier :
Réponds directement, sans préambule. Ne commence pas par des formules comme
« Voici... », « D'après... », « Bien sûr ». Commence par le contenu demandé.🐍 Parser proprement une réponse JSON en Python
Même avec des sorties structurées, ton code doit gérer l'échec. Voici un parseur défensif réutilisable.
import json
import re
from typing import Any
class ReponseInvalide(Exception):
"""Le modèle n'a pas renvoyé un JSON exploitable."""
def parser_json(reponse: str) -> Any:
"""Extrait et parse le JSON d'une réponse de modèle.
Gère les trois cas rencontrés en pratique :
1. du JSON nu,
2. du JSON entouré d'un bloc de code markdown,
3. du JSON précédé ou suivi d'une phrase.
"""
texte = reponse.strip()
# Cas 2 : retirer une cloture de bloc de code markdown.
# FENCE vaut les trois accents graves ; on le construit pour ne pas
# casser la mise en forme de ce cours.
fence = chr(96) * 3
motif_bloc = fence + r"(?:json)?\s*(.*?)" + fence
bloc = re.search(motif_bloc, texte, re.DOTALL)
if bloc:
texte = bloc.group(1).strip()
try:
return json.loads(texte)
except json.JSONDecodeError:
pass
# Cas 3 : isoler le premier objet ou tableau JSON du texte
candidat = re.search(r"[\{\[].*[\}\]]", texte, re.DOTALL)
if candidat:
try:
return json.loads(candidat.group(0))
except json.JSONDecodeError as err:
raise ReponseInvalide(f"JSON illisible : {err}") from err
raise ReponseInvalide("Aucun JSON trouve dans la reponse")
def valider_ticket(donnees: dict) -> dict:
"""Revalide ce que le schema ne peut pas contraindre."""
categories_valides = {
"incident_pipeline", "qualite_donnee",
"demande_acces", "nouvelle_demande", "autre",
}
if donnees.get("categorie") not in categories_valides:
raise ReponseInvalide(f"Categorie inconnue : {donnees.get('categorie')}")
confiance = donnees.get("confiance", 0)
if not 0 <= confiance <= 1:
raise ReponseInvalide(f"Confiance hors bornes : {confiance}")
return donneesLa fonction valider_ticket illustre le point vu plus haut : les bornes numériques ne sont pas contraignables par le schéma, donc tu les vérifies toi-même. En pipeline, entoure l'appel d'un mécanisme de reprise : la documentation Anthropic note que les modèles récents suivent des schémas complexes de façon fiable surtout quand c'est implémenté avec des tentatives de reprise.
A retenir
- Quatre niveaux : décrire, montrer un exemple, baliser, contraindre par schéma. Monte selon l'enjeu.
- Dis ce qu'il faut faire, jamais ce qu'il ne faut pas faire.
- Le style de ton prompt déteint sur la sortie : prompt en prose, réponse en prose.
- Un exemple de sortie fixe le niveau de détail mieux que n'importe quelle description.
- Les sorties structurées sont la seule méthode qui garantit la forme (
output_config.formatchez Anthropic,text.formatchez OpenAI,response_formatchez Google). - Ce que le schéma ne contraint pas (bornes numériques, longueurs de chaînes), revalide-le en Python.
- Le prefill n'est plus supporté sur les modèles Claude 4.6 et suivants : erreur 400. Utilise les sorties structurées ou une consigne anti-préambule.
- Écris toujours un parseur défensif avec reprise sur erreur.
Sources
- Prompting best practices — Anthropic
- Structured outputs — Anthropic
- Structured model outputs — OpenAI
- Structured output — Google Gemini API
- Prompt design strategies — Google Gemini API
- Prompt engineering techniques — Microsoft Foundry / Azure OpenAI
- openai/openai-cookbook — code d'exemple sur les sorties structurées :
examples/Structured_Outputs_Intro.ipynb - anthropics/claude-cookbooks — code d'exemple sur le JSON garanti côté Claude :
misc/how_to_enable_json_mode.ipynb