Building and Evaluating Agent Harnesses · Partie 1

Concevoir un harness d’agent : de la tâche à l’architecture

Traduction automatique

Cet article a été traduit automatiquement depuis la version originale en anglais.

Un harness d’agent est le code applicatif qui transforme les décisions d’un modèle en travail : il fournit le contexte, exécute les actions autorisées, maintient l’état et vérifie quand la tâche est terminée. Si vous construisez un agent pour votre propre application, concevoir ce code revient à décider de la liberté dont la tâche a besoin et de ce que l’application doit garantir, quelle que soit la réponse du modèle.

Ces décisions passent avant le framework. Un assistant de support, un coding agent et un classificateur de documents ont besoin de manières différentes d’agir, de se rétablir après une erreur et de démontrer leur réussite. Donner aux trois la même boucle, le même stockage de mémoire et le même ensemble d’outils masquerait les exigences qui les distinguent.

Cet article suit ces décisions depuis la description d’une tâche jusqu’à une première implémentation. Il prolonge Harness engineering pour les agents AI, qui présente les responsabilités d’un harness. Ici, nous verrons comment les choisir pour un projet précis.

Pourquoi le modèle a besoin d’un harness

Un modèle peut proposer un remboursement. Quelque chose d’autre doit identifier le compte, charger la commande, décider si l’opération proposée est autorisée, appeler le service de paiement et enregistrer ce qui s’est passé. Si ce service dépasse son délai, l’application doit aussi décider si une nouvelle tentative est sûre. Ces responsabilités existent même quand le modèle choisit très bien l’étape suivante.

LangChain utilise une définition large du harness, qui inclut le code, la configuration et la logique d’exécution autour du modèle. En pratique, une partie de tout cela appartient peut-être déjà à votre application : l’authentification, l’accès à la base de données, une file de tâches ou un système d’approbation. Concevoir un harness inclut de décider comment l’agent utilise ces mécanismes. Cela n’exige pas de les reconstruire dans un framework d’agents.

La version la plus petite peut faire un seul appel au modèle, valider sa sortie et renvoyer un résultat. Une version plus grande peut laisser le modèle inspecter des fichiers, exécuter des programmes et réviser son travail sur de nombreux tours. Les deux doivent répondre à la même question : quelles parties de cette tâche pouvons-nous déléguer, et comment saurons-nous qu’elles ont été correctement accomplies ?

Pour rendre cela concret, prenons un assistant de support client qui traite des remboursements, des questions de livraison et des modifications de compte. Il nous servira d’exemple de conception. Ses exigences nous mèneront vers une architecture précise ; une autre tâche devrait mener à d’autres choix.

Décrire le résultat avant l’agent

« Traiter les demandes de remboursement » laisse l’essentiel de l’ingénierie non spécifié. Supposons qu’un client demande le remboursement d’une commande livrée. Un résultat réussi exige un remboursement pour la bonne commande et le bon montant, une explication au client et aucune modification sans rapport sur le compte. Si la demande sort du cadre de la politique, un refus utile peut être le bon résultat. Si la commande ne peut pas être identifiée, l’assistant doit demander l’information manquante.

Ce sont des résultats différents. Considérer comme une réussite toute conversation qui se termine sans erreur les réduirait à un seul signal trompeur.

Nous devons aussi savoir qui fait la demande. Un identifiant de commande dans un message ne prouve pas que cette commande appartient à l’expéditeur. Le compte authentifié doit venir de l’application, et le service qui effectue le remboursement doit vérifier que l’opération concerne bien ce compte. Le modèle peut aider à interpréter la demande ; il ne peut pas établir l’autorité de l’appelant en produisant un identifiant plausible.

Nous obtenons ainsi le début d’un contrat de tâche : les modifications qui comptent comme une réussite, les modifications interdites et les situations qui exigent une question ou un transfert. Ce contrat révèle aussi des exigences qu’un prompt ne peut pas satisfaire à lui seul. Un montant de remboursement maximal exige un service qui l’applique. Un objectif de temps de réponse exige une échéance. Une obligation de reprendre demain exige un état durable.

À ce stade, nous en savons assez pour nous demander si un agent loop est utile.

Quelle part de la procédure connaissons-nous déjà ?

Si chaque demande suit la même séquence (extraire un numéro de commande, récupérer la commande, calculer l’éligibilité et expliquer le résultat), nous pouvons écrire cette séquence directement. Le modèle peut gérer le langage au début et à la fin. Il n’a pas besoin d’un appel supplémentaire pour décider de récupérer une commande que la procédure exige toujours.

Un workflow explicite devient utile quand la procédure comporte des branches obligatoires ou des états d’attente. Un remboursement peut nécessiter une approbation au-delà d’un seuil. Un changement d’adresse peut n’être autorisé qu’avant l’expédition. Nous pouvons représenter ces étapes sous forme de machine à états ou de graphe, le code applicatif décidant quelles transitions sont autorisées.

