Pannes en cascade (comment un agent amplifie une outage) + code

  • Repère la panne tôt, avant que la facture grimpe.
  • Comprends ce qui casse en prod, et pourquoi.
  • Copie des garde-fous : budgets, stop reasons, validation.
  • Sache quand ce n’est pas la vraie cause.
Signaux de détection
  • Tool calls/run explosent (ou se répètent avec args hash).
  • Spend/tokens montent sans amélioration des outputs.
  • Retries passent de rares à constants (429/5xx).
Quand les tools dégradent, des retries naïfs et des boucles agent amplifient l’outage. Circuit breakers, bulkheads et safe-mode évitent que ton agent DDoS tes dépendances.
Sur cette page
  1. Le problème (côté prod)
  2. Pourquoi ça casse en prod
  3. 1) Retries naïfs
  4. 2) Agent retry + tool retry
  5. 3) Pas de circuit breaker
  6. 4) Pas de bulkhead (limite de concurrence)
  7. 5) Pas de safe-mode / fallback
  8. Exemple d’implémentation (code réel)
  9. Incident réel (avec chiffres)
  10. Compromis
  11. Quand NE PAS l’utiliser
  12. Checklist (copier-coller)
  13. Config par défaut sûre (JSON/YAML)
  14. FAQ (3–5)
  15. Pages liées (3–6 liens)
Flux interactif
Scénario:
Étape 1/2: Execution

Normal path: execute → tool → observe.

Le problème (côté prod)

Une dépendance devient flaky.

Ton agent réagit en l’appelant plus.

Elle devient encore plus flaky.

L’agent appelle encore plus.

Les agents amplifient. C’est un loop, pas de la magie.

Pourquoi ça casse en prod

1) Retries naïfs

Retries sans backoff/jitter = thundering herd.

2) Agent retry + tool retry

Client retries + wrapper retries + “try again” dans la loop = storm.

3) Pas de circuit breaker

Si c’est clairement dégradé, tu dois fail-fast pendant une fenêtre.

4) Pas de bulkhead (limite de concurrence)

Un tool lent ne doit pas saturer tous les workers.

5) Pas de safe-mode / fallback

Parfois, le bon comportement c’est : partial, stop reason, cache.

Exemple d’implémentation (code réel)

Mini circuit breaker + bulkhead devant un tool :

PYTHON
from dataclasses import dataclass
import time
from typing import Callable, Any


@dataclass
class Breaker:
  fail_threshold: int = 5
  open_for_s: int = 30
  failures: int = 0
  opened_at: float | None = None

  def allow(self) -> bool:
      if self.opened_at is None:
          return True
      if time.time() - self.opened_at > self.open_for_s:
          self.failures = 0
          self.opened_at = None
          return True
      return False

  def on_success(self) -> None:
      self.failures = 0
      self.opened_at = None

  def on_failure(self) -> None:
      self.failures += 1
      if self.failures >= self.fail_threshold:
          self.opened_at = time.time()


class Bulkhead:
  def __init__(self, *, max_in_flight: int) -> None:
      self.max_in_flight = max_in_flight
      self.in_flight = 0

  def enter(self) -> None:
      if self.in_flight >= self.max_in_flight:
          raise RuntimeError("bulkhead full")
      self.in_flight += 1

  def exit(self) -> None:
      self.in_flight = max(0, self.in_flight - 1)


def guarded_tool_call(fn: Callable[..., Any], *, breaker: Breaker, bulkhead: Bulkhead, **kwargs) -> Any:
  if not breaker.allow():
      raise RuntimeError("circuit open (fail fast)")

  bulkhead.enter()
  try:
      out = fn(**kwargs)
      breaker.on_success()
      return out
  except Exception:
      breaker.on_failure()
      raise
  finally:
      bulkhead.exit()
JAVASCRIPT
export class Breaker {
constructor({ failThreshold = 5, openForS = 30 } = {}) {
  this.failThreshold = failThreshold;
  this.openForS = openForS;
  this.failures = 0;
  this.openedAt = null;
}

allow() {
  if (!this.openedAt) return true;
  const elapsedS = (Date.now() - this.openedAt) / 1000;
  if (elapsedS > this.openForS) {
    this.failures = 0;
    this.openedAt = null;
    return true;
  }
  return false;
}

onSuccess() {
  this.failures = 0;
  this.openedAt = null;
}

onFailure() {
  this.failures += 1;
  if (this.failures >= this.failThreshold) this.openedAt = Date.now();
}
}

