Aller au contenu
Formation Claude

Tapez au moins deux lettres.

Commencer

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.

12 min de lecturePartie 6 — L’API et les agentsTechnique

À 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
  1. Définir un outil
  2. La boucle
  3. Concevoir de bons outils
  4. Le mode strict
  5. Les outils que vous n’exécutez pas
  6. Les sorties structurées
  7. Ce qu’il faut retenir

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.

python
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

python
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.

python
{
    "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.

python
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.

python
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

python
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

python
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

python
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.

Question 1

Claude renvoie deux blocs tool_use dans la même réponse. Que devez-vous faire ?

Voir la réponse

B — Exécuter les deux et renvoyer les deux tool_result dans UN SEUL message utilisateur

Séparer les résultats en plusieurs messages entraîne une erreur de structure, et apprend au modèle à ne plus faire d’appels parallèles. Tous les résultats d’un même tour vont dans un seul message, un tool_result par tool_use, même en cas d’échec.

Question 2

Quelle est la différence entre une consigne « réponds en JSON » et une sortie structurée ?

Voir la réponse

A — La consigne est suivie la plupart du temps ; la sortie structurée contraint le décodage et le JSON est valide par construction

Une consigne bien écrite marche dans la grande majorité des cas, ce qui n’est pas suffisant en production. La sortie structurée impose le schéma au moment où le texte est produit : la réponse est conforme, pas probablement conforme.

Question 3

Que signifie stop_reason valant pause_turn ?

Voir la réponse

A — Un outil serveur a atteint sa limite d’itérations ; il faut renvoyer la conversation pour qu’il reprenne

C’est un cas facile à manquer : la réponse paraît finie mais elle est en pause. Il faut réémettre la requête avec la réponse partielle dans l’historique, sans ajouter de message utilisateur.

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.