Tool use et sorties structurées
Donner des outils à Claude et garantir le format de ses réponses : la boucle d’outils, les outils serveur, les sorties JSON contraintes et les outils stricts.
À la fin de ce chapitre, vous saurez
- Définir un outil et écrire la boucle qui l’exécute
- Distinguer les outils que vous exécutez de ceux qu’Anthropic exécute
- Garantir un JSON valide plutôt que l’espérer
- Éviter les pièges classiques : blocs orphelins, appels parallèles, pause de tour
Dans ce chapitre
Un modèle qui produit du texte est utile. Un modèle qui peut interroger votre base, appeler votre API et écrire dans vos systèmes est un composant logiciel. C’est ce que permettent les outils.
Le mécanisme reste celui du chapitre 1 : Claude ne fait rien lui-même. Il demande l’exécution d’un outil, votre code l’exécute, vous lui renvoyez le résultat, il continue.
Définir un outil
Un outil, c’est un nom, une description et un schéma d’entrée.
outils = [
{
"name": "chercher_commande",
"description": (
"Retrouve une commande par son numéro. Renvoie le statut, la date, "
"le montant et les articles. À utiliser dès qu'un numéro de commande "
"est mentionné, avant toute affirmation sur son contenu."
),
"input_schema": {
"type": "object",
"properties": {
"numero": {
"type": "string",
"description": "Numéro au format CMD-XXXXXX",
}
},
"required": ["numero"],
},
}
]La description est le seul élément qui décide de l’usage. C’est exactement la logique des compétences du chapitre 17. Elle doit dire ce que l’outil fait, ce qu’il renvoie, et quand l’appeler. Une description qui dit seulement « cherche une commande » sera utilisée au hasard.
La boucle
messages = [{"role": "user", "content": "La commande CMD-104857 a-t-elle été livrée ?"}]
for _ in range(10): # garde-fou : jamais de boucle infinie
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=outils,
messages=messages,
)
if reponse.stop_reason == "end_turn":
break
if reponse.stop_reason == "pause_turn":
# Un outil serveur a atteint sa limite : on renvoie tel quel.
messages.append({"role": "assistant", "content": reponse.content})
continue
if reponse.stop_reason != "tool_use":
break
# 1. On rejoue la réponse EN ENTIER, blocs tool_use compris.
messages.append({"role": "assistant", "content": reponse.content})
# 2. On exécute chaque appel et on collecte TOUS les résultats.
resultats = []
for bloc in reponse.content:
if bloc.type != "tool_use":
continue
try:
valeur = executer(bloc.name, bloc.input)
resultats.append({
"type": "tool_result",
"tool_use_id": bloc.id,
"content": valeur,
})
except Exception as e:
# Un échec se renvoie, il n'interrompt pas la boucle.
resultats.append({
"type": "tool_result",
"tool_use_id": bloc.id,
"content": f"Erreur : {e}",
"is_error": True,
})
# 3. Tous les résultats dans UN SEUL message utilisateur.
messages.append({"role": "user", "content": resultats})Quatre points font la différence entre une boucle qui tient et une boucle qui casse.
Rejouer reponse.content en entier. Un tool_result sans son tool_use correspondant provoque une erreur.
Un tool_result par tool_use, sans exception. Y compris pour un outil qui a échoué. Un appel sans réponse casse la requête suivante.
Tous les résultats dans un seul message. Les répartir sur plusieurs messages provoque une erreur — et, à long terme, dissuade le modèle de faire des appels parallèles.
Un nombre maximal d’itérations. Sans garde, deux outils mal conçus peuvent s’appeler mutuellement sans fin.
Concevoir de bons outils
Quelques principes qui font une vraie différence.
Peu d’outils, bien nommés. Vingt outils aux périmètres flous produisent de mauvais choix. Cinq outils nets valent mieux.
Une description qui dit quand. « Renvoie le statut d’une commande. À appeler avant toute affirmation sur une commande, ne devine jamais un statut. »
Des schémas contraints. Les énumérations et les formats réduisent les entrées invalides. Le mode strict va plus loin.
Des erreurs qui expliquent. "Numéro introuvable. Format attendu : CMD suivi de six chiffres." permet à Claude de corriger. "Error 404" ne permet rien.
Aucune action irréversible sans validation. La règle du chapitre 22 s’applique intégralement ici : un outil qui envoie, supprime ou paie doit passer par une confirmation.
Le mode strict
Il garantit que les entrées d’outil respectent exactement le schéma.
{
"name": "creer_ticket",
"description": "...",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"titre": {"type": "string"},
"priorite": {"type": "string", "enum": ["basse", "normale", "haute"]},
"assigne_a": {"type": "string"},
},
"required": ["titre", "priorite", "assigne_a"],
"additionalProperties": False,
},
}strict se place au même niveau que name, pas dans tool_choice. Le schéma doit comporter additionalProperties: false et lister tous les champs requis.
Les outils que vous n’exécutez pas
Certains outils tournent chez Anthropic : vous les déclarez, et le résultat revient dans la réponse. Il n’y a pas de boucle à écrire.
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
tools=[
{"type": "web_search_20260209", "name": "web_search", "max_uses": 5},
{"type": "web_fetch_20260209", "name": "web_fetch"},
{"type": "code_execution_20260521", "name": "code_execution"},
],
messages=[{"role": "user", "content": "..."}],
)Les principaux : recherche web, récupération d’une page, exécution de code dans un bac à sable, recherche d’outils quand vous en avez beaucoup.
L’exécution de code mérite une mention particulière. C’est elle qui transforme le calcul en calcul réel — le point répété depuis le chapitre 1. Un fichier de données joint plus l’exécution de code, et vous obtenez un résultat calculé au lieu d’un résultat plausible.
Les sorties structurées
Le chapitre 7 montrait comment obtenir du JSON par la consigne. En production, la consigne ne suffit pas : il faut la contrainte.
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=4096,
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"societe": {"type": "string"},
"montant_cents": {"type": "integer"},
"devise": {"type": "string", "enum": ["EUR", "USD", "GBP"]},
"echeance": {"type": "string", "format": "date"},
"confiance": {"type": "string",
"enum": ["haute", "moyenne", "basse"]},
},
"required": ["societe", "montant_cents", "devise",
"echeance", "confiance"],
"additionalProperties": False,
},
}
},
messages=[{"role": "user", "content": texte_facture}],
)En Python, client.messages.parse() avec un modèle Pydantic renvoie directement un objet validé, ce qui évite l’étape d’analyse manuelle. L’équivalent TypeScript passe par un schéma Zod.
Trois choses à savoir.
Le premier appel avec un schéma neuf est plus lent. Le schéma est compilé, puis mis en cache. En production, cela ne se voit pas ; en développement, si.
Un refus ne respecte pas le schéma. Vérifiez stop_reason avant d’analyser.
Certaines contraintes ne sont pas supportées — minimum, maximum, longueur de chaîne, schémas récursifs. Les bibliothèques Python et TypeScript les retirent du schéma envoyé et les valident côté client, ce qui est transparent mais bon à savoir.
Un assistant support qui ne ment jamais sur les commandes
La situation
Un assistant de support doit répondre sur l’état des commandes. Contrainte absolue : ne jamais affirmer un statut qui ne vient pas du système.
Les outils, et la description qui compte
outils = [
{
"name": "chercher_commande",
"description": (
"Retrouve une commande par son numéro exact. Renvoie statut, "
"date d'expédition, transporteur et articles. "
"OBLIGATOIRE avant toute affirmation sur une commande : "
"n'énonce jamais un statut sans avoir appelé cet outil."
),
"input_schema": {
"type": "object",
"properties": {"numero": {"type": "string"}},
"required": ["numero"],
},
},
{
"name": "chercher_par_email",
"description": (
"Liste les commandes récentes d'un client à partir de son adresse. "
"À utiliser quand le client ne connaît pas son numéro de commande."
),
"input_schema": {
"type": "object",
"properties": {"email": {"type": "string", "format": "email"}},
"required": ["email"],
},
},
]
SYSTEME = """Tu es l'assistant support de [entreprise].
Règle absolue : tu ne fais AUCUNE affirmation sur une commande sans
avoir appelé un outil. Si l'outil ne trouve rien, tu le dis. Tu ne
supposes jamais un délai de livraison.
Si le client demande un geste commercial, un remboursement ou une
annulation, tu ne t'engages pas : tu récapitules la demande et tu
indiques qu'un conseiller reprendra la main.
"""L’erreur qui aide
def chercher_commande(numero: str) -> str:
commande = base.trouver(numero)
if commande is None:
return (
f"Aucune commande {numero}. Vérifie le format : CMD suivi de "
f"six chiffres. Si le client n'est pas sûr de son numéro, "
f"utilise chercher_par_email."
)
return json.dumps(commande, ensure_ascii=False)Le message d’erreur indique la sortie de secours. Claude enchaîne tout seul sur le bon outil au lieu de s’excuser auprès du client.
Le garde-fou côté code
ACTIONS_INTERDITES = {"rembourser", "annuler", "modifier_adresse"}
def executer(nom: str, entree: dict):
if nom in ACTIONS_INTERDITES:
raise PermissionError(
"Cette action nécessite une validation humaine. "
"Récapitule la demande au client et transfère."
)
return OUTILS[nom](**entree)L’interdiction est dans le code, pas seulement dans l’instruction système. C’est la distinction du chapitre 25 : une instruction oriente, un contrôle applique.
Ce que ça donne
L’assistant ne peut pas inventer un statut : il n’a aucun moyen d’en produire un sans passer par la base. Et il ne peut pas s’engager, parce que l’engagement lève une exception.
Ce qu’il faut retenir
Vérifier que c’est passé
3 questions. Rien n’est enregistré, rien n’est envoyé : c’est pour vous.
La boucle qui ne casse pas
Écrivez une boucle d’outils avec deux outils, dont un qui échoue systématiquement. Vérifiez trois choses : que l’échec est renvoyé en tool_result avec is_error plutôt que d’interrompre la boucle, que deux appels dans le même tour donnent bien deux résultats dans un seul message, et que la boucle s’arrête au bout d’un nombre maximal d’itérations. Ces trois points couvrent la quasi-totalité des bugs de production.
Un indice, si vous bloquez
Pour le nombre maximal d’itérations : sans garde, une paire d’outils mal conçue peut se rappeler indéfiniment.