# Teacher Guide — Advanced Level, Session 2
# “Advanced tool use”

**Program:** Applied AI — Yann Isola
**Audience:** Solutions architects preparing for Claude Certified Architect certification
**Duration:** 2 hours
**Prerequisites:** Session 1 (Messages API architecture, context management), intermediate Python, basic JSON Schema

---

## Educational objectives

At the end of the session, each participant should be able to:

1. **Describe the complete life cycle** of a tool call: user message → `tool_use` block → client-side execution → `tool_result` message → continuation of the model.
2. **Design a certification quality tool definition**: naming, description-prompt, `input_schema` documented field by field.
3. **Choose the correct `tool_choice`** (`auto`, `any`, targeted `tool`) according to the use case.
4. **Force structured output** via the “fake tool” pattern.
5. **Distinguish and deal with syntactic and semantic errors**, including via `is_error: true`.
6. **Apply advanced patterns**: tool chaining, conditional selection, result caching.
7. **Answer certification questions** about `stop_reason: "tool_use"` and message flow edge cases.

---

## Session plan (120 min)

| Block | Duration | Content | Format |
|---|---|---|---|
| 0 | 5 mins | Home, reminder Session 1, objectives | Plenary |
| 1 | 20 mins | The tool_use life cycle, step by step | Live demo + slides |
| 2 | 20 mins | Anatomy of a tool definition | Slides + review code |
| 3 | 10 mins | `tool_choice`: the three modes | Slides + mini-demo |
| 4 | 15 mins | Structured output: the “false tool” pattern | Live demo |
| 5 | 15 mins | Error handling (syntactic, semantic, `is_error`) | Slides + demo |
| — | 5 mins | **Pause** | — |
| 6 | 10 mins | Advanced multi-tools and patterns | Slides |
| 7 | 15 mins | Workshop: flow debugger (interactive web page) | Guided practical work |
| 8 | 5 mins | Certification points and pitfalls | Plenary |

The exercises (file `exercises.md`) are given in guided work or at home depending on the time remaining. The quiz closes the session or serves as an asynchronous assessment.

---

## Block 1 — The tool_use life cycle (20 min)

### Key message to convey

> **The model never executes anything.** It *requests* an execution by producing a content block `tool_use`. It is YOUR code, with YOUR identifiers and YOUR permissions, that executes. The model only sees what you send back to it.

This is the most important safety point of the entire session — and an almost certain question in certification.

### The 5-step cycle (to draw on the board)```
┌─────────────┐   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. **Query**: you send the messages + the table `tools` (definitions).
2. **Model decision**: if the model wants to use a tool, the response contains a `tool_use` block and the `stop_reason` field is `"tool_use"`.
3. **Client-side execution**: Your code reads `name` and `input` from the block, executes the corresponding function.
4. **Return result**: You add a `role: "user"` message to the conversation containing a `tool_result` block with the corresponding `tool_use_id`.
5. **Continue**: the model integrates the result and responds (or requests another tool — loop).

### Live demo — Raw API (JSON)

