Ga naar inhoud

Evaluaties via de API

Het concept achter agents staat in Agents.

Op deze pagina schrijf je een evaluatie met de API, start je een run en lees je de uitkomst. Een evaluatie is een suite gesprekken die je één keer opschrijft en daarna tegen een agent laat draaien. Elk gesprek is een scenario, elk scenario bestaat uit stappen, en elke stap is één bericht van de gebruiker plus de regels waaraan het antwoord moet voldoen. Een run speelt die gesprekken opnieuw af tegen een vastgepinde agentversie en legt vast wat klopte en wat niet. Gebruik het om na een wijziging aan een agent te zien of hij zijn werk nog doet.

Je sleutel heeft de capability evaluations nodig. Evaluaties gelden alleen voor agents: daarom heeft evaluations ook agents nodig, en noemt een suite minstens één agent. Een run heeft daarnaast threads nodig: een scenario is een gesprek, en de stappen ketenen op één thread. Hetzelfde doe je met de muis in Evaluaties in de console.

Een suite maken

curl -s -X POST "$CORE_URL/evaluations" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"name": "triage-kwaliteit",
         "description": "Blijft triage beleefd en noemt hij het ticketnummer?",
         "judge_model": "<een model-id>",
         "run_as_key_id": "key_9Tc4...",
         "agents": ["agt_5Nj2..."]}'

Een geslaagd verzoek geeft 201.

Veld Wat het doet
name uniek binnen je organisatie
description waar deze suite over gaat; puur voor lezers
judge_model het model dat de beoordeelde checks uitvoert; laat je het weg, dan gebruikt elke run het model van de agent zelf
run_as_key_id de sleutel van je organisatie waaronder elke run van deze suite draait; laat je het weg, dan draait een run onder de sleutel die hem start
agents de agents die deze suite meet, minstens één en hoogstens zestien

Een agent die het beleid van je sleutel niet noemt, kun je niet koppelen: dat is 422 op agents. Een sleutel die niet van jouw organisatie is, ook niet: dat is 422 op run_as_key_id. PATCH /evaluations/{id} verandert wat je meegeeft; agents vervangt de hele set, en een expliciete null op judge_model of run_as_key_id maakt dat veld weer leeg.

Onder welke sleutel een run draait

Elke stap van een run is een gewone beurt, en een beurt staat altijd op naam van een sleutel. Elke run legt daarom vast onder welke sleutel hij draait:

  1. de sleutel die de suite noemt in run_as_key_id, als die er is;
  2. anders de sleutel die de run start;
  3. anders 409 — de suite moet eerst een sleutel noemen.

Dat derde geval is een beheerder in de console: een persoon heeft zelf geen sleutel om op terug te vallen. Noem je er een, dan kan de console runs gewoon starten.

Of de agent bereikbaar is, wordt getoetst aan het beleid van de sleutel waaronder de run gaat draaien — niet aan dat van jou. Kan die sleutel de agent niet zien, dan krijg je meteen 422 in plaats van een run die niets kan. Is de genoemde sleutel intussen verwijderd, dan is dat 422 op run_as_key_id: er wordt nooit stilletjes teruggevallen op jouw eigen sleutel.

created_by op een run blijft wie hem gestart heeft (een sleutel-id of operator:<sub>); run_as_key_id zegt op wiens naam de beurten staan.

Een sleutel die evaluaties mag beheren, mag hier elke sleutel van haar organisatie noemen, ook een die meer capabilities of een hogere limiet heeft dan zijzelf. De beurten van de runs van de suite vallen dan onder de policy van de genoemde sleutel en tellen mee voor de limiet per maand van die sleutel, niet voor de jouwe. Een planning werkt anders: je sleutel plant als zichzelf.

Een scenario schrijven

curl -s -X POST "$CORE_URL/evaluations/evl_7Qa1.../scenarios" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"title": "dubbele afschrijving",
         "steps": [
           {"input": "Mijn kaart is twee keer belast.",
            "checks": [{"kind": "contains_text", "expected": "ticket"},
                       {"kind": "polite_tone", "required": false}]},
           {"input": "Kun je het terugboeken?",
            "checks": [{"kind": "must_use_tool", "expected": "api__refund"}]}
         ]}'

Een scenario wordt achteraan toegevoegd. Je schrijft een scenario altijd in zijn geheel: met PUT /evaluations/{id}/scenarios/{scenario_id} vervang je titel, omschrijving en de hele stappenlijst in één keer. Per stap schrijven bestaat niet, juist omdat twee mensen die tegelijk aan één scenario werken elkaars wijzigingen anders kwijtraken.

