# Guide enseignant — Niveau Avancé, Session 2
# « Tool use avancé »

**Programme :** Applied AI — Yann Isola
**Public :** Architectes de solutions préparant la certification Claude Certified Architect
**Durée :** 2 heures
**Prérequis :** Session 1 (architecture API Messages, gestion du contexte), Python intermédiaire, JSON Schema de base

---

## Objectifs pédagogiques

À la fin de la session, chaque participant doit être capable de :

1. **Décrire le cycle de vie complet** d'un appel d'outil (tool use) : message utilisateur → bloc `tool_use` → exécution côté client → message `tool_result` → poursuite du modèle.
2. **Concevoir une définition d'outil de qualité certification** : nommage, description-prompt, `input_schema` documenté champ par champ.
3. **Choisir le bon `tool_choice`** (`auto`, `any`, `tool` ciblé) selon le cas d'usage.
4. **Forcer une sortie structurée** via le pattern du « faux outil » (fake tool).
5. **Distinguer et traiter les erreurs syntaxiques et sémantiques**, y compris via `is_error: true`.
6. **Appliquer les patterns avancés** : chaînage d'outils, sélection conditionnelle, mise en cache des résultats.
7. **Répondre aux questions de certification** sur `stop_reason: "tool_use"` et les cas limites du flux de messages.

---

## Plan de la session (120 min)

| Bloc | Durée | Contenu | Format |
|---|---|---|---|
| 0 | 5 min | Accueil, rappel Session 1, objectifs | Plénière |
| 1 | 20 min | Le cycle de vie tool_use, pas à pas | Démo live + slides |
| 2 | 20 min | Anatomie d'une définition d'outil | Slides + code review |
| 3 | 10 min | `tool_choice` : les trois modes | Slides + mini-démo |
| 4 | 15 min | Sortie structurée : le pattern « faux outil » | Démo live |
| 5 | 15 min | Gestion des erreurs (syntaxiques, sémantiques, `is_error`) | Slides + démo |
| — | 5 min | **Pause** | — |
| 6 | 10 min | Multi-outils et patterns avancés | Slides |
| 7 | 15 min | Atelier : débogueur de flux (page web interactive) | TP guidé |
| 8 | 5 min | Points de certification et pièges | Plénière |

Les exercices (fichier `exercises.md`) sont donnés en travail dirigé ou à la maison selon le temps restant. Le quiz clôture la session ou sert d'évaluation asynchrone.

---

## Bloc 1 — Le cycle de vie tool_use (20 min)

### Message clé à faire passer

> **Le modèle n'exécute jamais rien.** Il *demande* une exécution en produisant un bloc de contenu `tool_use`. C'est VOTRE code, avec VOS identifiants et VOS permissions, qui exécute. Le modèle ne voit que ce que vous lui renvoyez.

C'est le point de sécurité le plus important de toute la session — et une question quasi certaine en certification.

### Le cycle en 5 étapes (à dessiner au tableau)

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

1. **Requête** : vous envoyez les messages + le tableau `tools` (définitions).
2. **Décision du modèle** : si le modèle veut utiliser un outil, la réponse contient un bloc `tool_use` et le champ `stop_reason` vaut `"tool_use"`.
3. **Exécution côté client** : votre code lit `name` et `input` du bloc, exécute la fonction correspondante.
4. **Renvoi du résultat** : vous ajoutez à la conversation un message `role: "user"` contenant un bloc `tool_result` avec le `tool_use_id` correspondant.
5. **Poursuite** : le modèle intègre le résultat et répond (ou demande un autre outil — boucle).

### Démo live — API brute (JSON)

Montrer la requête HTTP (Application Programming Interface — interface de programmation applicative — au format REST) :

```json
POST https://api.anthropic.com/v1/messages
{
  "model": "claude-sonnet-4-5",
  "max_tokens": 1024,
  "tools": [
    {
      "name": "get_weather",
      "description": "Récupère la météo actuelle pour une ville donnée. Retourne la température en Celsius et les conditions. À utiliser dès que l'utilisateur pose une question sur la météo actuelle ou demande s'il doit prendre un parapluie, un manteau, etc.",
      "input_schema": {
        "type": "object",
        "properties": {
          "city": {
            "type": "string",
            "description": "Nom de la ville, en français, sans le pays. Exemple : \"Paris\", \"Genève\"."
          },
          "unit": {
            "type": "string",
            "enum": ["celsius", "fahrenheit"],
            "description": "Unité de température. Par défaut : celsius."
          }
        },
        "required": ["city"]
      }
    }
  ],
  "messages": [
    {"role": "user", "content": "Il fait combien à Lyon ?"}
  ]
}
```

