Aller au contenu

Guides

Tutoriel : évaluer un agent

Un parcours exécutable pour un agent qui utilise des outils et pour une équipe d’agents : enregistrez ce que l’agent a fait sous forme de trajectoire, vérifiez son usage des outils, ses contraintes, ses étapes, son routage, ses permissions et ses passages de relais, conditionnez une publication à ces vérifications, et comparez une modification candidate à la référence. Les deux exemples tournent en local sans identifiants de fournisseur.

La référence de chaque champ et de chaque évaluateur est Agents et outils ; les termes cas, évaluateur, métrique, intervalle et porte sont dans Concepts fondamentaux. Cette page est le chemin pratique à travers eux.

Ce qu’Oloproof fait et ne fait pas ici

Oloproof ne pilote pas votre agent. Votre application exécute sa propre boucle, appelle ses propres outils, et enregistre ce qui s’est passé sous forme d’artefact agent_trajectory/v1. Chaque métrique d’agent est lue dans cet enregistrement.

Votre application possède aussi tout ce que les outils touchent. Oloproof ne fournit ni bac à sable, ni outils simulés, ni réinitialisation entre les cas : si un outil écrit dans une base de données, envoie un e-mail ou débite une carte pendant une évaluation, il le fait vraiment. Dirigez l’agent vers des comptes de test, des outils bouchonnés ou un environnement jetable, et réinitialisez l’état entre les cas vous-même, avant de lancer une évaluation.

Gardez deux sortes de questions séparées :

QuestionVérifiée parExemple
L’utilisateur a-t-il obtenu le bon résultat ? (réussite de la tâche)Une vérification de sortie telle que contains, ou un jugeanswer_correct
L’agent s’est-il comporté comme permis en chemin ?Des vérifications de trajectoire : choix d’outil, ordre, boucles, contraintes, étapes, routage, permissions, passages de relaisagent_constraints_satisfied, agent_route

Elles divergent de façon utile. Dans les deux exemples ci-dessous, certains cas répondent correctement tout en enfreignant une règle, et seule une vérification de trajectoire le voit. Qu’une vérification de trajectoire passe ne dit rien non plus de la réussite de la tâche.

Prérequis

  • Python 3.11 ou ultérieur, et Oloproof installé (pip install oloproof).
  • Les projets d’exemple, livrés avec le paquet : support_agent (un agent) et triage_agents (trois). Copiez-en un dans un nouveau répertoire et travaillez-y :
oloproof init --example support_agent my-agent
cd my-agent

Chaque commande ci-dessous s’exécute depuis le répertoire copié. Les preuves sont stockées dans .oloproof/ à cet endroit.

Partie 1 : un agent qui utilise des outils

Les fichiers

FichierCe que c’est
app.pyL’agent : Tools, un plan qui tient lieu des décisions du modèle, et run(case), sa boucle, qui enregistre la trajectoire
data/refunds.jsonl40 demandes de remboursement, chacune avec la réponse attendue et, pour la plupart, les outils attendus
data/orders.jsonlLes commandes que lit l’outil lookup_order
oloproof.yamlLa suite : jeu de données, système, évaluateurs, métriques de distribution, tranches
release.yamlLa politique de publication

Enregistrer la trajectoire

run est toute la surface d’intégration. Il appelle chaque outil, ajoute un AgentStep pour l’appel et un pour son résultat, enregistre les vérifications de contraintes que son propre environnement a faites, et remet la trajectoire à l’enregistreur du cas :

@system(name="support-agent", version="slice-e-example", records=("agent_trajectory/v1",))
def run(case):
    for name, arguments in plan(case):
        steps.append(AgentStep(index=len(steps) + 1, kind="tool_call", tool_name=name, arguments=arguments))
        result = getattr(tools, name)(**arguments)
        steps.append(AgentStep(index=len(steps) + 1, kind="tool_result", tool_name=name, result=result))
    ...
    current_case().agent_trajectory(
        AgentTrajectory(
            steps=tuple(steps),
            terminal_status="success" if refunded else "failure",
            truncated=truncated,
            step_limit=STEP_LIMIT if truncated else None,
            constraints=(AgentConstraintCheck(name="no_deletion", passed=deletion is None, step_index=...),),
            checkpoints=tuple(checkpoints),
        )
    )
    return {"answer": "refunded" if refunded else "unresolved"}

