Tool use avancé

Applied AI — Niveau Avancé, Session 2

Yann Isola
Préparation Claude Certified Architect

Objectifs de la session

  1. Maîtriser le cycle de vie complet d'un appel d'outil
  2. Concevoir des définitions d'outils de qualité certification
  3. Utiliser tool_choice à bon escient (auto / any / tool)
  4. Forcer une sortie structurée avec le pattern « faux outil »
  5. Traiter les erreurs : syntaxiques vs sémantiques, is_error
  6. Patterns avancés : chaînage, sélection conditionnelle, cache
  7. Comprendre le modèle de sécurité

Le principe fondateur

Le modèle n'exécute jamais rien.
Il demande une exécution en produisant un bloc tool_use.
C'est votre code, avec vos authentifications, qui exécute.

Conséquences d'architecte :

  • Validation des entrées : chez vous
  • Contrôle d'accès : chez vous
  • Actions irréversibles : confirmation humaine chez vous

Le cycle de vie en 5 étapes

┌─────────────┐  1. requête + tools[]          ┌──────────┐
│             │ ─────────────────────────────► │          │
│  VOTRE CODE │  2. stop_reason:"tool_use"     │  MODÈLE  │
│  (client)   │ ◄───────────────────────────── │ (Claude) │
│             │                                │          │
│ 3. exécuter │  4. tool_result (role:user)    │          │
│   l'outil   │ ─────────────────────────────► │          │
│             │  5. réponse finale (end_turn)  │          │
└─────────────┘ ◄───────────────────────────── └──────────┘

Le cycle 2→4 peut boucler : c'est le cœur de tout agent.

Étape 1 — La requête avec tools

POST /v1/messages
{
  "model": "claude-sonnet-4-5",
  "max_tokens": 1024,
  "tools": [{
    "name": "get_weather",
    "description": "Récupère la météo actuelle d'une ville...",
    "input_schema": {
      "type": "object",
      "properties": {
        "city": {"type": "string",
                 "description": "Ville, ex. \"Paris\"."}
      },
      "required": ["city"]
    }
  }],
  "messages": [{"role": "user", "content": "Il fait combien à Lyon ?"}]
}

⚠ Nom de modèle volatile — vérifier la documentation.

Étape 2 — La réponse tool_use

{
  "role": "assistant",
  "stop_reason": "tool_use",
  "content": [
    {"type": "text", "text": "Je vérifie la météo à Lyon."},
    {"type": "tool_use",
     "id": "toolu_01XyZ...",
     "name": "get_weather",
     "input": {"city": "Lyon"}}
  ]
}
  • stop_reason: "tool_use" → le signal à détecter
  • Du texte peut précéder le bloc → ne jamais lire content[0] en aveugle
  • id (toolu_...) : indispensable pour la corrélation

Étapes 3-4 — Exécuter et renvoyer

{
  "role": "user",
  "content": [{
    "type": "tool_result",
    "tool_use_id": "toolu_01XyZ...",
    "content": "18°C, nuageux avec éclaircies"
  }]
}

Pièges de certification :

  • role: "user" — pas de rôle tool chez Anthropic (≠ OpenAI)
  • Le message assistant avec le tool_use doit rester intact dans l'historique
  • Un tool_use sans tool_result correspondant → erreur 400

La boucle agentique (SDK Python)

messages = [{"role": "user", "content": question}]
while True:
    resp = client.messages.create(model=MODEL, max_tokens=1024,
                                  tools=tools, messages=messages)
    if resp.stop_reason != "tool_use":
        break                                   # réponse finale
    messages.append({"role": "assistant",
                     "content": resp.content})  # historique intact
    results = []
    for block in resp.content:                  # multi-outils !
        if block.type == "tool_use":
            out = run_tool(block.name, block.input)
            results.append({"type": "tool_result",
                            "tool_use_id": block.id,
                            "content": out})
    messages.append({"role": "user", "content": results})

Anatomie d'une définition d'outil

{
  "name": "search_client_contracts",
  "description": "…c'est un PROMPT, pas de la doc…",
  "input_schema": { "type": "object", "properties": {} }
}
Champ Règle
name snake_case, verbe + objet, auto-descriptif
description 3-4 phrases : quoi / quand / quand PAS / retour
input_schema JSON Schema, racine object, chaque champ documenté