Un agent loop donne plus de latitude au modèle. Dans une conversation de support mêlant plusieurs sujets, le client peut commencer par se plaindre d’une livraison, révéler qu’il a reçu le mauvais article, puis demander un échange. La consultation utile suivante dépend de ce que l’assistant découvre. Une boucle lui permet de choisir une action, d’en examiner le résultat et de choisir à nouveau. Cette souplesse s’accompagne de davantage de chemins possibles à examiner, notamment des appels inutiles, des consultations répétées et un achèvement prématuré.

Le guide des workflows et des agents de LangGraph formule cette distinction en opposant des chemins d’exécution prédéterminés à des étapes choisies dynamiquement. Un graphe peut exprimer l’un comme l’autre. « Basé sur un graphe » et « agentic » ne sont pas des architectures qui s’excluent.

La même demande de remboursement représentée de trois façons. Un pipeline fixe lit la demande, récupère la commande, vérifie la politique et rédige la réponse dans un ordre fixe. Un workflow explicite ajoute une branche décidée par le code : un remboursement au-delà de la limite attend une approbation. Un agent loop laisse le modèle choisir chaque tool call à partir du dernier résultat et décider quand répondre.La même demande de remboursement représentée de trois façons. Un pipeline fixe lit la demande, récupère la commande, vérifie la politique et rédige la réponse dans un ordre fixe. Un workflow explicite ajoute une branche décidée par le code : un remboursement au-delà de la limite attend une approbation. Un agent loop laisse le modèle choisir chaque tool call à partir du dernier résultat et décider quand répondre.

Pour notre assistant de support, un compromis utile consiste à laisser un agent rassembler les informations et préparer une modification proposée, puis à transmettre cette proposition à une étape fixe de validation et d’exécution. Le modèle peut explorer au sein de la conversation, tandis que du code ordinaire détient les conditions d’une écriture. Si nous savons déjà que cette séparation est nécessaire, nous pouvons construire d’abord le workflow extérieur.

Nous pouvons aussi commencer par un agent loop borné dont les outils appliquent ces conditions. L’orchestration reste ainsi réduite pendant que nous apprenons de quels chemins la tâche a réellement besoin. Le choix dépend de ce qui fait déjà partie des exigences : des étapes explicites, des attentes d’approbation, des règles de reprise.

Dans un agent loop, le modèle appelle des outils de consultation en lecture seule et termine par une proposition de remboursement, que l’application enregistre. Une étape de code fixe vérifie le propriétaire, le montant et la politique. Si les vérifications réussissent, le code émet le remboursement, seule étape disposant d’un accès en écriture. Si elles échouent, l’assistant interroge le client ou transfère la demande.Dans un agent loop, le modèle appelle des outils de consultation en lecture seule et termine par une proposition de remboursement, que l’application enregistre. Une étape de code fixe vérifie le propriétaire, le montant et la politique. Si les vérifications réussissent, le code émet le remboursement, seule étape disposant d’un accès en écriture. Si elles échouent, l’assistant interroge le client ou transfère la demande.

Il est inutile d’introduire plusieurs agents simplement pour dessiner plus de boîtes. Des agents séparés deviennent utiles quand des sous-tâches ont besoin d’un contexte ou de permissions différents, ou peuvent avancer indépendamment. Il leur faut aussi un moyen de combiner leurs résultats et de résoudre les actions contradictoires. Deux exécutants qui peuvent tous deux rembourser la même commande créent un problème de coordination qu’un seul exécutant n’avait pas.

À gauche : un agent de livraison et un agent de facturation peuvent tous deux appeler l’API de remboursement, si bien qu’une commande est remboursée deux fois. À droite : les deux agents sont en lecture seule et renvoient leurs constats ; une étape de code les combine et effectue un seul appel de remboursement.À gauche : un agent de livraison et un agent de facturation peuvent tous deux appeler l’API de remboursement, si bien qu’une commande est remboursée deux fois. À droite : les deux agents sont en lecture seule et renvoient leurs constats ; une étape de code les combine et effectue un seul appel de remboursement.

Dans quel langage l’agent doit-il agir ?

Après avoir choisi qui contrôle l’étape suivante, il reste à choisir comment une action s’exprime. Ce sont deux décisions indépendantes : un workflow peut contenir un agent qui écrit du code, et une boucle ouverte peut n’utiliser que quelques outils étroits.

Pour l’assistant de support, une opération nommée comme issue_refund(order_id, amount_cents) est une interface naturelle. Son schéma rend explicite l’action demandée. L’outil peut renvoyer un identifiant de remboursement en cas de succès, ou une explication structurée du rejet de la demande. Nous pouvons inspecter et tester ce contrat sans demander au modèle de générer l’implémentation d’un remboursement.

Certaines tâches n’ont besoin d’aucun outil. Un classificateur qui dispose de toutes les données pertinentes dans son prompt peut renvoyer une étiquette validée. La recherche d’informations devient utile quand la tâche a besoin d’informations extérieures à ces données. Les écritures ne deviennent utiles que si accomplir la tâche exige de modifier quelque chose. Chaque capacité doit avoir une raison d’exister.