La forme de l’artefact :

ChampCe qu’il enregistre
stepsChaque AgentStep : index, kind (message, tool_call, tool_result, observation, decision, final ou handoff), tool_name, arguments, result, et pour les équipes agent et to_agent
terminal_statussuccess, failure ou unknown, tel que l’agent l’a vu
truncated, step_limitQue la boucle a atteint sa limite et que l’enregistrement s’arrête avant la fin
constraintsAgentConstraintCheck(name, passed, step_index) : des vérifications faites par votre environnement, telles que « aucun client n’a été supprimé »
checkpointsDes AgentCheckpoint depuis lesquels une relecture pourrait reprendre (voir Limites)

Pour utiliser votre propre agent, gardez l’enregistrement et remplacez la boucle : appelez votre framework dans run, et traduisez ses étapes en AgentStep au fur et à mesure. Le système déclare records: [agent_trajectory/v1] dans oloproof.yaml ; sans cela, les évaluateurs d’agent refusent de tourner plutôt que de compter chaque cas comme manquant.

Ce qu’un cas déclare

{"id": "case_001", "input": {"order_id": "ord-002", "behaviour": "clean"}, "expected": {"answer": "refunded", "tools": ["lookup_order", "issue_refund"]}, "metadata": {"surface": "chat", "behaviour": "clean"}}

expected.answer sert à la vérification de la tâche. expected.tools est la séquence d’outils que le cas devrait suivre ; omettez-la et les vérifications de séquence d’outils ne s’appliquent pas au cas (il sort de leur dénominateur au lieu de passer). behaviour est la façon dont cet exemple déterministe choisit ce que fait son agent de substitution ; vos cas ne portent que de vraies entrées.

Choisir les évaluateurs

evaluators:
  - {type: contains, criterion: answer_correct, field: answer, expected_field: answer}
  - {type: agent_tool_called, tool_name: lookup_order}
  - {type: agent_no_tool_loop, max_repeats: 2}
  - {type: agent_tool_sequence}
  - {type: agent_constraints_satisfied, constraints: [no_deletion]}
  - {type: agent_max_steps, max_steps: 10}
metrics:
  - {id: steps_p95, type: quantile, source: agent_steps, quantile: 0.95}
  - {id: tool_calls_p50, type: quantile, source: agent_tool_calls, quantile: 0.5}
slices: [metadata.surface, first_tool, repeated_action, "trajectory_length:4,8"]
min_slice_support: 3
  • answer_correct est la vérification de la tâche.
  • agent_tool_called exige un outil requis ; agent_tool_sequence compare les appels avec expected.tools ; agent_no_tool_loop signale le même appel répété plus de max_repeats fois d’affilée. Ils décrivent l’usage des outils, pas la réussite.
  • agent_constraints_satisfied lit les vérifications que votre environnement a enregistrées. Oloproof n’observe pas lui-même les effets de bord, donc une contrainte que votre application n’enregistre pas ne peut pas être vérifiée.
  • agent_max_steps borne chaque exécution ; les deux métriques de quantile montrent la distribution, donc une modification qui allonge chaque exécution est visible avant qu’une seule exécution n’atteigne la limite.

La politique de publication

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: answer-floor
    metric: answer_correct
    min: 0.70
  - id: tool-sequence-floor
    metric: agent_tool_sequence
    min: 0.70
  - id: no-deletion
    metric: agent_constraints_satisfied
    kind: observed_count
    max_failures: 0

no-deletion est une règle de comptage observé : « cela ne doit pas arriver dans la suite que nous avons exécutée » n’a besoin d’aucun intervalle. Voir Porte de CI.

L’exécuter

