Building and Evaluating Agent Harnesses · Deel 1

Een agent harness ontwerpen: van taak naar architectuur

Automatische vertaling

Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Een agent harness is de applicatiecode die de beslissingen van een model omzet in werk: ze levert context, voert toegestane acties uit, houdt state bij en controleert wanneer de taak af is. Als je een agent voor je eigen applicatie bouwt, betekent het ontwerpen van die code dat je beslist hoeveel vrijheid de taak nodig heeft en wat de applicatie moet garanderen, ongeacht het antwoord van het model.

Die beslissingen komen vóór het framework. Een supportassistent, een coding agent en een documentclassifier hebben elk een andere manier nodig om te handelen, te herstellen en succes aan te tonen. Als je alle drie dezelfde loop, dezelfde memory store en dezelfde verzameling tools geeft, verberg je precies de eisen die ze van elkaar onderscheiden.

Dit artikel volgt die beslissingen van een taakbeschrijving tot een eerste implementatie. Het bouwt voort op Harness Engineering for AI Agents, dat de verantwoordelijkheden van een harness introduceert. Hier werken we uit hoe je ze voor een concreet project kiest.

Waarom het model een harness nodig heeft

Een model kan een terugbetaling voorstellen. Iets anders moet het account identificeren, de bestelling laden, beslissen of de voorgestelde operatie is toegestaan, de betaaldienst aanroepen en vastleggen wat er gebeurde. Als die dienst een timeout geeft, moet de applicatie ook beslissen of een nieuwe poging veilig is. Deze verantwoordelijkheden bestaan ook als het model heel goed is in het kiezen van de volgende stap.

LangChain hanteert een brede definitie van een harness die de code, configuratie en uitvoeringslogica rond het model omvat. In de praktijk hoort een deel daarvan misschien al bij je applicatie: authenticatie, databasetoegang, een job queue of een goedkeuringssysteem. Een harness ontwerpen houdt in dat je beslist hoe de agent die voorzieningen gebruikt. Je hoeft ze niet opnieuw te bouwen binnen een agent-framework.

De kleinste versie doet misschien één model call, valideert de output en geeft een resultaat terug. Een grotere laat het model misschien bestanden inspecteren, programma’s uitvoeren en zijn werk over veel beurten herzien. Beide hebben een antwoord nodig op dezelfde vraag: welke delen van deze taak kunnen we delegeren, en hoe weten we dat ze correct zijn uitgevoerd?

Om dat concreet te maken nemen we een supportassistent voor klanten die terugbetalingen, vragen over levering en accountwijzigingen afhandelt. We gebruiken die als ontwerpvoorbeeld. Zijn eisen leiden ons naar een bepaalde architectuur; een andere taak hoort tot andere keuzes te leiden.

Beschrijf het resultaat vóór de agent

“Verwerk terugbetalingsverzoeken” laat het meeste engineeringwerk onbepaald. Stel dat een klant om een terugbetaling vraagt voor een geleverde bestelling. Een geslaagd resultaat vereist een terugbetaling voor de juiste bestelling en het juiste bedrag, een uitleg aan de klant en geen ongerelateerde accountwijzigingen. Als het verzoek buiten het beleid valt, kan een bruikbare weigering het juiste resultaat zijn. Als de bestelling niet te identificeren is, moet de assistent om de ontbrekende informatie vragen.

Dit zijn verschillende uitkomsten. Als je elk gesprek dat zonder fout eindigt als succes telt, vallen ze samen tot één misleidend signaal.

We moeten ook weten wie de vraag stelt. Een ordernummer in een bericht bewijst niet dat die bestelling van de afzender is. Het geauthenticeerde account moet van de applicatie komen, en de dienst die de terugbetaling uitvoert moet controleren dat de operatie bij dat account hoort. Het model kan helpen het verzoek te interpreteren; het kan de bevoegdheid van de aanroeper niet vaststellen door een plausibel nummer te produceren.

Dit geeft ons het begin van een taakcontract: de wijzigingen die als succes tellen, de wijzigingen die verboden zijn, en de situaties die een vraag of een overdracht vereisen. Het legt ook eisen bloot waaraan een prompt alleen niet kan voldoen. Een maximaal terugbetalingsbedrag vereist een dienst die het afdwingt. Een doel voor de responstijd vereist een deadline. Een eis om morgen verder te gaan vereist duurzame state.

Nu weten we genoeg om te vragen of een agent loop überhaupt nuttig is.

Hoeveel van de procedure kennen we al?

Als elk verzoek dezelfde volgorde volgt (een ordernummer extraheren, de bestelling ophalen, het recht op terugbetaling berekenen en het resultaat uitleggen), kunnen we die volgorde direct uitschrijven. Het model kan de taal aan het begin en het eind afhandelen. Het heeft geen extra call nodig om te beslissen of het een bestelling ophaalt die de procedure altijd vereist.

Een expliciete workflow wordt nuttig als de procedure verplichte vertakkingen of wachttoestanden heeft. Een terugbetaling boven een drempel kan goedkeuring vereisen. Een adreswijziging is misschien alleen toegestaan vóór verzending. We kunnen deze fasen weergeven als een state machine of graaf, waarbij applicatiecode beslist welke overgangen zijn toegestaan.

