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 :
- rechercher des véhicules (par immatriculation, statut, agence),
- planifier une intervention de maintenance,
- consulter l’historique d’entretien d’un véhicule.
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 :
- nom en
snake_case, verbe + objet ; - description de 3-4 phrases : quoi / quand l’utiliser / quand ne pas l’utiliser / ce qui est retourné (y compris le cas « aucun résultat ») ;
input_schemaavec unedescriptionpour chaque champ, desenumlà où c’est pertinent, et un usage justifié derequired.
Contraintes métier à intégrer :
- statuts véhicule possibles :
disponible,en_mission,en_maintenance,hors_service; - types d’intervention :
revision,pneus,freins,carrosserie,controle_technique; - une intervention se planifie sur un créneau (date ISO 8601 — norme internationale de format de date — + agence) ;
- l’immatriculation suit le format français
AA-123-BB.
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
- 3 outils à responsabilité unique, aucun paramètre « fourre-tout ».
- Chaque champ de chaque schéma a une
description. - Les statuts et types d’intervention sont des
enum, pas des chaînes libres. - Le format d’immatriculation est documenté (description + éventuellement
pattern). - Sur la requête ambiguë, le comportement observé est explicable par vos descriptions.
Éléments de corrigé (1.1)
- Responsabilité multiple : un outil qui fait tout via
action→ le modèle doit deviner les valeurs valides d’action, jamais listées. - Nommage :
FleetManagern’est pas ensnake_caseet ne décrit pas une action. - Description inutile : « Gère la flotte » n’indique ni quand déclencher, ni quoi attendre en retour.
dataopaque :"type": "object"sansproperties= zéro contrainte, le modèle invente la structure.- Aucun cas limite documenté (véhicule introuvable, créneau indisponible…).
requiredmal 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 :
- une boucle
whilegérant un nombre arbitraire de tours d’outils ; - l’itération sur tous les blocs
tool_use; - la gestion de la commande inexistante via
is_error: true(message actionnable pour le modèle) ; - une vérification de
stop_reasonavant tout parsing.
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
get_team_expenses(team, month)→ liste de dépenses[{label, amount_eur}](données simulées en dur).convert_currency(amount, from_currency, to_currency)→ montant converti (taux simulé, ex. 1 EUR = 1,08 USD ⚠ taux fictif).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 :
- boucle
whileavec garde-foumax_iterations = 10(question de certification classique : que faire si le modèle boucle ? → limite dure + sortie propre) ; - gestion multi-blocs (le modèle peut appeler
convert_currencyetget_team_budgetdans le même tour) ; - cache de résultats : un dictionnaire
{(tool_name, params_frozen): result}qui court-circuite les appels identiques répétés — loguez les hits de cache ; - journalisation de chaque tour :
stop_reason, outils appelés, paramètres.
3.3 — Test de robustesse (15 min). Trois scénarios :
- Nominal : la question ci-dessus. Vérifiez le chaînage complet (dépenses → conversion → budget → verdict).
- 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. - 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
- Le verdict final (au-dessus / en dessous du budget) est correct par rapport aux données simulées.
- Aucun crash sur le scénario CHF ; le modèle explique la limitation ou convertit en USD en le signalant.
- Au moins un hit de cache logué sur le scénario 3.
-
max_iterationsteste et interrompt proprement une boucle trop longue. - Chaque
tool_usereçu a exactement untool_resultrenvoyé.
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 :
- Pourquoi ne jamais mettre en cache un résultat
is_error: true? (L’erreur peut être transitoire ; et on veut que le modèle retente avec de meilleurs paramètres.) - Où placer la conversion : outil dédié (composable, traçable) vs laisser le modèle calculer (rapide mais non fiable pour la finance) ? Réponse d’architecte : toujours un outil pour l’arithmétique monétaire.
- Variante d’extension : ajouter
tool_choice: {"type": "any"}au premier tour pour garantir que l’agent commence par collecter des données plutôt que de répondre de mémoire.