Skip to content

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 :

ChampCe qu'il enregistre
AgentStep.kindmessage, tool_call, tool_result, observation, decision ou final
AgentStep.tool_name, arguments, resultl'appel et ce qu'il a renvoyé
terminal_statussuccess, failure ou unknown
truncated, step_limitque l'agent a atteint sa limite et que la trace s'arrête avant la fin
constraintsles vérifications faites par votre environnement, chacune avec l'étape qu'elle a observée
checkpointsdes 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
TypeRéussit quandPrend
agent_tool_calledl'outil a été appelé au moins min_calls foistool_name, min_calls (par défaut 1)
agent_no_tool_loopaucun appel identique, même outil et mêmes arguments, ne se répète plus de max_repeats fois d'affiléemax_repeats (par défaut 2)
agent_tool_sequenceles outils appelés correspondent à expected.toolsordered (par défaut true)
agent_no_undeclared_toolaucun outil hors de expected.tools n'a été appelérien
agent_constraints_satisfiedchaque contrainte nommée que l'environnement a enregistrée a été respectéeconstraints
agent_max_stepsla trace a pris au plus max_steps étapesmax_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èreLe cas tronquéPourquoi
agent_steps_le_10échoueles étapes enregistrées prouvent déjà qu'une limite de dix a été dépassée
agent_tool_lookup_order_calledmanquantl'appel se trouve peut-être dans la partie non enregistrée
agent_no_tool_loopmanquantune répétition qui atteint la fin de la trace peut se poursuivre au-delà
steps_p95, tool_calls_p50manquantun 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]
TypeRéussit quandPrend
agent_routeles agents qui ont eu la main, répétitions fusionnées, sont le expected.route du casrien
agent_tool_permissionschaque appel d'outil a été fait par un agent que la table autorise à appeler cet outilpermissions
agent_max_handoffsla main a changé au plus max_handoffs foismax_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

servent jamais de gate.