Réponse du modèle (à décortiquer ligne par ligne) :

```json
{
  "id": "msg_01AbC...",
  "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", "unit": "celsius"}
    }
  ]
}
```

**Points d'attention à verbaliser :**
- `stop_reason: "tool_use"` — c'est LE signal que votre boucle doit détecter.
- Le bloc `content` peut contenir du texte **avant** le bloc `tool_use` (chaîne de pensée visible). Ne jamais supposer que `content[0]` est le `tool_use`.
- L'`id` du bloc `tool_use` (`toolu_...`) est obligatoire pour corréler le résultat.

Puis le renvoi du résultat :

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

**Piège certification :** le `tool_result` est envoyé dans un message `role: "user"`, PAS `role: "tool"` (contrairement à l'API d'OpenAI). Le message assistant contenant le `tool_use` doit être renvoyé tel quel dans l'historique, sinon erreur 400.

### Démo live — SDK Python

SDK = Software Development Kit (kit de développement logiciel).

```python
import anthropic

client = anthropic.Anthropic()  # clé lue dans ANTHROPIC_API_KEY

tools = [{
    "name": "get_weather",
    "description": (
        "Récupère la météo actuelle pour une ville donnée. "
        "Retourne la température en Celsius et les conditions. "
        "À utiliser dès que l'utilisateur pose une question météo."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string",
                     "description": "Nom de la ville, ex. \"Paris\"."},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"],
                     "description": "Unité. Défaut : celsius."},
        },
        "required": ["city"],
    },
}]

def get_weather(city: str, unit: str = "celsius") -> str:
    # Ici : appel à une vraie API météo, avec VOTRE clé d'API météo.
    return f"18°{'C' if unit == 'celsius' else 'F'}, nuageux"

messages = [{"role": "user", "content": "Il fait combien à Lyon ?"}]

# Boucle agentique minimale
while True:
    response = client.messages.create(
        model="claude-sonnet-4-5",   # ⚠ nom de modèle volatile, vérifier la doc
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

    if response.stop_reason != "tool_use":
        break  # réponse finale

    # 1. Renvoyer le message assistant TEL QUEL dans l'historique
    messages.append({"role": "assistant", "content": response.content})

    # 2. Exécuter chaque bloc tool_use (il peut y en avoir plusieurs !)
    results = []
    for block in response.content:
        if block.type == "tool_use":
            if block.name == "get_weather":
                output = get_weather(**block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output,
                })

    # 3. Les résultats repartent dans UN message user
    messages.append({"role": "user", "content": results})

print(response.content[0].text)
```

**À souligner :**
- La boucle `while` : c'est le squelette de tout agent. Le modèle peut enchaîner plusieurs tours d'outils avant de répondre.
- `for block in response.content` : gère nativement le cas multi-outils.
- Tous les `tool_result` d'un même tour vont dans **un seul** message `user`.

---

## Bloc 2 — Anatomie d'une définition d'outil (20 min)

### La description EST un prompt

Insister lourdement : la `description` n'est pas de la documentation pour les humains, c'est un **prompt injecté dans le contexte du modèle**. Elle conditionne :
- **quand** le modèle choisit l'outil (déclenchement),
- **comment** il remplit les paramètres,
- ce qu'il **attend** en retour.

Bonne heuristique de rédaction (3-4 phrases) :
1. **Ce que fait l'outil** (une phrase, verbe d'action).
2. **Quand l'utiliser** (déclencheurs explicites, 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 vides).

### Contre-exemple vs bon exemple (code review collective)

Mauvais :

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

Problèmes à faire identifier par le groupe : nom générique (recherche de quoi ?), description inutile, paramètre `q` non documenté (quelle syntaxe ? quelle langue ? opérateurs booléens ?).

Bon :

```json
{
  "name": "search_client_contracts",
  "description": "Recherche en texte intégral dans la base des contrats clients signés. À utiliser quand l'utilisateur mentionne un contrat, une clause, un client ou une échéance contractuelle. Ne couvre PAS les devis ni les factures (utiliser search_invoices). Retourne au maximum 10 contrats avec id, titre, client et extrait pertinent ; retourne une liste vide si aucun résultat.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "Termes de recherche en langage naturel. Pas d'opérateurs booléens. Exemple : \"clause de résiliation Acme 2025\"."
      },
      "client_id": {
        "type": "string",
        "description": "Optionnel. Identifiant client (format CLI-XXXX) pour restreindre la recherche à un seul client."
      },
      "max_results": {
        "type": "integer",
        "minimum": 1,
        "maximum": 10,
        "description": "Nombre maximal de résultats. Défaut : 5."
      }
    },
    "required": ["query"]
  }
}
```

