DEV Community

Cover image for Résilience aux erreurs des agents IA : Modèles de Réessai, Délai d'attente, Backoff et Coupe-circuit
Antoine Laurent
Antoine Laurent

Posted on • Originally published at apidog.com

Résilience aux erreurs des agents IA : Modèles de Réessai, Délai d'attente, Backoff et Coupe-circuit

Votre agent appelle une API. L’API répond 429. L’agent réessaie immédiatement, reçoit un autre 429, puis recommence jusqu’à l’arrêt de l’exécution ou à l’explosion des coûts. Cette boucle n’est généralement pas intentionnelle : elle vient d’une gestion d’erreur naïve et constitue l’un des problèmes les plus fréquents évoqués sur le forum du SDK Anthropic.

Essayez Apidog dès aujourd’hui

La fiabilité d’un agent dépend moins du modèle que de son comportement lorsqu’un outil est lent, limité ou indisponible. Une récupération bien conçue transforme une dépendance instable en courte pause. Une récupération absente transforme un simple 500 en incident.

Ce guide couvre les quatre mécanismes à implémenter :

  1. Réessais avec backoff exponentiel et jitter.
  2. Délais d’attente à chaque niveau.
  3. Disjoncteurs par dépendance.
  4. Clés d’idempotence pour les opérations modifiant un état.

Vous verrez également comment tester ces scénarios contre un mock avant qu’un utilisateur ne découvre les failles en production. Pour une vue d’ensemble, consultez pourquoi les agents d’IA tombent en panne en production.

Vous ne pouvez pas tester la récupération contre une API saine

En développement, la dépendance répond souvent correctement. Vos appels réussissent, votre démo fonctionne, puis vous déployez sans avoir exécuté une seule fois le chemin de récupération.

Le problème est simple : une API saine ne produit pas les erreurs que votre code est censé gérer.

Pour tester la récupération, provoquez volontairement les échecs :

  1. Créez un mock de l’API appelée par l’outil de votre agent.
  2. Configurez des réponses 429, 500, des timeouts ou des corps invalides.
  3. Pointez votre agent vers le mock plutôt que vers le service réel.
  4. Vérifiez précisément les tentatives, les délais et la réponse finale.

L’échec devient ainsi un scénario de test reproductible, pas une alerte à 3 heures du matin. Apidog peut servir à configurer ce mock et à script les réponses nécessaires.

Réessayer avec un backoff exponentiel et du jitter

Un réessai immédiat est rarement une bonne stratégie. Lorsqu’un service est sous charge, chaque client en échec réessaie au même moment. Le trafic de récupération devient alors une charge supplémentaire qui empêche le service de récupérer.

Utilisez deux mécanismes ensemble :

  • Backoff exponentiel : augmentez l’attente entre les tentatives.
  • Jitter : ajoutez une variation aléatoire pour désynchroniser les clients.

Exemple de délais :

Tentative 1 : 1 s
Tentative 2 : 2 s
Tentative 3 : 4 s
Tentative 4 : 8 s
Enter fullscreen mode Exit fullscreen mode

Ajoutez ensuite un jitter, par exemple entre 0 et 500 ms, à chaque délai.

Exemple Python :

import random
import time

MAX_ATTEMPTS = 4
MAX_DELAY_SECONDS = 8

def retry_with_backoff(call):
    for attempt in range(MAX_ATTEMPTS):
        try:
            return call()
        except Exception:
            if attempt == MAX_ATTEMPTS - 1:
                raise

            base_delay = min(2 ** attempt, MAX_DELAY_SECONDS)
            jitter = random.uniform(0, 0.5)
            time.sleep(base_delay + jitter)
Enter fullscreen mode Exit fullscreen mode

Encadrez toujours les réessais avec :

  • un nombre maximal de tentatives ;
  • un délai maximal entre tentatives ;
  • une liste explicite d’erreurs réessayables.

En pratique, trois à cinq tentatives couvrent la plupart des erreurs transitoires. Au-delà, vous réessayez souvent un problème permanent.

Le SDK Anthropic applique déjà une partie de cette logique à ses propres appels : il réessaie certaines erreurs de connexion et certains codes HTTP avec backoff exponentiel, avec un plafond configurable via max-retries. En revanche, cela ne couvre pas les autres API appelées par les outils de votre agent.

