Un agent IA en production n’est pas un prompt entouré d’une fenêtre de contexte plus grande. C’est un service qui laisse un modèle choisir des actions, observe leurs résultats et continue jusqu’à obtenir un résultat borné ou demander l’aide d’une personne. Le plan de contrôle détermine ce que le modèle peut voir, les outils disponibles, les actions à approuver et la manière dont l’état survit à un redémarrage.
Cet article propose une architecture de référence pour un agent unique avec peu d’outils. Les mêmes frontières conviennent à un workflow aux étapes fixes et à une future architecture multi-agents. Commencez par la boucle la plus petite qui résout le besoin. Le guide Anthropic sur les agents efficaces distingue lui aussi les workflows prévisibles des systèmes où le modèle dirige dynamiquement l’utilisation des outils. L’autonomie est une décision produit, pas une valeur par défaut de l’architecture.
Architecture de référence
Séparez le chemin de la requête, la boucle de décision et les effets qui modifient le monde extérieur. Une passerelle authentifie l’appelant, crée les identifiants de requête et de trace, applique les quotas et retire ou classe les champs sensibles. Un orchestrateur possède la boucle. Un adaptateur de modèle transforme l’état neutre en requête fournisseur, puis la réponse fournisseur en un petit type de décision. Une passerelle d’outils valide les arguments, autorise le principal, applique la politique et appelle un connecteur isolé. L’état et le journal d’événements doivent être hors processus pour qu’un worker puisse reprendre après une panne.
[Utilisateur ou client API]
|
[Passerelle : identité, limites, request ID]
|
[Orchestrateur : policy -> model -> tool loop]
/ | \
[State store] [Approval service] [Tool gateway]
| | |
[Memory/RAG] [Décision humaine] [API, fichiers, files]
|
[Événements et traces]
Ne donnez pas au modèle des identifiants bruts, un accès réseau sans restriction ou une écriture directe en base. La passerelle d’outils doit exposer des opérations étroites comme "search_orders", "draft_refund" ou "send_message", et non un client HTTP générique ou une console SQL. Chaque opération doit avoir un responsable, un schéma d’entrée, une permission, une classe de risque, un délai et un format de résultat documenté.
Utilisez une enveloppe de requête explicite :
| Champ | Rôle |
|---|---|
| tenantId et actorId | Lier chaque lecture et chaque effet à un principal authentifié. |
| requestId et traceId | Corréler reprises, approbations, appels d’outils et journaux. |
| goal | Conserver l’objectif de l’utilisateur séparément des messages du modèle. |
| policyVersion et modelVersion | Rendre l’exécution assez reproductible pour une enquête. |
| deadline et stepBudget | Borner le temps et le nombre de tours modèle-outil. |
Le profil Generative AI du NIST AI RMF aide à organiser les risques sur tout le cycle de vie. Traduisez ses questions en contrôles concrets, sans considérer le framework comme une preuve de sécurité.
Boucle d’outils
La boucle doit avoir un seul propriétaire et une transition d’état visible à chaque tour :
- Charger l’exécution, la politique, le résumé de conversation et le catalogue d’outils autorisés.
- Construire une entrée de modèle bornée. Marquer texte utilisateur, texte récupéré, sortie d’outil et instructions système comme des classes de confiance différentes.
- Demander au modèle une réponse finale ou un appel d’outil typé. Rejeter les noms et arguments mal formés avant l’exécution.
- Résoudre autorisation et risque hors du modèle. Affirmer qu’une action est sûre ne constitue pas une décision d’autorisation.
- Si l’action franchit une barrière d’approbation, enregistrer une action en attente et s’arrêter. Reprendre à partir de cette action après une décision explicite.
- Exécuter le connecteur avec un délai et une clé d’idempotence. Enregistrer un résultat expurgé et l’ajouter à l’état.
- Vérifier les budgets d’étapes, de tokens, de coût et de temps. Continuer seulement si la politique autorise un tour supplémentaire.
- Renvoyer une réponse indiquant ce qui s’est produit, ce qui ne s’est pas produit et l’approbation ou l’incertitude restante.
N’entourez pas un appel de modèle d’une boucle "while" sans limite. Le budget d’étapes empêche une chaîne coûteuse. Le délai protège l’appelant et le pool de workers. Un détecteur d’appels répétés repère une action toujours en échec. Un circuit breaker peut désactiver un connecteur dégradé tout en laissant les lectures disponibles.
La spécification du Model Context Protocol standardise resources, prompts et tools, mais ne remplace pas le consentement, l’autorisation ou l’isolation de l’application hôte. Traitez un serveur MCP comme une dépendance externe. Fixez son identité, inspectez ses outils annoncés, limitez les données envoyées et appliquez la même politique de passerelle qu’à un connecteur interne.
État, mémoire et contexte
L’état est le registre durable d’une exécution. Stockez l’objectif, les messages ou leurs références, les appels et résultats d’outils, les approbations, les décisions de politique, les versions du modèle et des outils, les dates, le statut et la catégorie d’erreur. Ajoutez d’abord des événements, puis dérivez une vue courante. La récupération et l’audit sont ainsi plus simples qu’avec un seul blob JSON opaque. Chiffrez les champs sensibles, isolez les tenants, définissez la conservation et rendez la suppression effective dans snapshots, index, traces et caches.
L’historique de conversation n’est pas la mémoire. Gardez un contexte de travail à courte durée pour l’exécution courante. Ne conservez une mémoire utilisateur ou métier que si sa finalité, sa durée de conservation, sa source et sa correction par l’utilisateur ou l’administrateur sont définies. Stockez les faits avec leur provenance et un champ de confiance ou de fraîcheur. N’écrivez pas une supposition du modèle en mémoire durable parce qu’elle semble plausible.
La récupération est un outil avec une frontière de confiance. Filtrez par tenant et autorisation avant le classement. Transportez les identifiants de source et les dates dans le contexte. Dites au modèle de traiter le texte récupéré comme donnée, pas comme instruction. Limitez les fragments et journalisez la requête et les identifiants sans contenu privé. Avec PostgreSQL, comparez des candidats lexicaux et vectoriels, puis rerankez après autorisation. Le guide interne du RAG hybride avec pgvector décrit cette frontière.
La compaction du contexte doit être assez déterministe pour être déboguée. Résumez les anciens tours selon un schéma fixe qui conserve décisions, questions ouvertes, effets d’outils, citations et contraintes utilisateur. Gardez le journal d’événements original hors du prompt. Si un résumé change le sens d’une action en attente, arrêtez-vous et demandez une revue au lieu de continuer en silence.
Outils et frontières de menace
Un schéma d’outil est un contrat de sécurité, pas seulement des métadonnées pour le modèle. La passerelle valide types, longueurs, valeurs d’énumération, propriété des ressources et relations entre champs. Après l’autorisation, résolvez les noms en identifiants internes. Séparez les outils de lecture et d’écriture. Donnez aux connecteurs des identifiants de courte durée limités à un tenant et une opération. Exécutez le code ou le travail sur fichiers à risque dans un worker isolé avec des listes d’autorisation pour le système de fichiers et le réseau.
Considérez tout contenu externe comme hostile. Un e-mail, une page web, un ticket, un document, une description d’outil ou une ressource MCP peut contenir une injection indirecte. Délimitez-le dans l’entrée du modèle et laissez l’application décider des permissions. Validez les sorties indépendamment du modèle. Par exemple, vérifiez montant, devise, acteur et plafond avant un remboursement.
Le modèle de menace doit couvrir :
| Frontière | Échec à empêcher |
|---|---|
| Utilisateur vers passerelle | Prise de compte, requêtes surdimensionnées et identifiants d’un autre tenant. |
| Modèle vers passerelle d’outils | Injection d’arguments, confusion de privilèges et agency excessive. |
| Récupération ou MCP vers modèle | Injection indirecte et instructions malveillantes dans les données. |
| Connecteur vers service externe | Fuite d’identifiants, SSRF, replay et exfiltration de données. |
| Worker vers state store | Événements falsifiés, politique obsolète et audit incomplet. |
L’OWASP GenAI LLM Top 10 décrit l’injection, l’agency excessive, la validation de sortie insuffisante et la consommation illimitée comme des risques nécessitant des mesures au niveau de l’application. Le guide Dayfing sur l’injection de prompt et la sécurité MCP propose une checklist orientée menaces. Un prompt système est une consigne utile, mais ce n’est ni une sandbox, ni une couche d’autorisation, ni un coffre à secrets.
Barrières d’approbation et contrôle humain
Placez les approbations autour des effets, pas autour du raisonnement inoffensif. Lire le calendrier de l’utilisateur peut être automatique. Envoyer un message externe, modifier une fiche, verser de l’argent, supprimer des données ou déployer du code exige généralement une décision basée sur l’acteur, la cible, le montant, la réversibilité et la confiance. La barrière doit être dans le code applicatif afin qu’un prompt ne puisse pas la contourner.
Conservez une demande d’approbation contenant l’outil proposé, les arguments normalisés, les ressources touchées, le motif, la version de politique, l’expiration et un hash de l’état concerné. Le réviseur doit voir les mêmes données. Lie sa décision au hash de l’action et à l’acteur. Après approbation, revérifiez autorisation, fraîcheur et budget avant l’exécution. Après rejet ou expiration, enregistrez la décision et informez le modèle que l’action n’a pas eu lieu. Une ancienne approbation ne doit jamais autoriser une charge modifiée.
Les modes human-in-the-loop sont variés :
| Mode | Usage adapté |
|---|---|
| Observe | Enregistrer ou échantillonner les actions peu risquées en laissant l’agent automatique. |
| Confirm | Demander l’accord juste avant un effet irréversible. |
| Review | Faire inspecter un brouillon complet et les preuves sélectionnées. |
| Take over | Transférer l’exécution à un opérateur avec état et verrou actuels. |
Concevez la pause comme un état normal, pas comme une exception. Une file peut livrer les approbations en attente, une notification peut expirer et un worker peut reprendre sur une autre machine. L’utilisateur doit savoir si l’agent raisonne, attend des données, attend une approbation, réessaie ou a terminé.
Reprises, idempotence et récupération
Classez les erreurs avant toute reprise. Une erreur de validation demande un appel corrigé ou une question à l’utilisateur. Les erreurs d’authentification et d’autorisation doivent arrêter l’exécution. Les limites de débit doivent respecter le signal de reprise du fournisseur. Un délai dépassé ou une connexion rompue est ambigu pour une écriture, car le service distant a pu appliquer l’effet. Ne reprenez qu’une opération dont la sémantique ou la clé d’idempotence rend la répétition sûre. La section 9.2.2 de RFC 9110 explique pourquoi il ne faut pas reprendre automatiquement une méthode non idempotente sans moyen d’établir que l’effet est sûr.
Générez une clé stable à partir de l’exécution et de l’action logique, jamais du numéro de tentative. Le connecteur conserve la clé et le résultat final pendant la fenêtre de reprise. Si la même clé arrive avec d’autres arguments, rejetez-la. Utilisez un backoff exponentiel avec jitter et peu de tentatives. Une reprise n’est pas une nouvelle décision du modèle. Conservez l’appel original, le numéro de tentative et l’identifiant du connecteur.
La récupération relève d’une machine à états. Utilisez des statuts comme "running", "waiting_for_approval", "retrying", "failed", "completed" et "cancelled". Un lease empêche deux workers d’exécuter le même run en même temps. Après perte du lease, arrêtez-vous avant le prochain effet. Un reconciler peut comparer les actions en attente aux enregistrements du connecteur après un crash. L’annulation doit se propager aux requêtes modèle, appels d’outils, files et demandes d’approbation lorsque le fournisseur le permet.
Observabilité
Instrumentez une trace par requête utilisateur et des spans pour appels de modèle, récupération, contrôles de politique, approbations et outils. Enregistrez durée, statut, nombre de reprises, tokens d’entrée et de sortie lorsqu’ils sont disponibles, versions du modèle et du prompt, outil, classe de risque et estimation de coût. Masquez les secrets et contenus sensibles avant export. Utilisez un identifiant de run stable pour relier une approbation ou un ticket de support à la trace sans mettre de données personnelles dans le baggage. OpenTelemetry context propagation décrit le lien du contexte de trace entre services et avertit sur les en-têtes non fiables et le baggage sensible.
Mesurez réussite de tâche selon une grille, appels d’outils réussis, erreurs de validation, approbations, reprises, délais expirés, latence p50 et p95, coût par tâche, annulations et blocages. Segmentez par version, outil, tenant et release. Peu d’erreurs peuvent cacher des réponses fausses. Reliez les traces aux transcriptions échantillonnées et aux évaluateurs. Ne journalisez jamais la chaîne de pensée. Conservez seulement les métadonnées et citations visibles autorisées.
Le guide interne sur l’observabilité des agents IA montre comment rendre ces signaux utiles sans transformer les journaux en seconde base de secrets.
Évaluations avant et après release
Une évaluation d’agent est un scénario avec état initial, outils autorisés, invariants attendus et règle de score définis. La réponse finale ne suffit pas. Vérifiez que l’agent a utilisé un outil autorisé, conservé le périmètre du tenant, demandé une approbation quand elle était nécessaire, évité de répéter une écriture, cité la bonne source et respecté le budget. Ajoutez des scénarios adversariaux : texte récupéré malveillant, connecteur indisponible, délai dépassé après une écriture, approbation périmée, demande ambiguë et données d’outil mal formées.
Utilisez une suite en couches :
- Tests unitaires déterministes pour schémas, autorisation, masquage, idempotence, transitions d’état et budgets.
- Tests de rejeu avec réponses d’outils enregistrées et décisions de modèle fixes pour vérifier la récupération.
- Tests de scénarios avec un modèle et une grille pour résultat, sécurité et communication.
- Tests red team pour injections directes et indirectes, fuite de données, agency excessive et déni de service.
- Échantillonnage en production avec protection de la vie privée, revue humaine et conversion des échecs en cas de régression.
Versionnez scénario, outils, politique, prompts, modèle et évaluateur. Stockez les échecs avec la trace et la plus petite entrée reproductible. Comparez une release candidate à une baseline et n’acceptez aucune régression sur les invariants de sécurité stricts, même si la qualité moyenne progresse. Le guide Anthropic sur l’évaluation des agents explique pourquoi les appels d’outils multi-tours demandent une évaluation de trajectoire. Le guide Dayfing sur les évaluations d’agents IA fournit une matrice de tests.
Choix de coût et de latence
Chaque tour de modèle, token récupéré, appel d’outil, pause d’approbation et reprise ajoute du temps ou du coût. Définissez des budgets par classe de tâche plutôt qu’un nombre global. Utilisez un petit modèle pour routage, extraction et précontrôles de politique si sa précision mesurée suffit. Réservez un modèle plus puissant à la planification ambiguë ou à la synthèse finale. Mettez en cache les catalogues d’outils stables et les embeddings de récupération. Résumez le contexte avant qu’il ne grossisse, mais mesurez si les résumés causent des tours supplémentaires ou la perte de faits.
Parallélisez les appels de lecture indépendants, puis fusionnez les résultats avec leur provenance. Gardez les écritures séquentielles, sauf si le connecteur fournit une transaction ou une compensation conçue. Diffusez la progression sans révéler les secrets. Pour un travail long, persistez un job et laissez un worker continuer après la fin de la requête HTTP. Un modèle plus rapide n’est pas moins cher si ses erreurs déclenchent revue humaine, écritures compensatoires ou exécutions répétées. Mesurez le coût total par résultat correct et conforme à la politique.
Une boucle exécutable minimale
L’exemple TypeScript indépendant du protocole ci-dessous utilise un modèle fictif et des outils locaux. Il s’exécute sans SDK fournisseur ni réseau. Il montre une boucle de décision typée, une approbation d’écriture, des reprises bornées, une clé d’idempotence stable et une protection contre l’effet en double. Un adaptateur réel peut remplacer model sans déplacer la frontière de policy et tool.
type ToolCall = { id: string; name: string; input: unknown };
type Message =
| { role: "user"; content: string }
| { role: "assistant"; content: string; toolCall?: ToolCall }
| { role: "tool"; callId: string; content: string };
type Decision =
| { kind: "answer"; text: string }
| { kind: "call"; call: ToolCall };
type Tool = {
sideEffect: "read" | "write";
run(input: unknown, idempotencyKey: string): Promise<string>;
};
const issued = new Set<string>();
const tools: Record<string, Tool> = {
getBalance: {
sideEffect: "read",
async run(): Promise<string> {
return JSON.stringify({ account: "demo", cents: 4200 });
},
},
sendInvoice: {
sideEffect: "write",
async run(input: unknown, idempotencyKey: string): Promise<string> {
if (issued.has(idempotencyKey)) return "already-sent";
if (typeof input !== "object" || input === null) throw new Error("invalid input");
issued.add(idempotencyKey);
return "invoice-sent";
},
},
};
const model = {
async decide(messages: readonly Message[]): Promise<Decision> {
const toolCount = messages.filter((message) => message.role === "tool").length;
if (toolCount === 0) {
return { kind: "call", call: { id: "balance-1", name: "getBalance", input: {} } };
}
if (toolCount === 1) {
return { kind: "call", call: { id: "invoice-1", name: "sendInvoice", input: { cents: 1200 } } };
}
return { kind: "answer", text: "The balance was checked and the invoice was sent." };
},
};
const wait = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds));
async function execute(call: ToolCall, tool: Tool, key: string): Promise<string> {
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
return await tool.run(call.input, key);
} catch (error) {
if (attempt === 2) throw error;
await wait(10 * 2 ** attempt);
}
}
throw new Error("unreachable");
}
async function run(): Promise<string> {
const state: { messages: Message[]; completed: Set<string> } = {
messages: [{ role: "user", content: "Check the balance and send the invoice." }],
completed: new Set<string>(),
};
const sessionId = "session-demo";
for (let step = 0; step < 6; step += 1) {
const decision = await model.decide(state.messages);
if (decision.kind === "answer") return decision.text;
const tool = tools[decision.call.name];
if (!tool) throw new Error("unknown tool");
const key = sessionId + ":" + decision.call.id;
if (tool.sideEffect === "write" && !process.argv.includes("--approve")) {
return "Paused for approval: " + decision.call.name;
}
if (state.completed.has(key)) continue;
const result = await execute(decision.call, tool, key);
state.completed.add(key);
state.messages.push(
{ role: "assistant", content: "", toolCall: decision.call },
{ role: "tool", callId: decision.call.id, content: result },
);
}
throw new Error("step budget exceeded");
}
run().then(console.log).catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Compilez-le avec l’outil TypeScript du projet et exécutez le JavaScript produit sans l’option pour observer la pause d’approbation, puis avec --approve pour autoriser l’écriture. L’exemple conserve volontairement l’état en mémoire. En production, l’état doit être durable, limité au tenant, chiffré si nécessaire et récupéré par un worker qui respecte les leases.