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 :
| Question | Vérifiée par | Exemple |
|---|---|---|
| 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 juge | answer_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 relais | agent_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-agentChaque 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
| Fichier | Ce que c’est |
|---|---|
| app.py | L’agent : Tools, un plan qui tient lieu des décisions du modèle, et run(case), sa boucle, qui enregistre la trajectoire |
| data/refunds.jsonl | 40 demandes de remboursement, chacune avec la réponse attendue et, pour la plupart, les outils attendus |
| data/orders.jsonl | Les commandes que lit l’outil lookup_order |
| oloproof.yaml | La suite : jeu de données, système, évaluateurs, métriques de distribution, tranches |
| release.yaml | La 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 :
| Champ | Ce qu’il enregistre |
|---|---|
| steps | Chaque 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_status | success, failure ou unknown, tel que l’agent l’a vu |
| truncated, step_limit | Que la boucle a atteint sa limite et que l’enregistrement s’arrête avant la fin |
| constraints | AgentConstraintCheck(name, passed, step_index) : des vérifications faites par votre environnement, telles que « aucun client n’a été supprimé » |
| checkpoints | Des 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: 0no-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 runRun 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 missComment 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_035La 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: passedLe 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.05oloproof run
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yamlL’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-teamLes 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 runGate: 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 --failures6 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.yamlL’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ôme | Cause | Correction |
|---|---|---|
| Les évaluateurs d’agent refusent de tourner | records: [agent_trajectory/v1] manque sur le système | Déclarez-le dans oloproof.yaml et sur @system |
| Une trajectoire est refusée | Certaines étapes nomment un agent et d’autres non | Nommez l’agent de chaque étape, ou d’aucune |
| Beaucoup de cas missing sur un critère | Traces tronquées : la boucle a atteint sa limite | Relevez la limite, ou corrigez la boucle ; les cas manquants élargissent l’intervalle au lieu de passer |
| agent_tool_sequence a un petit dénominateur | Cas sans expected.tools | Déclarez la séquence là où elle compte ; [] signifie « n’attend aucun outil » |
| Une métrique de contrainte n’échoue jamais | L’application n’enregistre pas cette vérification | Enregistrez un AgentConstraintCheck là où votre environnement l’observe |
| Les résultats diffèrent entre exécutions de la même version | Les 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ée | La table des permissions est fermée | Dé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.