Le code généré offre un autre langage d’action. Imaginons qu’on demande à un assistant d’examiner une table, de regrouper les lignes par client et de calculer plusieurs totaux. Python peut exprimer ce travail en un seul programme, en gardant les valeurs intermédiaires dans l’environnement d’exécution. Exprimer chaque petite opération sous forme de tool call distinct pourrait exiger davantage d’échanges avec le modèle.

smolagents de Hugging Face illustre les deux approches. ToolCallingAgent produit des tool calls structurés. CodeAgent produit du Python qui peut appeler les outils fournis, stocker des valeurs et composer des opérations. Un code agent utilise toujours des outils ; il exprime leur composition sous forme de programme.

Cette expressivité change ce que nous devons exploiter. Il faut désormais gérer les erreurs de syntaxe, les limites d’exécution, les dépendances et l’accès aux fichiers ou aux réseaux. La réduction du nombre d’échanges avec le modèle compense-t-elle ces coûts ? Cela dépend de la tâche et du modèle. C’est une comparaison à mener, pas un avantage automatique du code.

Avec des tool calls, le modèle fait trois appels et chaque résultat intermédiaire revient dans son contexte. Avec du code généré, le modèle écrit un seul programme qui s’exécute dans un sandbox, appelle les mêmes opérations et ne renvoie que les totaux.Avec des tool calls, le modèle fait trois appels et chaque résultat intermédiaire revient dans son contexte. Avec du code généré, le modèle écrit un seul programme qui s’exécute dans un sandbox, appelle les mêmes opérations et ne renvoie que les totaux.

Plusieurs plateformes prennent en charge une approche hybride : des outils métier étroits plus un outil d’exécution de code en sandbox. L’API Responses d’OpenAI, l’API Claude, l’API Gemini et AWS Bedrock AgentCore proposent toutes l’exécution de code comme outil intégré, qui s’exécute à côté de vos propres fonctions. Le programmatic tool calling d’Anthropic va un peu plus loin : le code du sandbox peut appeler les outils que vous autorisez, et seuls certains résultats reviennent au modèle. Les recommandations d’Anthropic gardent les tool calls directs comme option par défaut et réservent le passage par le code aux résultats volumineux et aux chaînes d’appels dépendants. Il existe aussi des conceptions entièrement fondées sur le code, comme CodeAgent de smolagents et le Code Mode de Cloudflare.

Pour notre assistant de remboursement, je commencerais par des opérations métier typées. Si une tâche ultérieure exige des calculs importants, nous pourrons ajouter un calcul encadré sans donner à ce code une autorité directe sur les comptes clients.

MCP répond à une autre question, distincte : comment l’application doit-elle se connecter aux outils ? Une fonction locale suffit quand une seule application détient l’implémentation. MCP peut aider quand les outils sont servis séparément ou partagés entre plusieurs clients. Il fournit un protocole d’intégration ; la validation et le contrôle d’accès restent des responsabilités de l’application et du serveur participants, comme le décrit la spécification des outils MCP.

Placer les restrictions là où les actions ont lieu

La description d’un outil indique au modèle quand une opération est utile. L’implémentation doit décider si cet appel précis peut s’exécuter. Pour un remboursement, cela signifie vérifier le compte authentifié, la commande, le montant, la politique applicable et toute approbation requise, dans le service qui effectue l’écriture.

Certaines vérifications exigent une interprétation

Supposons que le client demande : « Puis-je retourner cette commande ? » et que l’agent propose un remboursement. La commande appartient au client, le montant est valide et le délai de retour est ouvert. Toutes ces vérifications peuvent réussir alors que l’action voulue reste floue : le client demande peut-être s’il est éligible, sans demander un remboursement immédiat. Nous devons interpréter le message avant de décider de la suite.

C’est un bon endroit pour séparer trois responsabilités. Le code ordinaire vérifie la propriété, les montants et les dates. Le modèle de langage principal mène la conversation et explique le résultat. Un modèle de décision distinct peut évaluer une question sémantique étroite, par exemple si le client a réellement demandé la modification proposée. Cette séparation nous donne une décision que nous pouvons tester indépendamment du reste de la conversation.

Jev, un modèle de décision de TypeSafe, est un candidat pour ce rôle. Il prend un état fourni et des questions typées, et renvoie des réponses structurées. Dans cet exemple, son type de question Noul renvoie une probabilité pour une proposition oui/non. Je demanderais séparément si le client a explicitement demandé le remboursement et si un message ultérieur a retiré cette demande. TypeSafe recommande de formuler chaque question de manière précise et de combiner les réponses dans le code ; plusieurs questions indépendantes peuvent partager un même appel d’API. Garder les deux jugements séparés permet de repérer plus facilement une interprétation erronée.