export class Bulkhead {
constructor({ maxInFlight = 10 } = {}) {
  this.maxInFlight = maxInFlight;
  this.inFlight = 0;
}
enter() {
  if (this.inFlight >= this.maxInFlight) throw new Error("bulkhead full");
  this.inFlight += 1;
}
exit() {
  this.inFlight = Math.max(0, this.inFlight - 1);
}
}

export async function guardedToolCall(fn, { breaker, bulkhead, args }) {
if (!breaker.allow()) throw new Error("circuit open (fail fast)");
bulkhead.enter();
try {
  const out = await fn(args);
  breaker.onSuccess();
  return out;
} catch (e) {
  breaker.onFailure();
  throw e;
} finally {
  bulkhead.exit();
}
}

Ce n’est pas de la résilience “enterprise”. C’est une ceinture.

Incident réel (avec chiffres)

Agent qui appelle une API vendor pour enrichissement. L’API devient flaky.

On avait :

  • client retries (2)
  • tool retries (2)
  • agent “try again” (illimité)

Impact :

  • vendor passe de “flaky” à “down”
  • workers saturés
  • latence p95 d’autres endpoints ~3x
  • on-call ~2h pour isoler le blast radius

Fix :

  1. circuit breaker (fail-fast 30s)
  2. bulkhead par tool
  3. retries à un seul endroit avec backoff+jitter
  4. safe-mode : skip enrichissement, renvoyer partiel

L’agent n’a pas déclenché l’outage. Il l’a amplifié.

Compromis

  • Fail-fast baisse le taux de succès pendant l’outage, augmente la survie globale.
  • Bulkheads peuvent rejeter sous charge. C’est mieux que tout bloquer.
  • Safe-mode est moins complet.

Quand NE PAS l’utiliser

  • Tools internes avec SLO solides : peut-être moins de breakers (mais budgets toujours).
  • Pas de safe-mode défini : ne fais pas d’autonomie pendant une outage.
  • Besoin de complétude stricte : async workflows.

Checklist (copier-coller)

  • [ ] Timeouts sur chaque tool call
  • [ ] Retries à un seul endroit (gateway), backoff + jitter
  • [ ] Circuit breaker par tool
  • [ ] Bulkhead concurrence par tool
  • [ ] Budgets/run
  • [ ] Safe-mode fallback
  • [ ] Alerting sur breaker open / erreurs / latence tools

Config par défaut sûre (JSON/YAML)

YAML
tools:
  timeouts_s: { default: 10 }
  retries: { max_attempts: 2, backoff_ms: [250, 750], jitter: true }
  circuit_breaker:
    fail_threshold: 5
    open_for_s: 30
  bulkhead:
    max_in_flight: 10
safe_mode:
  enabled: true
  allow_partial: true

FAQ (3–5)

Les retries ce n’est pas bien ?
Si, avec backoff et caps. Les retries non bornés dans une loop amplifient les outages.
Où mettre le circuit breaker ?
Dans le tool gateway. Un choke point, pas dans des prompts.
C’est quoi safe-mode ?
Comportement dégradé : moins de tools, read-only, cache, partiel, stop reason clair.
Je dois faire ça pour tous les tools ?
Commence par les externes et les coûteux. Timeouts + budgets partout.

Q : Les retries ce n’est pas bien ?
R : Si, avec backoff et caps. Les retries non bornés dans une loop amplifient les outages.

Q : Où mettre le circuit breaker ?
R : Dans le tool gateway. Un choke point, pas dans des prompts.

Q : C’est quoi safe-mode ?
R : Comportement dégradé : moins de tools, read-only, cache, partiel, stop reason clair.

Q : Je dois faire ça pour tous les tools ?
R : Commence par les externes et les coûteux. Timeouts + budgets partout.

Pages liées (3–6 liens)

Pas sur que ce soit votre cas ?

Concevez votre agent ->
⏱️ 5 min de lectureMis à jour Mars, 2026Difficulté: ★★☆
Implémenter dans OnceOnly
Guardrails for loops, retries, and spend escalation.
Utiliser dans OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Kill switch & arrêt incident
  • Audit logs & traçabilité
  • Idempotence & déduplication
  • Permissions outils (allowlist / blocklist)
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.
Exemple de policy (concept)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Auteur

Cette documentation est organisée et maintenue par des ingénieurs qui déploient des agents IA en production.

Le contenu est assisté par l’IA, avec une responsabilité éditoriale humaine quant à l’exactitude, la clarté et la pertinence en production.

Les patterns et recommandations s’appuient sur des post-mortems, des modes de défaillance et des incidents opérationnels dans des systèmes déployés, notamment lors du développement et de l’exploitation d’une infrastructure de gouvernance pour les agents chez OnceOnly.