Een agent loop geeft het model meer speelruimte. In een gemengd supportgesprek begint de klant misschien met een klacht over de levering, vertelt dan dat het verkeerde artikel is aangekomen en vraagt vervolgens om een omruiling. De volgende nuttige opzoeking hangt af van wat de assistent ontdekt. Een loop laat de assistent een actie kiezen, het resultaat bekijken en opnieuw kiezen. Die flexibiliteit brengt meer mogelijke paden mee om te inspecteren, waaronder onnodige calls, herhaalde opzoekingen en voortijdige afronding.

De workflow- en agent-gids van LangGraph maakt dit onderscheid in termen van vooraf bepaalde uitvoeringspaden tegenover dynamisch gekozen stappen. Een graaf kan beide uitdrukken. “Graafgebaseerd” en “agentic” zijn geen elkaar uitsluitende architecturen.

Hetzelfde terugbetalingsverzoek op drie manieren getekend. Een vaste pipeline voert verzoek lezen, bestelling ophalen, beleid controleren en antwoord schrijven in een vaste volgorde uit. Een expliciete workflow voegt een vertakking toe waarover code beslist: een terugbetaling boven de limiet wacht op goedkeuring. Een agent loop laat het model elke tool call kiezen op basis van het laatste resultaat en beslissen wanneer het antwoordt.Hetzelfde terugbetalingsverzoek op drie manieren getekend. Een vaste pipeline voert verzoek lezen, bestelling ophalen, beleid controleren en antwoord schrijven in een vaste volgorde uit. Een expliciete workflow voegt een vertakking toe waarover code beslist: een terugbetaling boven de limiet wacht op goedkeuring. Een agent loop laat het model elke tool call kiezen op basis van het laatste resultaat en beslissen wanneer het antwoordt.

Een bruikbaar compromis voor onze supportassistent is een agent informatie laten verzamelen en een voorgestelde wijziging laten voorbereiden, en dat voorstel daarna door te geven aan een vaste validatie- en uitvoeringsstap. Het model kan binnen het gesprek verkennen, terwijl gewone code de voorwaarden voor een schrijfactie beheert. Als we al weten dat deze scheiding nodig is, kunnen we eerst de buitenste workflow bouwen.

Je kunt ook beginnen met een begrensde agent loop waarvan de tools die voorwaarden afdwingen. Zo blijft de orchestration klein terwijl we leren welke paden de taak echt nodig heeft. De keuze hangt ervan af of expliciete fasen, wachten op goedkeuring en herstelregels al eisen zijn.

Binnen een agent loop roept het model alleen-lezen opzoektools aan en eindigt met een voorgestelde terugbetaling, die de applicatie opslaat. Een vaste codestap controleert eigenaar, bedrag en beleid. Als de controles slagen, voert code de terugbetaling uit; dat is de enige stap met schrijftoegang. Als ze falen, stelt de assistent de klant een vraag of draagt het gesprek over.Binnen een agent loop roept het model alleen-lezen opzoektools aan en eindigt met een voorgestelde terugbetaling, die de applicatie opslaat. Een vaste codestap controleert eigenaar, bedrag en beleid. Als de controles slagen, voert code de terugbetaling uit; dat is de enige stap met schrijftoegang. Als ze falen, stelt de assistent de klant een vraag of draagt het gesprek over.

Het heeft geen zin meerdere agents te introduceren alleen om meer blokken te tekenen. Afzonderlijke agents worden nuttig als deeltaken andere context of permissies nodig hebben, of onafhankelijk kunnen verlopen. Ze hebben dan ook een manier nodig om resultaten te combineren en conflicterende acties op te lossen. Twee workers die allebei dezelfde bestelling kunnen terugbetalen, creëren een coördinatieprobleem dat één worker niet had.

Links: een leveringsagent en een factureringsagent kunnen allebei de refund-API aanroepen, dus één bestelling wordt twee keer terugbetaald. Rechts: beide agents zijn alleen-lezen en geven bevindingen terug; een codestap combineert ze en doet één refund-call.Links: een leveringsagent en een factureringsagent kunnen allebei de refund-API aanroepen, dus één bestelling wordt twee keer terugbetaald. Rechts: beide agents zijn alleen-lezen en geven bevindingen terug; een codestap combineert ze en doet één refund-call.

In welke taal moet de agent handelen?

Nadat we hebben gekozen wie de volgende stap bepaalt, moeten we nog kiezen hoe een actie wordt uitgedrukt. Dat zijn onafhankelijke beslissingen: een workflow kan een agent bevatten die code schrijft, en een open loop kan slechts een paar smalle tools gebruiken.

Voor de supportassistent is een benoemde operatie zoals issue_refund(order_id, amount_cents) een natuurlijke interface. Het schema maakt de gevraagde actie expliciet. De tool kan bij succes een terugbetalings-ID teruggeven of een gestructureerde uitleg waarom het verzoek is afgewezen. We kunnen dit contract inspecteren en testen zonder het model te vragen de implementatie van een terugbetaling te genereren.