Show the HTTP (Application Programming Interface) request in REST format:```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 ?"}
  ]
}
```Model response (to be dissected line by line):```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 of attention to be verbalized:**
- `stop_reason: "tool_use"` — this is THE signal that your loop must detect.
- The `content` block may contain text **before** the `tool_use` block (visible chain of thought). Never assume that `content[0]` is `tool_use`.
- The `id` of the `tool_use` block (`toolu_...`) is required to correlate the result.

Then returning the result:```json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01XyZ...",
      "content": "18°C, nuageux avec éclaircies"
    }
  ]
}
```**Certification pitfall:** the `tool_result` is sent in a `role: "user"` message, NOT `role: "tool"` (unlike the OpenAI API). The wizard message containing `tool_use` must be returned unchanged in the history, otherwise error 400.

### Live demo — Python SDK

SDK = Software Development Kit.```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)
```**Note:**
- The `while` loop: it is the skeleton of any agent. The model can perform several tool turns before responding.
- `for block in response.content`: natively manages the multi-tool case.
- All `tool_result`s in the same round go into **a single** message `user`.

---

## Block 2 — Anatomy of a tool definition (20 min)

### Description IS a prompt

Heavy emphasis: `description` is not documentation for humans, it is a **prompt injected into the context of the model**. It conditions:
- **when** the model chooses the tool (triggering),
- **how** it fills the parameters,
- what he **expects** in return.

Good writing heuristic (3-4 sentences):
1. **What the tool does** (one sentence, action verb).
2. **When to use it** (explicit triggers, including indirect wordings).
3. **When NOT to use it** (boundaries with other tools).
4. **What it returns** (format, units, empty cases).

### Counterexample vs good example (collective code review)

Bad:```json
{
  "name": "search",
  "description": "Recherche",
  "input_schema": {
    "type": "object",
    "properties": {"q": {"type": "string"}},
    "required": ["q"]
  }
}
```Problems to be identified by the group: generic name (search for what?), unnecessary description, undocumented `q` parameter (what syntax? what language? Boolean operators?).

Good :```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"]
  }
}
```### Design principles (to be retained for certification)

1. **Single responsibility**: one tool = one action. No catch-all `manage_database(action, ...)`: the model is wrong more often on polymorphic tools.
2. **Clear naming**: `snake_case`, verb + object (`create_invoice`, `get_user_profile`). The name alone should be enough to guess the function.
3. **Document borderline cases**: what happens if empty, if too many results, if the entity does not exist? The model handles what it anticipates better.
4. **Minimum fields `required`**: each mandatory field is an opportunity for error or value hallucination. Make anything with a reasonable defect optional — and document that defect.
5. **`enum` and JSON Schema constraints** (`minimum`, `maximum`, `format`) rather than instructions in prose: the schema constrains better than the description.

### JSON Schema — express callback

JSON Schema (JavaScript Object Notation Schema): `input_schema` must be an object (`"type": "object"`) at the root level. Useful subset in certification: `type`, `properties`, `required`, `enum`, `description`, `items` (arrays), nested objects, `minimum`/`maximum`, `format` (indicative, not strictly validated).

---

## Block 3 — `tool_choice`: the three modes (10 min)

| Fashion | Syntax | Behavior | Use cases |
|---|---|---|---|
| **auto** (default) | `{"type": "auto"}` | The model decides: tool or text response | General conversational assistant |
| **any** | `{"type": "any"}` | The model MUST call a tool, but chooses which one | Intent router, dispatch |
| **tool** | `{"type": "tool", "name": "extract_data"}` | The model MUST call THIS tool | Structured extraction, deterministic pipeline |

Certification Points:

- With `any` or `tool`, the response **always** contains a `tool_use` and `stop_reason` is `"tool_use"`.
- With `any`/`tool`, the model does not produce free text reasoning before the call in the same way as in `auto` — parameters may be slightly less thoughtful. For complex tasks, `auto` + directive prompt is sometimes more reliable than forced `tool`.
- ⚠ Interaction with extended thinking: forced modes (`any`, `tool`) are incompatible with extended thinking at the time of writing this guide — check the current documentation.
- There is also `{"type": "none"}` to prohibit any tool calls while keeping definitions in context.

Mini-demo: same question (“Analyze this support ticket”) launched with `auto` then `tool` forced on an extraction tool, compare the outputs.

---

## Block 4 — Structured output: the “false tool” pattern (15 min)

### The problem

You want guaranteed JSON that conforms to a schema (to insert into the database, feed a pipeline, etc.). Asking “reply in JSON” in the prompt often works, but without guarantee: stray text around, missing field, renamed key.

### The solution

Define a tool that **does nothing** — it only serves to constrain the shape of the output — and force its call with `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, ...}
```**Note:**
- No need to return a `tool_result`: we do not continue the conversation, we just retrieve `input`. It’s a “one-way call.”
- The SDK parses the JSON for you — `block.input` is already a dictionary.
- The `enum` + `minimum`/`maximum` give a strong structural validation, but **not semantic** (the model can make the wrong category while respecting the enum) → perfect transition to Block 5.
- ⚠ Mention that native structured output modes are evolving rapidly on the API side; the “false tool” pattern remains the portable reference technique and on the certification program.

---

## Block 5 — Error handling (15 min)

### Taxonomy: syntactic vs semantic

| Type | Definition | Example | Remedy |
|---|---|---|---|
| **Syntactic** | Output violates expected format | JSON truncated (`max_tokens` too low), required field missing, incorrect type | **Retry** (retry), increase `max_tokens`, validate then re-request |
| **Semantics** | Format is valid but content is wrong | Wrong category, crazy city, wrong amount | **Refine the prompt**: better description, examples, stricter enum, task breakdown |

Mnemonic rule: *syntactic → retry; semantics → rewriting*. A retry on a semantic error will often give the same error (the model is consistent with its understanding); rewriting the prompt on a syntactic error treats the symptom, not the cause (often `max_tokens` or schema too complex).

Common cause of syntactic error to be aware of for certification: `stop_reason: "max_tokens"` in the middle of a `tool_use` block → the JSON of `input` is truncated. Detection: check `stop_reason` BEFORE parsing.

### Return an error to the model: `is_error: true`

When YOUR tool fails (exception, 404, timeout), do not break the loop: send the error back to the model, it often knows how to recover (reformulate, change parameters, try another tool, or explain the failure to the user).```json
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01XyZ...",
      "content": "Erreur : ville \"Lyno\" introuvable. Villes proches : Lyon, Lens.",
      "is_error": true
    }
  ]
}
```In 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}
```**Error message best practices:**
- Written **for the model**, not for a log: actionable, with suggested correction.
- Never return a complete raw stack trace (noise, wasted tokens, information leak).
- Always return ONE `tool_result` per `tool_use` received, even on failure — an orphaned `tool_use` without `tool_result` causes a 400 error on the next request.

