Tool calling et sorties structurées : le contrat qui tient
24 appels, zéro JSON cassé, deux dates utilisables. Puis le même endpoint mieux décrit, et ce qu’un schéma ne peut pas corriger.
Dans cet article
Donnez à un modèle un outil de recherche de vols et demandez-lui de trouver un vol de Madrid à Berlin. Voici ce qui revient :
<tool_call>
{"name": "search_flights",
"arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>Le JSON est valide. Le nom de l’outil est correct. Tous les champs requis sont présents. Et l’appel est inutile : aucune API de vols n’accepte "Madrid" là où elle attend un code d’aéroport, ni "3rd October 2026" là où elle attend une date.
Cet écart — syntaxiquement parfait, sémantiquement inutilisable — est le sujet de ce chapitre, et la première chose à établir est qu’il ne s’agit pas d’un problème de JSON. Sur vingt-quatre requêtes avec cet outil, le modèle a produit 24 appels d’outil valides et zéro JSON cassé. Il n’a pas échoué une seule fois sur la partie que tout le monde débogue.
Le modèle n’exécute rien
Lien vers la section : Le modèle n’exécute rienAvant la mécanique, la phrase qui évite le plus de confusion : un appel d’outil est une demande, pas une action.
Le modèle émet un message structuré qui dit je voudrais que search_flights soit appelé avec ces arguments. Puis il s’arrête. Votre code reçoit ce message, décide s’il l’honore, appelle ce qu’il doit appeler, puis renvoie le résultat sous forme d’un autre message. Le modèle n’a jamais touché votre base de données, n’a jamais fait de requête HTTP, n’a jamais eu d’identifiants.
Tout ce qui concerne la sécurité des agent dans le chapitre 30 découle de cette séparation, tout comme tout ce qui concerne la conception des agent dans le chapitre 23 : le modèle propose et votre code dispose, et c’est dans le code que vivent toutes les garanties.
Donc un outil, dépouillé du vocabulaire, est deux choses :
Un schéma. Un JSON Schema décrivant une fonction : son nom, ce qu’elle fait, et les arguments qu’elle accepte avec leurs types et contraintes. C’est ce qui entre dans le prompt, et c’est la seule chose que le modèle voit jamais.
Un endpoint. Une fonction dans votre code qui prend ces arguments et renvoie quelque chose. Le modèle ne la voit jamais, ne sait jamais dans quel langage elle est écrite, et ne peut pas distinguer une requête de base de données d’une chaîne codée en dur.
Vous envoyez les schémas avec la requête
Lien vers la section : Vous envoyez les schémas avec la requêteLes définitions d’outils vont dans le prompt, sérialisées dans le format sur lequel le modèle a été entraîné. Elles coûtent des tokens à chaque appel — un fait qui revient avec un chiffre plus loin dans ce chapitre.
Le modèle répond par un appel au lieu de texte
Lien vers la section : Le modèle répond par un appel au lieu de texteAu lieu de prose, la réponse contient une demande structurée, et l’API signale une raison de fin qui l’indique. Cette raison compte : c’est ainsi que votre code sait qu’il doit exécuter un outil plutôt que montrer une réponse à l’utilisateur.
Votre code l’exécute — ou refuse
Lien vers la section : Votre code l’exécute — ou refuseC’est l’étape où il n’y a pas de modèle. Validez les arguments par rapport au schéma, décidez si cet appelant a le droit de faire cela, puis exécutez.
Vous renvoyez le résultat comme message
Lien vers la section : Vous renvoyez le résultat comme messageLe résultat devient un autre tour dans la conversation, dans un rôle qui lui est réservé. Le modèle le lit comme n’importe quel autre context.
Le modèle répond, ou demande un autre outil
Lien vers la section : Le modèle répond, ou demande un autre outilC’est la boucle du chapitre 23, et la raison pour laquelle une seule requête peut devenir une douzaine d’allers-retours.
Rien de tout cela n’est émergent. Comme l’a établi le chapitre 11, le tool calling est un comportement entraîné :1 pendant le post-entraînement, le modèle a vu des milliers de conversations structurées exactement ainsi. C’est pourquoi le format dépend du modèle, pourquoi la fiabilité varie autant entre des modèles de taille similaire, et pourquoi un modèle peut appeler un outil qu’il n’a jamais vu — la forme a été apprise, l’outil précis vient de votre prompt.
Ce que coûte un mauvais schéma, mesuré
Lien vers la section : Ce que coûte un mauvais schéma, mesuréVoici l’outil tel que la plupart des gens l’écrivent au début. Notez que rien n’est faux ; il est simplement trop mince :
{
name: "search_flights",
description: "Search for flights.",
parameters: {
type: "object",
properties: {
from: { type: "string", description: "Airport." },
to: { type: "string", description: "Airport." },
date: { type: "string", description: "The date." },
},
required: ["from", "to", "date"],
},
}Vingt-quatre requêtes, six paires de villes croisées avec quatre façons d’exprimer une date (« le 3 du mois prochain », « vendredi prochain », « 15 décembre », « demain »), décodage greedy pour que les résultats soient reproductibles :
| outil appelé | JSON cassé | date en ISO | aéroports en IATA | tout correct | |
|---|---|---|---|---|---|
| le schéma ci-dessus | 24/24 | 0 | 2/24 | 4/24 | 1/24 |
Lisez les deux premières colonnes avant les trois dernières. Le modèle appelle le bon outil à chaque fois et produit un JSON bien formé à chaque fois. L’échec est entièrement dans les valeurs, et les valeurs sont inutilisables : "Madrid" au lieu de MAD, "3rd October 2026" au lieu de 2026-10-03.
Il vaut la peine d’insister, parce que cela détermine où regarder quand quelque chose casse. Le réflexe est d’ajouter un parseur JSON avec une nouvelle tentative, ou de demander plus fermement au modèle un JSON valide. Ni l’un ni l’autre ne traite quoi que ce soit de ce qui s’est passé ici.
Maintenant, changez seulement la description
Lien vers la section : Maintenant, changez seulement la descriptionMême endpoint. Même code derrière. Même modèle, mêmes prompts, même décodage. La seule chose qui change est le texte dans le schéma :
{
name: "search_flights",
description: "Search scheduled flights between two airports on a given day.",
parameters: {
type: "object",
properties: {
from: {
type: "string",
description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",
pattern: "^[A-Z]{3}$",
},
to: { /* same */ },
date: {
type: "string",
description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",
format: "date",
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
},
},
required: ["from", "to", "date"],
},
}| FORMAT de date | VALEUR de date | FORMAT d’aéroport | VALEUR d’aéroport | |
|---|---|---|---|---|
| schéma mince | 2/24 | 1/24 | 4/24 | 4/24 |
| schéma décrit | 24/24 | 12/24 | 16/24 | 8/24 |
Le format de date passe de 2 sur 24 à 24 sur 24. Parfait, grâce à un changement de texte, sans toucher au code et sans logique de nouvelle tentative. Si vous devez retenir une habitude opérationnelle de ce chapitre, c’est celle-ci : quand un outil est appelé de travers, la correction est presque toujours dans la description, et c’est la correction la moins chère du système.
Lisez maintenant la deuxième colonne, qui est la moitié la plus importante.
Un schéma contraint la forme. Il ne peut pas fournir la connaissance.
Lien vers la section : Un schéma contraint la forme. Il ne peut pas fournir la connaissance.La date est au format ISO 24 fois sur 24. C’est le bon jour 12 fois sur 24.
La moitié des appels contient donc maintenant une date parfaitement formatée qui est la mauvaise date. La description a dit au modèle quelle forme produire, et le modèle l’a produite sans faute — mais transformer « vendredi prochain » en 2026-09-11 exige de connaître la date du jour et de faire de l’arithmétique de calendrier, et aucune description ne fournit cela. Même histoire pour les aéroports : le format est passé de 4 à 16, mais la valeur seulement de 4 à 8, parce qu’écrire MAD exige de savoir que l’aéroport de Madrid est MAD.
Cette distinction est l’idée porteuse du chapitre :
Un schéma est un contrat sur la forme. Il peut rendre la sortie du modèle analysable, typée et cohérente. Il ne peut pas la rendre vraie, et tout mode d’échec qui survit à un bon schéma est un échec de connaissance, pas un échec de format.
Les deux demandent des corrections différentes, et les confondre fait perdre des semaines. Les échecs de format se corrigent dans la description ou avec du décodage contraint, ci-dessous. Les échecs de connaissance se corrigent en mettant la connaissance dans le prompt — la date actuelle dans le message système, une recherche d’aéroport comme second outil que le modèle appelle d’abord, un enum dans le schéma quand l’ensemble est assez petit pour être énuméré. Notez ce que les trois ont en commun : ils déplacent le problème hors de la mémoire du modèle et dans son entrée, ce qui est tout le sujet du chapitre 24.
Sorties structurées, et ce qu’est vraiment le « décodage contraint »
Lien vers la section : Sorties structurées, et ce qu’est vraiment le « décodage contraint »Tout ce qui précède repose encore sur le fait que le modèle choisit de produire la bonne forme. Il existe une garantie plus forte, et c’est le meilleur retour du chapitre 17.
Rappelez-vous comment fonctionne la génération : à chaque étape, le modèle produit un logit pour chaque token du vocabulaire, et l’échantillonneur en choisit un. Le décodage contraint insère une étape entre les deux. À partir d’une grammaire — dérivée de votre JSON Schema — il calcule quels tokens pourraient légalement venir ensuite, fixe les logits de tous les autres à l’infini négatif, puis laisse l’échantillonneur choisir parmi ce qui reste.
Si le schéma dit que la prochaine chose doit être un {, alors chaque token qui n’est pas { a une probabilité zéro. Pas « improbable » : zéro. Le modèle ne peut pas émettre de JSON invalide parce que les tokens invalides ont été retirés de la distribution avant l’échantillonnage.
C’est ce que sont, sous le capot, les « sorties structurées », le « JSON mode » et la « génération guidée », et cela explique leurs deux propriétés. La garantie est totale pour tout ce que la grammaire peut exprimer — types, champs requis, enums, imbrication — parce qu’elle est appliquée mécaniquement plutôt que demandée poliment. Et elle ne dit rien sur le contenu : une grammaire peut forcer "date" à être une chaîne correspondant à un motif de date, et ne peut pas le forcer à être le bon jour. C’est le même mur que dans la section précédente, atteint par l’autre côté.
Deux notes pratiques. Ce n’est pas gratuit : le masque doit être calculé à chaque étape, et les grammaires complexes coûtent une latence mesurable. Et cela change ce que fait le modèle — un modèle détourné de son token préféré peut produire un contenu moins bon tout en produisant une structure parfaite, ce qui explique pourquoi « demander gentiment et valider » reste un défaut raisonnable pour des formes simples, tandis que le décodage contraint mérite son coût quand la forme est complexe ou que le consommateur est strict.
Effets de bord, et la seule propriété qui compte
Lien vers la section : Effets de bord, et la seule propriété qui compteLe chapitre 14 a mesuré un timeout suivi d’une nouvelle tentative facturant deux générations pour une seule réponse. Avec les outils, le même échec empire, parce qu’un outil peut faire quelque chose.
Si votre code appelle charge_card, expire, puis réessaie, vous avez deux débits. Le modèle n’a aucune idée que cela s’est produit ; il voit un seul résultat d’outil. La correction est la même que dans n’importe quel système distribué, et ce n’est pas le problème du modèle : rendre l’opération idempotente en donnant une clé à l’appel, afin que la deuxième exécution reconnaisse la première et renvoie son résultat au lieu de refaire le travail.
La règle de conception qui en découle mérite d’être énoncée clairement. Séparez les lectures des écritures dans votre catalogue d’outils. Une lecture peut être relancée librement, exécutée en parallèle et mise en cache. Une écriture ne le peut pas, et doit porter une clé, une vérification d’autorisation et — pour tout ce dont un utilisateur voudrait être informé avant que cela n’arrive — une étape d’approbation qui place un humain entre la demande et l’action. Cette étape d’approbation n’est pas une politesse : c’est l’une des rares choses qui se dressent entre une injection de prompt et une conséquence réelle — et, le chapitre 30 le mesure, la plus faible d’entre elles.
Combien d’outils avant que cela se dégrade ?
Lien vers la section : Combien d’outils avant que cela se dégrade ?Le folklore dit que charger beaucoup d’outils fait mal choisir le modèle. Cela vaut la peine de mesurer plutôt que de répéter, donc : les mêmes vingt-quatre requêtes, avec l’outil de vols plus un ensemble croissant d’autres outils — dont trois volontairement confusables (horaires de train, traversées en ferry, lignes de bus).
| outils chargés | prompt tokens | a choisi search_flights | date en ISO |
|---|---|---|---|
| 1 | 353 | 24/24 | 24/24 |
| 5 | 730 | 24/24 | 24/24 |
| 10 | 1,193 | 21/24 | 21/24 |
| 20 | 2,119 | 24/24 | 24/24 |
La sélection ne s’est pas dégradée. Avec vingt outils, dont trois plausiblement confusables, un modèle d’un demi-milliard de paramètres a choisi le bon vingt-quatre fois sur vingt-quatre. Le creux à dix correspond à trois appels qui ont nommé un autre outil, et il ne survit pas au passage à vingt.
C’est un résultat négatif et il faut le présenter comme tel : sur cette tâche, avec ces outils, « trop d’outils » n’était pas le problème. Ce qui a bien augmenté, monotoniquement et d’un facteur six, c’est le prompt : de 353 tokens à 2,119, payés à chaque requête de la conversation, pour toujours, qu’un outil soit utilisé ou non.
La version honnête du folklore porte donc sur le coût et le context, pas sur l’exactitude. Vingt outils sont une taxe permanente sur chaque message, et le chapitre 16 a déjà montré ce qu’un préfixe permanent fait à une facture sur quarante tours. Quand des gens rapportent que beaucoup d’outils nuisent à la qualité, le mécanisme est généralement que les définitions ont évincé le context qui comptait — un problème de chapitre 24 déguisé en chapitre 18. Les outils réellement quasi doublons sont aussi un vrai problème, et la correction n’est pas de réduire le nombre d’outils mais d’avoir de meilleures descriptions et namespaces : préfixez-les par système (crm.search_customer, billing.search_customer) pour que deux catalogues fusionnés de deux équipes n’entrent pas en collision, et pour que le modèle ait quelque chose sur quoi discriminer.
Trois types d’outils, et celui qui ouvre la partie suivante
Lien vers la section : Trois types d’outils, et celui qui ouvre la partie suivanteIl est utile de classer les outils selon ce qu’ils font au monde, parce que l’ingénierie diffère pour chacun.
Les outils de données lisent : chercher, récupérer, interroger. Ils peuvent être relancés, parallélisés, mis en cache. Ils échouent en ne renvoyant rien d’utile, et leur principal risque est de faire entrer du texte non fiable dans le context — ce qui constitue toute la surface d’attaque du chapitre 30.
Les outils d’action écrivent : envoyer, créer, facturer, supprimer. Ils ne sont pas relançables sans clé, ne sont pas parallélisables en sécurité, et sont la raison d’être des flux d’approbation.
Les outils d’orchestration appellent d’autres modèles. Un outil dont l’implémentation est un autre agent, avec son propre prompt, ses propres outils et sa propre boucle — et pour le modèle appelant, il ressemble exactement aux deux autres, parce qu’un schéma et un endpoint sont tout ce qu’il voit jamais.
Ce troisième type n’est pas une curiosité. C’est le mécanisme derrière la moitié agent-as-a-tool du chapitre 25 — l’autre topologie, le handoff, abandonne la conversation et ne la récupère jamais — et il fonctionne précisément parce que l’interface de ce chapitre est assez étroite pour qu’un agent entier tienne derrière.
Où cela mène ensuite
Lien vers la section : Où cela mène ensuiteVous avez maintenant un modèle qui peut demander des choses, et un contrat qui rend la demande analysable. Ce que vous n’avez pas, c’est quoi que ce soit à lui faire demander à propos de au-delà de ce qui tient dans son prompt.
L’outil le plus courant en production, et de loin, est une recherche sur un corpus de texte que le modèle n’a jamais vu pendant l’entraînement : votre documentation, vos tickets, vos contrats. Cela ressemble à un problème résolu — faites-en un embedding, trouvez les plus proches voisins, collez-les — et les parties qui ne sont pas résolues sont celles qui décident si la réponse est digne de confiance : comment le texte est découpé avant d’être embedded, quel seuil de similarité est assez bas pour signifier je ne sais pas, et comment une citation est attachée à une affirmation pour qu’un lecteur puisse la vérifier.
Le chapitre 19 porte sur la retrieval, et c’est le chapitre où une mauvaise réponse cesse d’être une curiosité pour devenir une responsabilité.
Sources et méthode
Lien vers la section : Sources et méthodeLes mesures de ce chapitre viennent de Qwen/Qwen2.5-0.5B-Instruct avec décodage greedy, sur 24 requêtes générées croisant six paires de villes et quatre formulations de date, en utilisant le template de chat propre au modèle pour les définitions d’outils. Elles se reproduisent exactement, et il s’agit d’un petit modèle : lisez la séparation format/valeur comme une démonstration du mécanisme plutôt que comme un benchmark de ce que font les modèles actuels. Un modèle frontier résout « vendredi prochain » correctement bien plus souvent — et ne peut toujours pas y être contraint par un schéma, ce qui est la partie qui se généralise.
Le vocabulaire JSON Schema utilisé ci-dessus (type, properties, required, pattern, format, enum) est spécifié dans le draft JSON Schema nommé par la documentation de votre fournisseur ; le sous-ensemble utile est petit et le même d’un fournisseur à l’autre, et les différences qui existent — quels mots-clés sont imposés par le décodage contraint plutôt que simplement transmis au modèle — méritent d’être lues dans le guide de sorties structurées du fournisseur plutôt que supposées.
Pour le décodage contraint comme technique, les bibliothèques de style guidance et le projet outlines documentent la construction grammaire-vers-masque-de-logits d’une façon qui correspond directement à l’échantillonneur du chapitre 17. Et pour l’aller-retour lui-même, la spécification la plus claire n’est pas un tutoriel mais un protocole : le chapitre 26 le lit ligne par ligne.
Références
Lien vers la section : Références-
Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). L’article qui a standardisé la recette de post-entraînement ; la forme d’un appel d’outil y est apprise à partir de démonstrations, exactement comme la forme d’une réponse. ↩