Pour les API à fort impact, notamment financières, les réessais doivent être conçus avec encore plus de prudence. Consultez cette analyse de la logique de réessai pour les API à enjeux élevés.

Définir un délai d’attente pour chaque appel

Un réessai ne peut fonctionner que si l’appel échoue. Or, le pire cas est souvent un appel qui ne renvoie jamais : la connexion est établie, puis la dépendance se bloque.

Sans timeout :

  • l’appel d’outil reste suspendu ;
  • l’exécution de l’agent est bloquée ;
  • aucun mécanisme de récupération ne démarre ;
  • votre budget de temps et de tokens continue d’être consommé.

Définissez au minimum trois budgets :

Budget Rôle
Timeout de connexion Temps maximal pour établir la connexion
Timeout de lecture Temps maximal pour recevoir la réponse
Budget global d’exécution Temps maximal pour l’ensemble du run de l’agent

Exemple avec httpx :

import httpx

timeout = httpx.Timeout(
    connect=2.0,
    read=10.0,
    write=5.0,
    pool=2.0,
)

with httpx.Client(timeout=timeout) as client:
    response = client.get("https://api.example.com/data")
Enter fullscreen mode Exit fullscreen mode

Lorsqu’un timeout se déclenche, traitez-le comme une erreur potentiellement réessayable, dans les limites définies par votre stratégie.

Ne choisissez pas les valeurs au hasard. Basez-les sur la latence réelle de la dépendance :

  • partez du p99 observé ;
  • ajoutez une marge de sécurité ;
  • utilisez un budget séparé pour les réponses en streaming.

Un timeout trop court annule des requêtes qui auraient réussi. Un timeout trop long conserve un agent bloqué bien après le point d’utilité.

Déclencher un disjoncteur lorsqu’une dépendance est en panne

Le backoff convient à un service temporairement occupé. Il ne résout pas le cas d’un service durablement indisponible.

Si une API est en panne depuis plusieurs minutes, continuer à attendre puis réessayer dégrade l’expérience utilisateur et ajoute de la charge à une dépendance déjà cassée.

Un disjoncteur (circuit breaker) utilise trois états :

  1. Fermé : les requêtes passent normalement et les échecs sont comptabilisés.
  2. Ouvert : après un seuil d’échecs, les requêtes échouent immédiatement pendant une période de refroidissement.
  3. Semi-ouvert : après le refroidissement, une requête de test est autorisée.

Le comportement attendu est :

fermée -> échecs répétés -> ouverte
ouverte -> délai de refroidissement -> semi-ouverte
semi-ouverte + succès -> fermée
semi-ouverte + échec -> ouverte
Enter fullscreen mode Exit fullscreen mode

Implémentez le disjoncteur par dépendance, et non globalement.

Par exemple :

  • une panne de l’API de recherche ne doit pas empêcher les appels à une API de facturation saine ;
  • une panne de paiement doit produire une erreur rapide et compréhensible ;
  • l’agent doit pouvoir choisir une alternative ou informer l’utilisateur plutôt que d’accumuler des timeouts.

Sécuriser les réessais avec des clés d’idempotence

Les réessais ne sont sûrs que si répéter l’opération ne produit pas un deuxième effet.

Cas classique :

  1. L’agent envoie POST /charge.
  2. Le serveur traite le paiement.
  3. La réponse expire avant d’arriver au client.
  4. L’agent pense que l’appel a échoué.
  5. Il réessaie.
  6. Le client est facturé deux fois.

Le réessai a fonctionné comme prévu. C’est la conception de l’endpoint qui n’était pas sûre.

La solution est une clé d’idempotence. Générez une clé unique pour chaque action logique et envoyez-la dans l’en-tête Idempotency-Key.

import uuid
import httpx

idempotency_key = str(uuid.uuid4())

response = httpx.post(
    "https://api.example.com/charge",
    headers={
        "Idempotency-Key": idempotency_key,
    },
    json={
        "customer_id": "cus_123",
        "amount": 2500,
    },
)
Enter fullscreen mode Exit fullscreen mode

Point essentiel : générez la clé avant d’entrer dans la boucle de réessai.

import uuid

idempotency_key = str(uuid.uuid4())

