Concevoir des opérations asynchrones fiables pour les agents IA
Votre agent appelle un endpoint de transcodage vidéo. L’API répond 202 Accepted avec un ID de tâche, mais l’agent annonce que le transcodage est terminé et tente de lire un fichier qui n’existe pas encore. Le problème n’est pas le transcodage : c’est un contrat asynchrone ambigu.
Essayez Apidog dès aujourd’hui
Les appels synchrones ont un contrat simple : envoyer, attendre, recevoir. Un appel asynchrone sépare le démarrage de la fin. Sans instructions explicites, un agent peut :
- interpréter tout code
2xxcomme un succès ; - interroger l’API dans une boucle coûteuse ;
- oublier un ID de tâche perdu dans une longue conversation ;
- poursuivre un workflow avant que le résultat existe.
Ce guide montre comment concevoir une API et des outils qui rendent les tâches longues prévisibles. Pour les échecs d’API, consultez aussi ce guide sur la récupération d’erreurs d’agent IA.
Pourquoi les agents gèrent mal l’asynchrone
Un 2xx est souvent interprété comme une fin
202 Accepted signifie que la requête a été acceptée, pas que son traitement est terminé. C’est conforme à la spécification sémantique HTTP, mais un modèle entraîné sur des interactions requête/réponse classiques peut traiter tout 2xx comme une complétion.
Les boucles de polling dans le modèle coûtent cher
Interroger toutes les deux secondes une tâche qui dure quatre minutes produit 120 tours potentiels. Chaque tour consomme du contexte et des jetons. Les réponses d’outils s’accumulent plus vite qu’on ne le pense, comme l’explique ce guide sur la gestion des réponses d’outils hors de la fenêtre de contexte.
Les ID de tâches peuvent être oubliés
Un outil qui démarre une tâche crée un état durable. Si le job_id n’est conservé que dans la conversation, une compaction du contexte peut le faire disparaître.
Rendre la réponse impossible à mal interpréter
Le corps de réponse doit indiquer explicitement que le travail a commencé, qu’il n’est pas terminé et quelle action effectuer ensuite.
{
"status": "processing",
"job_id": "job_7f21c",
"message": "The transcode has STARTED and is NOT complete. Do not report success. Check status with getJobStatus(job_id) after at least 30 seconds.",
"poll_after_seconds": 30,
"estimated_duration_seconds": 240,
"status_url": "/v1/jobs/job_7f21c"
}
Même si ce message paraît verbeux pour un humain, il est utile pour un modèle. Trois éléments réduisent les erreurs :
- Indiquer explicitement que la tâche est non terminée.
- Nommer l’outil ou l’endpoint à appeler ensuite.
- Fournir un délai minimal avant le prochain polling.
Le modèle de Google AIP-151 sur les opérations de longue durée est une bonne référence : une ressource unique expose notamment done, error et response. Utilisez une forme cohérente pour tous les endpoints lents.
La réponse de statut doit être tout aussi explicite :
{
"job_id": "job_7f21c",
"status": "processing",
"done": false,
"progress_percent": 45,
"elapsed_seconds": 108,
"poll_after_seconds": 45,
"message": "Still processing. Do not proceed to the next step."
}
Lorsque la tâche réussit, renvoyez directement le résultat s’il est petit :
{
"job_id": "job_7f21c",
"status": "succeeded",
"done": true,
"result": {
"output_url": "https://cdn.example.com/out/7f21c.mp4",
"duration_seconds": 372
}
}
Faites le polling dans le wrapper d’outil
Ne laissez pas le modèle effectuer le polling dans sa boucle de raisonnement. Placez l’attente dans le wrapper d’outil :
import time
def start_and_await_transcode(client, source_url, max_wait=600):
job = client.post("/v1/transcode", json={"source_url": source_url}).json()
job_id = job["job_id"]
delay = job.get("poll_after_seconds", 5)
waited = 0
while waited < max_wait:
time.sleep(delay)
waited += delay
status = client.get(f"/v1/jobs/{job_id}").json()
if status.get("done"):
if status["status"] == "succeeded":
return {"status": "succeeded", "result": status["result"]}
return {"status": "failed", "error": status.get("error")}
delay = min(int(delay * 1.5), 60)
return {
"status": "timed_out",
"job_id": job_id,
"message": f"Still running after {max_wait}s. Job {job_id} continues in the background.",
}
Pour le modèle, cela devient un appel unique : il appelle l’outil, attend, puis reçoit un résultat final. Vous évitez les boucles dans le contexte, les ID oubliés et les dizaines de tours inutiles.
Appliquez ces règles :
- imposez toujours un
max_wait; - utilisez le
poll_after_secondsfourni par le serveur ; - augmentez progressivement le délai, par exemple avec un facteur de
1.5; - plafonnez le délai, par exemple à 60 secondes ;
- retournez toujours le
job_iden cas de timeout ; - distinguez clairement
succeeded,failedettimed_out.
Avant d’ajuster vos délais et vos reprises, lisez le guide AWS sur les timeouts, tentatives et backoff avec jitter.
Pour des tâches de plusieurs heures, un wrapper bloquant n’est plus adapté. Exposez alors deux outils : un pour démarrer la tâche, un pour vérifier son état. Enregistrez durablement le job_id, la tâche associée et l’heure de démarrage, puis relisez cette liste au début de chaque exécution.
Polling, webhooks ou SSE ?
Le polling est simple et universel. Les webhooks sont plus efficaces, mais nécessitent une infrastructure supplémentaire.
Utilisez le polling lorsque :
- la tâche dure de quelques secondes à quelques minutes ;
- l’agent doit attendre le résultat pour continuer ;
- vous ne disposez pas d’un endpoint public.
Utilisez les webhooks lorsque :
- la tâche dure des heures ;
- l’agent peut lancer le travail puis passer à autre chose ;
- de nombreuses tâches tournent en parallèle.
Avant d’adopter les webhooks, prévoyez un endpoint public, une vérification de signature, une gestion des tentatives et un mécanisme pour réveiller le workflow. Consultez cette comparaison webhooks vs polling, puis les guides sur la conception de webhooks fiables et la vérification de signature de webhook.
Pour un agent interactif, les événements envoyés par le serveur peuvent être une option intermédiaire. Les SSE fournissent une sémantique de poussée sans nécessiter de callback public, car le client conserve la connexion.
Quel que soit le mécanisme, rendez la complétion idempotente. Un webhook peut être réessayé, deux pollings peuvent se chevaucher, et un succès reçu deux fois ne doit jamais déclencher deux fois l’étape suivante. Les clés d’idempotence pour les agents IA sont essentielles ici.
Testez le chemin lent
Les tests locaux sont souvent trop rapides. Une tâche qui dure quatre minutes en production peut finir en 200 ms avec un mock naïf : l’agent ne rencontre alors jamais les états intermédiaires réels.
Ajoutez au minimum quatre scénarios à votre CI.
1. La tâche lente
Retournez processing plusieurs fois, puis succeeded.
Vérifiez que le wrapper :
- respecte le délai initial ;
- applique le backoff ;
- finit par retourner le résultat.
Dans Apidog, créez un mock qui varie selon le nombre de requêtes ou un paramètre de contrôle afin de rendre le test déterministe.
2. L’échec tardif
Retournez processing trois fois, puis failed avec un corps d’erreur.
L’agent doit signaler l’échec. Il ne doit jamais confondre une réponse de polling réussie avec la réussite de la tâche elle-même.
3. Le timeout
Maintenez la réponse à processing au-delà de max_wait.
Attendez-vous à recevoir :
{
"status": "timed_out",
"job_id": "job_7f21c"
}
Ne retournez ni exception opaque, ni faux succès.
4. La complétion dupliquée
Livrez le succès deux fois, par exemple à cause d’une nouvelle tentative de webhook ou de pollings concurrents.
Vérifiez que l’étape suivante ne s’exécute qu’une seule fois.
Conservez ces scénarios comme tests de contrat. Ils détectent les régressions causées par un timeout raccourci, une erreur ignorée ou une logique d’idempotence supprimée. Pour aller plus loin, consultez ce guide de test de contrat d’API.
Gérer les résultats partiels
Une tâche longue ne se termine pas toujours par un simple succès ou échec. Par exemple, une importation de 20 000 lignes peut écrire 19 860 lignes et en rejeter 140.
Exposez cet état explicitement :
{
"job_id": "job_a11f",
"status": "completed_with_errors",
"done": true,
"summary": {
"processed": 20000,
"succeeded": 19860,
"failed": 140
},
"errors_url": "/v1/jobs/job_a11f/errors?limit=50",
"message": "Import finished. 140 rows failed and were not written. Review errors before reporting success."
}
Les décomptes doivent être inclus dans la réponse, afin que l’agent décide sans appel supplémentaire. Les erreurs détaillées doivent rester derrière une URL paginée pour éviter d’inonder le contexte.
Cas concrets
Génération de rapports
Un agent financier demande une exportation trimestrielle qui prend 90 secondes. Sans wrapper, il reçoit un ID de tâche et annonce que le rapport est disponible. Avec un wrapper bloquant, il attend puis retourne la vraie URL de téléchargement.
Importations en masse
Un agent charge 20 000 enregistrements. L’import échoue partiellement à la ligne 14 000. Un champ done: true ne suffit pas : exposez les résultats partiels et forcez l’agent à lire les décomptes avant de poursuivre.
Builds CI et pipelines de modèles
Un build ou un entraînement peut prendre 40 minutes. Ne maintenez pas un appel d’outil ouvert pendant cette durée. Démarrez la tâche, stockez son ID durablement, terminez l’exécution actuelle, puis reprenez via une vérification planifiée ou un callback. Le guide sur le transfert inter-agents et le passage de contexte détaille cette transmission d’état.
Ne laissez pas un timeout sans responsable
Un timeout doit retourner un ID de tâche et indiquer que le travail continue. Mais ce résultat n’a de valeur que si une personne ou une équipe en est responsable.
Acheminez les tâches bloquées vers la file d’attente que votre équipe surveille déjà. Dans Sharkly, une exécution bloquée reste associée à sa tâche avec son état et son résultat, tandis que la boîte de réception distingue les éléments nécessitant une réponse humaine. Le principe est simple : « toujours en cours » doit avoir un responsable.
Liste de vérification
- Chaque endpoint lent retourne un ID de tâche, une URL de statut et un message clair indiquant que le travail n’est pas terminé.
- Les réponses de statut exposent un booléen
done. - Le polling se trouve dans le wrapper d’outil, avec backoff et plafond fixe.
- Les timeouts retournent le
job_id. -
succeeded,failedettimed_outsont des résultats distincts. - Les tâches de plus de quelques minutes sont enregistrées hors de la conversation.
- La complétion est idempotente, par polling comme par callback.
- Les chemins lent, en échec tardif, en timeout et dupliqué sont couverts par des tests CI.
Avec un contrat explicite et un wrapper correctement conçu, une opération longue redevient simple pour l’agent : appeler un outil, attendre, puis recevoir un résultat fiable. Téléchargez Apidog pour construire des mocks de tâches lentes en même temps que vos tests.
Questions fréquentes
Faut-il retourner 202 ou 200 au démarrage d’une tâche asynchrone ?
Utilisez 202 Accepted : c’est le code HTTP correct lorsque le traitement n’est pas terminé. Mais ne vous appuyez pas uniquement sur le code de statut ; le corps doit aussi l’indiquer explicitement.
Combien de temps le wrapper doit-il attendre ?
Fixez un plafond légèrement supérieur au pire cas réaliste de l’endpoint, généralement entre deux et dix minutes. Au-delà, démarrez la tâche et vérifiez-la ultérieurement.
Quel intervalle de polling utiliser ?
Commencez par poll_after_seconds si le serveur le fournit. Sinon, appliquez un backoff d’environ 1.5, plafonné à environ 60 secondes. Un polling fixe chaque seconde gaspille des requêtes et peut déclencher des limites de débit ; consultez ce guide sur les dépassements de limites de débit.
L’agent peut-il travailler pendant l’attente ?
Uniquement si votre orchestrateur prend en charge les appels d’outils concurrents. Dans ce cas, démarrez la tâche, exécutez le travail indépendant, puis vérifiez le statut. Sinon, le wrapper bloquant reste plus simple et plus fiable.
Comment empêcher un succès prématuré ?
Indiquez explicitement que la tâche n’est pas terminée, exposez done: false et faites du résultat final le seul endroit où la donnée exploitable apparaît.
Les webhooks fonctionnent-ils pour un agent exécuté sur un ordinateur portable ?
Pas directement, car il n’existe pas d’endpoint public. En développement, utilisez un tunnel comme expliqué dans ce guide de test d’API localhost avec des services de webhook, ou utilisez le polling jusqu’au déploiement dans un environnement accessible.


Top comments (0)