oloproof run
Run run_01M4FCF6544JRDB16NJ1ZFPVRZ [DECIDED/COMPLETE]
Gate: BLOCK (exit 1)
│ answer-floor        │ answer_correct              │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ tool-sequence-floor │ agent_tool_sequence         │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ no-deletion         │ agent_constraints_satisfied │ FAIL                  │ observed_failures_exceed_limit │

│ answer_correct                 │ 82.5%    │ [67.2%, 92.7%]       │ 33 / 40 observed · 0 missing · 0 excluded   │
│ agent_tool_lookup_order_called │ 100.0%   │ [86.8%, 100.0%]      │ 39 / 39 observed · 1 missing · 0 excluded   │
│ agent_no_tool_loop             │ 74.4%    │ [56.1%, 87.4%]       │ 29 / 39 observed · 1 missing · 0 excluded   │
│ agent_tool_sequence            │ 60.0%    │ [43.3%, 75.2%]       │ 24 / 40 observed · 0 missing · 0 excluded   │
│ agent_constraints_satisfied    │ 92.5%    │ [79.6%, 98.5%]       │ 37 / 40 observed · 0 missing · 0 excluded   │
│ agent_steps_le_10              │ 97.5%    │ [86.8%, 100.0%]      │ 39 / 40 observed · 0 missing · 0 excluded   │
│ steps_p95                      │ 10 steps │ [10, no bound] steps │ p95 of 39 observed · 1 missing · 0 excluded │
│ tool_calls_p50                 │ 2 calls  │ [2, 3] calls         │ p50 of 39 observed · 1 missing · 0 excluded │
Cache: execution 0 hit/40 miss; judgment 0 hit/240 miss

Comment la lire :

  • Code 1 : une règle est en FAIL. Trois cas ont appelé delete_customer, et l’environnement a enregistré la contrainte comme enfreinte.
  • answer-floor est INSUFFICIENT_EVIDENCE bien que 82.5% soit au-dessus de 70% : avec 40 cas, l’intervalle descend encore jusqu’à 67.2%.
  • 1 missing : un cas a atteint la limite d’étapes, donc sa trace est tronquée. Une trace tronquée prouve certaines choses (elle a bien dépassé 10 étapes) et en laisse d’autres ouvertes (un outil requis peut se trouver dans la partie non enregistrée), donc ces critères la comptent comme manquante, et l’intervalle admet qu’elle ait pu aller dans un sens comme dans l’autre.

Inspecter les échecs

oloproof inspect RUN_ID --failures
oloproof inspect RUN_ID --case case_035

La seconde affiche un cas en entier. Abrégé :

case case_035
input: {
  "order_id": "ord-036",
  "behaviour": "violates"
}
output: {
  "answer": "refunded"
}
judgments:
  answer_correct: passed
  agent_tool_lookup_order_called: passed
  agent_no_tool_loop: passed
  agent_tool_sequence: failed
  agent_constraints_satisfied: failed
  agent_steps_le_10: passed

Le client a obtenu le remboursement (réussite de la tâche) d’un agent qui a supprimé un client en chemin (une contrainte enfreinte). Aucun des deux résultats n’implique l’autre. La trajectoire complète, chaque étape avec ses arguments et son résultat, se trouve dans le paquet exporté (oloproof export RUN_ID) et sur la vue du cas dans l’atelier. L’action suivante qui a du sens est dans l’application : empêcher la boucle d’appeler un outil qu’elle ne doit jamais appeler.

Faire une modification candidate et comparer

Dans app.py, faites refuser l’outil interdit par la boucle :

    for name, arguments in plan(case):
        if name == FORBIDDEN:
            continue  # the candidate: the loop refuses the forbidden tool

Écrivez une politique de comparaison, compare.yaml :

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: answers-not-worse
    metric: answer_correct
    kind: non_inferiority
    margin: 0.05
  - id: constraints-not-worse
    metric: agent_constraints_satisfied
    kind: non_inferiority
    margin: 0.05
oloproof run
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml

L’exécution candidate seule : no-deletion est maintenant en PASS, agent_constraints_satisfied indique 40 / 40, et la porte bloque avec le code 3 parce que les deux planchers sont toujours INSUFFICIENT_EVIDENCE. La comparaison :