La description EST un prompt

Elle conditionne : le déclenchement, le remplissage des paramètres, l'interprétation du retour.

Gabarit en 4 phrases :

  1. Ce que fait l'outil — verbe d'action
  2. Quand l'utiliser — déclencheurs, y compris formulations indirectes
  3. Quand NE PAS l'utiliser — frontières avec les autres outils
  4. Ce qu'il retourne — format, unités, cas vide

Contre-exemple

{
  "name": "search",
  "description": "Recherche",
  "input_schema": {
    "type": "object",
    "properties": {"q": {"type": "string"}},
    "required": ["q"]
  }
}

Recherche de quoi ? q en quelle syntaxe ? Que retourne-t-il ?
Le modèle devine → il devine mal.

Version corrigée

{
  "name": "search_client_contracts",
  "description": "Recherche en texte intégral dans les contrats
    clients signés. À utiliser quand l'utilisateur mentionne un
    contrat, une clause ou une échéance. Ne couvre PAS les devis
    (utiliser search_invoices). Retourne max 10 contrats
    {id, titre, client, extrait} ; liste vide si aucun résultat.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {"type": "string",
        "description": "Langage naturel, pas d'opérateurs booléens."},
      "max_results": {"type": "integer", "minimum": 1, "maximum": 10,
        "description": "Défaut : 5."}
    },
    "required": ["query"]
  }
}

Principes de conception (certification)

  1. Responsabilité unique — pas d'outil fourre-tout à paramètre action
  2. Nommage clair — le nom seul doit suffire
  3. Documenter les cas limites — vide, introuvable, trop de résultats
  4. Minimum de required — chaque champ requis = un risque d'hallucination de valeur
  5. Contraindre par le schémaenum, minimum, maximum > consignes en prose

tool_choice — les trois modes

Mode Syntaxe Garantie Cas d'usage
auto (défaut) {"type":"auto"} aucune assistant général
any {"type":"any"} un outil sera appelé routeur d'intentions
tool {"type":"tool","name":"x"} cet outil sera appelé extraction structurée

Également : {"type": "none"} — définitions visibles, appels interdits.

tool_choice — pièges

  • Avec any / tool : stop_reason vaut toujours "tool_use"
  • Modes forcés = moins de raisonnement préalable en texte libre
    → pour les tâches complexes, auto + prompt directif peut battre tool forcé
  • ⚠ any / tool incompatibles avec l'extended thinking (réflexion étendue) — vérifier la doc courante

Sortie structurée : le « faux outil »

Problème : « réponds en JSON » dans le prompt = aucune garantie.

Solution : un outil jamais exécuté, qui ne sert qu'à mouler la sortie :

resp = client.messages.create(
    model=MODEL, max_tokens=1024,
    tools=[extraction_tool],
    tool_choice={"type": "tool", "name": "record_ticket_analysis"},
    messages=[{"role": "user", "content": f"Analyse :\n{ticket}"}],
)
data = next(b for b in resp.content
            if b.type == "tool_use").input   # dict déjà parsé !

Pas de tool_result à renvoyer : appel à sens unique.

Le schéma comme moule

{
  "sentiment": {"type": "string",
                "enum": ["positif", "neutre", "negatif"]},
  "urgence":   {"type": "integer", "minimum": 1, "maximum": 5},
  "resume":    {"type": "string",
                "description": "Une phrase, max 25 mots."}
}
  • Garantie syntaxique : types, enums, champs requis ✔
  • Garantie sémantique : ✘ — le modèle peut choisir la mauvaise valeur d'enum

→ D'où la question suivante : comment classer les erreurs ?

Erreurs : syntaxique vs sémantique

Syntaxique Sémantique
Définition le format est violé format OK, contenu faux
Exemples JSON tronqué, type incorrect mauvaise catégorie, montant erroné
Cause typique max_tokens trop bas, schéma trop complexe description ambiguë, tâche mal cadrée
Remède retry, ↑ max_tokens, valider puis re-demander affiner le prompt : descriptions, exemples, découpage

Mnémo : syntaxique → retry ; sémantique → réécriture.

Le cas max_tokens en plein tool_use