### Principes de conception (à retenir pour la certification)

1. **Responsabilité unique** : un outil = une action. Pas de `manage_database(action, ...)` fourre-tout : le modèle se trompe plus souvent sur les outils polymorphes.
2. **Nommage clair** : `snake_case`, verbe + objet (`create_invoice`, `get_user_profile`). Le nom seul doit suffire à deviner la fonction.
3. **Documenter les cas limites** : que se passe-t-il si vide, si trop de résultats, si l'entité n'existe pas ? Le modèle gère mieux ce qu'il anticipe.
4. **Minimum de champs `required`** : chaque champ obligatoire est une occasion d'erreur ou d'hallucination de valeur. Rendre optionnel tout ce qui a un défaut raisonnable — et documenter ce défaut.
5. **`enum` et contraintes JSON Schema** (`minimum`, `maximum`, `format`) plutôt que des consignes en prose : le schéma contraint mieux que la description.

### JSON Schema — rappel express

JSON Schema (JavaScript Object Notation Schema — schéma de description de données JSON) : `input_schema` doit être un objet (`"type": "object"`) au niveau racine. Sous-ensemble utile en certification : `type`, `properties`, `required`, `enum`, `description`, `items` (tableaux), objets imbriqués, `minimum`/`maximum`, `format` (indicatif, non strictement validé).

---

## Bloc 3 — `tool_choice` : les trois modes (10 min)

| Mode | Syntaxe | Comportement | Cas d'usage |
|---|---|---|---|
| **auto** (défaut) | `{"type": "auto"}` | Le modèle décide : outil ou réponse texte | Assistant conversationnel général |
| **any** | `{"type": "any"}` | Le modèle DOIT appeler un outil, mais choisit lequel | Routeur d'intentions, dispatch |
| **tool** | `{"type": "tool", "name": "extract_data"}` | Le modèle DOIT appeler CET outil | Extraction structurée, pipeline déterministe |

Points de certification :

- Avec `any` ou `tool`, la réponse contient **toujours** un `tool_use` et `stop_reason` vaut `"tool_use"`.
- Avec `any`/`tool`, le modèle ne produit pas de raisonnement en texte libre avant l'appel de la même manière qu'en `auto` — les paramètres peuvent être légèrement moins réfléchis. Pour les tâches complexes, `auto` + prompt directif est parfois plus fiable que `tool` forcé.
- ⚠ Interaction avec le « thinking » étendu : les modes forcés (`any`, `tool`) sont incompatibles avec la réflexion étendue (extended thinking) au moment de la rédaction de ce guide — vérifier la documentation courante.
- Il existe aussi `{"type": "none"}` pour interdire tout appel d'outil tout en gardant les définitions dans le contexte.

Mini-démo : même question (« Analyse ce ticket support ») lancée avec `auto` puis `tool` forcé sur un outil d'extraction, comparer les sorties.

---

## Bloc 4 — Sortie structurée : le pattern « faux outil » (15 min)

### Le problème

Vous voulez du JSON garanti et conforme à un schéma (pour insérer en base, alimenter un pipeline…). Demander « réponds en JSON » dans le prompt fonctionne souvent, mais sans garantie : texte parasite autour, champ manquant, clé renommée.

### La solution

Définir un outil qui **n'exécute rien** — il ne sert qu'à contraindre la forme de la sortie — et forcer son appel avec `tool_choice`.

```python
extraction_tool = {
    "name": "record_ticket_analysis",
    "description": "Enregistre l'analyse structurée d'un ticket de support client.",
    "input_schema": {
        "type": "object",
        "properties": {
            "sentiment": {
                "type": "string",
                "enum": ["positif", "neutre", "negatif"],
                "description": "Sentiment global du client."
            },
            "urgence": {
                "type": "integer", "minimum": 1, "maximum": 5,
                "description": "Urgence de 1 (faible) à 5 (critique)."
            },
            "categorie": {
                "type": "string",
                "enum": ["facturation", "technique", "commercial", "autre"],
                "description": "Catégorie principale du ticket."
            },
            "resume": {
                "type": "string",
                "description": "Résumé en une phrase, max 25 mots."
            }
        },
        "required": ["sentiment", "urgence", "categorie", "resume"]
    }
}

response = client.messages.create(
    model="claude-sonnet-4-5",  # ⚠ volatile
    max_tokens=1024,
    tools=[extraction_tool],
    tool_choice={"type": "tool", "name": "record_ticket_analysis"},
    messages=[{"role": "user", "content": f"Analyse ce ticket :\n{ticket}"}],
)

data = next(b for b in response.content if b.type == "tool_use").input
# data est un dict Python déjà parsé : {"sentiment": "negatif", "urgence": 4, ...}
```