Elke stap krijgt een evt_-id van de server. Stuur dat id mee als je de stap bewaart bij een volgende PUT, dan blijven de resultaten van eerdere runs eraan gekoppeld. Een id dat je zelf meestuurt, moet er een zijn die de server ook had kunnen maken (evt_ plus letters en cijfers) en moet uniek zijn binnen dat scenario; twee stappen in één scenario met hetzelfde id is 422.

Een suite bevat hoogstens 32 scenario's, een scenario hoogstens 32 stappen en een stap hoogstens 16 checks.

Waarden voor de prompt van de agent

Zet een agent een prompt vast die variabelen declareert, dan levert het scenario die waarden. variables is een mapping van naam naar waarde op het scenario, en elke stap van dat scenario stuurt dezelfde waarden mee:

curl -s -X POST "$CORE_URL/evaluations/evl_7Qa1.../scenarios" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"title": "een boze vaste klant",
         "variables": {"customer_name": "A. Bakker", "tier": "goud"},
         "steps": [{"input": "Mijn kaart is twee keer belast.", "checks": []}]}'

Ze staan op het scenario en niet op een stap, omdat één gesprek één persoon is: de beller verandert niet halverwege.

Een suite meet meerdere agents en je schrijft één scenario voor allemaal, dus een naam die de prompt van een agent niet declareert, blijft gewoon weg uit de beurten van die agent in plaats van ze te laten falen — schrijf customer_name voor de agent die erom vraagt en tier voor de agent die dat doet, in hetzelfde scenario. Een agent die helemaal geen prompt vastzet, krijgt geen enkele waarde, wat het scenario ook zegt.

Twee dingen falen wél: een verplichte variabele zonder waarde, en een waarde die niet past bij het type of de enum die de prompt van de agent ervoor declareert, zoals de tekst "3" voor een number. In beide gevallen komt elke stap van die agent terug als mislukte stap met een reden die de variabele noemt. Bij het schrijven van het scenario wordt geen van beide gecontroleerd, omdat de agents die een suite meet ook later gekoppeld kunnen worden. Een variabele met een standaard is niet verplicht: laat het scenario hem weg, dan vult Core bij elke stap de standaard in.

Namen moeten eruitzien als placeholders in een prompt ([A-Za-z_][A-Za-z0-9_]*), hoogstens 32 stuks. Een waarde is een JSON-tekst, -getal of -boolean -- stuur hem als het type dat het invoerschema van de prompt declareert -- en een tekst is hoogstens 8192 tekens; iets anders is 422 op variables. Een PUT vervangt de hele mapping, net zoals hij de stappen vervangt.

tool_variables op het scenario werkt net zo voor de tools van de gemeten agent: elke stap stuurt die waarden mee als de tool_variables van het verzoek, en een naam die geen tool van de beurt van een stap declareert, wordt daar genegeerd. Elke lezer van de suite ziet ze, dus ze zijn nooit geheim: een waarde voor een naam die een tool van je organisatie als geheim declareert, wordt geweigerd met 422 op tool_variables.<naam>.

DELETE /evaluations/{id}/scenarios/{scenario_id} verwijdert er één. De volgorde van de rest verandert daarbij niet: een run die al liep, heeft zijn eigen momentopname.

Versies

Elke wijziging van wat een suite meet -- een scenario geschreven of verwijderd, haar agents of haar beoordelaarsmodel gewijzigd -- is een genummerde versie, die meteen gepubliceerd is als een sleutel haar maakt. Je kunt de volgende versie ook in zijn geheel schrijven:

curl -s -X POST "$CORE_URL/evaluations/evl_7Qa1.../versions" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"agents": ["agt_5Nj2..."], "scenarios": [{"id": "evs_2Kd9...", "title": "Retourneren",
         "steps": [{"input": "Kan ik dit retourneren?"}]}]}'

Je stuurt elk scenario mee: een met een id blijft dat scenario, een zonder is nieuw, en een dat je weglaat verdwijnt. GET /evaluations/{id}/versions geeft de versies, oudste eerst, en GET /evaluations/{id}/versions/{version} leest er één. Een sleutel ziet alleen gepubliceerde versies: een persoon kan in de console een concept van een suite voorbereiden, en een suite die alleen een concept is, bestaat voor een sleutel helemaal niet.

De checks