for attempt in range(3):
    response = send_charge(
        idempotency_key=idempotency_key,
    )
Enter fullscreen mode Exit fullscreen mode

Ne faites pas ceci :

for attempt in range(3):
    # Incorrect : une nouvelle clé à chaque tentative
    idempotency_key = str(uuid.uuid4())
    response = send_charge(idempotency_key=idempotency_key)
Enter fullscreen mode Exit fullscreen mode

La clé doit :

  • rester identique pour tous les réessais d’une même action ;
  • changer pour une nouvelle action métier ;
  • être utilisée pour les appels qui créent ou modifient un état.

Cela concerne notamment :

  • les paiements ;
  • les commandes ;
  • les e-mails ;
  • les créations d’enregistrements ;
  • les mises à jour d’état.

Pour aller plus loin, consultez le guide sur les clés d’idempotence.

Survivre aux limites de débit et à la boucle RateLimitError

Les limites de débit demandent un traitement spécifique parce que le serveur fournit souvent une instruction explicite.

Une réponse d’excès de limite de débit est généralement un 429 avec l’en-tête Retry-After.

Cet en-tête peut indiquer :

  • un nombre de secondes à attendre ;
  • ou une date à laquelle la requête peut être tentée à nouveau.

Respectez cette valeur.

import time

def handle_rate_limit(response):
    retry_after = response.headers.get("Retry-After")

    if retry_after:
        time.sleep(int(retry_after))
    else:
        # Repli : backoff exponentiel avec jitter
        time.sleep(2)
Enter fullscreen mode Exit fullscreen mode

Si le serveur demande d’attendre 30 secondes et que vous réessayez après 2 secondes, vous obtenez probablement un autre 429. Répéter ce comportement crée la boucle RateLimitError : limitation, réessai prématuré, limitation plus sévère, puis arrêt de l’exécution.

Un fil de discussion du SDK traite de ce problème rencontré par les développeurs.

Votre stratégie doit être :

  1. Lire Retry-After.
  2. Attendre au moins la durée demandée.
  3. Utiliser le backoff exponentiel avec jitter uniquement si l’en-tête est absent.
  4. Limiter le nombre de tentatives.
  5. Retourner une erreur claire lorsque la limite persiste.

Le SDK Anthropic respecte déjà Retry-After pour ses propres appels. Appliquez la même règle aux autres API limitées appelées par votre agent.

Ajoutez également une protection proactive : si le fournisseur annonce une limite par minute, utilisez un mécanisme de limitation côté client, par exemple un seau de jetons (token bucket), pour rester sous le plafond.

Comment tester le chemin de récupération

Les mécanismes ci-dessus ne valent que s’ils sont couverts par des tests qui déclenchent les pannes attendues.

Voici un scénario de récupération complet :

  1. Mockez la dépendance

    Créez un mock de l’API appelée par l’outil. Vous devez contrôler les codes HTTP, les en-têtes, les corps et les délais sans déclencher de vrai paiement ni envoyer de vrai e-mail.

  2. Scriptz une séquence de réponses

    Configurez le mock pour répondre, dans cet ordre :

    • 429 avec Retry-After: 2 ;
    • 500 ;
    • 200 avec un corps valide.
  3. Pointez l’agent vers le mock

    Remplacez l’URL de production par l’URL du mock pour l’exécution du test.

  4. Affirmez le comportement

    Vérifiez que :

    • l’agent attend au moins deux secondes après le 429 ;
    • il réessaie après le 500 ;
    • il réussit au troisième appel ;
    • il ne dépasse jamais le nombre maximal de tentatives.

Exemple de test conceptuel :

def test_agent_recovers_from_429_then_500():
    result = run_agent(base_url=mock_server.url)

    assert mock_server.call_count == 3
    assert mock_server.delay_before_second_call >= 2
    assert result.status == "success"
Enter fullscreen mode Exit fullscreen mode

Ajoutez au minimum deux autres scénarios.

Scénario d’abandon

Configurez le mock pour échouer à chaque requête.

Vérifiez que l’agent :

  • arrête les tentatives à la limite configurée ;
  • retourne une erreur propre ;
  • ne boucle pas indéfiniment.

Scénario de disjoncteur

