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 :
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()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 :
- circuit breaker (fail-fast 30s)
- bulkhead par tool
- retries à un seul endroit avec backoff+jitter
- 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)
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)
Utilisé par les patterns
Pannes associées
Gouvernance requise
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)
- Foundations: Comment les agents utilisent des tools · Un agent prêt pour la prod
- Failure: Partial outage handling · Tool spam loops
- Governance: Tool permissions
- Production stack: Production stack