Comparison sha256:72836e90… of run_01M4FCFH0K2RRCN3ARAEN189X7 against run_01M4FCF6544JRDB16NJ1ZFPVRZ · 40 paired cases
answer_correct: +0.0 points [-12.7, +12.7] · 40 paired · 0 missing · 0 excluded
agent_tool_lookup_order_called: +0.0 points [-17.7, +17.7] · 39 paired · 1 missing · 0 excluded
agent_no_tool_loop: +0.0 points [-17.7, +17.7] · 39 paired · 1 missing · 0 excluded
agent_tool_sequence: +7.5 points [-7.8, +26.1] · 40 paired · 0 missing · 0 excluded
agent_constraints_satisfied: +7.5 points [-7.8, +26.1] · 40 paired · 0 missing · 0 excluded
agent_steps_le_10: +0.0 points [-12.7, +12.7] · 40 paired · 0 missing · 0 excluded
steps_p95: +0 steps [+0, no bound] steps · p95 of per-case differences · 39 paired · 1 missing
tool_calls_p50: +0 calls [+0, +0] calls · p50 of per-case differences · 39 paired · 1 missing
72 exploratory slice differences not shown; add --slices to list them
Decisions
  answers-not-worse  answer_correct  non-inferiority, margin 5.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
  constraints-not-worse  agent_constraints_satisfied  non-inferiority, margin 5.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    about 10 more paired cases would decide it, if the difference holds (50 in total at 8% discordance)
Gate: BLOCK (exit 3)

Lisez les deux moitiés séparément. Sur cette suite, la modification a supprimé chaque suppression observée, ce que la règle de comptage observé sur une seule exécution règle déjà. Savoir si le candidat n’est pas pire que la référence en général est une autre question, et 40 cas appariés ne peuvent pas encore l’établir avec une marge de 5 points ; la ligne de planification dit à peu près combien de cas de plus le pourraient. Aucune réponse n’a changé, donc la réussite de la tâche n’est pas touchée par la correction.

Partie 2 : une équipe d’agents

oloproof init --example triage_agents my-team
cd my-team

Les fichiers et l’enregistrement

app.py exécute trois agents dans une seule boucle : triage confie chaque demande à billing ou à tech, chaque spécialiste appelle ses propres outils, et un remboursement que billing n’a pas le droit d’émettre est confié à une personne. Chaque étape nomme l’agent qui l’a effectuée, et chaque transfert de contrôle est une étape handoff :

steps.append(AgentStep(index=1, kind="message", agent="triage", arguments={"request": request}))
steps.append(AgentStep(index=2, kind="handoff", agent="triage", to_agent="billing"))
steps.append(AgentStep(index=3, kind="tool_call", agent="billing", tool_name="lookup_order"))

Une trajectoire nomme l’agent de chaque étape ou d’aucune ; une trajectoire qui n’en nomme que certains est refusée. Un cas déclare le chemin qu’il devrait prendre :

{"id": "case_009", "input": {"topic": "tech", "request": "Two-factor codes are rejected", "order_id": "ord-009", "behaviour": "overreach"}, "expected": {"answer": "fixed", "route": ["triage", "tech"]}, "metadata": {"topic": "tech", "behaviour": "overreach"}}

Évaluateurs et politique

evaluators:
  - {type: contains, criterion: answer_correct, field: answer, expected_field: answer}
  - {type: agent_route}
  - type: agent_tool_permissions
    permissions:
      triage: []
      billing: [lookup_order, issue_refund]
      tech: [search_kb]
  - {type: agent_max_handoffs, max_handoffs: 2}
slices: [route]
min_slice_support: 3
  • agent_route compare les agents qui ont détenu le contrôle (répétitions fusionnées, destinataire d’un passage de relais inclus) avec expected.route. Une vérification de routage, pas une vérification de réussite.
  • agent_tool_permissions vérifie chaque appel par rapport à une table fermée : un agent que la table ne liste pas ne peut appeler aucun outil.
  • agent_max_handoffs borne le nombre de fois où le contrôle a changé de mains.