L’application combine ensuite ces résultats avec ses vérifications obligatoires. Des réponses incertaines ou contradictoires peuvent mener à une demande de précision ou à un examen humain ; un dépassement de délai du classificateur exige aussi un chemin d’erreur explicite. Un jugement sémantique positif ne peut pas outrepasser une vérification de propriété ni remplacer une confirmation requise. La probabilité de Jev est une sortie de modèle, pas un enregistrement d’autorisation ni une vérification indépendante de l’exactitude.

Une proposition de remboursement passe par des vérifications obligatoires dans le code et par un modèle de décision qui demande si le client l’a demandée et si un message ultérieur l’a retirée. Le code combine les deux : une vérification échouée mène à un refus, une réponse floue ou une erreur mène à une question ou à un examen, et seul le succès de toutes les vérifications mène au remboursement.Une proposition de remboursement passe par des vérifications obligatoires dans le code et par un modèle de décision qui demande si le client l’a demandée et si un message ultérieur l’a retirée. Le code combine les deux : une vérification échouée mène à un refus, une réponse floue ou une erreur mène à une question ou à un examen, et seul le succès de toutes les vérifications mène au remboursement.

Pour une telle vérification, je fournirais l’action proposée, les messages pertinents du client dans l’ordre et les tool results nécessaires à la décision, en gardant leurs sources distinctes. Ne transmettre que le dernier message pourrait faire perdre une annulation ; transmettre tout l’historique du compte pourrait noyer la demande pertinente. La question, le contexte et le comportement de repli font tous partie de la conception. Un petit modèle généraliste avec structured output est un autre candidat. Leur utilité et leur coût doivent être comparés sur les mêmes décisions.

Restreindre l’accès indépendamment des jugements du modèle

La source d’une instruction compte. Un message client ou un document récupéré peut contenir des instructions. Ce texte ne doit pas pouvoir accorder de nouvelles permissions ni remplacer la politique de référence de l’application. Si un document demande à l’agent d’exporter tous les dossiers clients, le système ne doit pas en avoir la permission, quelle que soit la force de persuasion du document.

C’est aussi là que se place la décision sur le sandbox. Si le modèle ne peut appeler que des fonctions revues aux permissions étroites, un sandbox de code généraliste apporte peut-être peu. Les fonctions ont toujours besoin d’autorisation, de validation, de délais d’expiration et d’opérations sûres sur la base de données.

Dès que nous autorisons du Python généré, des commandes shell ou des modifications de fichiers exécutables, nous devons décider de ce que cette exécution peut atteindre avant de l’activer. Quels répertoires sont accessibles en écriture ? L’accès réseau est-il nécessaire ? Quels identifiants sont disponibles ? Combien de temps CPU, de mémoire et de sortie une exécution peut-elle consommer ? Un processus séparé ne répond pas à lui seul à ces questions.

Le guide d’exécution sécurisée de Hugging Face décrit l’exécution locale restreinte et des alternatives plus isolées. Leurs restrictions diffèrent. Un conteneur avec de larges montages de l’hôte et des identifiants puissants peut encore exposer exactement les ressources que nous voulions protéger. Testez une tentative de lecture, d’écriture et de requête réseau interdites en plus d’un programme qui réussit.

Nous pouvons reporter l’exécuteur de code tant que notre tâche n’en a pas besoin. Son isolation doit être prête au moment où nous autorisons l’exécution de code. Et l’isolation ne peut pas empêcher l’usage abusif d’une API que nous mettons délibérément à disposition : le service de remboursement doit toujours appliquer ses propres règles.

Que reste-t-il quand une exécution s’arrête en cours de route ?

Supposons maintenant que l’assistant soumet un remboursement, que le service le crée et que la réponse se perd. Le modèle voit un dépassement de délai. Répéter l’appel peut créer un autre remboursement ; déclarer un échec peut dire au client quelque chose qui n’est plus vrai.

Ce problème relie l’état, les nouvelles tentatives et la reprise. Il nous faut un identifiant d’opération détenu par l’application, enregistré avant l’envoi et réutilisé lors de la reprise de la même action prévue. Le service doit reconnaître cet identifiant et renvoyer le résultat existant au lieu de refaire l’action. Les recommandations d’AWS sur les API idempotentes expliquent ce modèle. Si le service n’offre pas cette garantie, la reprise a besoin d’un moyen de vérifier ce qui s’est passé ou de demander une décision humaine avant une nouvelle tentative.

Le harness enregistre l’ID d’opération op-7f3, envoie le remboursement, et le service crée le remboursement r-1. La réponse se perd et le harness voit un dépassement de délai. Il réessaie avec le même ID, le service renvoie le remboursement existant r-1, et le client apprend qu’un seul remboursement existe.Le harness enregistre l’ID d’opération op-7f3, envoie le remboursement, et le service crée le remboursement r-1. La réponse se perd et le harness voit un dépassement de délai. Il réessaie avec le même ID, le service renvoie le remboursement existant r-1, et le client apprend qu’un seul remboursement existe.

Enregistrer la conversation ne suffit pas. Elle peut indiquer que le modèle a demandé un remboursement sans rien dire de l’achèvement de l’opération par le service externe. Un checkpoint aide à reprendre l’exécution ; il n’annule pas les effets externes et ne rend pas leur répétition sûre.