Sommige taken hebben geen tools nodig. Een classifier met alle relevante input in zijn prompt kan een gevalideerd label teruggeven. Retrieval wordt nuttig als de taak informatie buiten die input nodig heeft. Schrijfacties worden pas nuttig als de taak vereist dat er iets verandert. Elke capability hoort een reden te hebben om te bestaan.

Gegenereerde code biedt een andere actietaal. Stel dat je een assistent vraagt een tabel te inspecteren, rijen per klant te groeperen en een aantal totalen te berekenen. Python kan dat werk in één programma uitdrukken en houdt de tussenwaarden binnen de uitvoeringsomgeving. Als je elke kleine operatie als een aparte tool call uitdrukt, kan dat meer uitwisselingen met het model vereisen.

smolagents van Hugging Face laat beide benaderingen zien. ToolCallingAgent produceert gestructureerde tool calls. CodeAgent produceert Python dat de meegegeven tools kan aanroepen, waarden kan opslaan en operaties kan samenstellen. Een code agent gebruikt nog steeds tools; hij drukt hun samenstelling uit als een programma.

Die expressiviteit verandert wat we moeten beheren. We moeten nu syntaxfouten, uitvoeringslimieten, dependencies en toegang tot bestanden of netwerken afhandelen. Of de vermindering van uitwisselingen met het model die kosten rechtvaardigt, hangt af van de taak en het model. Het is een vergelijking die je moet uitvoeren, geen automatisch voordeel van code.

Met tool calls doet het model drie calls en komt elk tussenresultaat terug in zijn context. Met gegenereerde code schrijft het model één programma dat in een sandbox draait, dezelfde operaties aanroept en alleen de totalen teruggeeft.Met tool calls doet het model drie calls en komt elk tussenresultaat terug in zijn context. Met gegenereerde code schrijft het model één programma dat in een sandbox draait, dezelfde operaties aanroept en alleen de totalen teruggeeft.

Verschillende platforms ondersteunen een hybride vorm: smalle business tools plus een tool voor code-uitvoering in een sandbox. De Responses API van OpenAI, de Claude API, de Gemini API en AWS Bedrock AgentCore bieden allemaal code-uitvoering als ingebouwde tool die naast je eigen functies draait. Programmatic tool calling van Anthropic gaat een stap verder: code in de sandbox kan de tools aanroepen die je toestaat, en alleen geselecteerde resultaten gaan terug naar het model. De richtlijnen van Anthropic houden directe tool calls als standaard aan en gebruiken het codepad voor grote resultaten en ketens van afhankelijke calls. Er bestaan ook ontwerpen met alleen code, zoals CodeAgent van smolagents en Code Mode van Cloudflare.

Voor onze terugbetalingsassistent zou ik beginnen met getypeerde business-operaties. Als een latere taak veel rekenwerk vereist, kunnen we begrensde berekeningen toevoegen zonder die code directe zeggenschap over klantaccounts te geven.

MCP beantwoordt een verdere, aparte vraag: hoe moet de applicatie verbinding maken met tools? Een lokale functie volstaat als één applicatie de implementatie bezit. MCP kan helpen als tools apart worden aangeboden of door meerdere clients worden gedeeld. Het levert een integratieprotocol; validatie en toegangscontrole blijven verantwoordelijkheden van de deelnemende applicatie en server, zoals de MCP-specificatie voor tools beschrijft.

Leg beperkingen vast waar acties plaatsvinden

Een toolbeschrijving vertelt het model wanneer een operatie nuttig is. De implementatie moet beslissen of deze specifieke call mag draaien. Voor een terugbetaling betekent dat: het geauthenticeerde account, de bestelling, het bedrag, het geldende beleid en elke vereiste goedkeuring controleren bij de dienst die de schrijfactie uitvoert.

Sommige controles vereisen interpretatie

Stel dat de klant vraagt: “Kan ik deze bestelling retourneren?” en de agent stelt een terugbetaling voor. De bestelling is van de klant, het bedrag is geldig en de retourtermijn loopt nog. Al die controles kunnen slagen terwijl de bedoelde actie onduidelijk blijft: de klant vraagt misschien of het mag, in plaats van nu om een terugbetaling te vragen. We moeten het bericht interpreteren voordat we beslissen wat we nu doen.

Dit is een goede plek om drie verantwoordelijkheden te scheiden. Gewone code controleert eigendom, bedragen en datums. Het hoofdtaalmodel voert het gesprek en legt het resultaat uit. Een apart beslismodel kan een smalle semantische vraag beoordelen, zoals of de klant de voorgestelde wijziging echt heeft gevraagd. Deze scheiding geeft ons een beslissing die we los van de rest van het gesprek kunnen testen.

