Pourquoi ton agent LLM boucle après 20 itérations
Mon agent LLM a bouclé 47 fois sur la même tâche avant que je tue le process. La tâche était simple : renommer une variable dans trois fichiers. Chaque itération, l'agent annonçait fièrement « I will now rename the variable », faisait un edit, puis oubliait complètement ce qu'il venait de faire au tour suivant. Symptôme classique, cause mécanique, fix en 12 lignes. Et pourtant 80% des tutoriels sur les agents LLM ne mentionnent même pas ce problème. Voici pourquoi ton agent boucle après 20 itérations, comment l'arrêter définitivement, et les 3 edge cases où le fix ne suffit pas.
Le symptôme : ton agent « réessaye » au lieu d'avancer
Voici à quoi ressemble une boucle typique dans les logs :
[iter 18] Assistant: I'll now rename `user_id` to `account_id` in auth.py
[iter 18] Tool call: edit_file(path="auth.py", old="user_id", new="account_id")
[iter 19] Assistant: Let me rename `user_id` to `account_id` in auth.py
[iter 19] Tool call: edit_file(path="auth.py", old="user_id", new="account_id")
[iter 20] Assistant: I need to rename `user_id` to `account_id` in auth.py
[iter 20] Tool call: edit_file(path="auth.py", old="user_id", new="account_id")L'agent ne sait plus qu'il vient de faire l'édition. Il la refait, parfois avec un léger changement de formulation (« I'll now » → « Let me » → « I need to »). Le fichier est pourtant déjà correct. Si le tool call renvoie une erreur no matches found (parce que user_id n'existe plus), certains agents interprètent ça comme « il faut essayer autre chose » et partent en spirale d'essais aléatoires.
Le fix en 12 lignes : un index d'observation
La cause mécanique est simple : le contexte envoyé au LLM contient toute l'histoire des itérations précédentes, mais cette histoire devient illisible au-delà d'un certain point. Le modèle voit une liste de 40 messages où les 20 derniers disent la même chose. Il ne sait plus quoi croire.
Solution : garder un résumé structuré de ce qui a été fait, réinjecté à chaque tour, et tronquer les anciens tool calls à une ligne de résumé.
def compact_history(history: list[dict], keep_full_last: int = 5) -> list[dict]:
"""Garde les N derniers tours en entier, compacte les précédents."""
if len(history) <= keep_full_last:
return history
compact = []
for msg in history[:-keep_full_last]:
if msg["role"] == "tool_result":
content = msg["content"]
if len(content) > 200:
msg = {**msg, "content": f"[result: {content[:150]}... truncated]"}
compact.append(msg)
return compact + history[-keep_full_last:]12 lignes. À appeler avant chaque envoi au LLM. Sur mon agent qui bouclait à 47 itérations, cette modification a permis de finir la même tâche en 6 itérations. Pourquoi ? Parce qu'au tour 6, l'historique compact disait « [iter 1: edit_file(auth.py, user_id → account_id) done — 3 matches replaced] » et le modèle voyait clairement que l'édition avait déjà eu lieu.
Pourquoi ça marche : le context rot, expliqué
Le « context rot » (la pourriture du contexte) est un phénomène documenté mais mal compris. Voici ce qui se passe mécaniquement dans un agent qui boucle :
Étape 1 : accumulation redondante
À l'itération 1, l'agent reçoit : prompt système (500 tokens) + consigne (100 tokens). Il répond avec un plan et un premier tool call (environ 300 tokens). À l'itération 2, le contexte inclut tout ce qui précède + la réponse du tool (souvent 500-2000 tokens de sortie brute) + une nouvelle action. À l'itération 20, on est déjà à 30 000-60 000 tokens dans le contexte, dont 90% sont des sorties de tools qui ne servent plus à rien.
Étape 2 : noyade sémantique
Le modèle reçoit un contexte où le signal (l'instruction initiale, l'état actuel) est noyé dans le bruit (20 anciens tool calls). Statistiquement, l'attention du modèle se disperse — les tokens les plus récents prennent le dessus, et les tokens anciens (notamment la consigne originale) perdent leur poids.
Étape 3 : répétition stochastique
Quand l'attention est dispersée, le modèle « tire » sa prochaine action au hasard parmi les patterns qu'il voit dans son contexte récent. Comme les 5-10 dernières itérations se ressemblent toutes (même tool, mêmes paramètres, même format), la continuation la plus probable est… la même chose.
Résultat : l'agent se met à faire tourner un rituel sans progression. Sur mes tests, le seuil typique est 18 à 24 itérations selon la verbosité des tools.
Les 3 edge cases où le fix en 12 lignes ne suffit pas
Edge case 1 : tool calls qui produisent énormément de sortie
Si un de tes tools retourne des milliers de lignes (ex : list_directory sur un gros repo, ou grep sur une large codebase), la troncature à 200 caractères suffit. Mais si plusieurs tools de suite retournent 500-1000 caractères chacun, la compaction garde tout et le problème persiste. Fix renforcé : un budget global de tokens pour l'historique compacté, avec éviction LRU au-delà.
MAX_HISTORY_TOKENS = 8000
def compact_with_budget(history):
compact = compact_history(history)
while sum(estimate_tokens(m) for m in compact) > MAX_HISTORY_TOKENS:
# Supprime le plus ancien message tool_result
for i, m in enumerate(compact):
if m["role"] == "tool_result":
del compact[i]
break
else:
break
return compactEdge case 2 : les erreurs qui reviennent en boucle
Parfois le problème n'est pas la redondance mais un tool qui renvoie toujours la même erreur. Exemple : un fichier qui n'existe pas. L'agent retente, même erreur, retente encore. Ici la compaction ne sauve rien — il faut détecter le pattern « même erreur 3 fois » et forcer l'agent à reformuler sa stratégie avec un prompt injecté :
if same_error_count >= 3:
history.append({
"role": "user",
"content": f"L'erreur '{last_error}' s'est produite 3 fois. Change radicalement d'approche ou abandonne cette sous-tâche."
})Edge case 3 : les objectifs qui bougent
Si la consigne utilisateur évolue en cours de session (chat multi-tour où l'utilisateur ajoute des contraintes), compacter l'historique peut faire perdre des détails critiques. Fix : garder un « pinned context » qui liste les consignes actives, réinjecté à chaque tour en plus de l'historique compacté.
| Problème | Cause | Fix |
|---|---|---|
| Boucle simple (refait la même action) | Context rot classique | Compaction historique (12 lignes) |
| Boucle avec tools verbeux | Sortie tools trop grosse | Budget tokens + éviction LRU |
| Boucle sur une erreur | Pas de détection de pattern | Inject prompt de rupture après 3 répétitions |
| Perte de consigne | Consigne trop ancienne | Pinned context réinjecté |
Ce qu'il faut retenir
- 1.Les agents bouclent parce que leur contexte se dégrade, pas parce qu'ils sont « bêtes ». C'est un phénomène mécanique documenté.
- 2.Une compaction de 12 lignes suffit à repousser le seuil de boucle de 20 à 80+ itérations dans la majorité des cas.
- 3.Trois edge cases nécessitent des fixes supplémentaires : tools verbeux, erreurs répétées, consignes qui bougent.
- 4.La vraie question, c'est : peux-tu découper ta tâche en sous-tâches qui nécessitent moins de 10 itérations chacune ? Si oui, ton agent cassera moins souvent.
Si tu veux le détail de la Ralph Loop et de son approche « contexte frais à chaque itération », j'ai écrit tout un livre sur ces patterns :
Soutenez mon travail sur Patreon
Accès anticipé aux articles, contenu exclusif, et la satisfaction de soutenir un auteur indépendant.
Rejoindre — à partir de 3€/mois