**À souligner :**
- Pas besoin de renvoyer un `tool_result` : on ne continue pas la conversation, on récupère juste `input`. C'est un « appel à sens unique ».
- Le SDK parse le JSON pour vous — `block.input` est déjà un dictionnaire.
- Les `enum` + `minimum`/`maximum` donnent une validation structurelle forte, mais **pas sémantique** (le modèle peut se tromper de catégorie tout en respectant l'enum) → transition parfaite vers le Bloc 5.
- ⚠ Mentionner que des modes de sortie structurée natifs évoluent rapidement côté API ; le pattern « faux outil » reste la technique de référence portable et au programme de la certification.

---

## Bloc 5 — Gestion des erreurs (15 min)

### Taxonomie : syntaxique vs sémantique

| Type | Définition | Exemple | Remède |
|---|---|---|---|
| **Syntaxique** | La sortie viole le format attendu | JSON tronqué (`max_tokens` trop bas), champ requis absent, type incorrect | **Réessayer** (retry), augmenter `max_tokens`, valider puis re-demander |
| **Sémantique** | Le format est valide mais le contenu est faux | Mauvaise catégorie, ville hallucinée, montant erroné | **Affiner le prompt** : meilleure description, exemples, enum plus stricts, découpage de la tâche |

Règle mnémotechnique : *syntaxique → retry ; sémantique → réécriture*. Un retry sur une erreur sémantique redonnera souvent la même erreur (le modèle est cohérent avec sa compréhension) ; réécrire le prompt sur une erreur syntaxique traite le symptôme, pas la cause (souvent `max_tokens` ou schéma trop complexe).

Cause fréquente d'erreur syntaxique à connaître pour la certification : `stop_reason: "max_tokens"` au milieu d'un bloc `tool_use` → le JSON d'`input` est tronqué. Détection : vérifier `stop_reason` AVANT de parser.

### Renvoyer une erreur au modèle : `is_error: true`

Quand VOTRE outil échoue (exception, 404, timeout), ne cassez pas la boucle : renvoyez l'erreur au modèle, il sait souvent se rattraper (reformuler, changer de paramètre, essayer un autre outil, ou expliquer l'échec à l'utilisateur).

```json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01XyZ...",
      "content": "Erreur : ville \"Lyno\" introuvable. Villes proches : Lyon, Lens.",
      "is_error": true
    }
  ]
}
```

En Python :

```python
try:
    output = get_weather(**block.input)
    result = {"type": "tool_result", "tool_use_id": block.id,
              "content": output}
except CityNotFoundError as e:
    result = {"type": "tool_result", "tool_use_id": block.id,
              "content": f"Erreur : {e}. Vérifie l'orthographe de la ville.",
              "is_error": True}
```

**Bonnes pratiques du message d'erreur :**
- Rédigé **pour le modèle**, pas pour un log : actionnable, avec suggestion de correction.
- Ne jamais renvoyer une stack trace brute complète (bruit, tokens gaspillés, fuite d'information).
- Toujours renvoyer UN `tool_result` par `tool_use` reçu, même en cas d'échec — un `tool_use` orphelin sans `tool_result` provoque une erreur 400 à la requête suivante.

### Anecdote d'architecte

Un pattern robuste en production : valider `block.input` avec Pydantic (bibliothèque Python de validation de données) AVANT d'exécuter, et renvoyer les erreurs de validation en `is_error: true`. Le modèle corrige ses paramètres au tour suivant dans la grande majorité des cas.

---

## Bloc 6 — Multi-outils et patterns avancés (10 min)

### Plusieurs outils dans un tour

Le modèle peut émettre **plusieurs blocs `tool_use` dans une même réponse** (appels parallèles) ou enchaîner sur plusieurs tours (appels séquentiels). Votre code doit :
- itérer sur TOUS les blocs `tool_use` de la réponse,
- renvoyer TOUS les `tool_result` correspondants dans le message `user` suivant, chacun avec le bon `tool_use_id`.

### Trois patterns d'architecture

1. **Chaînage d'outils (tool chaining)** : la sortie de l'outil A nourrit l'entrée de l'outil B, orchestré par le modèle sur plusieurs tours. Exemple : `search_client` → `get_client_contracts` → `summarize_contract`. Conception : faire en sorte que la sortie de A contienne exactement les identifiants dont B a besoin (ex. A retourne `client_id`, B prend `client_id` en paramètre).
2. **Sélection conditionnelle** : exposer des outils différents selon l'état de la session (utilisateur authentifié ou non, module actif…). Le tableau `tools` est envoyé À CHAQUE requête : vous pouvez le faire varier dynamiquement. C'est un contrôle d'accès côté client, plus fiable que « ne l'utilise pas » en prompt.
3. **Cache de résultats (tool result caching)** : mémoïser côté client les appels identiques (même outil, mêmes paramètres) pendant une session. Économise latence et coûts d'API externes. Attention à l'invalidation (TTL — Time To Live, durée de vie — courte pour les données volatiles). Ne pas confondre avec le *prompt caching* de l'API, qui cache le préfixe du contexte (dont les définitions d'outils) côté Anthropic.

### Sécurité — le point non négociable

- Les outils s'exécutent **dans votre code, avec vos authentifications**. Le modèle ne détient aucune clé et n'exécute rien.
- Conséquence : **toute validation, autorisation et limitation doit être dans votre code**. Traiter chaque `input` d'outil comme une entrée non fiable (au même titre qu'un formulaire web) : injection SQL, chemins de fichiers (`../`), montants, permissions.
- Principe du moindre privilège : l'outil `get_invoice` interroge la base avec un compte en lecture seule, restreint au tenant de l'utilisateur courant — pas avec le compte admin.
- Actions irréversibles (paiement, suppression, envoi d'email) : confirmation humaine (human-in-the-loop) côté client avant exécution.

---

## Bloc 7 — Atelier : débogueur de flux (15 min)

Ouvrir `webpage/index.html` (fonctionne hors ligne). Deux modules :

1. **Débogueur de flux tool_use** : les participants avancent pas à pas dans une conversation complète (requête → `tool_use` → exécution → `tool_result` → réponse finale), voient le JSON exact à chaque étape, et peuvent **injecter des erreurs** (JSON tronqué, `tool_use_id` incorrect, erreur d'exécution) pour observer le traitement attendu.
2. **Validateur de schéma d'outil** : coller une définition d'outil, obtenir une validation contre la spécification + suggestions de qualité (description trop courte, champs non documentés, etc.).

Consigne d'atelier : chaque binôme doit (a) dérouler le scénario nominal, (b) injecter les 3 erreurs et noter pour chacune si elle est syntaxique ou sémantique et le remède, (c) faire passer au validateur le schéma de l'exercice 1 s'il est déjà rédigé.

---

## Bloc 8 — Points de certification et pièges (5 min)

Check-list à faire réciter :

- [ ] `stop_reason: "tool_use"` = le modèle attend des résultats d'outils.
- [ ] `tool_result` part dans un message `role: "user"`, avec `tool_use_id` obligatoire.
- [ ] Le message assistant contenant le(s) `tool_use` doit être conservé intact dans l'historique.
- [ ] Un `tool_result` par `tool_use`, tous dans le même message user, sinon erreur 400.
- [ ] La description d'outil est un prompt ; `input_schema` est du JSON Schema avec racine `object`.
- [ ] `tool_choice` : `auto` / `any` / `tool` ciblé / `none` ; `any` et `tool` garantissent un appel.
- [ ] Faux outil + `tool_choice: tool` = sortie structurée garantie syntaxiquement.
- [ ] Syntaxique → retry ; sémantique → affiner le prompt.
- [ ] `is_error: true` pour signaler un échec d'exécution sans casser la boucle.
- [ ] Sécurité : exécution côté client, moindre privilège, validation des inputs, human-in-the-loop pour l'irréversible.

---

## Matériel et logistique

- Projeter : `slides/slides.md` (format Marp/compatible markdown).
- Démos : compte API avec clé de test, Python ≥ 3.10, `pip install anthropic pydantic`. ⚠ Les noms de modèles et tarifs évoluent : vérifier https://docs.anthropic.com avant la session.
- Page interactive : `webpage/index.html` — aucun serveur requis.
- Prévoir un plan B hors ligne : captures d'écran des réponses API si le réseau de la salle est défaillant.

## Erreurs fréquentes des participants (retours de terrain)

1. Oublier de renvoyer le message assistant dans l'historique → erreur 400 incomprise. Faire reproduire l'erreur volontairement, c'est le meilleur vaccin.
2. Chercher `response.content[0]` au lieu d'itérer sur les blocs.
3. Confondre `is_error: true` (erreur d'exécution renvoyée AU modèle) avec une erreur HTTP de l'API Anthropic.
4. Écrire des descriptions d'outils pour des humains (« Cette fonction permet de… ») au lieu de prompts opérationnels.
5. Mettre tous les champs en `required` « par sécurité » — c'est l'inverse d'une sécurité.
