Série · Certification Claude Certified ArchitectÉpisode 01 / 20
Claude Architect, épisode 1 : la boucle agentique et stop_reason
Le moteur de tout agent Claude : les trois rôles, la boucle en quatre étapes, les deux valeurs de stop_reason, le tableau messages et les trois anti-patterns que l'examen adore. Code complet, schémas animés, quiz.
Je prépare la certification Claude Certified Architect. Cette série me sert de carnet de révision, et je la publie pour que d’autres francophones puissent s’en servir. Chaque épisode couvre une partie du programme et suffit pour la réviser : des schémas, du code qui tourne, les pièges à reconnaître et un quiz pour finir.
Domaine 1 — Architecture agentique et orchestration, le plus lourd de l'examen : 27 % des questions. Cet épisode couvre la tâche 1.1, la boucle agentique.
D'après le guide d'examen officiel, l'examen compte 60 questions à scénario en 120 minutes, et il faut un score de 720 sur une échelle de 100 à 1 000 pour le réussir. Tout le détail de l'examen est dans l'épisode 0.
Au programme de cet épisode
| Chapitre | Ce que vous saurez faire | |
|---|---|---|
| 0 | Préparer son poste | appeler l’API Claude depuis Python pour quelques centimes |
| 1 | Les trois rôles | dire qui parle dans une conversation : system, user, assistant |
| 2 | Chat ou agent | expliquer ce qui fait d’un programme un agent |
| 3 | Le cycle de la boucle | dérouler les quatre étapes, et savoir quand on en sort |
| 4 | stop_reason |
réagir correctement à tool_use et à end_turn |
| 5 | Le tableau messages |
gérer l’état, parce que l’API n’en garde aucun |
| 6 | Le code complet | lire et lancer un agent, respecter les règles de l’API, gérer un outil en échec |
| 7 | Qui choisit l’outil | distinguer « piloté par le modèle » et « préconfiguré » |
| 8 | Les trois anti-patterns | les reconnaître dans une question d’examen |
| 9 | Fiche, glossaire et quiz | vérifier que tout est acquis, avec 8 questions corrigées |
0. Préparer son poste
La partie pratique est facultative pour l’examen, mais écrire la boucle soi-même aide beaucoup à la retenir. Il faut quatre choses.
Un compte sur platform.claude.com, l’ancienne
console.anthropic.com, avec du crédit : 5 $ au minimum, soit environ 50 dirhams. Chaque
appel est facturé au jeton, et sans crédit l’API refuse de répondre. La page Limits indique
les plafonds de requêtes par minute de chaque modèle.
Le modèle le moins cher. Tous les exemples utilisent Claude Haiku 4.5
(claude-haiku-4-5), pour ne pas vider les 5 $ en une après-midi. Un appel de test coûte une fraction de centime.
Une clé API, à placer dans une variable d’environnement, jamais dans le code :
# macOS / Linux : ajoutez cette ligne à ~/.zshrc (ou ~/.bashrc), puis rechargez
export ANTHROPIC_API_KEY="sk-ant-..."
source ~/.zshrc
# Windows (PowerShell)
$env:ANTHROPIC_API_KEY = "sk-ant-..."
# Un environnement Python isolé, et le SDK
python3 -m venv .venv
source .venv/bin/activate
pip install anthropicOn voit souvent conseiller un fichier .env dans le projet. Attention : le SDK ne le lit pas tout seul. Il faut python-dotenv et un load_dotenv() au début du script, sinon la clé n'est jamais trouvée.
Un premier appel, pour vérifier que la clé fonctionne. Appelons ce fichier
api_check.py :
import anthropic
client = anthropic.Anthropic()
# Le SDK lit ANTHROPIC_API_KEY dans l'environnement :
# inutile de passer la clé à la main.
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=100,
messages=[
{"role": "user", "content": "Say hello in one word."}
]
)
print(response.content[0].text) # Doit afficher : HelloRemarquez déjà le "role": "user". C’est le premier concept.
1. Les trois rôles : qui parle ?
Pensez à une salle de classe. L’élève a le rôle d’apprendre, le professeur celui d’enseigner. Échangez les rôles, et l’élève devient professeur. Un rôle ne dit pas qui vous êtes, il dit ce que vous faites dans la conversation.
Avec Claude, chaque message porte un rôle :
| Rôle | Qui le porte | En une phrase |
|---|---|---|
system |
le développeur, une seule fois | « qui tu es » : le personnage, les consignes |
user |
celui qui demande | la question, ou toute donnée qui entre dans Claude |
assistant |
Claude | ce que le modèle répond |
Dans l’interface de Claude ou de ChatGPT, vous ne voyez jamais ces rôles : chaque message que
vous tapez est étiqueté user en coulisses. Avec le SDK, c’est vous qui les écrivez.
Prenons une campagne marketing pour du thé au gingembre, rôle par rôle :
- system
Tu es un expert de premier plan du marketing sur les réseaux sociaux. - user
Génère une campagne publicitaire pour du thé au gingembre. - assistant
Voici trois visuels et un post Instagram : « Un réveil épicé… » - user
Le post Instagram ne va pas, corrige-le. - assistant
Voici le post Instagram mis à jour : …
En code, cela donne :
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=1024,
system="Tu es un expert de premier plan du marketing sur les réseaux sociaux.",
messages=[
{"role": "user", "content": "Génère une campagne publicitaire pour du thé au gingembre."},
{"role": "assistant", "content": "Voici trois visuels et un post Instagram : ..."},
{"role": "user", "content": "Le post Instagram ne va pas, corrige-le."},
],
)Dans l'API Messages, le rôle system ne va pas dans le tableau messages. Il a son propre paramètre, system=. Le tableau ne contient que des messages user et assistant, et il commence toujours par user.
2. Chat ou agent : quelle différence ?
- chez Claude (API)
- dans votre code
Une image aide : le LLM est le cerveau de l’agent, les outils sont ses mains. Le cerveau décide quelle main utiliser, et à quel moment.
Prenons un agent de réservation de voyages. On lui demande un vol entre l’Inde et l’Australie à certaines dates :
- Claude comprend qu’il faut l’outil de réservation de vols, et le demande.
- L’outil s’exécute et répond.
- Si le vol est réservé, c’est fini.
- Si les dates sont ambiguës, Claude demande une précision à l’utilisateur, relance l’outil avec la réponse, et ainsi de suite jusqu’au résultat voulu.
3. Le cycle de vie de la boucle agentique
- API Claude
- votre code Python
- seule sortie
end_turn.
- Envoyer la requête : on appelle l’API avec le tableau
messageset la liste des outils disponibles. - Lire
stop_reason: la réponse de Claude indique pourquoi il s’est arrêté. - Exécuter : si Claude a demandé un outil (
tool_use), votre code l’exécute. Claude ne fait que le demander. - Ajouter : on ajoute la demande et le résultat à l’historique, puis on revient à l’étape 1.
On sort quand stop_reason vaut end_turn, et seulement dans ce cas.
4. stop_reason : les deux valeurs à connaître
tool_use |
end_turn |
|
|---|---|---|
| Ce que ça signifie | Claude veut agir | Claude a terminé |
| Ce que contient la réponse | des blocs tool_use (parfois du texte avant) |
des blocs text |
| Ce que fait votre code | exécuter, ajouter à l’historique, reboucler | sortir de la boucle, extraire le texte |
| La règle | ne jamais renvoyer ça à l’utilisateur | c’est la réponse finale |
Pour la boucle, ce sont bien ces deux valeurs qu'il faut maîtriser. Mais l'API en connaît six :
max_tokens: la réponse a été coupée par la limite de jetons, elle est incomplète ;stop_sequence: une séquence d'arrêt définie par vous a été rencontrée ;pause_turn: un tour long, avec des outils côté serveur, a été mis en pause et peut être relancé ;refusal: Claude a refusé pour des raisons de sécurité, etstop_detailsdonne la catégorie.
Une boucle de production les traite explicitement. On verra plus bas ce qu'il advient d'une boucle qui les oublie.
5. Pourquoi le tableau messages est vital
L’API Claude est 100 % sans état. Envoyez « Je m’appelle Yasmine », elle répond « Bonjour Yasmine ». Au prochain appel, elle a tout oublié. Chaque requête doit donc contenir tout l’historique de la conversation.
Dans une boucle agentique, cet historique grandit à chaque tour. Une phrase résume tout : c’est l’architecte qui est responsable de l’état. Un outil oublié dans la requête, et Claude invente une réponse. Un identifiant qui ne correspond pas, et la boucle casse.
assistant) et son résultat (user). Règle 2 : le
tool_use_id du résultat doit correspondre exactement à l'id de la
demande.
Une question revient souvent : pourquoi le résultat d’un outil porte-t-il le rôle user,
alors qu’aucun humain ne l’a tapé ? Parce que user désigne tout ce qui entre dans Claude
depuis votre code. Ce qui sort de Claude est assistant, ce qui y entre est user.
Rejouez la boucle, étape par étape
Voici le vrai déroulé de l’agent de la section suivante, sur la question « Where is my order #4821? ». Cliquez sur les étapes, ou utilisez les flèches du clavier.
Itération 1. Le tableau ne contient que la question. On l'envoie avec la liste des outils.
- user : "Where is my order #4821?"
Claude répond avec stop_reason = "tool_use" et un bloc qui demande lookup_order({"order_id": "4821"}), avec l'id toolu_01….
Ajout 1, rôle assistant. On ajoute la réponse de Claude telle quelle (response.content), bloc tool_use compris.
- user : "Where is my order #4821?"
- assistant : [tool_use · id toolu_01… · lookup_order]
Ça doit arriver avant le résultat : un tool_result doit toujours répondre à un tool_use qui le précède.
Exécution, puis ajout 2, rôle user. Votre code appelle execute_tool, qui renvoie {"status": "shipped", "eta": "March 30", "carrier": "FedEx"}.
- user : "Where is my order #4821?"
- assistant : [tool_use · id toolu_01… · lookup_order]
- user : [tool_result · tool_use_id toolu_01… · {"status": "shipped", …}]
Les trois messages repartent à Claude : c'est l'itération 2.
Itération 2. Claude lit le résultat et répond stop_reason = "end_turn" avec un bloc texte : « Your order #4821 has been shipped! … »
Le code renvoie ce texte et la boucle s'arrête. Il a suffi de deux appels à l'API. Cette dernière réponse n'est pas ajoutée au tableau, puisqu'on n'en a plus besoin.
6. Le code complet : un agent de suivi de commande
Le cas d’usage est un service client. On demande « où est ma commande ? », l’agent consulte la commande et répond, sans personne pour chercher à la main. Pas de base de données ici : trois commandes sont écrites en dur pour garder l’exercice simple.
Voici agent.py, complet et prêt à lancer. Il se découpe en six étapes.
Étapes 1 à 3 : le client et la définition de l’outil. Un outil a trois champs. Le
name, c’est son nom dans les blocs tool_use. La description permet à Claude de décider
quand s’en servir. L’input_schema décrit ses paramètres en JSON Schema.
# ÉTAPE 1 : installer et importer — pip install anthropic
import anthropic
import json
import os
# ÉTAPE 2 : créer le client.
# Il lit ANTHROPIC_API_KEY dans l'environnement. Ne jamais écrire la clé en dur ici.
client = anthropic.Anthropic()
# ÉTAPE 3 : définir les outils.
# name - le nom que Claude utilise dans ses blocs tool_use
# description - ce qui permet à Claude de décider QUAND s'en servir (épisode 05)
# input_schema - le schéma JSON des paramètres
tools = [
{
"name": "lookup_order",
"description": (
"Look up an order by its order ID. "
"Returns current status, estimated delivery date, and carrier name. "
"Use this when the customer asks where their order is or when it will arrive."
),
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The numeric order ID (e.g. '4821')"
}
},
"required": ["order_id"]
}
}
]Étape 4 : la fonction de l’outil. C’est elle qui fait le travail. Claude ne peut pas l’appeler : il la demande, et votre code l’exécute.
# ÉTAPE 4 : implémenter les fonctions des outils.
# Claude NE PEUT PAS appeler ces fonctions lui-même.
# Claude DEMANDE un appel -> votre code l'exécute -> vous renvoyez le résultat.
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Exécute un outil et renvoie son résultat sous forme de chaîne JSON."""
if tool_name == "lookup_order":
order_id = tool_input.get("order_id", "")
# Fausse base de données
mock_orders = {
"4821": {"status": "shipped", "eta": "March 30", "carrier": "FedEx"},
"9910": {"status": "processing", "eta": "April 2", "carrier": "UPS"},
"0042": {"status": "delivered", "eta": "March 25", "carrier": "DHL"},
}
if order_id in mock_orders:
return json.dumps(mock_orders[order_id])
else:
return json.dumps({"error": f"Order {order_id} not found"})
# Outil inconnu : renvoyer une erreur en résultat (ne jamais lever d'exception ici)
return json.dumps({"error": f"Unknown tool: {tool_name}"})Étape 5 : la boucle agentique. C’est le cœur du fichier, et c’est lui que l’examen interroge.
# ÉTAPE 5 : la boucle agentique
def run_agent(user_message: str) -> str:
"""Fait tourner la boucle jusqu'à ce que Claude produise une réponse finale."""
# On commence avec le seul message de l'utilisateur.
# Le tableau messages grandit à chaque tour de boucle.
messages = [
{"role": "user", "content": user_message}
]
# Soupape de sécurité : PAS la condition d'arrêt principale.
MAX_ITERATIONS = 50
iteration = 0
while iteration < MAX_ITERATIONS:
iteration += 1
# APPEL API : les messages actuels + les outils disponibles.
# Claude renvoie une réponse accompagnée d'un stop_reason.
response = client.messages.create(
model="claude-haiku-4-5", # rapide et peu cher : idéal pour apprendre
max_tokens=4096,
tools=tools,
messages=messages
)
# SORTIE : "end_turn" signifie que Claude a terminé.
# C'est la SEULE sortie principale valide de la boucle.
if response.stop_reason == "end_turn":
for block in response.content:
if block.type == "text":
return block.text
return "" # end_turn sans texte (rare, mais possible)
# APPEL D'OUTIL : "tool_use" signifie que Claude veut appeler des outils.
# À faire : exécuter les outils, puis ajouter les DEUX messages à l'historique.
if response.stop_reason == "tool_use":
# AJOUT 1 : le message de l'assistant, AVANT les résultats.
messages.append({
"role": "assistant",
"content": response.content # contient les blocs tool_use
})
# Exécuter chaque outil demandé par Claude
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f" → Calling tool: {block.name}({block.input})")
result = execute_tool(block.name, block.input)
print(f" ← Result: {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # ← DOIT correspondre à l'id du message assistant
"content": result
})
# AJOUT 2 : les résultats, rôle "user" : ces données ENTRENT dans Claude
# depuis votre code. Aucun humain ne les a tapées, mais c'est le rôle user.
messages.append({
"role": "user",
"content": tool_results
})
# La boucle continue : Claude va lire les résultats et décider de la suite.
# Soupape déclenchée : ne devrait jamais arriver en fonctionnement normal
return "Error: agent did not complete within the iteration limit"
# ÉTAPE 6 : lancer l'agent
if __name__ == "__main__":
print("Running agent...")
answer = run_agent("Where is my order #4821?")
print(f"\nFinal answer: {answer}")Et voici ce que donne python3 agent.py :
$ python3 agent.py
Running agent...
→ Calling tool: lookup_order({'order_id': '4821'})
← Result: {"status": "shipped", "eta": "March 30", "carrier": "FedEx"}
Final answer: Your order #4821 has been **shipped**! Here are the details:
- **Status:** Shipped
- **Carrier:** FedEx
- **Estimated Delivery Date:** March 30
Your order is on its way. You can track it further through FedEx using your tracking number…Ce que l’API vérifie à chaque tour
Ces règles ne sont pas des conventions : si l’une est violée, l’API rejette la requête avec une erreur 400. L’examen les teste en décrivant un historique mal construit.
| Règle | Exemple de violation |
|---|---|
le tableau messages commence par un message user |
un historique qui démarre par la réponse de Claude |
chaque tool_result répond à un tool_use du message assistant juste avant |
le résultat ajouté sans la demande qui le précède |
le tool_use_id reprend exactement l’id du bloc tool_use |
un id recopié à la main, ou celui d’un tour précédent |
chaque tool_use reçoit son tool_result |
Claude demande deux outils, on n’en renvoie qu’un |
dans le message user, les blocs tool_result viennent en premier |
un texte placé avant les résultats |
Une conséquence importante : tous les résultats d’un même tour partent dans un seul message
user. Si Claude demande trois outils en parallèle, sa réponse contient trois blocs
tool_use. La liste tool_results en contient alors trois, et un seul append les envoie.
Les répartir sur trois messages casse la règle d’alternance.
Quand un outil échoue
Un outil peut planter : base injoignable, commande introuvable, délai dépassé. Ne levez pas d’exception qui interromprait la boucle : renvoyez l’échec à Claude, comme un résultat. Il pourra alors réessayer, demander une précision à l’utilisateur, ou expliquer le problème.
Le code ci-dessus renvoie un JSON {"error": ...}, ce qui fonctionne. L’API prévoit aussi un
champ dédié, plus explicite :
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": "Order 7777 not found",
"is_error": True, # signale à Claude que l'appel a échoué
})Ce code ne traite que deux cas. Si la réponse est coupée (stop_reason = "max_tokens"), aucun des deux if ne s'exécute. On ne sort pas, on n'ajoute rien, et la boucle renvoie exactement la même requête, jusqu'à 50 fois. Cinquante appels facturés pour rien.
Une version de production termine la boucle par un cas par défaut : lever une erreur, ou journaliser le stop_reason inattendu et sortir.
7. Pourquoi c’est le modèle qui choisit l’outil
Dans l’exemple, personne n’a écrit « si la question parle de commande, appelle
lookup_order ». C’est Haiku qui l’a décidé : il a lu la description de l’outil, l’a jugé
utile, et l’a demandé avec stop_reason = "tool_use". Votre code l’a exécuté et a renvoyé
le résultat. Claude a jugé que c’était suffisant et a terminé avec end_turn.
| Piloté par le modèle (agentique) | Préconfiguré (pas agentique) |
|---|---|
| Claude lit tout le contexte de la conversation | une logique if / else codée en dur |
| il choisit l’outil suivant en raisonnant | une séquence d’outils rigide, toujours la même |
| il peut appeler les outils dans n’importe quel ordre, ou en sauter | fragile dès qu’une situation nouvelle se présente |
| l’intelligence est dans le modèle | l’intelligence est dans le code |
Une question décrit un agent qui enchaîne des étapes fixes, ou qui teste le texte de la réponse pour décider de la suite ? Ce n'est pas une boucle agentique. La bonne réponse fait décider le modèle, et fait réagir le code à stop_reason, à rien d'autre.
8. Les trois anti-patterns testés à l’examen
Ils ont une cause commune : remplacer le signal structuré de l’API par une astuce bricolée.
Anti-pattern 1 : lire les mots de Claude
# ✗ Mauvais
if "I have completed" in response.text:
break
# ✓ Bon
if response.stop_reason == "end_turn":
breakPourquoi ça échoue. Claude ne formule jamais deux fois sa fin de la même façon. Et lors
d’un tour tool_use, il peut n’y avoir aucun bloc texte : lire le texte provoque alors
une IndexError et tout plante. C’est pour ça que le code de la section 6
prévoit return "". La règle : ne lisez pas le langage naturel, lisez le signal mécanique de l’API.
Anti-pattern 2 : un plafond d’itérations comme condition d’arrêt
# ✗ Mauvais : le plafond décide de la fin
if iterations >= 5:
break
# ✓ Bon : stop_reason décide, le plafond ne sert que de disjoncteur
while iteration < MAX_ITERATIONS: # garde-fou
...
if response.stop_reason == "end_turn": # vraie sortie
...Pourquoi ça échoue. Un plafond arbitraire ignore la réalité de la tâche. Une tâche qui finit en 3 itérations gaspille des appels si on la force à continuer. Une tâche qui en demande 8 est coupée au milieu. Le plafond reste utile, mais comme un disjoncteur : il protège l’installation, il n’éteint pas la lumière à votre place.
Anti-pattern 3 : tester le type du contenu
Pourquoi ça échoue. Claude écrit souvent une phrase avant d’appeler un outil (« Laissez-moi
vérifier… »), puis place le bloc tool_use. Sortir dès le premier bloc texte abandonne
l’appel d’outil en silence. L’agent échoue sans lever la moindre erreur. Le type de contenu
et stop_reason mesurent deux choses différentes : on ne remplace jamais l’un par l’autre.
9. L’implémentation de référence
Réduite à l’essentiel, voici la boucle de référence. Chaque ligne correspond à une règle :
while iteration < MAX_ITERATIONS: # la soupape de sécurité
response = client.messages.create(...)
if response.stop_reason == "end_turn": # la sortie principale
break
messages.append({"role": "assistant", # ajout 1 : la demande d'outil
"content": response.content})
# ... exécuter l'outil demandé dans response ...
messages.append({"role": "user", "content": [ # ajout 2 : le résultat, même id
{"type": "tool_result", "tool_use_id": block.id, "content": result}
]})Écrire la boucle soi-même, ou la déléguer
En production, on n’écrit pas toujours cette boucle à la main :
- le SDK Anthropic fournit un tool runner qui envoie la requête, exécute vos fonctions
outils et reboucle jusqu’à
end_turn; - le Claude Agent SDK va plus loin : il embarque la boucle complète de Claude Code, avec ses outils intégrés (fichiers, shell, recherche).
Ces outils automatisent exactement ce qui précède. Ils ne changent pas les règles : les
mêmes stop_reason, les mêmes paires tool_use / tool_result, le même historique. C’est
la mécanique que l’examen vérifie, et on la diagnostique d’autant mieux quand on l’a écrite
une fois soi-même.
Fiche de révision
| Les 5 règles d’or | |
|---|---|
| 1 | end_turn est la seule sortie valide de la boucle |
| 2 | tool_use signifie : exécuter, puis continuer |
| 3 | toujours ajouter le message assistant d’abord, le message user ensuite |
| 4 | le tool_use_id doit correspondre exactement à l’id de la demande |
| 5 | l’API est sans état : renvoyer tout l’historique à chaque appel |
| Anti-pattern | Conséquence |
|---|---|
| lire les mots de Claude | plantages (IndexError) |
| plafond d’itérations comme arrêt | tâches coupées trop tôt, appels gaspillés |
| tester le type du contenu | outils abandonnés en silence |
Cherchez l'ajout à l'historique qui manque, l'identifiant qui ne correspond pas, ou la condition d'arrêt bricolée. Dans les questions sur cet épisode, la mauvaise réponse est presque toujours l'une de ces trois-là.
Glossaire
| Terme | Définition |
|---|---|
| agent | un programme où le modèle décide lui-même des actions à enchaîner pour atteindre un but |
| boucle agentique | le cycle requête → stop_reason → exécution → ajout, répété jusqu’à end_turn |
| outil (tool) | une fonction de votre code, décrite à Claude par un nom, une description et un schéma |
tool_use |
un bloc de la réponse où Claude demande un outil, avec un id, un name et un input |
tool_result |
un bloc envoyé par votre code avec le résultat, rattaché par tool_use_id |
stop_reason |
le champ de la réponse qui dit pourquoi Claude s’est arrêté |
| sans état (stateless) | l’API ne garde rien d’un appel à l’autre : l’historique est à votre charge |
| soupape de sécurité | le plafond d’itérations, qui protège sans jamais servir de sortie normale |
Quiz d’entraînement
Ces questions sont de moi, écrites dans le style de l’examen. Ce ne sont pas des questions officielles. Choisissez une réponse : la correction s’affiche aussitôt, et le score se calcule en bas. Connecté, votre meilleur score est gardé dans votre compte, et « Recommencer » efface seulement vos réponses à l’écran. Sans compte, rien n’est enregistré.
Pour la suite
L’épisode 2 passe aux systèmes multi-agents et au modèle du coordinateur. Un agent coordinateur répartit le travail entre des sous-agents, en étoile. La bonne nouvelle : le coordinateur utilise exactement la boucle de cet épisode. La seule différence, c’est que ses outils lancent d’autres agents.