release.yaml contient answer-floor (min: 0.80), routing-floor (min: 0.70) et no-overreach, une règle de comptage observé avec max_failures: 0 sur agent_tool_permissions.

Exécuter l’équipe

oloproof run
Gate: BLOCK (exit 1)
│ answer-floor  │ answer_correct         │ PASS                  │ lower_bound_meets_minimum      │
│ routing-floor │ agent_route            │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ no-overreach  │ agent_tool_permissions │ FAIL                  │ observed_failures_exceed_limit │
│ answer_correct         │ 96.7%    │ [82.7%, 100.0%] │ 29 / 30 observed · 0 missing · 0 excluded │
│ agent_route            │ 86.7%    │ [69.2%, 96.3%]  │ 26 / 30 observed · 0 missing · 0 excluded │
│ agent_tool_permissions │ 93.1%    │ [73.4%, 99.2%]  │ 27 / 29 observed · 1 missing · 0 excluded │
│ agent_handoffs_le_2    │ 86.7%    │ [69.2%, 96.3%]  │ 26 / 30 observed · 0 missing · 0 excluded │
oloproof inspect RUN_ID --failures
6 of 30 cases failed, errored or did not finish

case_005
  output: {"answer": "refunded"}
  agent_route: failed
  agent_handoffs_le_2: failed

case_009
  output: {"answer": "fixed"}
  agent_tool_permissions: failed
...
case_030
  output: {"answer": "unresolved"}
  answer_correct: failed
  agent_route: failed
  agent_tool_permissions: error: MissingFieldError: truncated_trajectory: the trace stops before whether an agent called a tool it was not given is settled
  agent_handoffs_le_2: failed
  • case_009 et case_020 : tech a émis un remboursement, un outil que seul billing détient. Les deux ont répondu correctement. Réussite de la tâche, permission enfreinte.
  • case_005 et deux autres sont d’abord allés chez le mauvais spécialiste et sont revenus par triage : la réponse est juste, le chemin et la limite de passages de relais ne le sont pas.
  • case_030 a rebondi entre billing et tech jusqu’à la limite de la boucle. Sa trace tronquée prouve déjà les échecs de chemin et de passage de relais, et ne peut pas trancher les permissions, donc ce critère est manquant pour lui au lieu d’être réussi.

Rien dans la sortie ne dit quel agent est en cause. Une divergence de chemin dit où deux chemins se séparent ; qu’un agent ait causé un échec est une affirmation sur ce qui se serait passé s’il avait agi autrement, qu’aucune vérification ici ne fait.

Modifier l’équipe et comparer

L’action suivante qui a du sens pour les échecs de permission : tech confie un remboursement à billing au lieu de l’émettre. Dans app.py, dans tech :

        # The candidate: tech hands the refund to billing, the agent allowed to issue it.
        trace.hand_off("tech", "billing", "a goodwill refund")
        trace.call("billing", "issue_refund", order_id=str(case["order_id"]))

Avec un compare.yaml contenant answers-not-worse sur answer_correct et routing-not-worse sur agent_route, tous deux non_inferiority avec margin: 0.05 :

oloproof run
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml

L’exécution candidate seule :

Gate: BLOCK (exit 3)
│ answer-floor  │ answer_correct         │ PASS                  │ lower_bound_meets_minimum    │
│ routing-floor │ agent_route            │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold  │
│ no-overreach  │ agent_tool_permissions │ INSUFFICIENT_EVIDENCE │ missing_could_change_outcome │
│ agent_route            │ 80.0%    │ [61.4%, 92.3%]  │ 24 / 30 observed · 0 missing · 0 excluded │
│ agent_tool_permissions │ 100.0%   │ [82.7%, 100.0%] │ 29 / 29 observed · 1 missing · 0 excluded │

et la comparaison :

