Applied AI · Avancé 🔴 · Session 2
✏️ Exercices
← Retour au programme 📄 Source .md

Exercices — Niveau Avancé, Session 2

« Tool use avancé »

Programme : Applied AI — Yann Isola Niveau : Avancé (préparation Claude Certified Architect) Durée totale estimée : 2 h 30 (en TD ou à la maison) Prérequis techniques : Python ≥ 3.10, pip install anthropic pydantic, clé API dans ANTHROPIC_API_KEY

⚠ Les noms de modèles utilisés dans les corrigés (claude-sonnet-4-5) sont volatils — vérifier la documentation officielle au moment de l’exercice.


Exercice 1 — Défi de conception de schéma d’outil (45 min)

Contexte

Vous êtes architecte pour une plateforme de gestion de flotte automobile. L’assistant IA (Intelligence Artificielle) doit permettre aux gestionnaires de :

Un stagiaire a produit cette première version — elle est volontairement mauvaise :

{
  "name": "FleetManager",
  "description": "Gère la flotte",
  "input_schema": {
    "type": "object",
    "properties": {
      "action": {"type": "string"},
      "data": {"type": "object"}
    },
    "required": ["action", "data"]
  }
}

Travail demandé

1.1 — Critique (10 min). Listez au moins 6 défauts de cette définition, en les rattachant chacun à un principe de conception vu en cours (responsabilité unique, nommage, description-prompt, documentation des champs, cas limites, champs requis minimaux).

1.2 — Refonte (25 min). Remplacez cet outil unique par 3 outils bien conçus. Pour chacun, produisez la définition JSON complète avec :

Contraintes métier à intégrer :

1.3 — Test croisé (10 min). Échangez vos schémas avec un binôme. Chacun rédige 3 requêtes utilisateur en langage naturel (dont une ambiguë) et prédit quel outil sera déclenché et avec quels paramètres. Testez ensuite contre l’API réelle avec tool_choice: {"type": "auto"} et comparez. Passez enfin vos schémas dans le validateur de webpage/index.html.

Critères de réussite

Éléments de corrigé (1.1)

  1. Responsabilité multiple : un outil qui fait tout via action → le modèle doit deviner les valeurs valides d’action, jamais listées.
  2. Nommage : FleetManager n’est pas en snake_case et ne décrit pas une action.
  3. Description inutile : « Gère la flotte » n’indique ni quand déclencher, ni quoi attendre en retour.
  4. data opaque : "type": "object" sans properties = zéro contrainte, le modèle invente la structure.
  5. Aucun cas limite documenté (véhicule introuvable, créneau indisponible…).
  6. required mal utilisé : tout est requis mais rien n’est défini — le pire des deux mondes.

Exercice 2 — Déboguer un flux d’outil cassé (45 min)

Contexte

Le script ci-dessous est censé répondre à des questions sur des commandes e-commerce. Il contient 5 bugs (certains provoquent des erreurs API, d’autres des comportements silencieusement faux). Il vous est fourni dans exo2_broken.py :

import anthropic, json

client = anthropic.Anthropic()

tools = [{
    "name": "get_order",
    "description": "Récupère une commande par son identifiant. Retourne statut, montant et date. Utiliser dès qu'une question porte sur une commande précise.",
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string",
                         "description": "Identifiant de commande, format ORD-XXXXX."}
        },
        "required": ["order_id"],
    },
}]

DB = {"ORD-10042": {"statut": "expédiée", "montant": 129.90,
                    "date": "2026-06-28"}}

def get_order(order_id):
    return json.dumps(DB[order_id])          # BUG ?

messages = [{"role": "user",
             "content": "Où en est ma commande ORD-10042 ? Et la ORD-99999 ?"}]

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=50,                            # BUG ?
    tools=tools,
    messages=messages,
)

tool_block = response.content[0]              # BUG ?

result = get_order(tool_block.input["order_id"])

messages.append({"role": "user", "content": [{   # BUG ?
    "type": "tool_result",
    "tool_use_id": tool_block.id,
    "content": result,
}]})

final = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)
print(final.content[0].text)

Travail demandé

2.1 — Audit statique (15 min). Sans exécuter, identifiez les 5 bugs. Pour chacun : la ligne, le symptôme attendu (erreur 400 ? exception Python ? JSON tronqué ? réponse incomplète ?), et la classification syntaxique / sémantique / protocolaire.

2.2 — Correction (20 min). Réécrivez le script avec :

2.3 — Validation (10 min). Exécutez et vérifiez que le modèle : (a) donne le statut de ORD-10042, (b) explique proprement que ORD-99999 est introuvable — sans crash. Rejouez le scénario dans le débogueur de webpage/index.html en injectant l’erreur « commande introuvable ».

Corrigé — les 5 bugs

