Imaginons un agent de recherche qui entre dans une boucle de réessai non détectée après avoir reçu une réponse d'outil ambiguë. Sans condition de terminaison autre qu'un plafond de pas par session (que la boucle réinitialise à chaque nouvelle sous-tâche) et sans alerte de coût en place, l'agent effectue 24 847 appels API sur 9 heures et 14 minutes, accumulant 3 218 $ en dépenses API avant qu'un ingénieur ne remarque l'anomalie lors d'une vérification matinale de routine. Ce billet parcourt cette défaillance de bout en bout.
Minute par minute, puis heure par heure
La chronologie de l'incident ci-dessous enregistre chaque changement d'état, de l'invocation normale à la contention. Les chiffres de dépenses indiquent le coût API cumulatif à chaque point de contrôle.
| heure | état | ce qui s'est passé | dépenses |
|---|---|---|---|
| 22 h 04 | Normal | Agent de recherche invoqué pour l'analyse concurrentielle nocturne. Tâche planifiée. L'agent avait pour mission de rechercher les changements de tarification de 12 produits concurrents. Invocation normale. Aucune anomalie dans les 3 premières minutes d'exécution. | s.o. |
| 22 h 07 | Avertissement (non détecté) | La réponse de l'outil retourne un résultat ambigu pour le concurrent no 7. Le scraper a retourné un statut 200 avec un corps vide, un cas limite connu sur un site à limitation de débit. L'agent a interprété cela comme « tâche incomplète, réessayer ». C'était la bonne interprétation. Le problème résidait dans ce qui s'est passé ensuite. | ~0,12 $ |
| 22 h 07 – 22 h 19 | Début de la boucle | L'agent génère une sous-tâche et réinitialise son propre compteur de pas. La logique de réessai de l'agent a généré une nouvelle sous-tâche pour le concurrent no 7. La sous-tâche a hérité d'un budget de pas neuf. Elle a également échoué. Elle a généré une autre sous-tâche. Le plafond de pas, censé briser la boucle, était par tâche, non par workflow. L'agent avait trouvé l'écart entre les deux portées et s'y engouffrait. | ~18 $ · ~1,50 $/min |
| 22 h 19 – 04 h 30 nuit | Boucle active · non détectée | La boucle tourne pendant 6 heures et 11 minutes sans alerte. Aucune alerte de seuil de coût n'existait. Le tableau de bord des dépenses API se mettait à jour toutes les heures. L'équipe de permanence n'y était pas abonnée. L'agent était sain selon toutes les métriques surveillées : aucune erreur, aucun délai d'attente, aucune requête échouée. Il effectuait des appels API réussis en boucle. Du point de vue de l'infrastructure, il fonctionnait parfaitement. | ~2 900 $ · ~24 000 appels |
| 07 h 18 | Détection | Un ingénieur remarque une utilisation API anormale lors de la vérification matinale. Pas une alerte automatique. Un ingénieur a ouvert le tableau de bord du fournisseur lors de sa routine matinale et a vu le graphique d'utilisation. Détection manuelle, 9 heures et 14 minutes après le début de la boucle. L'agent a été arrêté immédiatement. | 3 218 $ · 24 847 appels |
| 07 h 22 | Contenu | Agent arrêté, incident déclaré, post-mortem commencé. Le workflow a été interrompu. Aucun système orienté client n'a été affecté, il s'agissait d'une tâche d'analyse en arrière-plan. Le rapport d'analyse concurrentielle n'a pas été produit. Le post-mortem a été lancé le matin même. | s.o. |
La courbe des dépenses, heure par heure
Dépenses API totales sur la fenêtre de l'incident. La boucle s'emballe après 23 h 00, plafonne à environ 400 $/heure pendant la nuit, et est interrompue par l'arrêt manuel à 07 h 18.
Trois mesures de protection absentes. Toutes les trois requises.
Ce n'était pas un cas limite rare. Le déclencheur, une réponse 200 vide d'un scraper à limitation de débit, survient sur environ 3 % des invocations du scraper. La boucle était un résultat prévisible d'une entrée prévisible, sans défense en profondeur. Le post-mortem a identifié cinq facteurs contributifs. Trois étaient des mesures de protection indépendantes qui, si l'une d'elles avait été en place, auraient contenu l'incident.
| # | facteur | sévérité |
|---|---|---|
| 01 | Absence de budget de confiance, cause racine principale. L'agent n'avait aucun mécanisme pour détecter que sa progression stagnait. Chaque réessai était indiscernable d'un travail productif au niveau de l'infrastructure : un appel était effectué, un résultat était reçu. Un budget de confiance aurait mesuré la nouveauté de chaque observation par rapport au contexte de session et aurait détecté, en 8 à 10 itérations, que l'agent ne recevait aucune nouvelle information. Il aurait déclenché un repli gracieux plutôt que de continuer à dépenser. Voir Construire un planificateur d'agent qui sait s'arrêter. | Cause racine |
| 02 | Plafond de pas par tâche, non par workflow. Le plafond de pas existant était une bonne idée, mal délimitée. Plafonner les pas par sous-tâche tout en autorisant une génération illimitée de sous-tâches revient à n'avoir aucun plafond : tout agent capable de récurser s'y soustraira. Le plafond devait être au niveau du workflow, comptabilisant tous les pas de toutes les sous-tâches dans un seul arbre d'invocation. C'est une erreur de conception, non de configuration. Voir Pourquoi nous avons (presque) arrêté de construire des orchestrateurs custom. | Cause racine |
| 03 | Absence d'alerte de coût, l'échec de détection. L'incident a duré 9 heures et 14 minutes sans être détecté. Le tableau de bord du fournisseur se mettait à jour toutes les heures et n'était pas surveillé en dehors des heures de bureau. Aucune alerte n'existait pour les anomalies de taux de dépenses, les dépenses cumulatives par session, ou les appels par minute au-delà d'un seuil. Si une alerte de coût de 50 $ par session avait existé, l'incident aurait été détecté en 33 minutes et aurait coûté moins de 100 $. Voir 0,0004 $ par étape d'agent : comment nous avons fait du coût une métrique prioritaire. | Cause racine |
| 04 | Absence de test de régression pour le comportement en boucle. Le harnais d'éval de l'époque testait la qualité des sorties sur des cas nominaux. Il n'incluait pas de test vérifiant que l'agent se terminait correctement sur des réponses d'outil ambiguës, exactement l'entrée qui a déclenché cet incident. Un seul cas de test (« réponse 200 vide de l'outil, l'agent doit se terminer gracieusement en N pas ») aurait détecté la condition de terminaison manquante au moment du déploiement. Voir La suite d'éval en 6 lignes que nous livrons avec chaque agent. | Contributif |
| 05 | Équipe de permanence non abonnée aux anomalies de dépenses. Même sans alerte automatique, le tableau de bord du fournisseur affichait des dépenses anormales dès 23 h 00. L'ingénieur de permanence n'était pas abonné aux notifications du tableau de bord et ne l'a pas consulté pendant la nuit. La lacune de surveillance était organisationnelle, non technique, ce qui explique pourquoi la correction technique (alertes de coût automatiques) était nécessaire mais pas suffisante. | Détection manquée |
À quoi ressemblait le code. Avant et après.
La condition de terminaison manquante tenait en trois lignes : un compteur de pas au niveau du workflow et une vérification avant chaque génération de sous-tâche. L'alerte de coût était une seule règle CloudWatch. Aucune des deux n'exigeait de changements architecturaux. Les deux ont nécessité l'incident pour sembler nécessaires.
# ── BEFORE: sub-task spawn with no workflow-level guard ────────── def spawn_subtask(task: str, context: dict) -> dict: # No check on total workflow steps. This is the gap. sub_agent = ResearchAgent(max_steps=20) # per-task cap only return sub_agent.run(task, context) # ── AFTER: workflow-level budget enforced across all sub-tasks ──── @dataclass class WorkflowBudget: max_steps: int = 100 # across ALL sub-tasks max_spend_usd: float = 5.00 # hard dollar cap steps_used: int = 0 spend_usd: float = 0.0 def check(self) -> None: if self.steps_used >= self.max_steps: raise WorkflowBudgetExceeded(f"Step limit reached") if self.spend_usd >= self.max_spend_usd: raise WorkflowBudgetExceeded(f"Spend limit $ reached") def spawn_subtask(task: str, context: dict, budget: WorkflowBudget) -> dict: budget.check() # enforced before spawn sub_agent = ResearchAgent( max_steps=20, budget=budget, # shared budget object on_step=lambda cost: setattr( budget, "spend_usd", budget.spend_usd + cost ) ) return sub_agent.run(task, context) # The confidence budget (the planner post) sits above this. It # detects stalling before the dollar cap is reached. The dollar # cap is the last line of defense, not the first.
Le WorkflowBudget est transmis à chaque génération de sous-tâche. Les sous-tâches ne reçoivent pas un budget neuf, elles puisent dans le même pool. Le plafond en dollars est fixé de façon conservatrice pour le premier déploiement de tout nouveau workflow et relevé au fur et à mesure que la confiance dans la plage de dépenses attendue du workflow se consolide. Le budget de confiance du billet sur le planificateur se situe en amont et détecte généralement les boucles emballées avant qu'elles n'atteignent le plafond en dollars. Le plafond en dollars est la terminaison de dernier recours.
Ce qui a changé. Dans son intégralité.
| action | responsable | statut |
|---|---|---|
| Livraison du budget de confiance à tous les workflows d'agents. Le budget de confiance déployé en production. Détecte les boucles de stagnation en nouveauté avant qu'elles ne consomment un budget significatif. La latence p99 a chuté de 38 % en effet de bord. | Infra | Terminé |
Remplacement du plafond de pas par tâche par un budget au niveau du workflow. WorkflowBudget livré. Toutes les générations de sous-tâches exigent une référence de budget partagé. max_spend_usd par défaut : 5 $ pour les nouveaux workflows, relevé par workflow après révision.
| Infra | Terminé |
| Alertes de coût par session, notification en moins de 5 minutes. La trace de coût des travaux sur le coût comme métrique déployée. L'alerte CloudWatch se déclenche lorsqu'une session dépasse 10 $. L'équipe de permanence est notifiée en moins de 4 minutes. Testé chaque semaine. | Plateforme | Terminé |
| Ajout de cas de test de comportement en boucle au harnais d'éval. Trois nouveaux cas de test ajoutés à la suite d'éval : réponse d'outil 200 vide, réponse de limitation de débit, réponse de délai d'attente. Tous doivent se terminer en 5 pas via un repli gracieux. | AQ | Terminé |
| Migration des workflows restants vers LangGraph avec budget partagé. 7 des 11 workflows à runtime maison migrés vers LangGraph, qui applique l'état au niveau du workflow, budget inclus. Les 4 restants figurent sur la liste de migration du T3. | Infra | En cours |
| Runbook d'anomalie de dépenses, procédure de réponse de permanence. Runbook rédigé. Couvre : confirmer l'alerte, identifier le workflow, l'arrêter, préserver les journaux pour le post-mortem, vérifier les effets en aval. Pas encore intégré à la formation de permanence. | Permanence | Ouvert |
Trois choses que cet incident a changées définitivement
La défense en profondeur n'est pas optionnelle pour les boucles agentiques. Toute mesure de protection unique peut être contournée : le plafond de pas l'a été. Le budget de confiance, le plafond de dépenses au niveau du workflow, et l'alerte de coût en temps réel sont trois défenses indépendantes. Si l'une manque, les autres peuvent encore contenir un incident. Si les trois manquent, vous rédigez un post-mortem.
Le succès invisible est le mode de défaillance le plus difficile à détecter. La boucle a effectué 24 847 appels API réussis. Chaque appel a retourné 200. Aucune erreur n'a été levée. La pile de surveillance voyait un système sain. Le seul signal anormal était les dépenses, que personne ne surveillait. Les métriques de succès ne détectent pas les boucles, elles les confirment.
Les mesures de protection de cette série sont listées par ordre de prévention des incidents, non de complexité d'implémentation. Le budget de confiance avait été écrit avant cet incident mais n'avait pas été déployé sur tous les workflows. Le harnais d'éval existait mais ne couvrait pas ce mode de défaillance. Le traceur de coût n'existait pas encore. Les trois billets ont été écrits, au moins en partie, à cause de cette nuit-là.
Si vous souhaitez que nous testions les mesures de protection de votre propre stack d'agents, le formulaire de contact est le moyen le plus rapide. Nous offrons des révisions de 30 minutes pour les stacks d'agents en production, gratuitement.