### Anecdote of an architect

A robust pattern in production: validate `block.input` with Pydantic (Python data validation library) BEFORE executing, and return validation errors in `is_error: true`. The model corrects its parameters in the next round in the vast majority of cases.

---

## Block 6 — Advanced multi-tools and patterns (10 min)

### Multiple tools in one lathe

The model can issue **several `tool_use` blocks in the same response** (parallel calls) or chain over several rounds (sequential calls). Your code must:
- iterate over ALL blocks `tool_use` of the response,
- return ALL matching `tool_result`s in the following `user` message, each with the correct `tool_use_id`.

### Three architectural patterns

1. **Tool chaining**: the output of tool A feeds the input of tool B, orchestrated by the model over several turns. Example: `search_client` → `get_client_contracts` → `summarize_contract`. Design: ensure that A's output contains exactly the identifiers that B needs (e.g. A returns `client_id`, B takes `client_id` as a parameter).
2. **Conditional selection**: expose different tools depending on the state of the session (authenticated user or not, active module, etc.). The `tools` array is sent EACH request: you can vary it dynamically. This is client-side access control, more reliable than "don't use it" prompt.
3. **Result cache (tool result caching)**: store identical calls (same tool, same parameters) on the client side during a session. Saves latency and external API costs. Beware of invalidation (TTL — Time To Live, lifespan — short for volatile data). Do not confuse with *prompt caching* of the API, which hides the context prefix (including tool definitions) on the Anthropic side.

### Safety — the non-negotiable point

- The tools run **in your code, with your authentications**. The model holds no keys and executes nothing.
- Consequence: **all validation, authorization and limitation must be in your code**. Treat each tool `input` as unreliable input (like a web form): SQL injection, file paths (`../`), amounts, permissions.
- Principle of least privilege: the `get_invoice` tool queries the database with a read-only account, restricted to the current user's tenant — not with the admin account.
- Irreversible actions (payment, deletion, email sending): human confirmation (human-in-the-loop) on the client side before execution.

---

## Block 7 — Workshop: flow debugger (15 min)

Open `webpage/index.html` (works offline). Two modules:

1.**Flow debugger tool_use**: Participants advance step by step through a complete conversation (request → `tool_use` → execution → `tool_result` → final response), see the exact JSON at each step, and can **inject errors** (truncated JSON, incorrect `tool_use_id`, runtime error) to observe the expected treatment.
2. **Tool schema validator**: paste a tool definition, get validation against the specification + quality suggestions (too short description, undocumented fields, etc.).

Workshop instructions: each pair must (a) unfold the nominal scenario, (b) inject the 3 errors and note for each one whether it is syntactic or semantic and the remedy, (c) pass the diagram from exercise 1 to the validator if it is already written.

---

## Block 8 — Certification points and pitfalls (5 min)

Checklist to recite:

- [ ] `stop_reason: "tool_use"` = Model is waiting for tool results.
- [ ] `tool_result` leaves in a message `role: "user"`, with `tool_use_id` mandatory.
- [ ] The wizard message containing the `tool_use` must be kept intact in the history.
- [ ] A `tool_result` by `tool_use`, all in the same user message, otherwise error 400.
- [ ] The tool description is a prompt; `input_schema` is JSON Schema with root `object`.
- [ ] `tool_choice`: `auto` / `any` / `tool` targeted / `none`; `any` and `tool` guarantee a call.
- [ ] False tool + `tool_choice: tool` = syntactically guaranteed structured output.
- [ ] Syntactic → retry; semantics → refine the prompt.
- [ ] `is_error: true` to report an execution failure without breaking the loop.
- [ ] Security: client-side execution, least privilege, input validation, human-in-the-loop for the irreversible.

---

## Material and logistics

- Project: `slides/slides.md` (Marp format/markdown compatible).
- Demos: API account with test key, Python ≥ 3.10, `pip install anthropic pydantic`. ⚠ Model names and prices are changing: check https://docs.anthropic.com before the session.
- Interactive page: `webpage/index.html` — no server required.
- Plan an offline plan B: screenshots of API responses if the room network is faulty.

## Frequent errors by participants (field feedback)

1. Forgetting to send the wizard message back to the history → error 400 misunderstood. Reproducing the error voluntarily is the best vaccine.
2. Search for `response.content[0]` instead of iterating over blocks.
3. Confusing `is_error: true` (runtime error returned TO model) with an Anthropic API HTTP error.
4. Write tool descriptions for humans (“This function allows you to…”) instead of operational prompts.
5. Set all fields to `required` “for security” — this is the opposite of security.