De « Go Ahead » à terminé : État, approbation et idempotence pour les agents d’action
Découvrez comment les propositions explicites, les approbations conditionnelles, la révalidation, les clés d’idempotence et la vérification des résultats transforment un flux de travail de remboursement multi-agents en un système fiable.
Quels changements surviennent lorsque l’assistant doit agir
Jusqu’au moment de l’approbation, l’assistant n’avait qu’à interpréter une demande et produire une réponse utile. Une fois que l’utilisateur donne son accord, un ensemble différent d’obligations apparaît. Le système doit se rappeler précisément ce qu’il a proposé, localiser le dossier correspondant, sélectionner la composante responsable de la tâche, vérifier que l’action reste pertinente, s’assurer que cet utilisateur est habilité à l’autoriser, appeler un service externe, gérer les pannes, et enfin déterminer quelque chose qui semble simple mais l’est rarement : savoir si l’action a réellement eu lieu.
L’architecture de démonstration classique ne couvre que la première partie. Un orchestrateur reçoit la demande, la délègue à des spécialistes, ces derniers appellent des outils, et une réponse revient. Cette structure est précieuse, mais pour les actions du monde réel, elle n’est qu’un point d’entrée. Agir en toute sécurité exige un état explicite, une autorisation, des workflows capables de résister aux temps d’attente et à la révalidation, de l’idempotence, une capacité de récupération en cas d’échecs ambigus, ainsi que la preuve que la tâche est terminée. C’est à ce stade que la conception des agents devient un problème d’architecture plutôt qu’un problème de formulation de commandes.
Suivi d’une seule demande de remboursement
Prenons une demande qu’un client pourrait envoyer à un assistant de support : examiner la commande n°4821, déterminer si elle est éligible à un remboursement, et le cas échéant en rédiger une version pour approbation. Un agent humain diviserait cette tâche en environ dix étapes :
- Décider de ce que le client demande.
- Récupérer la commande.
Dans le logiciel, chacune de ces étapes nécessite un responsable et un endroit où elle est traitée. Une séquence fonctionnelle transmet la demande de l’utilisateur à un orchestrateur, puis à un spécialiste qui recueille des éléments et rédige une proposition, laquelle est ensuite approuvée, exécutée et enfin vérifiée.
- C’est au orchestrateur de diriger la demande.
- Le spécialiste mène l’enquête dans le domaine concerné.
- Des outils relient le système à la documentation et aux données en temps réel.
- L’état explicite capture l’action proposée.
- L’utilisateur approuve une proposition spécifique.
Un seul principe relie ces éléments :
Le modèle peut décider de ce qui devrait se produire ; l’application décide de ce qui est autorisé à se produire.
Les extraits suivants sont des esquisses architecturales en Python utilisant LlamaIndex. La configuration du modèle, les intégrations de stockage et de services sont omises, et des noms tels que llm et policy_retriever représentent les composants que votre application fournirait. Certains extraits présentent également des sauts de ligne fusionnés (par exemple, des instructions regroupées en une seule ligne) ; il convient donc de les lire comme des schémas plutôt que comme du code prêt à être copié-collé.
Séparer le routage du travail spécialisé
Le moyen le plus rapide de créer un agent est de lui fournir toutes les outils en même temps : recherche de documentation, consultation des commandes, vérification des paiements, remboursements, e-mail, génération de rapports. Cela est tout à fait raisonnable pour une petite application. Cependant, à mesure que les responsabilités s’accumulent, il devient difficile de décrire le travail de l’agent en une seule phrase, et la tâche liée aux délais de livraison se retrouve à partager un ensemble d’outils surchargé avec une opération de transfert d’argent.
Une séparation plus claire distingue deux aspects : déterminer à quel service appartient une demande et effectuer le travail spécialisé correspondant. Pour l’assistant de support, cela signifie qu’un coordinateur répond aux questions générales et transmet les demandes de remboursement à un spécialiste dédié. Ce dernier est construit à partir de FunctionAgent de LlamaIndex :
from llama_index.core.agent.workflow import FunctionAgent
Sa définition lui confère un mandat restreint, trois outils ainsi que la permission de rendre le contrôle au coordinateur. Notez que l’invite du système lui demande d’expliquer ses preuves avant de proposer quoi que ce soit :
refund_specialist = FunctionAgent(
name="refund_specialist",
description="Checks refund eligibility and prepares proposals.",
system_prompt=(
"Check the order and the applicable policy. "
"Explain your evidence before proposing a refund. "
"Hand back unrelated requests to the coordinator."
),
tools=[get_order, search_policy, prepare_refund],
llm=llm,
can_handoff_to=["coordinator"],
)
Le champ description n’est pas une simple décoration ; il fait partie de l’architecture. Une étiquette telle que « assistant utile » ne communique rien. « Vérifie l’éligibilité au remboursement et prépare des propositions » définit une responsabilité qui peut être testée et auditée. Dans LlamaIndex, les classes FunctionAgent et AgentWorkflow sont conçues précisément pour ce type de transfert explicite. Si vous choisissez encore un framework, la comparaison entre LangChain et LlamaIndex couvre les compromis plus larges.
Ajouter des agents n’améliore pas automatiquement les choses. Chaque spécialiste entraîne des coûts de coordination supplémentaires, il convient donc que chacun justifie son existence. Ici, une paire d’agents – un coordinateur et un spécialiste des remboursements – définit une frontière claire sans coûts supplémentaires.
Connexion du coordinateur au spécialiste
Le coordinateur ainsi que le flux de travail qui relie les deux agents proviennent du même module :
from llama_index.core.agent.workflow import AgentWorkflow
Le coordinateur a accès à la recherche de politiques pour les questions générales et peut transférer ces demandes au spécialiste des remboursements. Le AgentWorkflow enregistre les deux agents et fait du coordinateur le point de départ qui reçoit toutes les demandes. Dans cet extrait, la parenthèse de fermeture du coordinateur et l’affectation du flux de travail se trouvent sur la même ligne ; il s’agit en réalité de deux instructions distinctes :
coordinator = FunctionAgent(
name="coordinator",
description="Answers general questions and routes refund requests.",
system_prompt=(
"Use documentation for general questions. "
"Hand refund requests to the refund specialist."
),
tools=[search_policy],
llm=llm,
can_handoff_to=["refund_specialist"],
)workflow = AgentWorkflow(
agents=[coordinator, refund_specialist],
root_agent="coordinator",
)
Avec cela en place, la demande dispose d’un chemin. Le coordinateur reconnaît qu’il s’agit d’une question de remboursement et la transmet ; le spécialiste récupère la commande, consulte les règles en vigueur, note ce qui manque encore et, une fois que les preuves sont suffisantes, rédige une proposition.
Le déroulement de la conversation variera. Un client indique d’emblée la date de livraison ; un autre demande d’abord des informations sur les règles de remboursement et ne mentionne la commande que trois messages plus tard. Les agents peuvent s’adapter à ces variations, tandis que l’application continue de définir quels fonctionnalités existent et quelles en sont les limites d’utilisation.
Les conversations peuvent rester flexibles tout en maintenant le contrôle sur les opérations importantes.
Preuves avant évaluation de l’éligibilité
Envoyer la demande au bon agent ne garantit pas pour autant que sa conclusion sera correcte. Le spécialiste doit encore collecter des preuves.
Supposons que la politique autorise les produits non ouverts à être retournés dans un délai de 30 jours, tandis que les enregistrements des commandes indiquent que la commande n°4821 est arrivée il y a 12 jours. Ces informations proviennent de deux systèmes différents et sont toutes deux nécessaires : la politique énonce la règle, la commande décrit la situation, et l’éligibilité ne se détermine qu’en combinant ces deux éléments. La récupération devient donc une étape du flux de travail. Le spécialiste recherche les documents de politique tout en chargeant séparément l’enregistrement de la commande en cours.
Un outil minimal de recherche de politique commence par une signature asynchrone et une documentation qui explique à l’agent à quoi sert cet outil :
async def search_policy(question: str) -> list[dict]:
"""Find policy passages relevant to a customer request."""
Le corps de l’outil récupère les passages correspondants et les renvoie chacun accompagné de son titre d’origine et de sa section. Comme dans l’extrait précédent, l’appel aretrieve et l’instruction return doivent se trouver sur des lignes distinctes :
matches = await policy_retriever.aretrieve(question) return [
{
"text": match.node.get_content(),
"source": match.node.metadata.get("title"),
"section": match.node.metadata.get("section"),
}
for match in matches
]
Ce qui compte, c’est ce qui accompagne le texte : son origine. Si l’assistant affirme par la suite que la commande tombe dans la période de remboursement, l’explication doit pouvoir citer le passage du règlement qui définit cette période. Cependant, l’origine a ses limites :
Une citation ne peut pas prouver qu’une interprétation est correcte ; elle permet simplement à quelqu’un de la vérifier.
La condition de colis non ouvert révèle un manque : rien dans le système de commandes ne note si le colis a été ouvert. La bonne approche n’est pas de deviner, mais de demander au client. Une conception solide indique explicitement les informations manquantes, plutôt que de laisser l’incertitude se transformer en recommandation catégorique.
L’historique des conversations n’est pas l’état de l’application
On suppose que l’enquête est terminée. Le spécialiste indique que la commande n°4821 remplit les conditions pour un remboursement de 79 € et demande s’il faut procéder. L’utilisateur répond par l’affirmative.
Le routage a fonctionné, l’enquête a abouti et les outils ont fourni leurs preuves, mais un manque persiste. Le système ne peut pas préciser ce qui a été approuvé : la commande, le montant et la monnaie, ainsi que la version de l’offre, sont tous supposés plutôt que enregistrés. Si la conversation portait sur deux commandes, ou si le montant a été recalculé après l’offre, le mot « oui » devient ambigu.
C’est pourquoi les actions qui en découlent ne doivent pas se limiter au transcript des conversations. L’application a besoin d’un enregistrement explicite de l’action en attente, comme celui-ci :
pending_action = {
"proposal_id": "refund-proposal-17",
"order_id": "4821",
"amount_minor": 7900,
"currency": "EUR",
"status": "awaiting_approval",
"evidence": [
"returns-policy:section-3",
"order:4821",
],
}
Le champ status est utile, mais c’est proposal_id qui a la plus grande importance. L’approbation doit faire référence à la proposition exacte que l’utilisateur a vue. Si le montant change, il s’agit d’une nouvelle proposition. Si l’ordre change, il s’agit d’une nouvelle proposition. Si de nouvelles preuves modifient la recommandation, il s’agit d’une nouvelle proposition. L’argent est stocké sous forme de amount_minor en centimes entiers, ce qui évite les surprises liées au arrondi des nombres à virgule flottante, et la liste evidence conserve indiquée quelle section de politique et quel enregistrement d’ordre justifient l’offre.
La transcription existe pour permettre la compréhension ; l’état structuré existe pour permettre des actions concrètes.
La conversation aide le modèle à interpréter ce que signifie « aller de l’avant ». La proposition stockée fournit à l’application quelque chose d’univoque à exécuter.
L’approbation en tant que données plutôt qu’en mots
La manière naïve d’enregistrer le consentement repose sur un seul fait :
user said yes
Un modèle plus sûr relie l’identité, la proposition, l’action et le contexte :
user X approved proposal Y,
containing action Z,
under the current conditions.
Cette distinction devient décisive dès que l’agent peut affecter des systèmes externes. Le consentement doit relier une personne spécifique à une action proposée précise, et non à quelque chose que le modèle reconstruit par la suite. La proposition doit donc contenir tous les détails opérationnels importants :
- le registre cible
- le montant
- la monnaie
- les preuves justificatives
- l’état actuel
- un identifiant unique de proposition
Le langage naturel explique l’action à l’utilisateur ; la proposition structurée la définit pour le système.
L’attente fait partie du flux de travail
L’approbation humaine introduit également un élément temporel. L’utilisateur peut répondre immédiatement, après le déjeuner ou le jour suivant, une fois le navigateur fermé. Lorsqu’un flux de travail dépend d’une personne, la pause n’est pas un cas particulier mais une phase normale ; par conséquent, le travail en attente doit être sauvegardé pour pouvoir reprendre ultérieurement.
LlamaIndex fournit des objets de contexte pour les flux de travail ainsi que des moyens de mettre en pause l’exécution en attendant une intervention humaine, ce qui donne une certaine structure à cette interaction. Cependant, le stockage durable, les autorisations, les règles de péremption ainsi que le cycle de vie global de l’application restent de votre responsabilité.
Pour quelque chose d’aussi important qu’un remboursement, une répartition claire des tâches consiste à laisser l’agent préparer la proposition et à faire exécuter l’opération approuvée par du code d’application ordinaire. Le gestionnaire de l’approbation commence par charger la proposition enregistrée :
async def approve_refund(proposal_id, user):
proposal = await proposals.load(proposal_id)
Il vérifie ensuite les permissions, confirme que la proposition est toujours en attente, la révérifie, appelle le service de paiement en utilisant l’ID de la proposition comme clé d’idempotence, et enregistre le reçu. Dans cet extrait, ces appels asynchrones s’exécutent ensemble sur des lignes communes ; chaque await constitue une instruction indépendante :
await permissions.require(
user,
"approve_refund",
proposal,
) await proposals.require_pending(proposal) await refunds.revalidate(proposal) receipt = await payments.refund(
order_id=proposal.order_id,
amount_minor=proposal.amount_minor,
idempotency_key=proposal.id,
) await proposals.mark_completed(
proposal.id,
receipt,
) return receipt
Divers points limites sont cachés dans cette fonction courte :
- La proposition provient de l’état stocké, et non de la conversation.
- L’autorisation est vérifiée en dehors du modèle.
- Le code confirme que la proposition est toujours en attente d’approbation, ce qui empêche une deuxième exécution par cette voie (en situation de concurrence, cette vérification doit être atomique avec la mise à jour d’état).
- L’éligibilité est révérifiée juste avant d’agir.
- L’appel de paiement utilise une clé d’idempotence stable.
- Le résultat est persisté.
La séquence va d’une proposition enregistrée, en passant par une approbation autorisée, une validation nouvelle et l’exécution elle-même, jusqu’à un résultat stocké. Cette séquence est bien plus importante que le fait de savoir si l’agent a formulé son message parfaitement.
Révalider immédiatement avant d’agir
Pourquoi vérifier à nouveau après que l’utilisateur a déjà donné son accord ? Parce que le monde continue de bouger pendant que le système attend. La commande a peut-être été remboursée par un autre canal de support, l’état du paiement a pu changer, quelqu’un a peut-être modifié la commande, ou un flux de travail parallèle a déjà pu effectuer l’opération.
Un « oui » accorde l’autorisation d’agir ; il n’empêche pas le monde de changer.
Plus une proposition reste en attente, plus cela devient important. Associer une révalidation à une date d’expiration pour les propositions permet de limiter la durée des hypothèses obsolètes.
Les tentatives de réessai ne doivent pas reproduire l’action
Prenons un autre échec : l’utilisateur approuve, l’application envoie le remboursement, le prestataire de paiement le traite, mais le réseau tombe en panne avant l’arrivée de la réponse. Le client tente à nouveau. Le client devrait-il recevoir un deuxième remboursement de 79 € ? Évidemment non.
L’idempotence empêche cela. Un identifiant d’opération stable permet à tout service externe prenant en charge l’idempotence de reconnaître deux requêtes comme une seule opération logique. Au lieu d’interpréter la tentative de réessai comme une « nouvelle demande de remboursement », le service la traite comme une requête concernant l’état ou le résultat de l’opération déjà associée à la proposition n°17. L’utilisation de l’ID de la proposition en tant que clé fonctionne car chaque proposition approuvée ne devrait donner lieu qu’à un seul remboursement au maximum. Les mécanismes du côté serveur sont abordés plus en détail dans comprendre les clés d’idempotence dans les endpoints POST de Node.js.
Ce concept apparaît rarement dans les démonstrations sophistiquées impliquant plusieurs agents. Mais dès qu’un système peut transférer de l’argent, envoyer des messages, modifier des données, ouvrir des tickets ou déclencher tout autre effet dans le monde réel, les tentatives de réessai font partie intégrante de son architecture.
« Inconnu » est un résultat légitime
Voici maintenant le cas le plus délicat : l’appel de paiement atteint une limite de temps. Peut-être rien ne s’est-il passé, ou peut-être l’argent a-t-il été transféré juste avant que la connexion ne s’éteigne. Pour le client, la différence est de 79 €.
Indiquer à l’utilisateur que le remboursement a échoué et qu’une nouvelle tentative est en cours serait erroné, car rien ne vient étayer cette affirmation. Tout ce que le système possède, c’est l’absence de confirmation, ce qui est un fait différent. Un flux de travail fiable rend cette incertitude visible : au lieu de répéter l’opération, il vérifie l’état de la transaction existante et, en attendant, informe l’utilisateur de manière précise, par exemple que la finalisation ne peut pas être confirmée et que l’état est en cours de vérification.
Cette discipline s’applique à chaque étape, et non seulement au moment du paiement :
- Lorsqu’une recherche de commande échoue, l’existence de la commande est inconnue, mais pas infirmée.
Les systèmes fiables modélisent ces situations comme des états distincts plutôt que de les regrouper sous les catégories « échec » ou « non trouvé ».
Le retour d’une appel à outil ne signifie pas que le travail est terminé
Ici, l’architecture va au-delà de l’orchestration : un appel à outil qui revient n’équivaut pas à la fin du travail. Une réponse d’erreur indique que ce n’est pas le cas, tout comme un délai d’attente. Si l’application ne peut pas déterminer si le remboursement a eu lieu, elle n’est certainement pas terminée.
Une définition plus stricte de la finalisation est que le système puisse présenter des preuves que le résultat souhaité s’est réellement produit. Cela modifie les exigences du contrat. L’objectif n’est pas le suivant :
call refund()
L’objectif est le suivant :
establish that refund proposal #17
for order #4821
was successfully processed exactly once
Ces garanties sont très différentes, et c’est cette différence qui explique pourquoi les orchestrateurs et les spécialistes ne constituent que le début de la conception.
Neuf questions avant qu’un système multi-agents ne puisse agir
Au préalable de relier un flux de travail basé sur l’IA à des opérations ayant des conséquences réelles, répondez aux questions suivantes :
- Chaque agent a-t-il une responsabilité définie que l’on peut formuler en une phrase ?
- Pouvez-vous retracer une décision importante jusqu’aux documents et enregistrements qui la sous-tendent ?
- L’information manquante reste-t-elle explicitement inconnue, ou l’incertitude se transforme-t-elle silencieusement en une recommandation catégorique ?
- L’action proposée est-elle stockée sous forme d’état structuré plutôt que de n’exister que dans la conversation ?
- L’utilisateur approuve-t-il une proposition précise, de sorte que le « oui » autorise quelque chose de spécifique ?
Même si certaines de ces questions n’ont pas de réponse claire, vous pouvez encore disposer d’une démonstration impressionnante, mais il s’agit probablement pas encore d’un système fiable pour prendre des décisions.
Conclusion
Revenez sur la demande initiale : vérifiez la commande n°4821 et préparez un remboursement à valider. Chaque élément a désormais sa place. L’orchestrateur dirige les opérations, le spécialiste collecte les preuves, les outils se connectent à la documentation et aux enregistrements en temps réel, l’état explicite conserve la proposition, une personne approuve une action précise, le code d’application vérifie les permissions et les révérifie, le système de paiement exécute la transaction, et le résultat est stocké et confirmé. À ce stade, l’assistant peut fournir une réponse agréablement banale : le remboursement de 79 € pour la commande n°4821 a été traité, avec une confirmation jointe. Cette phrase est fiable grâce à tout ce qui se cache derrière elle.
Au fur et à mesure que le système se développe, les mêmes questions restent pertinentes. Chaque fonctionnalité est-elle gérée par quelqu’un ? Les éléments justifiant une décision peuvent-ils être examinés ? Le flux de travail permet-il de conserver l’incertitude et de se remettre en marche en cas d’échec d’une dépendance ? Une personne peut-elle voir exactement ce qu’elle approuve, et le système peut-il prouver que le résultat souhaité a été atteint ? Les réponses indiquent où un agent spécialisé apporte de la valeur, où une simple fonction suffit, et où des limites plus strictes sont nécessaires. La meilleure expérience avec des agents peut se terminer par un simple mot ordinaire, « Terminé », et c’est l’architecture qui en fait la crédibilité.
Lectures complémentaires
- Agentic AI expliquée : des modèles de langue aux agents autonomes — Une présentation structurée montrant comment les LLM évoluent vers des systèmes agents grâce à des outils, une mémoire, de la planification, des architectures multi-agents et l’intégration MCP.
- Routing, Fan-Out, ReAct, Critique et Approbation : cinq patterns LangGraph — Découvrez cinq patterns de flux de travail agents dans LangGraph, allant des routeurs et des boucles ReAct aux portes d’évaluation et à l’approbation humaine, ainsi que les contraintes nécessaires pour chacun en environnement de production.