Kind Klopt wanneer Extra veld
contains_text het antwoord bevat expected, hoofdletterongevoelig —
not_contains het antwoord bevat expected juist niet —
matches_pattern het antwoord bevat geen treffer van pattern, een reguliere expressie (RE2) pattern en max_match (allebei verplicht)
exact_match het antwoord is, ontdaan van witruimte, precies expected — hoofdletters tellen mee —
close_match de gelijkenis van beide is minstens min_similarity (standaard 0,8) min_similarity
response_time de beurt was binnen max_seconds klaar max_seconds (verplicht)
must_use_tool de beurt riep expected aan, of welke tool dan ook als je geen naam noemt —
tool_found een zoekopdracht van de toolrouter in de beurt, of de tools die hij vooraf koos, had expected (verplicht) onder de resultaten —
correct_format een beoordelaar vindt dat het antwoord het formaat uit expected volgt threshold
polite_tone een beoordelaar vindt de toon professioneel, behulpzaam en beleefd threshold
no_hallucination een beoordelaar vindt niets verzonnen threshold
llm_judge een beoordelaar vindt dat het antwoord voldoet aan de regel in expected threshold

Elke check heeft required, standaard true. Een check weigert een veld dat bij zijn soort niet hoort: schrijf je max_seconds op een contains_text, dan is dat 422 in plaats van een regel die stilletjes nooit aanslaat.

De vier beoordeelde soorten zijn één modelaanroep per check. Bij llm_judge is jouw expected de regel waaraan getoetst wordt, niet een modelantwoord: schrijf er "het antwoord biedt aan om op te schalen", niet het antwoord dat je verwacht. De beoordelaar ziet het bericht van de stap, het uiteindelijke antwoord van de agent en die regel — meer niet. Hij geeft een score van 0 tot 1 en één zin met een reden; de check klopt vanaf threshold, standaard 0,7.

matches_pattern is het patroon waaraan een live check een echte beurt houdt, zodat een suite de regel kan meten voordat de agent hem afdwingt. Een treffer is de mislukking. max_match, 1 tot 200, is de langste treffer die het patroon kan hebben: een patroon waarvan een treffer langer kan zijn, zoals .*, of dat niets kan treffen, is 422.

must_use_tool kijkt naar de tool-aanroepen van de beurt zelf, niet naar de tekst. Een model dat zegt dat het gaat zoeken, heeft niet gezocht. tool_found onderscheidt een model dat een tool nooit vond van een model dat hem vond en niet gebruikte; een beurt zonder zoekopdracht faalt erop (Een agent met veel tools).

Een run starten

curl -s -X POST "$CORE_URL/evaluations/evl_7Qa1.../runs" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{}'

Dit antwoordt 202 met één run per gekoppelde agent. Geef {"agent_id": "agt_5Nj2..."} mee om er één te starten voor die agent; die moet wel aan de suite gekoppeld zijn. Een suite zonder scenario's is 409.

Er draait nog niets: elke run is werk in de wachtrij, opgepakt door de worker. Elke run pint de versie die de agent op dat moment als laatste had en legt de scenario's vast zoals ze er dan uitzien, dus een latere wijziging verandert niet meer wat die run gemeten heeft.

Of laat een klok hem starten. Een planning met kind: "evaluation" noemt deze suite en zet er op zijn eigen cron een run voor in de wachtrij -- elke nacht, elke week, wat je ook schrijft -- als de sleutel die de suite noemt, en slaat een nacht over waarin nog een run loopt. Wat zo'n planning heeft gedaan lees je in de runlijst van deze suite, beperkt tot die planning:

curl -s "$CORE_URL/evaluations/evl_7Qa1.../runs?schedule_id=sch_3Qp8..." \
    -H "Authorization: Bearer $CORE_KEY"

Elke run noemt schedule_id -- leeg bij de runs die je zelf hebt gevraagd -- en fired_for, het moment dat aan de beurt was.

Een run voor één persoon

Een run kan voor één persoon werken, zodat elke stap de beurt is die je agent voor haar zou maken: wat hij mag doorzoeken, welke tools en welk model hij mag gebruiken en wat hij over haar onthoudt, allemaal bepaald door je regels voor haar, precies zoals bij een live beurt. Noem haar met X-Core-Subject op het verzoek dat de run start, zoals je haar op een beurt noemt (bouwen voor personen):

curl -s -X POST "$CORE_URL/evaluations/evl_7Qa1.../runs" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -H 'X-Core-Subject: {"id": "anna", "attributes": {"department": "payroll"}}' \
    -d '{}'

Elke run die dat verzoek start, werkt voor haar en zegt dat in subject_id. De attributen die je meestuurt gelden voor elke stap tot de run klaar is, en worden daarna nergens bewaard. De gesprekken van de stappen zijn van haar: ze staan tussen haar threads en responses en verdwijnen met haar als je haar wist; de run zelf blijft en noemt haar dan niet meer. De checks worden nog steeds beoordeeld als meting van je organisatie, niet als haar beurt.