Pour cet assistant, trois types d’état ont des rôles différents. La conversation contient la demande du client et les faits recueillis jusqu’ici. Les enregistrements de commandes et de remboursements décrivent l’état métier. Un registre d’exécution durable suit les opérations et les approbations en attente, afin qu’un worker redémarré puisse continuer en toute sécurité. Ils peuvent partager une infrastructure, mais aucun ne doit en remplacer un autre.

La mémoire entre sessions est un autre choix. Si l’assistant doit se souvenir d’une préférence la semaine prochaine, il nous faut des règles : à qui appartient cette préférence, qui peut la modifier et comment la supprimer. Si chaque tâche est indépendante, un service de mémoire est peut-être inutile. Conserver plus de texte crée aussi plus d’occasions de réutiliser des informations obsolètes ou hors sujet.

Le même raisonnement s’applique aux résumés. Quand une conversation dépasse son contexte utile, nous pouvons avoir besoin de recherche ou de résumé. Un résumé qui perd un identifiant de commande, une condition d’approbation ou une opération non résolue peut casser la procédure. Gardez l’état opérationnel essentiel dans des champs explicites, et testez ce qui survit à la compression avant de vous y fier.

Donner à la boucle un moyen de s’arrêter

Une boucle souple peut toujours trouver autre chose à examiner. Il nous faut donc une politique d’arrêt qui couvre le succès, l’information manquante, l’épuisement des ressources et l’échec. Atteindre une limite doit produire un statut exact : le remboursement peut être effectué, non tenté ou encore incertain. « L’agent s’est arrêté » ne donne pas assez d’informations au client ni au worker suivant.

Une limite d’appels au modèle ne borne qu’une partie de l’exécution. Un outil peut rester bloqué. Une nouvelle tentative peut répéter du travail. Un résumeur ou un garde-fou peut faire des appels au modèle supplémentaires. L’application a besoin de limites sur le temps écoulé, les tentatives, la taille des sorties et les dépenses, qui incluent les composants qu’elle invoque réellement. L’annulation doit aussi tenir compte du travail déjà envoyé à un service externe.

Le choix du modèle fait partie de cette conception, car il influe sur ce que la boucle peut faire dans ces limites. Testez le format d’action requis sur des tâches représentatives. Tenez compte du contexte utilisable, de la latence, du coût et de l’endroit où les données de la tâche peuvent être envoyées. Un modèle servi localement et une API hébergée peuvent implémenter la même interface tout en imposant des contraintes de capacité et d’exploitation différentes.

Choisissez un modèle et un endpoint initiaux, enregistrez leurs paramètres, puis gardez-les fixes pendant l’évaluation d’une modification du harness. Sinon, une exécution plus rapide peut venir d’un autre fournisseur ou d’un autre modèle plutôt que de la modification que nous voulions tester.

Évaluer l’achèvement, y compris les cas qui ne doivent rien faire

Nous pouvons désormais exécuter une demande, mais il reste à décider si le résultat est utile. Pour l’exemple du remboursement, il y a au moins trois observations : ce que l’assistant a dit, quelles actions il a tentées et ce qui a changé dans le service. Chacune détecte des échecs que les autres peuvent manquer.

« J’ai remboursé votre commande » peut accompagner une base de données inchangée. Le bon remboursement peut accompagner un changement d’adresse sans rapport. Une base de données inchangée peut signifier un refus correct, une question de précision utile, un plantage ou un assistant qui n’a rien fait. L’état final seul ne permet pas de distinguer ces quatre derniers cas.

Quatre exécutions laissent la base de données inchangée : un refus valide, une question de précision, un plantage et une exécution qui n’a rien fait mais affirme que le remboursement est effectué. Seuls la réponse et les actions permettent de les distinguer.Quatre exécutions laissent la base de données inchangée : un refus valide, une question de précision, un plantage et une exécution qui n’a rien fait mais affirme que le remboursement est effectué. Seuls la réponse et les actions permettent de les distinguer.

La simulation de support qui accompagne cet article rend cette limite concrète. Ses douze tâches personnalisées en comptent sept qui n’attendent aucune modification de la base de données. Passer à chaque vérificateur une base de départ inchangée réussit donc 7 vérifications d’état sur 12. C’est une propriété des vérifications, pas un taux de réussite mesuré du support. Les définitions des tâches examinent les différences finales de la base de données ; elles n’évaluent ni l’explication ni les actions intermédiaires. Une écriture annulée par la suite peut aussi disparaître de cette comparaison.

Pour votre premier jeu d’évaluation, incluez une action réussie, un refus valide, une demande à laquelle manque une information essentielle et un échec en cours d’exécution. Définissez la réponse attendue et les actions autorisées en plus de l’état final. Un cas de précision doit exiger la bonne question ; un cas de refus doit exiger une explication utile. Ces cas révèlent si l’agent comprend la tâche et si l’application la fait respecter.

