Leçon 01 · Fondamentaux

Sans boucle, pas d'agent

Tout le secret de Claude Code tient en une ligne : while stop_reason == "tool_use"

⏱ ~10 min · 📝 3 widgets interactifs · 🧑‍💻 Basé sur shareAI-lab · s01_agent_loop.py

Que fait concrètement un agent ?

Quand vous lancez Claude Code dans votre terminal et lui demandez de « regrouper tous les commentaires TODO dans une liste », vous le voyez : décider de lancer grep, lire quelques fichiers avec cat, puis produire du Markdown. Le modèle n'exécute pas le code lui-même — il se contente de demander son exécution. Ce qui lui donne l'apparence d'un vrai acteur, c'est la trentaine de lignes de code d'assemblage qui l'entourent.

Cette leçon démonte ce code d'assemblage pour l'examiner. On appelle cela l'agent loop (boucle d'agent), et sa structure est la suivante :

while response.stop_reason == "tool_use":
    response = LLM(messages, tools)      # 1. ask the model
    execute_tools(response.tool_calls)   # 2. run what it asked for
    messages.append(tool_results)        # 3. feed results back

C'est tout. Rien de plus. Le Claude Code de production empile par-dessus des permissions, des hooks, des sous-agents, l'isolation par worktree et la compression mémoire — mais le noyau reste ces quatre lignes.

Intuition clé : à chaque réponse, le modèle dit soit « je veux appeler un outil », soit « j'ai terminé ». La boucle continue tant qu'il est dans le premier état ; elle s'arrête dès qu'il entre dans le second. Le critère est le champ stop_reason de la réponse.

Voir messages[] grandir pas à pas

La boucle ci-dessous simule la tâche suivante : « Quels fichiers se trouvent dans le répertoire courant ? Et lis package.json. » Cliquez sur Step : chaque appui avance d'une action dans la boucle. À gauche, la vue conversation lisible par un humain ; à droite, le vrai tableau messages[] transmis au modèle — observez comment il s'allonge.

Les résultats d'outils doivent réintégrer l'historique

L'erreur la plus fréquente chez les débutants est de traiter « exécuter un outil » comme un effet de bord — une fois exécuté, affaire classée. Mais le modèle ne voit que messages[] lors du prochain cycle d'inférence : ce qui n'y figure pas n'a tout simplement pas existé à ses yeux. Oublier l'étape append suffit à casser toute la boucle.

Les erreurs ne sont pas toutes identiques. Deux variantes courantes :

  • Oublier l'append : le modèle ne verra pas le résultat et redemandera le même outil — vous obtenez une boucle infinie.
  • Append sans tool_use_id : l'API Anthropic retourne directement une erreur tool_result must have tool_use_id, et la boucle plante à l'appel API.

Même tâche : « Combien de fichiers Python y a-t-il dans ce projet ? » Les deux versions tournent en parallèle — observez la différence de résultat.

Décoder stop_reason

Chaque réponse du modèle contient un stop_reason. C'est ce champ qui décide si la boucle continue :

  • tool_use — le modèle veut appeler un outil, la boucle continue.
  • end_turn — le modèle a terminé, on sort de la boucle.
  • max_tokens — la génération a atteint la limite de tokens et a été tronquée, on sort de la boucle (à traiter comme une exception, en avertissant l'utilisateur que la sortie est incomplète).
  • stop_sequence — une séquence d'arrêt personnalisée a été rencontrée, on sort de la boucle.

Écrire à tort « continuer tant qu'il n'y a pas tool_use », ou ignorer max_tokens en le traitant comme un end_turn normal, sont des bugs classiques.

Tester localement

Clonez shareAI-lab/learn-claude-code en local :

git clone https://github.com/shareAI-lab/learn-claude-code
cd learn-claude-code
pip install -r requirements.txt
cp .env.example .env  # fill in ANTHROPIC_API_KEY
python agents/s01_agent_loop.py

Donnez-lui ensuite une vraie tâche : Compte combien de fichiers .py il y a dans ce dépôt. Vous le verrez émettre un ls -R, lire la sortie, envoyer un find . -name "*.py" | wc -l, puis vous donner la réponse. Tout cela, c'est la boucle en action.

Interactif

Widget 1 · Loop Stepper · croissance de messages[]

À retenir : les résultats d'outils sont réinjectés dans messages sous un format spécial (bloc tool_result avec tool_use_id) ; c'est le seul moyen pour le modèle de savoir, au prochain cycle, que « l'outil a été exécuté et a retourné X ».

Vue conversationNon démarré
Cliquez sur Step pour commencer →
messages[] JSONlength: 0
[]
stop_reason:
Interactif

Widget 2 · Break the Loop · implémentation correcte vs append oublié

À retenir : il suffit d'omettre une ligne messages.append(tool_results) pour que l'agent cesse d'en être un — il devient une machine qui oublie chaque tour ce qu'elle a fait au tour précédent.

while True:
    resp = LLM(msgs, tools)
    msgs.append({"role":"assistant",
                 "content": resp.content})
    if resp.stop_reason != "tool_use":
        return
    results = execute(resp.tool_uses)
    msgs.append({"role":"user",
                 "content": results})
Simulation
while True:
    resp = LLM(msgs, tools)
    msgs.append({"role":"assistant",
                 "content": resp.content})
    if resp.stop_reason != "tool_use":
        return
    results = execute(resp.tool_uses)
    # BUG: forgot to append results
    # msgs.append(...)
Simulation
Interactif

Widget 3 · Repérer stop_reason · 4 réponses réelles à analyser

Chaque exemple est un fragment de réponse possible du modèle. Cliquez sur « Continuer la boucle » ou « Sortir de la boucle » et voyez immédiatement si vous avez raison.

Correct : 0 / 4