# Ligne Bug Classification Symptôme
1 max_tokens=50 Trop bas : le JSON du bloc tool_use risque d’être tronqué (stop_reason: "max_tokens") Syntaxique input incomplet ou absent, parsing qui échoue
2 response.content[0] Suppose que le premier bloc est le tool_use ; il peut y avoir du texte avant, et il y a ICI deux appels attendus (deux commandes demandées) Protocolaire AttributeError (bloc texte sans .input) ou second appel ignoré
3 messages.append(...) sans avoir ajouté la réponse assistant L’historique envoyé contient un tool_result sans le tool_use correspondant Protocolaire Erreur 400 de l’API (tool_use_id inconnu)
4 DB[order_id] KeyError sur ORD-99999 : l’erreur d’exécution n’est pas convertie en tool_result avec is_error: true Exécution / protocolaire Crash Python, boucle rompue
5 Absence de boucle + pas de test de stop_reason Si le modèle répond en texte (pas d’outil) ou enchaîne un 2ᵉ tour d’outil, le script est faux Protocolaire / sémantique Comportement erratique selon la réponse

Squelette de correction attendu :

messages = [...]
while True:
    response = client.messages.create(model="claude-sonnet-4-5",
                                      max_tokens=1024, tools=tools,
                                      messages=messages)
    if response.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type != "tool_use":
            continue
        try:
            out = get_order(**block.input)
            results.append({"type": "tool_result",
                            "tool_use_id": block.id, "content": out})
        except KeyError:
            results.append({"type": "tool_result",
                            "tool_use_id": block.id,
                            "content": f"Commande {block.input.get('order_id')} "
                                       "introuvable. Vérifier l'identifiant "
                                       "(format ORD-XXXXX).",
                            "is_error": True})
    messages.append({"role": "user", "content": results})

print(next(b.text for b in response.content if b.type == "text"))

Exercice 3 — Pipeline multi-outils (60 min)

Contexte

Construire un mini-agent « analyste de dépenses » qui enchaîne trois outils pour répondre à :

« Quel est le total des dépenses de l’équipe Data en juin 2026, converti en USD, et est-il au-dessus du budget ? »

USD = dollar des États-Unis (United States Dollar).

Les trois outils à implémenter

  1. get_team_expenses(team, month) → liste de dépenses [{label, amount_eur}] (données simulées en dur).
  2. convert_currency(amount, from_currency, to_currency) → montant converti (taux simulé, ex. 1 EUR = 1,08 USD ⚠ taux fictif).
  3. get_team_budget(team, currency) → budget mensuel de l’équipe dans la devise demandée.

Travail demandé

3.1 — Conception (15 min). Rédigez les 3 définitions d’outils. Point clé de chaînage : les sorties de l’outil 1 doivent fournir exactement ce que l’outil 2 attend (montants + devise explicite). Documentez les devises supportées en enum (EUR, USD).

3.2 — Implémentation (30 min). Écrivez la boucle agentique complète :

3.3 — Test de robustesse (15 min). Trois scénarios :

  1. Nominal : la question ci-dessus. Vérifiez le chaînage complet (dépenses → conversion → budget → verdict).
  2. Erreur sémantique provoquée : demandez « en francs suisses » (CHF, non supporté par l’enum). Observez : le modèle appelle-t-il quand même l’outil ? Renvoyez is_error: true (« devise non supportée, devises valides : EUR, USD ») et vérifiez le rattrapage.
  3. Cache : posez deux questions successives nécessitant les mêmes dépenses. Vérifiez le hit de cache au second tour.

Critères de réussite

Indications de corrigé (structure)

import anthropic, json

client = anthropic.Anthropic()
cache: dict[tuple, str] = {}

def call_tool(name: str, args: dict) -> tuple[str, bool]:
    """Retourne (contenu, is_error). Passe par le cache."""
    key = (name, json.dumps(args, sort_keys=True))
    if key in cache:
        print(f"[cache HIT] {name}{args}")
        return cache[key], False
    try:
        out = TOOL_IMPLS[name](**args)      # dict name -> fonction
        cache[key] = out
        return out, False
    except ToolError as e:
        return str(e), True                  # jamais mis en cache

messages = [{"role": "user", "content": QUESTION}]
for turn in range(10):                       # garde-fou
    resp = client.messages.create(model="claude-sonnet-4-5",
                                  max_tokens=2048, tools=TOOLS,
                                  messages=messages)
    print(f"[tour {turn}] stop_reason={resp.stop_reason}")
    if resp.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": resp.content})
    results = []
    for b in resp.content:
        if b.type == "tool_use":
            content, is_err = call_tool(b.name, b.input)
            r = {"type": "tool_result", "tool_use_id": b.id,
                 "content": content}
            if is_err:
                r["is_error"] = True
            results.append(r)
    messages.append({"role": "user", "content": results})
else:
    raise RuntimeError("max_iterations atteint — boucle interrompue")

Points de discussion en correction collective :