La vérification d’intention par Jev a aussi besoin de ses propres cas. Donnez-lui des actions proposées avec des décisions attendues attribuées indépendamment : une demande de remboursement explicite, une question d’éligibilité, une demande retirée et une instruction intégrée dans un tool result. Ce dernier cas teste aussi si l’assemblage du contexte préserve la distinction entre l’intention du client et le texte fourni par un outil. Comptez les actions inappropriées qu’elle autorise et les actions légitimes qu’elle bloque, puis suivez la conversation complète après un blocage. Empêcher une écriture ne règle qu’une partie de la tâche si l’assistant tourne ensuite en boucle ou ne demande jamais de précision au client. Choisissez les seuils sur des exemples de développement, figez-les avant la comparaison sur les données réservées, et enregistrez la version de la question, le contexte fourni, les probabilités renvoyées, la règle appliquée, les erreurs et le coût. La démo enregistrée ci-dessous n’a déclenché aucun rejet par Jev ; elle ne peut donc pas établir ce bénéfice.

Enregistrez assez d’informations pour diagnostiquer un échec : la tâche, les versions du modèle et du harness, les actions proposées, les résultats d’exécution, l’état final, les erreurs, la latence et le coût. Gardez les secrets et les données personnelles inutiles hors de cet enregistrement. Donnez au candidat les données de sa tâche et les règles dont il a besoin, mais gardez inaccessibles les réponses de référence et les cas de test réservés. L’évaluateur et ses éléments probants doivent aussi rester hors du contrôle du candidat. Si un futur optimiseur peut modifier le harness, il ne doit pas pouvoir réécrire les vérifications ni les résultats qui jugent ses modifications.

Ces éléments probants donnent une raison d’être aux composants optionnels. Ajoutez un routeur quand les traces montrent que la sélection de la tâche nécessite une étape distincte. Essayez un résumé quand les longs historiques provoquent un échec précis. Testez un garde-fou contre les actions interdites, y compris sur les cas qu’il doit autoriser. Les contrôles d’accès requis découlent des contraintes de la tâche ; les ajouts optionnels exigent des éléments probants montrant que leur bénéfice justifie leur coût.

Transformer la conception en première implémentation

Pour notre exemple de support, nous disposons maintenant d’un point de départ réfléchi : une boucle bornée pour interpréter des demandes variées, des outils étroits pour les opérations sur les comptes, des vérifications de référence dans les services qui effectuent les écritures, et des enregistrements explicites pour les actions en attente. Nous n’avons encore besoin ni de code généré ni de mémoire conversationnelle persistante.

Je construirais d’abord un chemin complet. Créez un petit compte de test et un résultat de remboursement attendu. Implémentez les opérations de consultation et de remboursement, y compris leurs chemins de rejet. Connectez le modèle à ces opérations. Testez ensuite les cas de refus, d’information manquante et d’écriture interrompue. Cela révèle les exigences manquantes avant qu’un ensemble de tâches plus grand ne les rende plus difficiles à voir.

Une boucle sans framework peut suffire à ce stade. Le flux de base consiste à assembler le contexte, demander l’action suivante, la valider et l’exécuter, puis renvoyer le résultat au modèle. Détenir cette boucle signifie aussi gérer la conversion des messages, l’annulation, l’état et le traçage. Réutiliser un framework existant est utile quand il épargne ce travail sans masquer les décisions que nous devons contrôler.

Pour cette série, create_agent de LangChain est un point de départ pratique, car il fournit la boucle modèle/outils et des hooks pour modifier son comportement. Il s’exécute déjà sur LangGraph. Passer plus tard à un workflow LangGraph explicite revient à prendre le contrôle de davantage d’étapes autour de cet agent ; cela ne revient pas à ajouter un graphe à un système qui n’en avait pas.

La première conception n’a pas besoin de contenir toutes les capacités évoquées ci-dessus. Elle doit en contenir assez pour accomplir sa tâche dans ses contraintes, avec des éléments probants qui révèlent où elle échoue. Avant d’écrire l’implémentation, je mettrais les décisions sur une seule page :

QuestionDécision à noter
Quelle tâche accomplissons-nous ?L’appelant, le résultat attendu, le refus ou le transfert valide, et les effets interdits
Qui choisit l’étape suivante ?Étapes connues dans le code ; décisions déléguées au modèle ; raison de chaque choix
Quelles vérifications exigent une interprétation ?La question sémantique, son contexte, le modèle candidat et le comportement en cas d’incertitude
Que peut demander le modèle ?Aucun outil, des opérations typées, du code généré ou une combinaison
Qui l’autorise et l’exécute ?Ressources autorisées, service qui applique les règles, règles d’approbation et éventuelles restrictions du sandbox
Que doit-il rester après une interruption ?État de la conversation, enregistrements métier, opérations en attente et comportement de reprise
Quelles limites s’appliquent ?Contraintes du modèle et du fournisseur, traitement des données, appels, temps, dépenses et annulation
Qu’est-ce qui prouve l’achèvement ?Vérifications de la réponse, des actions et de l’état ; qui détient leurs éléments probants
Qu’avons-nous laissé de côté ?Composants optionnels, et l’échec ou la nouvelle exigence qui justifierait chacun

