Guides
Agents et outils
Oloproof ne pilote pas d'agent. Votre agent exécute sa propre boucle, appelle ses propres outils et enregistre ce qui s'est passé sous la forme d'un artefact agent_trajectory/v1. Chaque métrique d'agent est lue à partir de cet enregistrement : l'enregistrement constitue donc toute l'intégration.
examples/support_agent/ est le projet que cette page exécute : quarante demandes de remboursement, un agent déterministe, et aucun identifiant de fournisseur.
Enregistrer une trajectoire
Dans le système, construisez les étapes au fur et à mesure et remettez-les à l'enregistreur du cas :
from oloproof import AgentConstraintCheck, AgentStep, AgentTrajectory, current_case, system
@system(name="support-agent", version="slice-e-example", records=("agent_trajectory/v1",))
def run(case):
steps = []
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=...),
),
)
)
return {"answer": "refunded" if refunded else "unresolved"}À quoi sert chaque partie :
| Champ | Ce qu'il enregistre |
|---|---|
| AgentStep.kind | message, tool_call, tool_result, observation, decision ou final |
| AgentStep.tool_name, arguments, result | l'appel et ce qu'il a renvoyé |
| terminal_status | success, failure ou unknown |
| truncated, step_limit | que l'agent a atteint sa limite et que la trace s'arrête avant la fin |
| constraints | les vérifications faites par votre environnement, chacune avec l'étape qu'elle a observée |
| checkpoints | des points à partir desquels un rejeu pourrait reprendre, sous forme de AgentCheckpoint |
Le système déclare qu'il enregistre la trajectoire, dans le décorateur et dans oloproof.yaml :
system:
name: support-agent
version: slice-e-example
callable: app:run
records: [agent_trajectory/v1]Sans records:, les évaluateurs d'agent refusent de s'exécuter plutôt que de compter chaque cas comme manquant.
Ce que déclare un cas
Un cas qui doit appeler des outils précis, dans un ordre précis, l'indique sous expected.tools :
{"id": "case_001", "input": {"order_id": "ord-002", "behaviour": "clean"}, "expected": {"answer": "refunded", "tools": ["lookup_order", "issue_refund"]}, "metadata": {"surface": "chat", "behaviour": "clean"}}expected.tools: [] indique que le cas n'attend aucun appel d'outil, et il est jugé là-dessus. Omettre la clé indique que le choix des outils ne s'applique pas au cas : agent_tool_sequence et agent_no_undeclared_tool l'excluent avec no_declared_tools. Il sort du dénominateur plutôt que de compter comme un succès, car un taux gonflé par des cas qui n'ont jamais été mesurés n'est pas un taux. Une valeur qui n'est pas une liste de noms d'outils est refusée avant l'exécution.
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| Type | Réussit quand | Prend |
|---|---|---|
| agent_tool_called | l'outil a été appelé au moins min_calls fois | tool_name, min_calls (par défaut 1) |
| agent_no_tool_loop | aucun appel identique, même outil et mêmes arguments, ne se répète plus de max_repeats fois d'affilée | max_repeats (par défaut 2) |
| agent_tool_sequence | les outils appelés correspondent à expected.tools | ordered (par défaut true) |
| agent_no_undeclared_tool | aucun outil hors de expected.tools n'a été appelé | rien |
| agent_constraints_satisfied | chaque contrainte nommée que l'environnement a enregistrée a été respectée | constraints |
| agent_max_steps | la trace a pris au plus max_steps étapes | max_steps |
agent_steps et agent_tool_calls sont les distributions qui sous-tendent le dernier, sous forme de métriques de quantile.
oloproof run│ 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 ││ no-deletion │ agent_constraints_satisfied │ FAIL │ observed_failures_exceed_limit │Une contrainte sert de gate comme n'importe quel autre critère. no-deletion est une règle sur un décompte observé avec max_failures: 0 : trois cas ont appelé delete_customer, et « cela ne doit pas se produire dans la suite que nous avons exécutée » n'a besoin d'aucun intervalle pour être tranché.
Une trace qui s'arrête avant la fin
Un cas atteint la limite d'étapes de l'agent, si bien que sa trace est enregistrée truncated. Tout décompte sur une trace tronquée est une borne inférieure, et une borne inférieure règle certaines questions et pas d'autres :
| Critère | Le cas tronqué | Pourquoi |
|---|---|---|
| agent_steps_le_10 | échoue | les étapes enregistrées prouvent déjà qu'une limite de dix a été dépassée |
| agent_tool_lookup_order_called | manquant | l'appel se trouve peut-être dans la partie non enregistrée |
| agent_no_tool_loop | manquant | une répétition qui atteint la fin de la trace peut se poursuivre au-delà |
| steps_p95, tool_calls_p50 | manquant | un quantile sur des bornes inférieures n'est pas un quantile |
Manquant plutôt qu'exclu : le cas reste non observé dans le dénominateur, et l'intervalle admet qu'il ait pu basculer dans un sens comme dans l'autre. C'est pourquoi agent_tool_lookup_order_called affiche [86.8%, 100.0%] sur 39 cas observés plutôt que l'intervalle plus étroit que donneraient 39 cas seuls.
Segments sur les trajectoires
first_tool regroupe les cas selon l'outil auquel l'agent a eu recours en premier, quels que soient les outils appelés par l'exécution. repeated_action sépare les cas qui ont répété un appel identique de ceux qui ne l'ont pas fait. trajectory_length:4,8 répartit les cas entre 1 à 4, 5 à 8, et 9 étapes ou plus, selon des bornes que vous déclarez, car une limite de tranche change ce que dit un segment.
│ first_tool=search │ agent_no_tool_loop │ 0.0% │ [0.0%, 57.9%] │ 0 / 6 observed · 1 missing · 0 excluded · │
│ first_tool=search │ agent_tool_sequence │ 0.0% │ [0.0%, 41.0%] │ 0 / 7 observed · 0 missing · 0 excluded · │
│ repeated_action=false │ answer_correct │ 93.1% │ [77.2%, 99.2%] │ 27 / 29 observed · 0 missing · 0 excluded · │
│ repeated_action=true │ answer_correct │ 60.0% │ [26.2%, 87.9%] │ 6 / 10 observed · 0 missing · 0 excluded · │Une boucle est un signal, pas une explication. Rien dans la sortie n'affirme qu'un appel répété est la raison de l'échec d'un cas ; seul un rejeu qui le supprimerait le pourrait.
Plusieurs agents
examples/triage_agents/ exécute une équipe de trois : triage confie chaque demande à billing ou à tech, et un remboursement que billing n'a pas le droit d'émettre est confié à une personne. Dans un système à plusieurs agents, chaque étape nomme l'agent qui l'a effectuée, et un transfert de la main 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 toutes ses étapes ou d'aucune. Un enregistrement qui n'en nomme que certaines est refusé, et un enregistrement qui n'en nomme aucune est manquant pour chacun des critères ci-dessous plutôt que de les réussir. Une étape de transfert est facultative, puisque la main change aussi lorsque l'étape suivante est effectuée par un autre agent. C'est ainsi qu'une trace enregistre un transfert vers un agent qui n'effectue aucune étape, comme une personne.
Un cas déclare les agents par lesquels il doit passer sous expected.route :
{"id": "case_012", "input": {"topic": "billing", "request": "I was charged twice for order ord-012", "order_id": "ord-012", "behaviour": "escalated"}, "expected": {"answer": "escalated", "route": ["triage", "billing", "person"]}, "metadata": {"topic": "billing", "behaviour": "escalated"}}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]| Type | Réussit quand | Prend |
|---|---|---|
| agent_route | les agents qui ont eu la main, répétitions fusionnées, sont le expected.route du cas | rien |
| agent_tool_permissions | chaque appel d'outil a été fait par un agent que la table autorise à appeler cet outil | permissions |
| agent_max_handoffs | la main a changé au plus max_handoffs fois | max_handoffs |
Le parcours inclut le destinataire d'un transfert, de sorte qu'une trace qui se termine par une escalade vers une personne a pour parcours cette personne. Un cas sans expected.route n'est pas compté par agent_route. La table des permissions est fermée : un agent qu'elle ne liste pas ne peut appeler aucun outil.
│ 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 │Dans deux demandes, tech a émis un remboursement. Toutes deux réussissent answer_correct et échouent à agent_tool_permissions : le client a obtenu la bonne réponse d'un système qui a enfreint ses propres permissions. Une demande est renvoyée de billing à tech et inversement jusqu'à la limite de la boucle. Sa trace est tronquée : elle échoue donc au parcours et à la limite de transferts, que son préfixe règle déjà, et elle est manquante pour les permissions, que ce préfixe ne règle pas.
route regroupe les cas selon le parcours suivi par leurs agents, écrit triage>billing. Une trace tronquée ne rejoint aucun groupe de parcours, car son parcours est un préfixe de l'endroit où ses agents sont allés ensuite.
Rien dans la sortie n'indique quel agent est en cause. Une divergence de parcours indique où deux parcours se séparent. Qu'un agent ait causé un échec est une affirmation sur ce qui se serait passé s'il avait agi autrement, que seul un rejeu substituant cet agent pourrait montrer, et Oloproof n'en exécute pas.
Pour aller plus loin
- Enregistrer ce qu’a fait un système couvre les autres artefacts typés.
- Segments couvre l'effectif des segments et explique pourquoi les segments ne
servent jamais de gate.