Twee weigeringen horen erbij: een suite die onder een andere sleutel dan de jouwe draait (run_as_key_id) geeft 422 op header.X-Core-Subject, omdat een run alleen voor een persoon werkt als de sleutel die haar noemde; en een agent die je regels haar niet laten gebruiken geeft 422 op agents of agent_id voordat er iets draait. Een run zonder de header werkt voor niemand, zoals altijd, en dat geldt ook voor elke run die de beheeromgeving, een planning of een verbeterproef start. Een sleutel waarvan de policy een subject vereist, moet er hier ook een noemen, net als bij elke andere evaluatie-aanroep.

Een run stoppen kan met POST /evaluations/{id}/runs/{run_id}/cancel. Staat hij nog in de wachtrij, dan stopt hij meteen; draait hij al, dan stopt hij bij de volgende stap en wordt de stap die op dat moment liep niet opgeslagen. Een run die al klaar is, is 409.

Een run lezen

curl -s "$CORE_URL/evaluations/evl_7Qa1.../runs/evr_3Kd8..." \
    -H "Authorization: Bearer $CORE_KEY"

Vraag het gewoon opnieuw om de voortgang te zien: na elke stap staan completed_steps, score en het resultaat van die stap erin.

Veld Wat het zegt
status queued, running, passed, warnings, failed, errored of cancelled
run_as_key_id de sleutel waaronder deze run draaide
score geslaagde stappen gedeeld door het totaal van de momentopname, als percentage
scenarios de suite zoals die was toen de run in de wachtrij kwam
results per scenario de stappen, per stap de checks met outcome, score en reason
thread_ids één thread per scenario, zodat je het gesprek kunt teruglezen
expired_at wanneer de bewaartermijn van je organisatie scenarios, results en thread_ids heeft leeggemaakt; null zolang de run ze nog heeft

Heeft je organisatie een bewaartermijn ingesteld, dan houdt een afgeronde run die ouder is dan die termijn zijn status, score, kosten en tijdstippen, terwijl scenarios, results en thread_ids leeg terugkomen met expired_at gevuld. De threads waar ze naar verwezen worden door dezelfde termijn verwijderd. Een vergelijking van zulke runs geeft nog wel de totalen, maar geen stappen om naast elkaar te leggen.

Een stap slaagt als elke verplichte check klopt. De run is failed zodra een stap met een verplichte check niet slaagde, warnings als alleen optionele checks niet klopten, en passed als alles klopte. errored en cancelled zeggen niets over de agent: dan is de run zelf niet aan een oordeel toegekomen.

Een check kent drie uitkomsten. errored betekent dat de beoordelaar geen antwoord kon geven — een time-out, een onbereikbaar model, een antwoord dat niet in het schema past — en telt mee als "niet geslaagd". Zo blijft het verschil zichtbaar tussen "de agent deed het fout" en "we weten het niet".

Terugvinden welke beurt bij welke stap hoorde

Elke stap is een gewone beurt: hij staat op een thread, is leesbaar via GET /responses/{response_id}, staat in je auditspoor en telt mee in je verbruik. results geeft per stap het response_id, en thread_ids geeft per scenario de thread. De beoordeelde checks tellen als model_tokens, net als elke andere modelaanroep.

Fouten

Status Oorzaak
404 geen zo'n suite, scenario of run — of evaluations geldt niet voor je sleutel
409 de naam is al bezet; de suite heeft geen scenario's; de run is al klaar; of een mens start een run van een suite die geen sleutel noemt
422 op agents een agent bestaat niet, of het beleid van de sleutel waaronder de run draait noemt hem niet
422 op run_as_key_id die sleutel bestaat niet, is niet van jouw organisatie, of is verwijderd
422 op agent_id die agent is niet aan deze suite gekoppeld
403 de header noemt een service account via een andere sleutel dan die van het account zelf; of het verzoek kwam met een subject-token, dat geen runs mag starten
422 de policy van je sleutel vereist een subject en het verzoek noemt er geen
422 op header.X-Core-Subject het verzoek noemt een persoon en de suite draait onder een andere sleutel dan de jouwe (een run voor één persoon)
422 op steps een scenario zonder stappen, of meer dan tweeëndertig, of twee stappen met hetzelfde id
422 op scenarios de suite heeft er al tweeëndertig
422 op een checkveld die soort check gebruikt dat veld niet, of mist er juist een

De beoordeelde checks draaien onder diezelfde sleutel: hun tokens komen op dezelfde rekening als de beurten. Mag die sleutel het beoordelingsmodel niet gebruiken, dan is elke beoordeelde check errored met de reden van het beleid — nooit stilletjes geslaagd.