Circuit breaker agents IA : prévenir les pannes
Implémentez un circuit breaker pour agents IA avec états, seuils, timeout, fallback et tests afin d'isoler un provider ou un outil instable.
Introduction
Un provider LLM qui ralentit peut immobiliser plusieurs runs avant que chaque timeout expire. Le pattern circuit breaker agents IA isole cette dépendance : après des échecs pertinents, il refuse temporairement les appels puis autorise une sonde de rétablissement. Ce tutoriel approfondit ce mécanisme précis, en complément du guide sur la résilience des agents IA. Il convient aux intégrations externes sujettes à des pannes répétées et dotées d’un mode dégradé sûr. En revanche, ce n’est probablement pas le bon choix pour un prototype peu sollicité ou une erreur ponctuelle : restez sur un timeout explicite. Vous allez définir les états, implémenter PyBreaker et vérifier les transitions par un test reproductible.
Résumé rapide
- Placez un breaker autour d’une dépendance cohérente, jamais autour de l’agent entier.
- Comptez les timeouts, erreurs réseau et indisponibilités, pas les refus métier ou les erreurs de validation.
- Ouvrez après un seuil adapté, refusez vite les nouveaux appels, puis autorisez une sonde après
reset_timeout. - Ne refermez qu’après le nombre de succès requis ; rouvrez si une sonde échoue.
- Déclenchez un fallback ou une revue humaine et testez que le provider n’est pas appelé quand le circuit est ouvert.
Quand un circuit breaker est la bonne décision
Le circuit breaker répond à une question étroite : faut-il encore tenter d’appeler cette dépendance maintenant ? Il devient utile lorsque des échecs transitoires se répètent, que chaque attente mobilise une ressource et qu’un mode dégradé existe. Il ne répare pas le provider et ne garantit pas sa disponibilité. Il peut seulement limiter la propagation d’une panne dans votre système.
| Mécanisme | Déclencheur | Décision | Effet |
|---|---|---|---|
| Timeout | Une tentative dépasse sa durée maximale | Interrompre cette tentative | Borne l’attente unitaire |
| Retry | Échec transitoire jugé récupérable | Relancer un nombre limité de fois | Donne une nouvelle chance à l’opération |
| Circuit breaker | Échecs pertinents au-delà du seuil | Refuser temporairement les appels | Isole la dépendance défaillante |
| Fallback | Appel épuisé ou circuit ouvert | Produire un mode dégradé sûr | Maintient un parcours partiel ou escalade |
| Rate limiting | Débit ou quota atteint | Retarder ou rejeter l’admission | Protège une capacité, pas la santé du service |
| Budget | Coût ou temps maximal atteint | Arrêter le run | Borne la consommation globale |
Un simple timeout suffit si le trafic est faible, si les erreurs ne se répètent pas ou si refuser les appels suivants n’apporte aucun bénéfice. À l’inverse, ajouter des retries bornés sans isolation peut accentuer la charge sur un service déjà instable. Avant de coder, identifiez donc la dépendance, le signal de panne, le mode dégradé et les conditions de retour au trafic normal.
Modéliser et implémenter le circuit autour d’une dépendance
Représenter les trois états
La machine d’état tient en trois états et quatre transitions principales :
FERMÉ -- seuil d'échecs atteint --> OUVERT
OUVERT -- reset_timeout écoulé --> SEMI-OUVERT
SEMI-OUVERT -- success_threshold atteint --> FERMÉ
SEMI-OUVERT -- une sonde échoue --> OUVERT
En état fermé, les appels traversent le breaker. Les succès maintiennent le circuit fermé ; les échecs comptabilisés augmentent son compteur. En état ouvert, l’appel est refusé rapidement sans invoquer la dépendance. Après la fenêtre de rétablissement, un nouvel appel sert de sonde et fait passer le circuit en semi-ouvert. Avec plusieurs succès requis, un premier succès peut laisser le circuit semi-ouvert ; seule l’atteinte de success_threshold le referme.
fail_max, reset_timeout et success_threshold sont des paramètres d’exploitation, pas des constantes universelles. Choisissez-les à partir du SLO de la dépendance, du volume d’appels, de la durée habituelle des incidents et du coût d’une sonde. Un seuil trop bas ouvre sur du bruit ; un seuil trop haut laisse chaque run subir la panne. Une fenêtre trop courte sollicite de nouveau un service encore indisponible.
Délimiter le breaker et sa politique d’erreurs
Créez une instance par point d’intégration cohérent : un provider LLM, un endpoint de recherche ou un outil de paiement, par exemple. Un breaker global autour de l’agent mélange des causes indépendantes et peut bloquer un outil sain à cause d’un autre. Conservez l’instance au niveau applicatif afin que plusieurs runs voient le même état dans un processus donné.
Comptez ce qui renseigne réellement sur la santé du service : timeout, rupture réseau, 502, 503 ou indisponibilité transitoire équivalente. Par défaut, un 401, un 403, un payload invalide, une erreur de validation ou un refus métier signale plutôt un problème de requête, de droit ou de logique. Traitez ces cas avant l’appel protégé ou excluez-les explicitement de votre politique ; ils ne doivent pas ouvrir le circuit sans justification documentée.
Avec plusieurs processus ou instances, une instance Python locale crée autant d’états indépendants. Un stockage partagé devient pertinent si tous les workers doivent prendre la même décision d’isolation. Il n’est pas obligatoire : un seul worker ou une isolation volontaire par instance peut rester local. La topologie et le rayon d’impact attendu déterminent le choix, pas une préférence systématique pour Redis.
Installer et encapsuler PyBreaker
Installez PyBreaker et pytest dans un environnement virtuel, puis figez les versions résolues dans le lockfile de votre projet avant le passage en CI :
python -m pip install pybreaker pytest
Créez provider_guard.py. La fausse dépendance rend les appels visibles et permet d’injecter des réponses ou des exceptions sans contacter un service externe.
from collections.abc import Iterable
import pybreaker
class FakeProvider:
def __init__(self, outcomes: Iterable[object]):
self.outcomes = iter(outcomes)
self.calls = 0
def complete(self, prompt: str) -> str:
self.calls += 1
outcome = next(self.outcomes)
if isinstance(outcome, BaseException):
raise outcome
return str(outcome)
provider_breaker = pybreaker.CircuitBreaker(
fail_max=2,
reset_timeout=0.05,
success_threshold=2,
)
def call_provider(provider: FakeProvider) -> str:
return provider_breaker.call(provider.complete, "requête de test")
def answer_with_fallback(provider: FakeProvider) -> dict[str, str]:
try:
return {"status": "ok", "answer": call_provider(provider)}
except (TimeoutError, pybreaker.CircuitBreakerError):
return {
"status": "degraded",
"answer": "Traitement différé pour revue humaine",
}
L’exemple emploie un reset_timeout très court uniquement pour accélérer le test local. Ce n’est pas une recommandation de production. provider_breaker.current_state expose l’état courant, tandis que CircuitBreakerError permet de distinguer le refus du breaker. La documentation officielle PyBreaker reste la référence de l’API.
Composer les couches sans compter deux fois
L’ordre opérationnel doit être explicite : admission par budget et limite de débit, timeout sur chaque tentative, retries strictement bornés sur les transitoires, breaker autour du résultat logique choisi, puis fallback pour l’agent IA ou escalade humaine. L’ordre entre retry et breaker change le compteur : si le breaker enveloppe chaque tentative, chacune peut l’incrémenter ; s’il enveloppe l’opération complète avec ses retries internes, seul l’échec final compte.
Choisissez cette granularité consciemment. Pour protéger un provider contre une rafale, compter chaque tentative peut être cohérent. Pour mesurer l’échec d’un run logique, compter seulement l’épuisement des retries est plus lisible. Dans les deux cas, le timeout reste nécessaire : le breaker ne peut pas classer rapidement un appel qui ne se termine jamais.
Exemple concret : tester les transitions et le mode dégradé
Créez test_provider_guard.py. Le scénario ouvre le circuit après deux échecs, prouve qu’un appel ouvert ne touche pas le provider, attend la vraie fenêtre de rétablissement, exige deux succès avant fermeture, puis vérifie qu’une sonde ultérieure en échec rouvre le circuit.
import time
import pybreaker
import pytest
from provider_guard import FakeProvider, call_provider, provider_breaker
def test_circuit_breaker_transitions():
provider = FakeProvider([
TimeoutError("timeout-1"),
TimeoutError("timeout-2"),
"ok-1",
"ok-2",
TimeoutError("timeout-3"),
TimeoutError("timeout-4"),
TimeoutError("probe-failed"),
])
with pytest.raises(TimeoutError):
call_provider(provider)
with pytest.raises(pybreaker.CircuitBreakerError):
call_provider(provider)
assert provider_breaker.current_state == "open"
assert provider.calls == 2
with pytest.raises(pybreaker.CircuitBreakerError):
call_provider(provider)
assert provider.calls == 2
time.sleep(0.06)
assert call_provider(provider) == "ok-1"
assert provider_breaker.current_state == "half-open"
assert call_provider(provider) == "ok-2"
assert provider_breaker.current_state == "closed"
with pytest.raises(TimeoutError):
call_provider(provider)
with pytest.raises(pybreaker.CircuitBreakerError):
call_provider(provider)
assert provider_breaker.current_state == "open"
assert provider.calls == 6
time.sleep(0.06)
with pytest.raises(pybreaker.CircuitBreakerError):
call_provider(provider)
assert provider_breaker.current_state == "open"
assert provider.calls == 7
Lancez python -m pytest -q test_provider_guard.py. Un code de sortie nul signifie que toutes les assertions ont réussi ; le test n’imprime ni benchmark ni disponibilité supposée. S’il devient instable sur une CI lente, augmentez ensemble le délai du test et reset_timeout au lieu d’inventer un hook temporel non documenté.
Bonnes pratiques et checklist de production
Évitez le breaker global, les seuils copiés sans mesure, les sondes concurrentes non maîtrisées et le fallback qui appelle le même service défaillant. Un réarmement manuel silencieux masque l’incident : toute transition doit rester observable. Journalisez seulement dependency, ancien état, nouvel état, classe d’erreur, durée d’ouverture et issue de la sonde. N’enregistrez ni prompt, email, nom, identifiant client, secret, contenu utilisateur ou trace brute.
Checklist avant déploiement :
- une dépendance cohérente par breaker et une politique d’erreurs documentée ;
- état local ou partagé choisi selon le nombre de workers et le rayon d’impact ;
- métriques par dépendance et alerte sur une ouverture prolongée ;
- timeout par tentative, retries bornés et ordre de comptage explicite ;
- fallback indépendant, sûr et compréhensible par l’utilisateur ;
- sonde limitée, runbook de réarmement et test des transitions en CI ;
- logs expurgés de toute donnée personnelle, secret ou contenu utilisateur.
Fiabiliser le déploiement de votre agent
Ce CTA est une prochaine étape éditoriale, pas une promesse de résultat. Un éventuel suivi ne doit partir qu’après consentement analytics explicite ; sans consentement, aucun événement. Son allowlist constante est : event_name=cta_click, cta_name=circuit_breaker_deployment_next, cta_location=after_production_checklist, source=OC-348+circuit-breaker-agents-ia, page_template=tutorial, page_path=/tutorials/circuit-breaker-agents-ia et destination_path=/tutorials/deployer-agent-ia. Excluez toute PII, tout secret et tout contenu utilisateur. Ce clic mesure une interaction interne : il ne constitue ni un lead, ni une vente, ni un revenu.
Questions fréquentes
Qu’est-ce qu’un circuit breaker pour agents IA ?
C’est une machine d’état placée autour d’un provider ou d’un outil externe. Elle laisse passer les appels en état fermé, les refuse rapidement en état ouvert et autorise une sonde en état semi-ouvert. Son rôle est d’isoler une dépendance défaillante, pas de rendre le provider fiable ni de remplacer les autres mécanismes de résilience.
Quelle différence entre retry et circuit breaker ?
Un retry relance une opération dont l’échec paraît transitoire. Le breaker observe une série d’échecs et décide de ne plus appeler temporairement la dépendance. Les deux se composent, mais leur ordre compte : le breaker peut comptabiliser chaque tentative ou uniquement l’échec final après épuisement des retries.
Quelles erreurs doivent ouvrir le circuit ?
Comptez les erreurs corrélées à la santé de la dépendance, comme les timeouts, ruptures réseau et indisponibilités transitoires. N’incluez pas par défaut les 401, 403, payloads invalides, validations ou refus métier. Ces erreurs décrivent souvent la requête ou les droits ; les compter produirait de faux signaux de panne provider LLM.
Faut-il partager l’état du breaker entre les workers ?
Oui si plusieurs processus doivent appliquer une décision d’isolation commune. Non si l’application n’a qu’un worker ou si chaque instance doit conserver son propre rayon d’impact. Le stockage partagé dépend donc de la topologie et du comportement voulu. Redis est une option possible, pas une obligation pour tout circuit breaker Python.
Articles liés
Un breaker est pertinent pour isoler une dépendance fragile et rendre ses transitions testables ; il est excessif lorsqu’un timeout simple couvre déjà le risque. En production, gardez chaque responsabilité séparée et choisissez le mode dégradé avant l’incident. La prochaine étape consiste à intégrer cette protection dans un déploiement observable.
Restez informé sur les agents IA
Nouveaux tutoriels, comparatifs et guides pratiques directement dans votre boîte mail.