Jev, een beslismodel van TypeSafe, is een kandidaat voor die rol. Het neemt aangeleverde state en getypeerde vragen en geeft gestructureerde antwoorden terug. In dit voorbeeld geeft het vraagtype Noul een kans terug voor een ja/nee-propositie. Ik zou apart vragen of de klant expliciet om de terugbetaling vroeg en of een later bericht dat verzoek introk. TypeSafe raadt aan elke vraag specifiek te maken en de antwoorden in code te combineren; meerdere onafhankelijke vragen kunnen één API-call delen. Als je de twee oordelen gescheiden houdt, is een verkeerde interpretatie makkelijker te lokaliseren.

De applicatie combineert die resultaten daarna met haar verplichte controles. Onzekere of tegenstrijdige antwoorden kunnen leiden tot een verduidelijkende vraag of een menselijke review; een timeout van de classifier heeft ook een expliciet foutpad nodig. Een positief semantisch oordeel kan een eigendomscontrole niet opheffen en een vereiste bevestiging niet vervangen. De kans die Jev geeft is model-output, geen autorisatierecord en geen onafhankelijke controle van correctheid.

Een voorgestelde terugbetaling gaat naar verplichte codecontroles en naar een beslismodel dat vraagt of de klant erom vroeg en of een later bericht het verzoek introk. Code combineert beide: een mislukte controle leidt tot een weigering, een onduidelijk antwoord of een fout leidt tot een vraag of review, en alleen als alle controles slagen volgt de terugbetaling.Een voorgestelde terugbetaling gaat naar verplichte codecontroles en naar een beslismodel dat vraagt of de klant erom vroeg en of een later bericht het verzoek introk. Code combineert beide: een mislukte controle leidt tot een weigering, een onduidelijk antwoord of een fout leidt tot een vraag of review, en alleen als alle controles slagen volgt de terugbetaling.

Voor zo’n controle zou ik de voorgestelde actie aanleveren, de relevante klantberichten in volgorde en alle tool results die voor de beslissing nodig zijn, waarbij hun bronnen te onderscheiden blijven. Als je alleen het laatste bericht meegeeft, kan een annulering verloren gaan; als je de hele accountgeschiedenis meegeeft, kan het relevante verzoek ondersneeuwen. De vraag, de context en het fallbackgedrag horen allemaal bij het ontwerp. Een klein model voor algemeen gebruik met structured output is een andere kandidaat. Hun bruikbaarheid en kosten moet je op dezelfde beslissingen vergelijken.

Beperk toegang los van oordelen van het model

De bron van een instructie doet ertoe. Een klantbericht of opgehaald document kan instructies bevatten. Die tekst mag geen nieuwe permissies kunnen toekennen of het gezaghebbende beleid van de applicatie kunnen vervangen. Als een document de agent opdraagt alle klantgegevens te exporteren, hoort het systeem daar geen permissie voor te hebben, hoe overtuigend het document het verzoek ook formuleert.

Hier hoort ook de beslissing over de sandbox. Als het model alleen gereviewde functies met smalle permissies kan aanroepen, voegt een sandbox voor algemene code misschien weinig toe. De functies hebben nog steeds autorisatie, validatie, timeouts en veilige databaseoperaties nodig.

Zodra we gegenereerd Python, shell-commando’s of uitvoerbare bestandswijzigingen toestaan, moeten we beslissen wat die uitvoering kan bereiken voordat we het inschakelen. Welke mappen zijn schrijfbaar? Is netwerktoegang nodig? Welke credentials zijn beschikbaar? Hoeveel CPU-tijd, geheugen en output mag een run verbruiken? Een apart proces beantwoordt die vragen niet vanzelf.

De gids van Hugging Face over veilige uitvoering beschrijft beperkte lokale uitvoering en sterker geïsoleerde alternatieven. Hun beperkingen verschillen. Een container met brede host-mounts en krachtige credentials kan nog steeds precies de resources blootstellen die we wilden beschermen. Test een verboden leespoging, schrijfpoging en netwerkverzoek naast een programma dat slaagt.

We kunnen een code runner uitstellen zolang onze taak er geen nodig heeft. De isolatie moet klaar zijn op het moment dat we code-uitvoering toestaan. En isolatie kan geen misbruik voorkomen van een API die we bewust beschikbaar maken: de terugbetalingsdienst moet nog steeds zijn eigen regels afdwingen.

Wat blijft er over als een run halverwege stopt?

Stel nu dat de assistent een terugbetaling indient, de dienst die aanmaakt en de response verloren gaat. Het model ziet een timeout. De call herhalen kan een tweede terugbetaling aanmaken; een mislukking melden kan de klant iets vertellen dat niet meer klopt.

Dit probleem verbindt state, retries en herstel. We hebben een operatie-ID nodig dat de applicatie beheert, dat vóór verzending wordt opgeslagen en wordt hergebruikt bij het herstellen van dezelfde bedoelde actie. De dienst moet dat ID herkennen en het bestaande resultaat teruggeven in plaats van de actie opnieuw uit te voeren. De richtlijnen van AWS over idempotente API’s leggen dit patroon uit. Als de dienst zo’n garantie niet biedt, heeft herstel een manier nodig om na te gaan wat er gebeurde of om een menselijke beslissing te vragen vóór een nieuwe poging.