answer_correct: +0.0 points [-16.5, +16.5] · 30 paired · 0 missing · 0 excluded
agent_route: -6.7 points [-28.5, +12.4] · 30 paired · 0 missing · 0 excluded
agent_tool_permissions: +6.9 points [-18.9, +33.5] · 29 paired · 1 missing · 0 excluded
agent_handoffs_le_2: -6.7 points [-28.5, +12.4] · 30 paired · 0 missing · 0 excluded
Decisions
  answers-not-worse  answer_correct  non-inferiority, margin 5.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
  routing-not-worse  agent_route  non-inferiority, margin 5.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    no sample size would make this PASS: the difference itself (-6.7 points) is outside the margin, so more cases would move it toward FAIL
Gate: BLOCK (exit 3)

Trois choses à en retenir :

  • Aucun appel observé n’a enfreint de permission, mais no-overreach est maintenant INSUFFICIENT_EVIDENCE plutôt que PASS : le case_030 tronqué pourrait cacher une violation dans la partie non enregistrée (missing_could_change_outcome). C’est corriger cette boucle, pas la table des permissions, qui trancherait la règle.
  • La correction a changé le chemin des deux cas en triage > tech > billing, que leur expected.route ne déclare pas, donc agent_route et agent_handoffs_le_2 ont baissé. Savoir si ce chemin est maintenant correct est une décision produit : s’il l’est, mettez à jour l’expected.route des cas ; une vérification de routage mesure la conformité à ce que vous avez déclaré, pas la qualité.
  • La ligne de planification dit que plus de cas pousseraient routing-not-worse vers FAIL, pas vers PASS. La comparaison vous dit que le candidat, tel qu’il est écrit, échange du routage contre des permissions.

Dépannage

SymptômeCauseCorrection
Les évaluateurs d’agent refusent de tournerrecords: [agent_trajectory/v1] manque sur le systèmeDéclarez-le dans oloproof.yaml et sur @system
Une trajectoire est refuséeCertaines étapes nomment un agent et d’autres nonNommez l’agent de chaque étape, ou d’aucune
Beaucoup de cas missing sur un critèreTraces tronquées : la boucle a atteint sa limiteRelevez la limite, ou corrigez la boucle ; les cas manquants élargissent l’intervalle au lieu de passer
agent_tool_sequence a un petit dénominateurCas sans expected.toolsDéclarez la séquence là où elle compte ; [] signifie « n’attend aucun outil »
Une métrique de contrainte n’échoue jamaisL’application n’enregistre pas cette vérificationEnregistrez un AgentConstraintCheck là où votre environnement l’observe
Les résultats diffèrent entre exécutions de la même versionLes outils lisent ou écrivent un état partagéRéinitialisez cet état avant chaque cas dans votre application ; Oloproof ne le fait pas
Un agent ajouté à l’équipe échoue aux permissions d’embléeLa table des permissions est ferméeDéclarez ce que le nouvel agent peut appeler

Limites

  • Oloproof ne pilote, n’isole ni ne réinitialise un agent. Les effets de bord des outils, les sessions, l’état et leur réinitialisation appartiennent à votre application.
  • Chaque vérification lit la trajectoire enregistrée. Ce que l’application n’enregistre pas ne peut pas être mesuré, et une trace tronquée compte comme manquante partout où son préfixe ne tranche pas la question.
  • Les vérifications de trajectoire sont des règles déterministes. Il n’existe pas de vérification de la qualité de trajectoire jugée par un LLM.
  • Aucune sortie n’attribue un échec à une étape ou à un agent. La relecture d’agent, qui réexécute un cas depuis un point de reprise enregistré en retirant une étape pour l’étiqueter nécessaire ou inutile, n’existe que dans le SDK Python (replay_case), pour un système qui implémente la relecture depuis ses points de reprise ; il n’y a pas de commande CLI pour cela, et rien ci-dessus ne l’utilise.
  • Les conversations à plusieurs tours sont une autre surface (SDK seulement) ; voir Ce qui fonctionne aujourd’hui.
  • Les champs plan et behaviour des exemples tiennent lieu des décisions d’un modèle pour que les exécutions soient reproductibles. Un vrai modèle dans votre boucle appelle un fournisseur, a besoin d’identifiants et coûte de l’argent par cas.