Le mois dernier mon agent a appelé avec confiance un outil nommé search_invoices. Cet outil n'existait pas. Il n'a jamais existé. Le modèle a inventé le nom, inventé les arguments, et émis un bloc JSON parfaitement formé pour lui. Mon harness, confiant et naïf, a essayé de le dispatcher et a planté.
Si vous construisez des agents, vous avez vu ça. Le modèle appelle un outil qui n'est pas dans votre schema, ou il appelle un vrai outil avec des arguments qui ne correspondent pas au schema d'entrée, ou il narre un appel d'outil en texte brut au lieu d'émettre le vrai bloc tool_use. Les gens appellent tout ça "halluciner des appels d'outils". Je veux expliquer pourquoi ça se produit au niveau mécanisme, parce qu'une fois que vous le voyez, les corrections cessent de sembler de la superstition.
Le modèle prédit des tokens, il n'exécute pas votre code
Voici ce que personne ne vous dit quand vous démarrez. Un appel d'outil n'est pas une invocation de fonction du côté du modèle. C'est du texte. Du texte structuré, certes, mais du texte. Le modèle émet un bloc tool_use de la même façon qu'il émet un paragraphe : en prédisant les tokens suivants les plus probables étant donné tout ce qui précède. Votre schema est partie de ce contexte, mais ce n'est pas une contrainte dure à moins que vous en fassiez une.
Alors quand est-ce que ça déraille ? Quand la distribution des tokens suivants plausibles pointe quelque part que votre schema ne couvre pas. Si votre historique de conversation est plein de références aux factures, et que le modèle décide qu'il a besoin de données de factures, search_invoices est une séquence de tokens follement plausible — même si l'outil que vous lui avez donné s'appelle en réalité query_billing. Le modèle fait du pattern-matching contre tout ce qu'il a vu, et "search_" + nom est l'une des formes les plus courantes de noms d'outils dans ses données d'entraînement.
C'est pourquoi les appels d'outils hallucinés augmentent dans deux situations : quand vos noms d'outils sont inhabituels, et quand votre contexte est long et bruyant.
Correction un : faites du bon appel le plus probable
Nommez vos outils comme le modèle s'attend à ce que les outils soient nommés. get_weather, send_email, search_database. L'ennuyeux c'est bien. L'ennuyeux c'est prévisible, et prévisible signifie que la supposition du prochain token du modèle tombe sur votre vrai outil plutôt que sur un cousin inventé.
Puis écrivez des descriptions qui disent quand appeler, pas seulement ce que l'outil fait. Ça compte plus sur les modèles Claude récents — Opus 4.7 et 4.8 sont plus conservateurs pour atteindre les outils que 4.6, donc une description comme "Appellez ça quand l'utilisateur demande les prix actuels ou les événements récents" augmente mesurément le taux d'appel quand il le devrait. La condition déclenchante fait partie de ce sur quoi le modèle se conditionne. Mettez-la dans la description, pas enfouie dans le system prompt.
Correction deux : ne laissez pas passer les mauvais appels
Quand le modèle émet un appel malformé, votre harness devrait le rejeter bruyamment, pas se planter. Retournez un tool_result avec is_error: true et un message que le modèle peut lire :
{
"type": "tool_result",
"tool_use_id": "toolu_abc",
"content": "Pas d'outil nommé 'search_invoices'. Disponibles : query_billing, get_customer.",
"is_error": true
}
Le modèle lit cette erreur au tour suivant et corrige. J'ai vu Opus 4.8 se remettre d'un nom d'outil halluciné en un seul tour de cette façon — il voit l'erreur, regarde la liste disponible, et réémet le bon appel. Ne jetez pas silencieusement l'appel échoué. Ne relancez pas le même prompt en espérant. Renvoyez l'erreur.
Correction trois : schemas stricts pour le problème des arguments
L'hallucination de nom et l'hallucination d'argument sont des bugs différents. Pour les arguments — mauvais types, champs requis manquants, champs supplémentaires — utilisez l'utilisation stricte des outils. Définissez strict: true sur la définition de l'outil elle-même (pas sur tool_choice, ça ne fait rien), avec additionalProperties: false et une liste required. Maintenant l'API garantit que l'input se valide exactement contre votre schema. Le modèle ne peut littéralement pas émettre une forme d'argument qui ne correspond pas.
Une mise en garde : le mode strict n'est pas compatible avec tout. Il ne fonctionnera pas avec les appels d'outils programmatiques, le tool_choice forcé, ou les outils MCP. Pour ceux-là vous devez valider vous-même.
Correction quatre : raccourcissez le contexte dont dépend l'appel
Vous souvenez-vous du déclencheur long-et-bruyant ? Dans une longue boucle agentique, les vieux résultats d'outils s'accumulent. Au tour 40 le modèle se conditionne sur 39 tours de sortie majoritairement non pertinente, et le signal pour "quel outil, quels arguments" se noie. C'est là où l'édition de contexte gagne sa place — en effaçant les résultats tool_use obsolètes pour que la prochaine prédiction du modèle soit basée sur ce qui est réellement pertinent. C'est une fonctionnalité bêta (clear_tool_uses_20250919), et ce n'est pas la même chose que la compaction, qui résume au lieu d'effacer. Pour la précision des appels d'outils spécifiquement, effacer bat résumer, parce qu'un résumé des anciennes sorties d'outils pousse quand même la distribution.
Celui qui surprend les gens
Quand le modèle narre un appel d'outil au lieu de l'émettre — "Je vais maintenant chercher ça dans la base de données" sans aucun vrai bloc tool_use — ce n'est pas vraiment une hallucination. C'est le modèle qui termine son tour tôt. Sur de longues exécutions Claude, il écrit parfois une déclaration d'intention sans l'appel. La correction est un nudge dans le system prompt : dites-lui qu'avant de terminer un tour, si son dernier paragraphe est une promesse sur du travail qu'il n'a pas fait, il devrait faire ce travail maintenant avec un appel d'outil. Un simple "continue" le récupère de manière interactive. Pour les pipelines autonomes, intégrez l'instruction.
Ce que je veux que vous reteniez c'est ceci : aucune de ces corrections n'est de la magie. Elles font toutes la même chose — plier la distribution de probabilité vers l'appel que vous voulez, ou attraper l'appel que vous ne voulez pas. Le modèle ne dysfonctionne pas quand il invente search_invoices. Il fait exactement ce pour quoi il a été conçu, avec un contexte qui pointait dans la mauvaise direction. Pointez-le mieux.