De harness slaat operatie-ID op-7f3 op, verstuurt de terugbetaling en de dienst maakt terugbetaling r-1 aan. De response gaat verloren en de harness ziet een timeout. Hij probeert opnieuw met hetzelfde ID, de dienst geeft de bestaande terugbetaling r-1 terug, en de klant hoort dat er één terugbetaling bestaat.De harness slaat operatie-ID op-7f3 op, verstuurt de terugbetaling en de dienst maakt terugbetaling r-1 aan. De response gaat verloren en de harness ziet een timeout. Hij probeert opnieuw met hetzelfde ID, de dienst geeft de bestaande terugbetaling r-1 terug, en de klant hoort dat er één terugbetaling bestaat.

Het gesprek opslaan is niet genoeg. Daarin staat misschien dat het model om een terugbetaling vroeg, maar niet of de externe dienst die heeft uitgevoerd. Een checkpoint helpt de uitvoering te hervatten; het draait geen externe effecten terug en maakt herhaling ervan niet veilig.

Voor deze assistent hebben drie soorten state verschillende taken. Het gesprek bevat het verzoek van de klant en de feiten die tot nu toe zijn verzameld. De bestel- en terugbetalingsrecords beschrijven de business-state. Een duurzaam uitvoeringsrecord houdt openstaande operaties en goedkeuringen bij, zodat een herstarte worker veilig verder kan. Ze kunnen infrastructuur delen, maar mogen elkaar niet vervangen.

Memory over sessions heen is een aparte keuze. Als de assistent volgende week een voorkeur moet onthouden, hebben we regels nodig voor van wie die voorkeur is, wie hem mag bijwerken en hoe hij verwijderd kan worden. Als elke taak op zichzelf staat, is een memory-dienst misschien overbodig. Meer bewaarde tekst betekent ook meer kansen om verouderde of irrelevante informatie te hergebruiken.

Dezelfde redenering geldt voor samenvattingen. Als een gesprek groter wordt dan zijn bruikbare context, hebben we misschien retrieval of samenvatting nodig. Een samenvatting die een ordernummer, een goedkeuringsvoorwaarde of een openstaande operatie weglaat, kan de procedure breken. Houd essentiële operationele state in expliciete velden, en test wat compressie overleeft voordat je erop vertrouwt.

Geef de loop een manier om te stoppen

Een flexibele loop kan steeds iets nieuws vinden om te inspecteren. We hebben daarom een stopbeleid nodig dat succes, ontbrekende informatie, uitgeputte middelen en falen dekt. Het bereiken van een limiet moet een waarheidsgetrouwe status opleveren: de terugbetaling kan voltooid zijn, niet geprobeerd of nog onzeker. “De agent is gestopt” is te weinig informatie voor de klant of de volgende worker.

Een limiet op model calls begrenst slechts een deel van de uitvoering. Een tool kan blijven hangen. Een retry kan werk herhalen. Een summarizer of guard kan extra model calls doen. De applicatie heeft limieten nodig op verstreken tijd, pogingen, outputgrootte en uitgaven die de componenten meetellen die ze werkelijk aanroept. Annulering moet ook rekening houden met werk dat al naar een externe dienst is verstuurd.

De modelkeuze hoort in dit ontwerp omdat die bepaalt wat de loop binnen die limieten kan doen. Test het vereiste actieformaat op representatieve taken. Houd rekening met bruikbare context, latency, kosten en waar taakdata naartoe gestuurd mag worden. Een lokaal gehost model en een gehoste API kunnen dezelfde interface implementeren en toch verschillende capaciteits- en operationele beperkingen opleggen.

Kies een eerste model en endpoint, leg hun instellingen vast en houd ze vervolgens vast terwijl je een wijziging aan de harness evalueert. Anders komt een snellere run misschien van een andere provider of een ander model in plaats van de wijziging die we wilden testen.

Evalueer voltooiing, ook voor de gevallen waarin niets moet gebeuren

We kunnen nu een verzoek uitvoeren, maar we moeten nog beslissen of het resultaat bruikbaar is. Voor het terugbetalingsvoorbeeld zijn er minstens drie observaties: wat de assistent zei, welke acties hij probeerde en wat er in de dienst veranderde. Elk vangt fouten die de andere kunnen missen.

“Ik heb je bestelling terugbetaald” kan samengaan met een ongewijzigde database. De juiste terugbetaling kan samengaan met een ongerelateerde adreswijziging. Een ongewijzigde database kan een terechte weigering betekenen, een nuttige verduidelijkende vraag, een crash of een assistent die niets deed. De eindtoestand alleen kan die laatste vier gevallen niet van elkaar onderscheiden.

Vier runs laten de database ongewijzigd: een terechte weigering, een verduidelijkende vraag, een crash en een run die niets deed maar beweert dat de terugbetaling is uitgevoerd. Alleen de response en de acties maken het verschil zichtbaar.Vier runs laten de database ongewijzigd: een terechte weigering, een verduidelijkende vraag, een crash en een run die niets deed maar beweert dat de terugbetaling is uitgevoerd. Alleen de response en de acties maken het verschil zichtbaar.

