---
marp: true
theme: default
paginate: true
style: |
  :root {
    --ink: #1A2230;
    --teal: #0F7A6C;
    --copper: #B4612A;
    --light-teal: #E9F6F3;
    --bg: #F4F7F6;
  }
  section {
    background: var(--bg);
    color: var(--ink);
    font-family: 'Inter', 'Segoe UI', sans-serif;
    font-size: 26px;
  }
  h1 { color: var(--teal); }
  h2 { color: var(--ink); border-bottom: 3px solid var(--teal); padding-bottom: 6px; }
  strong { color: var(--copper); }
  code { background: var(--light-teal); color: var(--ink); }
  pre { background: #10141C; border-radius: 8px; font-size: 19px; }
  pre code { background: transparent; color: #E9F6F3; }
  blockquote { border-left: 5px solid var(--copper); background: var(--light-teal); padding: 10px 16px; }
  table { font-size: 22px; }
---

<!-- _class: lead -->

# 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`

```json
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`

```json
{
  "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

```json
{
  "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)

```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

```json
{
  "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

```json
{
  "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

```json
{
  "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éma** — `enum`, `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 :

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

```json
{
  "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

```json
{"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.**

```json
{
  "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** :

```json
"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 :

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

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

---

<!-- _class: lead -->

## 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 ?**
