Tool calling forcé — Comment tuer la phrase à-peu-près-juste dans les chatbots en production
Le mode de défaillance qui fait tomber la plupart des agents conversationnels en production n'est pas l'hallucination — c'est la phrase qui sonne juste et qui est presque correcte. Voici l'architecture qui règle le problème.

Le vrai mode de défaillance
La plupart des chatbots de production ne tombent pas à cause d'hallucinations. Ils tombent à cause de phrases qui sonnent juste et qui sont presque correctes.
Un SLA inventé. Une signature qui usurpe l'identité d'un responsable opérationnel. Une promesse "je vous rappelle avant 17h" que le système ne peut pas tenir. Un faux numéro de dossier d'escalade. Un résumé de politique subtillement inexact.
Ces défaillances érodent la confiance. Elles sont difficiles à détecter à l'évaluation — la phrase se parse, obtient un score raisonnable en fidélité, paraît même correcte à un relecteur superficiel — et impossibles à contenir avec des règles de prompt. Le modèle a été entraîné sur des millions de phrases assurées. Lui demander poliment de ne pas en produire une n'est pas un contrôle.
Sur les agents conversationnels que nous avons livrés en production — voix et texte, orientés client et orientés opérateur — nous avons convergé vers une réponse architecturale unique pour les contextes à enjeux élevés : ne pas laisser le LLM écrire du tout le texte destiné à l'utilisateur.
Cet article présente le pattern, les compromis et le chemin de code en production.
Pourquoi les règles de prompt ne règlent pas le problème
Le correctif instinctif quand une phrase "à-peu-près-juste" passe en prod, c'est d'ajouter une règle au system prompt :
Ne jamais promettre d'heure de rappel. Ne jamais signer les messages. Ne jamais inventer de SLA. En cas de doute, dire "je vais escalader vers un humain."
Ça fonctionne une semaine. Puis ça casse. Le modèle excelle à produire du texte qui paraît conforme — il signera d'un rôle générique ("l'équipe", "votre agent support") qui ne figurait pas sur la liste interdite. Il promettra "une réponse rapide" au lieu d'un horaire précis. Il paraphrasera la politique avec 90% d'exactitude.
On peut empiler les règles indéfiniment. C'est un jeu du chat et de la souris avec un modèle qui a plus de moyens de contourner les règles qu'on n'a de temps pour les énumérer.
La solution structurelle est de retirer au LLM la capacité d'écrire du texte visible par l'utilisateur.
L'architecture en une phrase
Le seul travail du LLM est de choisir un outil dans une palette fermée et de fournir des arguments structurés. La réponse qui arrive sur l'écran de l'utilisateur provient d'un gabarit déterministe rempli avec ces arguments. Versionné, révisable, testable en A/B, structurellement impossible à halluciner.
Ce pattern comporte trois couches en production :
- Une porte déterministe pour les cas triviaux (accusés de réception, salutations) afin de ne pas brûler des tokens sur "ok merci 👍"
- Un LLM à tool calling forcé avec une palette fermée (un outil par tour, pas de texte libre)
- Un second modèle de vérification de sécurité quand l'outil choisi est "rester silencieux", pour détecter les signaux supprimés
Chaque couche est observable, interchangeable, et apporte une garantie précise.
Couche 1 — porte déterministe par regex
Une part significative des messages dans tout produit de chat-ops sont de purs ACK : "ok", "merci", "👍", "compris", "thanks". Les envoyer à un LLM est du gaspillage — en tokens comme en latence. Un petit classifieur basé sur des regex les capture avec zéro faux positif :
const ACK_PATTERNS = [
/^(ok|okay|d'accord|compris|merci|thanks?)\s*[!.👍🙏]*$/i,
/^(super|parfait|nickel|cool|great|perfect)\s*[!.]*$/i,
/^👍+$/,
/^[\p{Emoji}\s]+$/u, // emoji-only
]
function maybeAck(text: string): boolean {
const trimmed = text.trim()
if (trimmed.length > 30) return false
return ACK_PATTERNS.some(p => p.test(trimmed))
}
Quand cela retourne true, on route directement vers un résultat silent_acknowledge — aucun token dépensé, aucune latence ajoutée. Dans nos déploiements, cette porte capture 15–25% des messages entrants selon la surface.
Le seuil est important. On plafonne à ~30 caractères pour éviter d'avaler des messages du type "ok mais j'ai besoin de…" où le ACK est un marqueur discursif, pas le message entier. Il faut calibrer de manière conservatrice — un faux positif ici signifie qu'un vrai signal est silencieusement perdu. Mieux vaut envoyer trop au LLM que pas assez.
Couche 2 — tool calling forcé
C'est le cœur du pattern. On utilise tool_choice: { type: 'any' } d'Anthropic pour forcer le modèle à appeler exactement un outil par tour. La sortie en texte libre est structurellement interdite par l'API.
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
tool_choice: { type: 'any' },
tools: [
{ name: 'silent_acknowledge', description: '...', input_schema: { ... } },
{ name: 'respond_procedure', description: '...', input_schema: { ... } },
{ name: 'respond_redirect', description: '...', input_schema: { ... } },
{ name: 'escalate_question', description: '...', input_schema: { ... } },
{ name: 'escalate_perturbation', description: '...', input_schema: { ... } },
// ... typiquement 8–12 outils par agent
],
system: SYSTEM_PROMPT_WITH_RULES, // mis en cache
messages: conversationHistory,
})
La palette est fermée. Ajouter un nouveau comportement signifie livrer un nouvel outil, avec un nouveau schéma, un nouveau gabarit, et (idéalement) de nouveaux evals. La dérive ne peut pas se produire par accident.
Concevoir la palette
La conception de la palette est là où se concentre la majeure partie du travail d'ingénierie. Quelques principes qui ont survécu à la production :
Distinguer "répondre" de "escalader" de "rester silencieux". Ce sont trois résultats catégoriquement différents du point de vue de l'utilisateur : le système m'a répondu, le système m'a dit qu'un humain me contactera, le système n'a rien fait de visible. Ce ne doit jamais être un seul outil avec un argument string qui décide entre les trois — le LLM va mal router.
Un outil par gabarit de réponse. Si on a deux façons de dire "la procédure est X", créer deux outils. Les arguments structurés (category, severity, next_step) vivent dans chaque outil, mais le type de réponse est encodé dans le nom de l'outil. Cela rend les décisions de routage inspectables et gabarisables.
Les outils doivent avoir des descriptions non superposées. Une description qui dit "utiliser cet outil quand l'utilisateur demande X ou Y ou Z" recoupe presque toujours un autre outil. Le modèle choisit alors de façon quasi-aléatoire entre les outils qui se superposent — c'est exactement là où on constate de la dérive d'une semaine à l'autre. La description de chaque outil doit décrire un domaine qu'aucun autre outil ne couvre.
Garder le nombre d'outils en dessous de ~15. Au-delà, le modèle commence à se mélanger et la précision de sélection chute. Si on en a besoin de plus, il faut presque certainement un routage en deux étapes : un méta-outil ("dans quelle famille de comportements cela s'inscrit-il ?") puis un outil spécifique dans cette famille.
Ce qui reste dans le system prompt
Le system prompt contient les règles qui régissent quel outil choisir, jamais le texte qui revient à l'utilisateur. Contenu typique :
- Le rôle de l'agent et les limites de son autorité
- Les règles de sélection d'outil ("si l'utilisateur mentionne un document manquant, préférer X à Y")
- Les planchers de sécurité ("si l'utilisateur mentionne une atteinte à lui-même, ignorer la palette et escalader immédiatement vers un humain via
escalate_emergency") - Les règles absolues : ne jamais prétendre être humain, ne jamais inventer des horaires, ne jamais signer au nom d'une personne
Marquer ce bloc entier avec cache_control: ephemeral pour que les appels répétés dans les 5 minutes frappent le cache. On constate ~75–90% de taux de cache-hit sur le bloc de règles statiques en production à régime stable.
Couche 3 — générer le texte visible par l'utilisateur
Quand le LLM choisit respond_procedure({ category: 'document_missing', sub_type: 'identity' }), le code choisit le gabarit et le remplit. Pas le LLM.
const TEMPLATES = {
respond_procedure: {
document_missing: {
identity: ({ ctx }) =>
`Pour finaliser, il nous manque encore une copie de votre pièce d'identité. Vous pouvez la déposer directement depuis votre espace, dans Profil > Documents. Si vous avez besoin d'aide pour trouver le bon document, répondez "comment trouver la pièce d'identité" et je vous enverrai les étapes.`,
contract: ({ ctx }) =>
`...`,
},
schedule_change: {
// ...
},
},
respond_redirect: {
pricing_question: ({ ctx }) =>
`Pour les détails tarifaires, ${ctx.accountManagerFirstName} de l'équipe va vous recontacter — il a tout le contexte sur votre compte.`,
},
}
function render(toolName: string, args: any, ctx: Context): string {
const path = [toolName, args.category, args.sub_type].filter(Boolean)
const template = path.reduce((acc, k) => acc?.[k], TEMPLATES)
if (typeof template !== 'function') {
throw new TemplateNotFound({ path, args }) // logged + escalated
}
return template(ctx)
}
Cinq avantages immédiats :
- Versionnement. Les gabarits sont du code. L'historique Git montre quand le contenu a changé et pourquoi.
- Révisabilité. Un non-développeur peut lire toutes les réponses possibles dans un seul fichier. Essayez de faire ça avec un LLM en texte libre.
- Tests A/B. Deux gabarits par résultat, randomisés au rendu, mesurés en aval.
- i18n. Localiser les gabarits ; le LLM reste le même.
- Impossibilité structurelle d'hallucination. L'ensemble des chaînes que l'utilisateur peut recevoir est fini et commité.
Le compromis : la variété de ton est plus faible. Un chatbot en texte libre a des milliers de façons de formuler "nous vous recontacterons". Un chatbot gabarisé en a les quelques-unes qu'on a écrites. Dans notre expérience, c'est un avantage net — la cohérence est perçue comme du professionnalisme dans les contextes opérationnels. Pour un chatbot marketing, ça pourrait être contraignant.
Couche 4 — la vérification de sécurité sur les chemins silencieux
Le mode de défaillance le plus dangereux de l'architecture à tool calling forcé est le silence incorrect. L'utilisateur écrit :
"Bonjour, je suis malade aujourd'hui, je ne pourrai pas venir. Au fait merci pour le planning, ça me convient 🙏"
Le LLM voit la queue polie et route l'ensemble du message vers silent_acknowledge. Le "je suis malade aujourd'hui" ne va nulle part. Pas d'escalade. Pas de réponse. L'opérateur l'apprend à 8h du matin.
C'est le mode de défaillance qu'on ne peut pas éliminer à la Couche 2 seule — même un très bon Sonnet pondère parfois trop la queue polie.
La solution : quand l'outil choisi est silent_acknowledge, lancer un second modèle (on utilise Haiku 4.5) sur la même entrée avec une question précise : y a-t-il un signal actionnable dans ce message que le premier modèle vient de supprimer ?
if (chosenTool === 'silent_acknowledge') {
const safetyCheck = await anthropic.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 256,
tool_choice: {
type: 'tool',
name: 'classify',
},
tools: [{
name: 'classify',
description: 'Classify whether the message contains an actionable signal.',
input_schema: {
type: 'object',
properties: {
actionable: { type: 'boolean' },
signal: {
type: 'string',
enum: ['urgent', 'absent', 'reschedule', 'medical', 'none'],
},
confidence: { type: 'number' },
},
required: ['actionable', 'signal', 'confidence'],
},
}],
system: SAFETY_RECHECK_SYSTEM, // petit, mis en cache
messages: [{ role: 'user', content: userMessage }],
})
const result = parseToolUse(safetyCheck)
if (result.actionable && result.confidence > 0.6) {
return overrideToEscalation(result.signal)
}
}
Haiku est le bon modèle ici pour trois raisons. Il est peu coûteux (l'appel coûte ~10× moins que Sonnet). Il est rapide (~300ms). Et c'est un modèle différent de Sonnet — ses biais sur ce qui compte comme "actionnable" ne sont pas les mêmes. Ce dernier point est ce qui donne à cette couche sa valeur réelle : si on revérifiait avec le même modèle, on paierait juste un second rendu de la même erreur.
Dans nos déploiements, cela capture 0,5–2% des décisions "silencieuses", selon la surface. Ce n'est pas beaucoup. Mais chacune de ces décisions est un message critique qui aurait été silencieusement perdu — et le coût opérationnel d'un seul d'entre eux est bien supérieur au coût de tous les appels Haiku combinés.
Observabilité — chaque décision émet un événement
On ne peut pas opérer ce pipeline à l'aveugle. Chaque décision émet un événement structuré :
posthog.capture({
event: 'agent_decision',
properties: {
agent: 'support_chat',
flow: 'inbound_message',
layer: 'sonnet_forced_tool', // 'regex_gate' | 'sonnet_forced_tool' | 'haiku_safety'
tool_called: 'respond_procedure',
template_path: 'respond_procedure.document_missing.identity',
silent: false,
model: 'claude-sonnet-4-6',
cache_read_tokens: 5832,
cache_write_tokens: 0,
input_tokens: 412,
output_tokens: 87,
latency_ms: 950,
user_org_id: ctx.orgId,
decision_id: nanoid(),
},
})
Ce que ça donne dans le dashboard :
- Dérive de sélection d'outil : l'outil A était appelé 40% du temps la semaine dernière, 25% cette semaine — quelque chose a changé.
- Taux de cache-hit : s'il chute, l'ordre du prompt ou le format des gabarits a probablement changé.
- Taux d'override silencieux : à quelle fréquence Haiku détecte-t-il une erreur de Sonnet ? Si ça monte en flèche, la sélection de Sonnet se dégrade.
- Budget de latence : regex (1ms) → Sonnet (700–1200ms) → Haiku (300ms).
- Coût par décision calculé en direct, totalisé par org par jour pour la facturation ou les alertes.
C'est le changement le plus précieux après avoir livré l'architecture. On passe de "l'agent a-t-il fait quelque chose de bizarre cette semaine ?" à "l'outil X est sélectionné 12% plus souvent, voici pourquoi."
Quand NE PAS utiliser ce pattern
Le tool calling forcé ajoute une charge de conception. Ce n'est pas gratuit. On ne l'utilise pas pour :
- Les chats purement informationnels sans conséquences (un assistant docs, un bot Q&R interne). Un LLM en texte libre avec citations est plus rapide à livrer et les modes de défaillance sont visibles — une mauvaise réponse est mauvaise, on peut grep les transcripts.
- Les assistants d'écriture créative. Tout l'enjeu est justement le texte.
- Les démos et prototypes. Ne pas optimiser pour la sécurité sur quelque chose qui ne sera peut-être jamais livré.
- Les classifieurs à tour unique qui ne répondent pas à l'utilisateur. On est déjà en territoire tool-call ; la couche de gabarits n'est pas nécessaire.
Le pattern brille spécifiquement quand une phrase incorrecte est coûteuse : chats opérationnels, support client avec des implications de politique, domaines réglementés, agents vocaux qui représentent l'entreprise au téléphone, tout contexte où l'utilisateur pourrait vous citer devant un tribunal ou un journaliste.
Ce que ça coûte en conception
Franchement : plus de temps qu'un chatbot en texte libre. Le travail se décompose approximativement comme suit :
- Conception de la palette d'outils et des schémas d'arguments : 2–4 jours
- Gabarits (écriture, révision avec les parties prenantes, i18n) : 3–7 jours
- Le pipeline à deux couches + vérification de sécurité : 1–2 jours
- Jeu d'evals couvrant la précision de sélection d'outil : 1–2 jours
- Câblage de l'observabilité : une demi-journée
Soit 1,5–3 semaines de conception et d'implémentation en amont. La contrepartie : neuf à douze mois plus tard, quand le système a traité des dizaines de milliers de conversations, on n'a pas eu un seul incident "l'agent a promis X". Le coût en corrections de bugs qu'on aurait payé sans ce pattern aurait absorbé ce budget initial plusieurs fois.
Pour conclure
Le changement mental est petit mais important : arrêter d'essayer de rendre un LLM en texte libre sûr, et commencer à traiter le LLM comme un classifieur à sortie structurée dont les résultats sont rendus par du code. Le LLM excelle à choisir dans un ensemble fermé selon le contexte. Il est mauvais pour ne pas contourner les règles dans le texte. L'utiliser pour la première chose ; ne pas l'utiliser pour la seconde.
Si vous livrez un agent conversationnel dans un contexte où les phrases incorrectes sont coûteuses, c'est l'architecture vers laquelle on se tournerait. Si vous souhaitez un avis extérieur sur une conception qui dérive vers "encore une règle dans le system prompt", contactez-nous. Nous avons conçu ce pipeline plusieurs fois — nous pouvons le faire pour vous plus rapidement.
Travailler avec Ikki
Chatbot qui défaille en production ?
Nous auditons 100 conversations de votre assistant et identifions où la génération de texte libre doit devenir un tool call déterministe.
Autres articles
La semaine où un gouvernement a coupé le meilleur modèle d'Anthropic
Fable 5 suspendu par décret américain le 12 juin, deux modèles legacy retirés le 15, et le SDK livre le fallback pour tous les triggers. La semaine a posé le risque et la réponse en même temps. Voici comment durcir votre stack.
AgentsLe middleware du SDK Anthropic : arrêtez d'écrire vos propres wrappers d'observabilité
Le SDK Anthropic livre une API middleware native, le SDK agent enchaîne 10 releases en 7 jours, et Nuxt 4.4.7 est un correctif de sécurité. Les revues de dépendances trimestrielles sont devenues trop lentes pour l'AI en production.