De supportsimulatie bij dit artikel maakt die beperking concreet. Van de twaalf eigen taken verwachten er zeven geen databasewijziging. Als je aan elke checker een ongewijzigde startdatabase doorgeeft, slagen daarom 7 van de 12 state-controles. Dat is een eigenschap van de controles, geen gemeten succespercentage voor support. De taakdefinities bekijken de verschillen in de uiteindelijke database; ze beoordelen niet de uitleg of de tussenliggende acties. Een schrijfactie die later wordt teruggedraaid, kan ook uit die vergelijking verdwijnen.

Neem in je eerste evaluatieset een geslaagde actie op, een terechte weigering, een verzoek waarin essentiële informatie ontbreekt en een fout tijdens de uitvoering. Definieer zowel de verwachte response en de toegestane acties als de eindtoestand. Een verduidelijkingsgeval moet de juiste vraag vereisen; een weigeringsgeval moet een bruikbare uitleg vereisen. Deze gevallen laten zien of de agent de taak begrijpt en of de applicatie haar afdwingt.

De intentiecontrole met Jev heeft ook eigen gevallen nodig. Geef hem voorgestelde acties met onafhankelijk toegekende verwachte beslissingen: een expliciet terugbetalingsverzoek, een vraag of terugbetaling mogelijk is, een ingetrokken verzoek en een instructie die in een tool result is verstopt. Dat laatste geval test ook of de opbouw van de context het onderscheid tussen de intentie van de klant en tekst uit een tool bewaart. Tel de ongepaste acties die hij toestaat en de legitieme acties die hij blokkeert, en volg daarna het hele gesprek na een blokkade. Een schrijfactie voorkomen helpt maar voor een deel van de taak als de assistent daarna in een loop raakt of de klant nooit om verduidelijking vraagt. Kies drempels op ontwikkelvoorbeelden, zet ze vast vóór de vergelijking op de achtergehouden set, en leg de vraagversie, de aangeleverde context, de teruggegeven kansen, de toegepaste regel, fouten en kosten vast. De opgenomen demo hieronder heeft geen afwijzing door Jev getest, dus die kan dat voordeel niet aantonen.

Leg genoeg vast om een fout te kunnen diagnosticeren: de taak, de model- en harness-versies, voorgestelde acties, uitvoeringsresultaten, eindtoestand, fouten, latency en kosten. Houd secrets en onnodige persoonsgegevens buiten dat record. Geef de kandidaat zijn taakinput en de regels die hij nodig heeft, maar houd referentieantwoorden en achtergehouden testgevallen ontoegankelijk. De evaluator en zijn bewijs moeten ook buiten de controle van de kandidaat blijven. Als een toekomstige optimizer de harness kan aanpassen, mag die de controles of resultaten die zijn wijzigingen beoordelen niet kunnen herschrijven.

Dit bewijs geeft optionele componenten een doel. Voeg een router toe als traces laten zien dat taakselectie een aparte fase nodig heeft. Probeer een samenvatting als lange geschiedenissen een specifieke fout veroorzaken. Test een guard tegen verboden acties, inclusief gevallen die hij moet toestaan. Verplichte toegangscontroles volgen uit de beperkingen van de taak; optionele toevoegingen hebben bewijs nodig dat hun voordeel de kosten rechtvaardigt.

Zet het ontwerp om in een eerste implementatie

Voor ons supportvoorbeeld hebben we nu een onderbouwd startpunt: een begrensde loop om uiteenlopende verzoeken te interpreteren, smalle tools voor accountoperaties, gezaghebbende controles in de diensten die schrijfacties uitvoeren, en expliciete records voor openstaande acties. We hebben nog geen eis voor gegenereerde code of persistent memory voor gesprekken.

Ik zou eerst één volledig pad bouwen. Maak een klein testaccount en een verwachte terugbetalingsuitkomst. Implementeer de opzoek- en terugbetalingsoperaties, inclusief hun afwijzingspaden. Verbind het model met die operaties. Test daarna de gevallen met een weigering, ontbrekende informatie en een onderbroken schrijfactie. Zo komen ontbrekende eisen boven voordat een grotere verzameling taken ze moeilijker zichtbaar maakt.

Een loop zonder framework kan in deze fase volstaan. De basisflow is: context samenstellen, de volgende actie opvragen, die valideren en uitvoeren, en het resultaat teruggeven aan het model. Wie die loop zelf beheert, beheert ook berichtconversie, annulering, state en tracing. Een bestaand framework hergebruiken is nuttig als het dat werk bespaart zonder de beslissingen te verbergen die we zelf moeten beheersen.

Voor deze serie is create_agent van LangChain een handig startpunt, omdat het de model/tool-loop levert en hooks om het gedrag te wijzigen. Het draait al op LangGraph. Later overstappen op een expliciete LangGraph-workflow betekent dat je meer fasen rond die agent zelf in handen neemt; het betekent niet dat je een graaf toevoegt aan een systeem dat er eerst geen had.

Het eerste ontwerp hoeft niet elke hierboven besproken capability te bevatten. Het heeft genoeg nodig om zijn taak binnen zijn beperkingen te voltooien, plus bewijs dat laat zien waar het faalt. Voordat ik de implementatie schrijf, zou ik de beslissingen op één pagina zetten:

