Idée en 30 secondes
Tool mocking et fault injection permettent de reproduire des erreurs API de façon contrôlée, puis de vérifier comment l'agent les gère, sans réseau réel et sans bruit non déterministe.
La valeur principale : reproduire précisément timeout, 5xx ou une réponse cassée, puis valider retry, fallback et stop reason.
Le problème
Sans mocks ni fault injection, l'équipe voit généralement seulement le happy path :
- l'outil répond vite ;
- la réponse est valide ;
- l'agent termine le run sans erreur.
En production, c'est rare. Les outils peuvent retourner timeout, pannes partielles, champs vides ou latency instable.
Sans tests dédiés aux erreurs, cela mène souvent à :
- des pannes imprévisibles sur des scénarios critiques ;
- des répétitions d'appels sans fin ;
- des incidents coûteux et bruités, difficiles à reproduire.
Quand l'utiliser
Cette approche est nécessaire si l'agent dépend d'outils externes :
- API de paiement, CRM, recherche, services backend ;
- outils avec retry/backoff ;
- scénarios où un
stop_reasoncorrect est critique ; - scénarios avec fallback (par exemple outil de secours ou réponse sûre).
Si une panne d'outil peut être modélisée localement, c'est un bon candidat pour un test de fault injection.
Implémentation
En pratique, la règle est simple : un type de panne, un test, des conditions contrôlées. Les exemples ci-dessous sont schématiques et non liés à un framework précis.
Comment cela fonctionne dans un test
Cycle court d'un test avec fault injection
- Test case - un comportement à vérifier.
- Mock tool - fixer le contrat input/output.
- Inject fault - injecter une panne précise (
timeout,5xx,bad_payload). - Run - exécuter une étape concrète de l'agent.
- Assertions - vérifier retry, fallback,
stop_reasonet format d'erreur.
1. Fixer le contrat du mock tool
class FakePaymentsAPI:
def __init__(self, mode: str = "ok"):
self.mode = mode
def refund(self, order_id: str):
if self.mode == "ok":
return {"status": "approved", "order_id": order_id}
if self.mode == "timeout":
raise TimeoutError("payments_timeout")
if self.mode == "http_500":
raise RuntimeError("payments_500")
return {"status": None}
Le mock doit reproduire le contrat réel de l'outil aussi fidèlement que possible. Sinon, les tests donnent un faux sentiment de sécurité.
2. Injecter la panne de manière contrôlée
def test_timeout_fault_is_injected():
payments = FakePaymentsAPI(mode="timeout")
agent = Agent(payments_api=payments)
result = agent.handle_refund("order-8472")
assert result.stop_reason in {"tool_error_handled", "fallback_used"}
Le profil de panne doit être explicite et répétable : le même test doit toujours reproduire la même forme de panne.
3. Vérifier retry et fallback
def test_retry_then_fallback():
payments = FlakyPaymentsAPI(fail_times=2, then="timeout")
backup = FakeBackupTool()
agent = Agent(payments_api=payments, backup_tool=backup, max_retries=2)
result = agent.handle_refund("order-9001")
assert payments.calls == 2
assert result.selected_tool == "backup_tool"
assert result.stop_reason == "fallback_used"
Il faut vérifier non seulement le fait qu'une erreur se produit, mais aussi la politique de récupération qui suit.
Pour les flux de retry, il faut contrôler non seulement le nombre de tentatives, mais aussi les conditions où le système arrête de réessayer et passe en fallback ou en échec.
Pour les outils avec effets secondaires (changements d'état), il faut vérifier que retry ne crée pas d'opérations dupliquées.
4. Fixer la structure d'erreur
def test_error_envelope_is_stable():
payments = FakePaymentsAPI(mode="http_500")
agent = Agent(payments_api=payments)
result = agent.handle_refund("order-1122")
assert result.error["code"] == "tool_error"
assert result.error["tool"] == "payments_api"
assert result.error["retryable"] is True
Un format d'erreur stable simplifie le debug, les alertes et les checks de regression.
5. Exécuter ces tests dans CI
Ces tests doivent tourner sur chaque PR via l'étape standard pytest dans CI, si les changements touchent la logique d'outils, les retries ou les règles de fallback.
Erreurs typiques
Le mock ne correspond pas au contrat réel
Le test passe, mais en production l'agent tombe parce que la structure des champs ou le code d'erreur diffère.
Cause typique : le mock renvoie une payload simplifiée qui ne ressemble pas à l'API réelle.
Vérifier seulement le happy path
Les tests n'ont que "réponse réussie", sans timeout, 5xx ni payload invalide.
Cause typique : pas de liste obligatoire de profils de panne pour chaque outil critique.
Fault injection aléatoire
Le même test passe parfois et échoue parfois.
Cause typique : pannes aléatoires sans seed fixé ou timeouts instables.
Pas de vérification de stop_reason et error shape
L'équipe vérifie seulement le texte final, et la logique de recovery reste non testée.
Cause typique : absence d'assertions structurelles sur stop_reason, error.code, selected_tool.
Pas de vérification des effets secondaires pendant retry
Le retry gère l'erreur en apparence, mais crée une opération dupliquée ou une écriture en double.
Cause typique : tests limités à stop_reason et fallback, sans vérification de l'idempotence de la couche outil.
Mélange des checks unit et intégration
Le test est nommé unit, mais appelle une API réelle.
Cause typique : pas de frontière claire entre tests locaux (mocks/fault injection) et couche d'intégration.
En bref
- Tool mocking et fault injection valident la gestion des pannes d'outils par l'agent.
- Un type de panne doit être couvert par un test déterministe dédié.
- Vérifiez non seulement le texte, mais aussi retry, fallback,
stop_reasonet format d'erreur. - Les tests fault critiques doivent tourner sur chaque PR.
FAQ
Q : Peut-on tester les pannes sans API réelle ?
R : Oui. Au niveau unit, c'est standard : fakes et mocks donnent un signal stable et reproductible.
Q : Qu'est-ce qui compte le plus : retry ou fallback ?
R : Les deux. Retry couvre les pannes courtes ; fallback protège le scénario quand l'outil principal reste indisponible.
Q : Combien de profils de panne faut-il par outil ?
R : Minimum trois : timeout, erreur serveur (5xx) et payload invalide.
Q : Est-ce que cela remplace eval harness et regression ?
R : Non. Ces tests couvrent le comportement local de la couche outil. Le comportement système sur scénarios complets est validé via eval harness et regression.
Et ensuite
Ajoutez les cas de pannes dans Eval Harness et figez-les dans Golden Datasets. Pour contrôler les changements entre versions, ajoutez Regression Testing, et analysez les incidents avec Replay and Debugging.