Exercice : données structurées
Sur cette page
Objectif
Écrire une fonction generate_json(description, schema=None) qui renvoie un objet Python exploitable à partir d’une demande en langage naturel. Quand vous connaissez la structure attendue, la fonction utilise les sorties structurées ; quand vous ne la connaissez pas, elle s’appuie sur une consigne stricte, une validation et une nouvelle tentative en cas d’échec. Ce sont les deux approches que recommande aujourd’hui la documentation d’Anthropic, le pré-remplissage n’étant plus accepté par les modèles actuels.
Cahier des charges
- La fonction reçoit une description en texte libre et, en option, un schéma JSON
- Elle retourne un objet Python (
dictoulist), jamais une chaîne - Avec un schéma, elle passe par
output_config.format; sans schéma, elle demande « uniquement du JSON » et valide le résultat - Sans schéma, elle retente une fois si le JSON est invalide, en signalant l’erreur au modèle
Cas de test
"un utilisateur avec nom, email et âge", avec un schéma"une liste de 3 produits avec nom et prix", sans schéma"une config nginx avec server_name et location", sans schéma
À vous de jouer
Commencez par la branche avec schéma, la plus simple, puis écrivez la branche sans schéma. Pour celle-ci, réfléchissez à ce que vous renvoyez au modèle lors de la seconde tentative : le message d’erreur du parseur est une information précieuse.
import anthropic
import json
client = anthropic.Anthropic()
def generate_json(description, schema=None):
"""Renvoie un objet Python (dict ou list) décrit par `description`."""
# À vous de jouer !
pass
Solution
import anthropic
import json
client = anthropic.Anthropic()
MODELE = "claude-sonnet-5-5"
CONSIGNE = (
"Réponds uniquement par un document JSON valide, sans texte avant ni après, "
"sans bloc Markdown."
)
def generate_json(description, schema=None, tentatives=2):
demande = f"Génère un JSON correspondant à : {description}"
# Branche 1 : la structure est connue, on la fait respecter par l'API
if schema is not None:
response = client.messages.create(
model=MODELE,
max_tokens=4096,
messages=[{"role": "user", "content": demande}],
output_config={"format": {"type": "json_schema", "schema": schema}},
)
return json.loads(next(b.text for b in response.content if b.type == "text"))
# Branche 2 : structure libre, consigne stricte + validation + nouvel essai
messages = [{"role": "user", "content": demande}]
for _ in range(tentatives):
response = client.messages.create(
model=MODELE, max_tokens=4096, system=CONSIGNE, messages=messages,
)
texte = next(b.text for b in response.content if b.type == "text").strip()
try:
return json.loads(texte)
except json.JSONDecodeError as erreur:
messages += [
{"role": "assistant", "content": texte},
{"role": "user",
"content": f"Ce JSON est invalide ({erreur}). Renvoie-le corrigé, seul."},
]
return None
Le message assistant ajouté lors de la seconde tentative n’est pas un pré-remplissage : il s’agit de la réponse complète précédente, suivie d’un message utilisateur. Placer des réponses de l’assistant dans l’historique reste parfaitement autorisé ; seul un dernier message assistant partiel est refusé.
Script de test
SCHEMA_UTILISATEUR = {
"type": "object",
"properties": {
"nom": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["nom", "email", "age"],
"additionalProperties": False,
}
cas = [
("un utilisateur avec nom, email et âge", SCHEMA_UTILISATEUR),
("une liste de 3 produits avec nom et prix", None),
("une config nginx avec server_name et location", None),
]
for description, schema in cas:
resultat = generate_json(description, schema)
assert isinstance(resultat, (dict, list)), f"Échec pour : {description}"
print(json.dumps(resultat, indent=2, ensure_ascii=False))
Ce que vous devriez observer
Le premier cas renvoie toujours un objet avec exactement les trois champs du schéma, et un âge entier. Les deux autres renvoient du JSON valide dont la forme peut varier d’un appel à l’autre — par exemple prix en nombre décimal ou en chaîne, location en objet ou en liste. C’est précisément la différence entre une structure garantie et une structure seulement demandée : dès que votre code dépend d’une forme précise, écrivez le schéma.
Pour aller plus loin
- Remplacez le schéma écrit à la main par un modèle Pydantic et
client.messages.parse() - Mesurez, sur une vingtaine d’appels, combien de fois la branche sans schéma a besoin de sa seconde tentative
- Pour une classification, testez un schéma dont le champ
categorieest une énumération de vos étiquettes
Sources officielles : Structured data (Claude Academy) · Structured outputs (documentation Claude)
Points clés à retenir
- Quand la structure est connue, le schéma passe par
output_config.format: la forme est garantie. - Sans schéma, une consigne stricte, une validation par
json.loadset une seconde tentative qui renvoie l’erreur au modèle. - Dans les deux cas, on lit le bloc de type
text, pascontent[0]: un bloc de réflexion peut le précéder. - Une structure seulement demandée varie d’un appel à l’autre ; dès que du code en dépend, on la garantit par un schéma.