VraagOp te schrijven beslissing
Welke taak voeren we uit?De aanroeper, het verwachte resultaat, een geldige weigering of overdracht, en verboden effecten
Wie kiest de volgende stap?Bekende fasen in code; beslissingen gedelegeerd aan het model; de reden voor elk
Welke controles vereisen interpretatie?De semantische vraag, de context, het kandidaat-model en het gedrag bij onzekerheid
Wat mag het model vragen te doen?Geen tools, getypeerde operaties, gegenereerde code of een combinatie
Wie autoriseert en voert het uit?Toegestane resources, afdwingende dienst, goedkeuringsregels en eventuele sandbox-beperkingen
Wat moet een onderbreking overleven?Gespreksstate, business-records, openstaande operaties en herstelgedrag
Welke limieten gelden?Beperkingen van model/provider, omgang met data, calls, tijd, uitgaven en annulering
Wat bewijst voltooiing?Controles op response, acties en state; wie hun bewijs beheert
Wat hebben we weggelaten?Optionele componenten en de fout of nieuwe eis die elk ervan zou rechtvaardigen

Een bruikbaar antwoord is specifiek genoeg om de implementatie te veranderen. “Heeft memory nodig” is vaag. “Moet een goedgekeurde terugbetaling na een herstart van de worker hervatten zonder een tweede terugbetaling aan te maken” vertelt ons wat we moeten opslaan en welke fout we moeten testen.

Dit is deel 1 van Building and Evaluating Agent Harnesses. Deel 2 gaat verder in op de vraag over control flow: wanneer verbetert een expliciete workflow de afhandeling van een taak genoeg om de extra fasen te rechtvaardigen? We bouwen de kleinste onderbouwde workflow rond de agent en vergelijken die met de loop. Routers, parallelle workers en samenvattingen zijn kandidaten om te onderzoeken, geen vooraf bepaalde bestemming. Latere delen scheiden de evaluator van de kandidaat en meten de variatie over herhaalde runs.

Optioneel: bekijk de implementatie en haar grenzen

De bijbehorende demo is een kleine, uitvoerbare versie van de supportassistent. Die draait in twee omgevingen:

  • Eigen supporttaken: twaalf taken, elk tegen een nieuwe SQLite-database met klanten en bestellingen.
  • τ³-bench retail: acht taken uit een openbare benchmark waarin een gesimuleerde klant met de agent praat en databasecontroles het resultaat beoordelen.

Deze sectie beschrijft wat de demo bevat, wat hij bewust weglaat, wat de resultaten laten zien en hoe je hem uitvoert. Je kunt ze overslaan en het ontwerpproces hierboven toch gebruiken.

Wat de demo bevat

Eén LangChain-agent kiest de calls. Die heeft vijf accounttools (look_up_account, list_orders, issue_refund, update_address en set_preference) en een MCP-server voor het opzoeken van beleid en FAQ’s. Een checkpointer in het geheugen houdt de uitvoeringsstate bij. Er is geen code runner en geen memory over sessions heen.

De demo legt twee configuraties vast:

SpecComponenten
plainSamenvatting, een limiet van 20 model calls per run en 2 retries bij een toolfout
baseAlles uit plain, plus Schema-Guided Reasoning en een Jev-guard voor schrijfacties

Schema-Guided Reasoning (SGR) laat het model een gestructureerd object voor de volgende stap teruggeven in plaats van een native tool call. De Jev-guard draait vóór elke schrijfactie en stelt één samengestelde vraag: zou deze call het beleid schenden of verder gaan dan waar de klant om vroeg? De twee aparte vragen eerder in dit artikel, over het verzoek en de intrekking ervan, zijn een voorgesteld herontwerp. De opgenomen runs gebruikten ze niet.

base voegt SGR en Jev tegelijk toe, dus de resultaten op de eigen taken kunnen niet laten zien welke van de twee een verschil veroorzaakte.

De model-node draagt samenvatting, de limiet op model calls en Schema-Guided Reasoning. De tools-node draagt tool-retry en de Jev-guard voor schrijfacties. Onder τ³-bench stopt de graaf vóór de tools-node, dus draaien alleen de componenten aan de modelkant.De model-node draagt samenvatting, de limiet op model calls en Schema-Guided Reasoning. De tools-node draagt tool-retry en de Jev-guard voor schrijfacties. Onder τ³-bench stopt de graaf vóór de tools-node, dus draaien alleen de componenten aan de modelkant.

Wat de demo bewust weglaat

  • De tools controleren de data-integriteit maar laten het business-beleid aan de agent over, zodat een experiment een schrijfactie kan blootleggen die het beleid verbiedt. Een echte dienst zou het beleid bij de schrijfactie afdwingen.
  • Er is geen limiet op tijd of uitgaven voor de hele taak, en herhaalde schrijfacties hebben geen idempotency key.
  • De evaluator draait in hetzelfde proces als de agent.

Deze resultaten testen daarom niet de handhaving in productie, de veiligheid van retries of de bescherming van het bewijs tegen manipulatie.