Une réponse utile est assez précise pour changer l’implémentation. « A besoin de mémoire » est vague. « Doit reprendre un remboursement approuvé après le redémarrage d’un worker sans créer de second remboursement » nous dit quoi persister et quel échec tester.

Ceci est la partie 1 de Building and Evaluating Agent Harnesses. La partie 2 approfondit la question du flux de contrôle : quand un workflow explicite améliore-t-il assez le traitement d’une tâche pour justifier ses étapes supplémentaires ? Nous construirons le plus petit workflow justifié autour de l’agent et le comparerons à la boucle. Les routeurs, les workers parallèles et les résumés sont des pistes à étudier, pas une destination fixée d’avance. Les parties suivantes séparent l’évaluateur du candidat et mesurent la variation entre des exécutions répétées.

Optionnel : examiner l’implémentation et ses limites

La démo associée est une petite version exécutable de l’assistant de support. Elle tourne dans deux environnements :

  • Tâches de support personnalisées : douze tâches, chacune sur une base SQLite neuve de clients et de commandes.
  • τ³-bench retail : huit tâches d’un benchmark public dans lequel un client simulé parle à l’agent et des vérifications de la base de données notent le résultat.

Cette section décrit ce que contient la démo, ce qu’elle laisse de côté volontairement, ce que montrent les résultats et comment l’exécuter. Vous pouvez la sauter et utiliser quand même le processus de conception ci-dessus.

Ce que contient la démo

Un seul agent LangChain choisit les appels. Il dispose de cinq outils de compte (look_up_account, list_orders, issue_refund, update_address et set_preference) et d’un serveur MCP pour consulter la politique et la FAQ. Un checkpointer en mémoire conserve l’état d’exécution. Il n’y a pas d’exécuteur de code ni de mémoire entre sessions.

La démo enregistre deux configurations :

SpecComposants
plainRésumé, une limite de 20 appels au modèle par exécution et 2 nouvelles tentatives en cas d’erreur d’outil
baseTout ce que contient plain, plus Schema-Guided Reasoning et un garde-fou d’écriture Jev

Schema-Guided Reasoning (SGR) fait renvoyer au modèle un objet structuré décrivant l’étape suivante au lieu d’un tool call natif. Le garde-fou Jev s’exécute avant chaque écriture et pose une question composée : cet appel enfreindrait-il la politique ou irait-il au-delà de ce que le client a demandé ? Les deux questions séparées évoquées plus haut dans cet article, sur la demande et son retrait, sont une refonte proposée. Les exécutions enregistrées ne les ont pas utilisées.

base ajoute SGR et Jev ensemble, si bien que les résultats personnalisés ne peuvent pas montrer lequel des deux a causé une différence.

Le nœud du modèle porte le résumé, la limite d’appels au modèle et Schema-Guided Reasoning. Le nœud des outils porte la nouvelle tentative d’outil et le garde-fou d’écriture Jev. Sous τ³-bench, le graphe s’arrête avant le nœud des outils, si bien que seuls les composants côté modèle s’exécutent.Le nœud du modèle porte le résumé, la limite d’appels au modèle et Schema-Guided Reasoning. Le nœud des outils porte la nouvelle tentative d’outil et le garde-fou d’écriture Jev. Sous τ³-bench, le graphe s’arrête avant le nœud des outils, si bien que seuls les composants côté modèle s’exécutent.

Ce que la démo laisse de côté volontairement

  • Les outils vérifient l’intégrité des données mais laissent la politique métier à l’agent, si bien qu’une expérience peut révéler une écriture que la politique interdit. Un vrai service appliquerait la politique au moment de l’écriture.
  • Il n’y a pas de limite de temps ni de dépenses à l’échelle de la tâche, et les écritures réessayées n’ont pas de clé d’idempotence.
  • L’évaluateur s’exécute dans le même processus que l’agent.

Ces résultats ne testent donc ni l’application des règles en production, ni la sûreté des nouvelles tentatives, ni la protection des éléments probants contre la falsification.

Résultats sur les tâches personnalisées

Chaque ligne correspond à une exécution par tâche, avec le modèle et les prix de l’endpoint enregistrés avec les résultats. Une vérification d’état compare la base de données finale à celle attendue. Le coût et la latence sont des moyennes par tâche ; le coût inclut Jev là où il s’exécute.

