Le problème
La demande paraît standard : construire un profil client et préparer une réponse courte.
Dans les traces, c'est différent : un tool externe a commencé à renvoyer timeout,
l'agent est passé en retries, a surchargé le worker pool après 4 minutes,
et 7 minutes plus tard, des workflows et services non liés ont commencé à se dégrader.
L'échec initial était local. Mais via la boucle de l'agent, il est devenu systémique.
Le système ne tombe pas immédiatement.
Il entraîne progressivement de plus en plus de dépendances.
Analogie : imagine un embouteillage sur une seule voie d'un pont. Au début, une seule voie ralentit. Puis l'onde d'arrêt atteint toutes les routes d'accès au pont. Une panne en cascade dans un agent fonctionne pareil : un problème local sans limites devient vite un problème partagé du système.
Pourquoi ça arrive
Une défaillance en cascade n'apparaît pas à cause d'une seule réponse "mauvaise" d'outil, mais parce que l'erreur est amplifiée simultanément dans plusieurs couches.
En production, cela ressemble souvent à ceci :
- un
toolse dégrade (5xx,429,timeout) ; - des retries démarrent à plusieurs endroits (SDK, gateway, agent) ;
- la queue grossit, les workers se bloquent en attente ;
- la latency augmente aussi pour d'autres runs, même sans ce
tool; - sans fail-fast et safe-mode, le système continue à multiplier les appels.
Le problème n'est pas seulement un service instable. Runtime n'arrête pas la vague tant qu'elle est encore locale.
Pannes les plus fréquentes
En production, quatre patterns de cascading failures reviennent le plus souvent.
Amplification des retries entre couches (Retry amplification)
Une panne se répète dans le client HTTP, le tool gateway et la boucle de reasoning de l'agent.
Le nombre d'appels augmente de façon géométrique.
Mini-exemple : 1 failure -> 3 retries dans SDK -> 3 retries dans gateway -> 3 retries dans agent loop = 27 appels.
Cause typique : retry policy dispersée à plusieurs endroits.
Saturation du pool partagé (Shared pool saturation)
Un tool dégradé occupe la majorité des workers.
Les autres runs attendent en queue, même si leurs dépendances sont saines.
Cause typique : pas de per-tool bulkhead limits.
Dominos de timeout dans services voisins (Timeout domino)
Quand la queue grossit, le wait time augmente.
En conséquence, les services upstream/downstream tombent plus souvent en timeout.
Cause typique : pas de max_seconds strict et pas de fail-fast lors de dégradation de dépendance.
Cascade de coûts sur panne technique (Cost cascade)
La cascade augmente aussi le coût du run : plus de retries, plus de tokens, cycle de run plus long. Même les fins "réussies" deviennent trop coûteuses.
Cause typique : absence d'execution budgets (max_tool_calls, max_retries, max_usd).
Comment détecter ces problèmes
Les pannes en cascade se voient bien via la combinaison des métriques gateway, runtime et queue.
| Métrique | Signal de cascading failure | Action |
|---|---|---|
retry_amplification_rate | une panne crée beaucoup de retries dupliqués | centraliser les retries dans un seul gateway |
circuit_open_rate | breaker s'ouvre souvent sur un tool | activer safe-mode et réduire le fan-out |
queue_backlog | la queue grossit avec un trafic d'entrée normal | ajouter bulkhead limits et timeout sur le run |
cross_service_timeout_rate | des timeouts apparaissent dans des services non liés | isoler le tool dégradé et limiter la concurrence |
cascading_stop_reason_rate | cascade:* stop reasons fréquentes | revoir breaker/bulkhead et stratégie fallback |
Comment distinguer une panne en cascade d'une erreur locale d'outil
Un tool_timeout ne signifie pas toujours cascade.
Question clé : la panne reste-t-elle locale, ou impacte-t-elle déjà d'autres parties du système.
Normal si :
- la panne reste isolée dans un seul tool ;
- queue et latency des autres runs restent stables ;
- après un court cooldown, le système revient au baseline.
Dangereux si :
- l'erreur d'un
toolaugmente globalementqueue_backlog; - des timeouts apparaissent dans des workflows non liés ;
- coût et durée des runs augmentent même là où ce
tooln'est pas utilisé.
Comment stopper ces pannes
En pratique :
- garder les retries dans un seul choke point (tool gateway) ;
- mettre per-tool circuit breaker + cooldown + bulkhead limits ;
- définir des execution budgets pour retries, tool calls, temps et coût ;
- en dégradation, basculer le run en safe-mode (partial/fallback), pas "forcer".
Guard minimal contre cascade :
from dataclasses import dataclass
import time
RETRYABLE = {408, 429, 500, 502, 503, 504}
@dataclass(frozen=True)
class CascadeLimits:
max_steps: int = 25
max_seconds: int = 90
max_tool_calls: int = 18
max_retries: int = 4
max_in_flight_per_tool: int = 8
open_circuit_after: int = 3
circuit_cooldown_s: int = 30
class CascadeGuard:
def __init__(self, limits: CascadeLimits = CascadeLimits()):
self.limits = limits
self.steps = 0
self.tool_calls = 0
self.retries = 0
self.in_flight: dict[str, int] = {}
self.fail_streak: dict[str, int] = {}
self.circuit_open_until: dict[str, float] = {}
self.started_at = time.time()
def on_step(self) -> str | None:
self.steps += 1
if self.steps > self.limits.max_steps:
return "cascade:budget_max_steps"
if (time.time() - self.started_at) > self.limits.max_seconds:
return "cascade:budget_timeout"
return None
def before_tool_call(self, tool: str) -> str | None:
if time.time() < self.circuit_open_until.get(tool, 0.0):
return "cascade:circuit_open"
current = self.in_flight.get(tool, 0)
if current >= self.limits.max_in_flight_per_tool:
return "cascade:bulkhead_full"
self.tool_calls += 1
if self.tool_calls > self.limits.max_tool_calls:
return "cascade:budget_tool_calls"
self.in_flight[tool] = current + 1
return None
def after_tool_call(self, tool: str, status_code: int) -> str | None:
self.in_flight[tool] = max(0, self.in_flight.get(tool, 1) - 1)
if status_code in RETRYABLE:
self.retries += 1
if self.retries > self.limits.max_retries:
return "cascade:retry_budget"
streak = self.fail_streak.get(tool, 0) + 1
self.fail_streak[tool] = streak
if streak >= self.limits.open_circuit_after:
self.circuit_open_until[tool] = time.time() + self.limits.circuit_cooldown_s
return "cascade:circuit_open"
return "cascade:retry_allowed"
self.fail_streak[tool] = 0
return None
C'est un guard de base.
Dans cette version, tool_calls compte les tentatives d'appel, pas seulement les appels admis avec succès.
En production, on l'étend généralement avec priorisation de requêtes,
limites séparées pour tool critiques et route safe-mode explicite.
before_tool_call(...) est appelé avant l'appel externe,
et after_tool_call(...) juste après la réponse pour étouffer la cascade le plus tôt possible.
Où c'est implémenté dans l'architecture
En production, le contrôle des cascading failures est généralement réparti sur trois couches système.
Tool Execution Layer est la première barrière : retry policy, circuit breaker, bulkhead, timeout et normalisation d'erreurs. Si cette couche est faible, une panne locale devient vite une vague.
Agent Runtime gère les budgets,
stop reasons (cascade:*) et transitions safe-mode.
C'est ici qu'il faut stopper un run avant saturation système.
Orchestration Topologies définit comment isoler des branches workflow dégradées, pour qu'un chemin dégradé ne bloque pas tout le workflow.
Auto-vérification
Vérification rapide avant release. Coche les points et regarde le statut ci-dessous.
C'est un sanity-check court, pas un audit formel.
Progression: 0/8
⚠ Il y a des signaux de risque
Il manque des contrôles de base. Fermez les points clés de la checklist avant release.
FAQ
Q : Les retries sont utiles. Pourquoi peuvent-ils casser le système ?
R : Utiles seulement avec backoff, caps et point de contrôle unique. Quand les retries sont dupliqués entre couches, la charge monte plus vite que la récupération.
Q : Pourquoi les systèmes d'agents sont plus sensibles à la cascade que les API classiques ?
R : Parce qu'un agent a une boucle de reasoning et peut répéter le même tool_call plusieurs fois. La panne de dépendance se multiplie à chaque étape du run.
Q : Timeout ne suffit pas ? Pourquoi ajouter breaker et bulkhead ?
R : Timeout limite un appel. Breaker arrête la vague de répétition, et bulkhead empêche un tool de monopoliser tous les workers.
Q : Safe-mode ne dégrade pas la qualité de réponse ?
R : Partiellement oui, mais c'est une dégradation contrôlée. Mieux vaut renvoyer un résultat partiel correct qu'attendre un outage complet.
Une cascading failure ressemble rarement à une seule grosse erreur. Le plus souvent, c'est une chaîne de petites pannes que le système amplifie lui-même. Principe clé : agent loops amplify failures (les agents amplifient les pannes). C'est pourquoi les agents de production ont besoin non seulement de bons modèles, mais aussi de limites strictes au niveau runtime et gateway.
Pages liées
Si ce problème apparaît en production, ces pages sont aussi utiles :
- Pourquoi les agents IA échouent - carte générale des pannes en production.
- Tool failure - comment une panne locale d'outil devient une cascade.
- Tool spam - comment les appels répétés accélèrent la dégradation.
- Budget explosion - comment une cascade devient un incident financier.
- Partial outage - comment opérer en mode dégradation partielle des dépendances.
- Agent Runtime - où implémenter budgets, stop reasons et safe-mode.
- Tool Execution Layer - où placer retries, breaker et bulkhead.