Resultaten op de eigen taken

Elke rij is één run per taak, met het model en de endpoint-prijzen vastgelegd bij de resultaten. Een state-controle vergelijkt de uiteindelijke database met de verwachte. Kosten en latency zijn gemiddelden per taak; de kosten omvatten Jev waar het draait.

SpecModelState-controlesKostenLatency
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
  • Met gpt-6-luna slaagden beide specs voor alle twaalf taken. base kostte ongeveer 2,2 keer zoveel en duurde ongeveer twee keer zo lang.
  • Jev stond elke schrijfactie toe die het controleerde: twaalf over de runs met gpt-6-luna en GLM, en vier in de MiMo-run. Het blokkeerde nooit een schrijfactie, dus deze runs testen niet of het een foute tegenhoudt.
  • De vijf mislukkingen van MiMo waren fouten in het SGR-outputformaat. Er is geen plain-run met MiMo, dus we kunnen niet zeggen of native tool calling beter zou werken.
  • Samenvatting is nooit uitgevoerd. Het grootste verzoek in de zes opgenomen runs (deze vier en de twee τ³-bench-runs hieronder) had 8.351 input tokens, onder de drempel van 12.000 tokens. Deze resultaten kunnen niet laten zien of samenvatting helpt.

Resultaten op τ³-bench

τ³-bench beoordeelt een run door de tool calls van de agent opnieuw af te spelen in een nieuwe omgeving, dus de benchmark moet de tools zelf uitvoeren. De adapter pauzeert de graaf vóór de tools-node, geeft de tool calls terug en schrijft de resultaten van de benchmark terug voordat hij hervat.

Eén beurt van de τ³-adapter. De model-node draait. Als de graaf vóór de tools-node is gepauzeerd, geeft de adapter de tool calls terug, voert τ³-bench ze uit, en schrijft de adapter de resultaten in het checkpoint en hervat. Anders geeft hij het tekstantwoord terug.Eén beurt van de τ³-adapter. De model-node draait. Als de graaf vóór de tools-node is gepauzeerd, geeft de adapter de tool calls terug, voert τ³-bench ze uit, en schrijft de adapter de resultaten in het checkpoint en hervat. Anders geeft hij het tekstantwoord terug.

Dit heeft twee gevolgen. Ten eerste komen de prompt, de tools en het beleid van τ³-bench, dus deze resultaten zijn niet vergelijkbaar met die op de eigen taken, ook niet met hetzelfde spec-bestand. Ten tweede draaien tool-retry en Jev hier nooit: het enige verschil tussen base en plain is SGR.

Beide runs gebruiken openai/gpt-6-luna voor de agent en voor de gesimuleerde klant. De simulatielatency omvat het werk van de simulator. Agentkosten en simulatielatency zijn gemiddelden per taak; de simulatorkosten zijn het totaal voor alle acht taken.

SpecGeslaagdAgentkostenSimulatielatencySimulatorkosten, totaal
base6/8$0.0023859.0 s$0.00453
plain7/8$0.0013638.9 s$0.00471
  • De acht taken komen uit de retail-testsplit en hebben geen assertions in natuurlijke taal, dus alleen databasecontroles bepalen de reward.
  • Beide specs voltooiden alle acht taken zonder adapterfouten. Elke mislukking was een ontbrekende schrijfactie. base faalde op taken 9 en 26. plain droeg taak 27 over aan een mens in plaats van de omruiling af te ronden.
  • Een tweede base-run haalde ook 6/8, maar faalde op taken 9 en 17. De falende taken verschillen per run, dus een verschil van één taak is geen reden om plain te verkiezen. Deze runs meten die variatie niet.

Controleer de vastgelegde cijfers

De opgeslagen rapporten en traces bewaren de uitkomsten per taak, token-aantallen, configuraties, pakketversies en vastgelegde prijzen. Voor de 393 chat calls van de agent in de zes opgenomen runs kwamen de uit tokens berekende kosten in elke response overeen met het gefactureerde bedrag. Deze controle sluit calls van Jev en van de gesimuleerde klant uit. De prijzen zijn historisch, geen offerte voor jouw run.

Voer het zelf uit

Installeer uv en gebruik de getagde versie hieronder. De tests draaien offline zodra de dependencies zijn geïnstalleerd. De twee omgevingsruns doen betaalde model calls via OpenRouter en hebben een API-key nodig.

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

Elke run vervangt de resultaten onder reports/article-a1/. Gebruik git diff om ze te vergelijken met de gecommitte baseline. De Makefile installeert vanuit de bevroren uv.lock.

Wil je een ander model of een andere toolset proberen, kopieer dan een spec onder harness/spec/, wijzig die velden en geef het pad door via SPEC. Instellingen voor providers en eigen componenten accepteren vrije keys, dus validatie kan niet elke geneste typefout opvangen.

Je kunt het ontwerpproces in dit artikel gebruiken zonder de demo uit te voeren. De demo maakt één set keuzes inspecteerbaar; je eigen taakcontract bepaalt welke daarvan in jouw harness thuishoren.