{"stop_reason": "max_tokens",
 "content": [ ..., {"type": "tool_use", "input": {"city": "Ly```

Le JSON d'`input` peut être **tronqué en plein vol**.

Réflexe d'architecte :

```python
if resp.stop_reason == "max_tokens":
    # NE PAS parser : relancer avec un budget plus grand
    ...

Toujours vérifier stop_reason avant de parser.

Erreur d'exécution : is_error: true

Votre outil échoue (404, timeout, exception) ? Ne cassez pas la boucle.

{
  "type": "tool_result",
  "tool_use_id": "toolu_01XyZ...",
  "content": "Erreur : ville \"Lyno\" introuvable.
              Villes proches : Lyon, Lens.",
  "is_error": true
}

Le modèle sait se rattraper : corriger le paramètre, changer d'outil, ou expliquer l'échec à l'utilisateur.

Rédiger un bon message d'erreur

✅ Pour le modèle : actionnable, avec suggestion de correction
✅ Toujours un tool_result par tool_use, même en échec
❌ Stack trace brute (bruit, tokens, fuite d'infos internes)
❌ tool_use orphelin → erreur 400 à la requête suivante

Pattern production : valider input avec Pydantic avant d'exécuter, renvoyer les erreurs de validation en is_error: true.

Plusieurs outils dans un tour

Le modèle peut émettre plusieurs blocs tool_use dans une même réponse :

"content": [
  {"type": "tool_use", "id": "toolu_A", "name": "convert_currency", ...},
  {"type": "tool_use", "id": "toolu_B", "name": "get_team_budget", ...}
]

Règles :

  • Itérer sur tous les blocs
  • Tous les tool_result dans un seul message user suivant
  • Chacun corrélé par son tool_use_id

Pattern 1 — Chaînage d'outils

search_client ──► get_client_contracts ──► summarize_contract
   (client_id)          (contract_id)

Le modèle orchestre sur plusieurs tours de boucle.

Clé de conception : la sortie de A contient exactement les identifiants que B attend en entrée. Sinon le modèle bricole — et hallucine.

Pattern 2 — Sélection conditionnelle

Le tableau tools est envoyé à chaque requête → faites-le varier :

tools = PUBLIC_TOOLS
if session.user.is_authenticated:
    tools += ACCOUNT_TOOLS
if session.user.role == "admin":
    tools += ADMIN_TOOLS

Retirer l'outil du tableau = contrôle d'accès structurel.
« Ne l'utilise pas » en prompt = contrôle d'accès décoratif.

Pattern 3 — Cache de résultats

key = (tool_name, json.dumps(args, sort_keys=True))
if key in cache:
    return cache[key]          # hit : 0 latence, 0 coût externe
  • TTL (Time To Live — durée de vie) court pour les données volatiles
  • Jamais de cache sur un résultat is_error: true
  • prompt caching API (cache du préfixe de contexte, dont les définitions d'outils, côté Anthropic)

Sécurité — synthèse d'architecte

Menace Parade (dans VOTRE code)
Paramètres malveillants / hallucinés Validation stricte (Pydantic, regex, bornes)
Injection SQL, traversée de chemin Requêtes paramétrées, allow-lists
Escalade de privilèges Moindre privilège : comptes lecture seule, scoping par tenant
Action irréversible Human-in-the-loop (confirmation) avant exécution

Traiter chaque input comme un formulaire web non fiable.

Check-list certification

  • stop_reason: "tool_use" = demande d'exécution
  • tool_result → message role: "user" + tool_use_id
  • Message assistant conservé intact dans l'historique
  • 1 tool_result par tool_use, groupés, sinon 400
  • tool_choice : auto / any / tool / none
  • Faux outil = garantie syntaxique, pas sémantique
  • Syntaxique → retry ; sémantique → réécriture
  • is_error: true : l'échec reste dans la boucle
  • Sécurité : exécution côté client, moindre privilège

Atelier (15 min)

Ouvrir webpage/index.html :

  1. Débogueur de flux — dérouler la conversation pas à pas, JSON à chaque étape, puis injecter les 3 erreurs : pour chacune, classer syntaxique / sémantique / protocolaire + remède
  2. Validateur de schéma — passer vos définitions de l'exercice 1

En binôme. Restitution : 1 piège découvert par binôme.

Prochaine session

Session 3 — Agents et orchestration

Boucles multi-tours en production, planification,
sous-agents, gestion d'état

Travail à rendre : exercice 3 (pipeline multi-outils) + quiz

Questions ?