Configurez plusieurs échecs successifs, puis vérifiez que :

  • le disjoncteur passe à l’état ouvert ;
  • les appels suivants échouent rapidement ;
  • aucune attente de timeout n’est payée pour chaque nouvelle requête.

Scénario d’idempotence

C’est le test qui évite les doubles envois coûteux.

  1. Acceptez un appel POST modifiant un état.
  2. Supprimez volontairement la réponse du mock.
  3. Laissez l’agent réessayer.
  4. Vérifiez que les deux requêtes utilisent la même Idempotency-Key.
  5. Vérifiez que le mock ne compte qu’une seule action logique.

Une nouvelle clé au second essai, ou deux opérations créées, signifie que vous avez détecté un double envoi avant vos utilisateurs.

Pour mettre en place un harnais plus complet, consultez comment tester les agents qui appellent vos API.

Liste de contrôle avant la production

Avant de déployer un agent, vérifiez les points suivants :

  • [ ] Chaque appel sortant possède un timeout de connexion, de lecture et un budget global d’exécution.
  • [ ] Les réessais utilisent un backoff exponentiel avec jitter.
  • [ ] Les délais et le nombre de tentatives sont plafonnés.
  • [ ] Les réponses 429 lisent et respectent Retry-After.
  • [ ] Un disjoncteur est configuré par dépendance.
  • [ ] Chaque opération modifiant un état utilise une clé d’idempotence stable.
  • [ ] Le chemin d’abandon retourne une erreur claire plutôt qu’une attente infinie.
  • [ ] Chaque comportement est validé contre un mock qui force la défaillance.

Si ces points sont couverts, votre agent se rétablit volontairement au lieu de dépendre de la chance.

Où Apidog s’intègre — et où il ne s’intègre pas

Gardez le rôle de l’outil clair : Apidog n’est pas un framework d’agent, un hébergeur de modèle ou un environnement d’exécution. Il ne construit pas, n’exécute pas et n’orchestre pas votre agent. Il n’évalue pas non plus la sortie du modèle.

Son rôle concerne la couche API appelée par votre agent, précisément l’endroit où la récupération se gagne ou se perd.

Rôle d’Apidog dans le cycle de vie d’une API

Apidog peut vous aider à :

  1. Simuler les dépendances utilisées par votre agent.
  2. Programmer des réponses d’échec : 429 avec Retry-After, 500, timeout ou corps malformé.
  3. Vérifier les requêtes reçues par le mock :
    • présence de Idempotency-Key ;
    • stabilité de la clé entre les tentatives ;
    • forme du payload ;
    • nombre d’appels attendu.

L’intégration est donc simple : Apidog simule les échecs auxquels votre agent doit survivre et vérifie ce qu’il renvoie.

Foire aux questions

Le SDK Anthropic ne gère-t-il pas les réessais pour moi ?

Pour ses propres appels, oui. Le SDK réessaie certaines erreurs avec un backoff exponentiel, respecte Retry-After et expose une limite via max-retries.

Il ne couvre pas les autres API appelées par les outils de votre agent. Vous devez appliquer les mêmes mécanismes à ces dépendances.

Quand ai-je besoin d’une clé d’idempotence ?

Utilisez-en une pour tout appel qui crée ou modifie un état : paiements, commandes, messages, créations d’enregistrements ou mises à jour.

Les appels en lecture seule peuvent généralement être réessayés sans clé. Générez néanmoins la clé une fois par action logique afin qu’elle reste stable pendant tous les réessais.

Répétez un échec cette semaine

Vous n’avez pas besoin d’implémenter les quatre mécanismes immédiatement.

Commencez par le scénario le plus risqué pour votre système :

  • boucle de limite de débit ;
  • timeout sans abandon ;
  • réessai non idempotent ;
  • dépendance durablement indisponible.

Programmez un 429, supprimez une réponse après un appel modificateur, puis observez les requêtes envoyées par l’agent. Lorsque vous verrez un backoff propre et une seule clé d’idempotence là où vous craigniez un double paiement, votre confiance reposera sur un test reproductible plutôt que sur une démo réussie.

Utilisez Apidog pour simuler ces échecs, script les séquences de réponse et vérifier le comportement de votre agent lorsque l’API résiste.

Top comments (0)