SpecModèleVérifications d’étatCoûtLatence
baseopenai/gpt-6-luna12/12$0.0007311.9 s
plainopenai/gpt-6-luna12/12$0.000335.8 s
basez-ai/glm-5.3-flash12/12$0.001019.7 s
basexiaomi/mimo-v2.6-flash7/12$0.0008718.6 s
  • Avec gpt-6-luna, les deux specs ont réussi les douze tâches. base a coûté environ 2,2 fois plus et a pris environ deux fois plus de temps.
  • Jev a autorisé toutes les écritures qu’il a vérifiées : douze sur les exécutions gpt-6-luna et GLM, et quatre sur l’exécution MiMo. Il n’a jamais bloqué d’écriture, si bien que ces exécutions ne testent pas s’il en arrête une mauvaise.
  • Les cinq échecs de MiMo étaient des erreurs de format de sortie SGR. Il n’y a pas d’exécution plain avec MiMo, donc nous ne pouvons pas dire si le tool calling natif ferait mieux.
  • Le résumé ne s’est jamais déclenché. La plus grande requête des six exécutions enregistrées (ces quatre-ci et les deux exécutions τ³-bench ci-dessous) comptait 8 351 tokens d’entrée, sous le seuil de déclenchement de 12 000 tokens. Ces résultats ne peuvent pas montrer si le résumé aide.

Résultats sur τ³-bench

τ³-bench note une exécution en rejouant les tool calls de l’agent dans un environnement neuf, si bien que le benchmark doit exécuter lui-même les outils. L’adaptateur met le graphe en pause avant son nœud d’outils, renvoie les tool calls et réinscrit les résultats du benchmark avant de reprendre.

Un tour de l’adaptateur τ³. Le nœud du modèle s’exécute. Si le graphe est en pause avant le nœud des outils, l’adaptateur renvoie les tool calls, τ³-bench les exécute, et l’adaptateur écrit les résultats dans le checkpoint puis reprend. Sinon, il renvoie la réponse textuelle.Un tour de l’adaptateur τ³. Le nœud du modèle s’exécute. Si le graphe est en pause avant le nœud des outils, l’adaptateur renvoie les tool calls, τ³-bench les exécute, et l’adaptateur écrit les résultats dans le checkpoint puis reprend. Sinon, il renvoie la réponse textuelle.

Cela a deux conséquences. D’abord, le prompt, les outils et la politique viennent de τ³-bench, si bien que ces résultats ne sont pas comparables aux résultats personnalisés, même avec le même fichier de spec. Ensuite, la nouvelle tentative d’outil et Jev ne s’exécutent jamais ici : la seule différence entre base et plain est SGR.

Les deux exécutions utilisent openai/gpt-6-luna pour l’agent et pour le client simulé. La latence de simulation inclut le travail du simulateur. Le coût de l’agent et la latence de simulation sont des moyennes par tâche ; le coût du simulateur est le total pour les huit tâches.

SpecRéussiesCoût de l’agentLatence de simulationCoût du simulateur, total
base6/8$0.0023859.0 s$0.00453
plain7/8$0.0013638.9 s$0.00471
  • Les huit tâches proviennent du split de test retail et n’ont pas d’assertions en langage naturel, si bien que seules les vérifications de la base de données décident de la récompense.
  • Les deux specs ont terminé les huit tâches sans erreur d’adaptateur. Chaque échec était une écriture manquante. base a échoué aux tâches 9 et 26. plain a transféré la tâche 27 à un humain au lieu de mener l’échange à bien.
  • Une deuxième exécution de base a aussi réussi 6/8, mais a échoué aux tâches 9 et 17. Les tâches en échec changent d’une exécution à l’autre, donc une différence d’une tâche ne justifie pas de préférer plain. Ces exécutions ne mesurent pas cette variation.

Vérifier les chiffres enregistrés

Les rapports et traces sauvegardés conservent les résultats par tâche, le nombre de tokens, les configurations, les versions des paquets et les prix enregistrés. Pour les 393 appels de chat de l’agent dans les six exécutions enregistrées, le coût calculé à partir des tokens correspondait au montant facturé dans chaque réponse. Cette vérification exclut les appels de Jev et du client simulé. Les prix sont historiques ; ils ne constituent pas un devis pour votre exécution.

L’exécuter vous-même

Installez uv et utilisez la version taguée ci-dessous. Les tests s’exécutent hors ligne une fois les dépendances installées. Les deux exécutions d’environnement font des appels payants aux modèles via OpenRouter et nécessitent une clé d’API.

git clone https://github.com/slavadubrov/agent-harness-lab-public
cd agent-harness-lab-public
git checkout v0.1.2-a1
cp .env.example .env # set OPENROUTER_API_KEY
make test
make a1-custom SPEC=harness/spec/plain.yaml
make a1-tau3 SPEC=harness/spec/plain.yaml

Chaque exécution remplace les résultats sous reports/article-a1/. Utilisez git diff pour les comparer à la référence commitée. Le Makefile installe à partir du uv.lock figé.

Pour essayer un autre modèle ou un autre ensemble d’outils, copiez une spec sous harness/spec/, modifiez ces champs et passez son chemin via SPEC. Les paramètres du fournisseur et des composants personnalisés acceptent des clés libres, donc la validation ne peut pas détecter toutes les fautes de frappe imbriquées.

Vous pouvez utiliser le processus de conception de cet article sans exécuter la démo. La démo rend inspectable un ensemble de choix ; votre propre contrat de tâche décide lesquels ont leur place dans votre harness.