Schémas JSON et validation
Schémas JSON et validation
Le schéma d’une fonction n’est pas de la plomberie de validation : c’est le seul contrat entre le modèle et votre code. Il n’y a pas d’autre canal. Le modèle ne lit pas votre implémentation, ne connaît pas vos conventions internes, et ne devinera pas qu’un champ date attend le format ISO plutôt que le format français.
Tout ce qui n’est pas écrit dans le schéma sera donc décidé au hasard des inférences du modèle. C’est ce qui explique le déséquilibre d’effort recommandé dans cette leçon : mieux vaut passer dix minutes sur un schéma que deux heures à déboguer des arguments erratiques.
Structure d’un schéma de fonction
Chaque fonction déclarée dans tools suit le format JSON Schema. Voici la structure complète :
tool_definition = {
"type": "function",
"name": "creer_facture",
"description": "Créer une facture pour un client existant",
"parameters": {
"type": "object",
"properties": {
"client_id": {
"type": "string",
"description": "Identifiant unique du client (format: CLI-XXXXX)"
},
"lignes": {
"type": "array",
"description": "Liste des lignes de facturation",
"items": {
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "Description du produit ou service"
},
"quantite": {
"type": "integer",
"minimum": 1,
"description": "Nombre d'unites"
},
"prix_unitaire": {
"type": "number",
"minimum": 0,
"description": "Prix HT par unité en euros"
}
},
"required": ["description", "quantite", "prix_unitaire"]
}
},
"devise": {
"type": "string",
"enum": ["EUR", "USD", "GBP"],
"default": "EUR"
},
"echeance_jours": {
"type": "integer",
"enum": [30, 45, 60, 90],
"description": "Délai de paiement en jours"
}
},
"required": ["client_id", "lignes"],
"additionalProperties": False
},
"strict": True
}
Le mode strict
Le mode strict transforme une probabilité en garantie. Sans lui, le modèle produit généralement des arguments conformes — et vous découvrez les exceptions en production, sur les cas tordus. Avec lui, la conformité est assurée par construction au niveau du décodage. Activez-le par défaut : les contraintes qu’il impose, détaillées juste après, sont un prix modique face à une classe entière de bugs qui disparaît.
tools = [{
"type": "function",
"name": "enregistrer_commande",
"description": "Enregistrer une nouvelle commande",
"parameters": {
"type": "object",
"properties": {
"produit": {"type": "string"},
"quantite": {"type": "integer"},
"urgent": {"type": "boolean"}
},
"required": ["produit", "quantite", "urgent"],
"additionalProperties": False
},
"strict": True
}]
Contraintes du mode strict
En mode strict, certaines règles s’appliquent :
- Tous les champs doivent être dans
required additionalPropertiesdoit êtrefalse- Les types optionnels utilisent
anyOfavecnull - Pas de
pattern,minLength,maxLengthau premier niveau
# Champ optionnel en mode strict
"commentaire": {
"anyOf": [
{"type": "string"},
{"type": "null"}
],
"description": "Commentaire libre optionnel"
}
Validation côté client
Le mode strict garantit la forme, jamais le sens. Une date bien formée peut être dans le passé, un identifiant bien typé peut ne correspondre à aucun client, un montant valide peut dépasser le plafond autorisé. Ces règles-là sont métier : aucun schéma JSON ne les exprime, et elles doivent être vérifiées dans votre code — avec, en cas de rejet, un message d’erreur renvoyé au modèle qui explique ce qui n’allait pas.
import json
from jsonschema import validate, ValidationError
SCHEMA_FACTURE = {
"type": "object",
"properties": {
"client_id": {"type": "string", "pattern": "^CLI-[0-9]{5}$"},
"lignes": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"description": {"type": "string", "minLength": 1},
"quantite": {"type": "integer", "minimum": 1},
"prix_unitaire": {"type": "number", "minimum": 0}
},
"required": ["description", "quantite", "prix_unitaire"]
}
}
},
"required": ["client_id", "lignes"]
}
def executer_fonction(name: str, arguments: str) -> str:
args = json.loads(arguments)
try:
validate(instance=args, schema=SCHEMA_FACTURE)
except ValidationError as e:
return json.dumps({"error": f"Arguments invalides: {e.message}"})
# Logique metier apres validation
return creer_facture(**args)
Descriptions efficaces
Rédigez la description d’une fonction comme une consigne à un nouveau collègue : dans quelles situations l’appeler, et surtout dans lesquelles ne pas l’appeler. C’est cette seconde partie qu’on oublie systématiquement, alors qu’elle règle la plupart des appels erronés — « ne pas utiliser pour les commandes archivées » évite un aller-retour inutile que trois phrases de description positive n’auraient pas empêché.
# Mauvais : trop vague
{
"name": "get_data",
"description": "Recuperer des données"
}
# Bon : spécifique et contextuel
{
"name": "rechercher_produits_catalogue",
"description": "Rechercher des produits dans le catalogue e-commerce. "
"Retourne nom, prix, stock et catégorie. "
"Utiliser pour les questions sur la disponibilité ou les prix."
}
Descriptions des paramètres
Au niveau du paramètre, l’ennemi est l’implicite. Une unité (euros ou centimes ?), un format (ISO ou local ?), une convention (identifiant interne ou référence client ?) qui ne sont pas écrits seront devinés — parfois juste, parfois non, et sans que rien ne le signale. Le enum est ici votre meilleur outil : il ne décrit pas la valeur attendue, il la contraint.
"properties": {
"date_debut": {
"type": "string",
"description": "Date de début au format ISO 8601 (YYYY-MM-DD). "
"Par defaut, date du jour si non spécifié."
},
"statut": {
"type": "string",
"enum": ["actif", "inactif", "suspendu"],
"description": "Filtrer par statut du compte. "
"'actif' = comptes en service, "
"'suspendu' = comptes temporairement geles."
}
}
Schémas imbriqués et références
Un schéma profondément imbriqué est difficile à relire pour vous et à remplir correctement pour le modèle. Si vous atteignez trois niveaux, la question à se poser n’est pas comment mieux l’écrire mais si la fonction n’en fait pas trop : deux fonctions au périmètre net sont presque toujours préférables à une fonction paramétrable qui couvre les deux cas.
adresse_schema = {
"type": "object",
"properties": {
"rue": {"type": "string"},
"code_postal": {"type": "string"},
"ville": {"type": "string"},
"pays": {"type": "string", "enum": ["FR", "BE", "CH", "LU"]}
},
"required": ["rue", "code_postal", "ville", "pays"]
}
tools = [{
"type": "function",
"name": "creer_livraison",
"description": "Créer une livraison avec adresses d'expédition et destination",
"parameters": {
"type": "object",
"properties": {
"expedition": adresse_schema,
"destination": adresse_schema,
"poids_kg": {"type": "number", "minimum": 0.1}
},
"required": ["expedition", "destination", "poids_kg"],
"additionalProperties": False
},
"strict": True
}]
Points clés à retenir
- Activez
strict: truepour garantir la conformité des arguments - Validez quand même côté client avec
jsonschemapour les règles métier - Rédigez des descriptions précises : c’est le premier levier de qualité
- Utilisez
enumpour contraindre les valeurs acceptées - Décomposez les schémas complexes en sous-structures réutilisables