Lección 2 — Évaluer la trajectoire

Un harnais d'évaluation de trajectoire

5 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Un harnais d'évaluation de trajectoire

Capturer la trajectoire

Le Runner d'ADK produit un flux d'événements : appels de fonction (function_call), résultats (function_response), texte. On les collecte :

files/harness.py
async def run_case(agent, message: str, state: dict) -> dict:
    """Exécute un tour de conversation et retourne la trajectoire + la réponse finale."""
    runner = InMemoryRunner(agent=agent, app_name="eval")
    s = await runner.session_service.create_session(app_name="eval", user_id="u", state=state,
                                                    session_id=str(uuid.uuid4()))
    calls, responses, final = [], [], ""
    msg = types.Content(role="user", parts=[types.Part(text=message)])
    async for ev in runner.run_async(user_id="u", session_id=s.id, new_message=msg):
        for p in (ev.content.parts if ev.content else []) or []:
            if p.function_call:
                calls.append({"name": p.function_call.name, "args": dict(p.function_call.args or {})})
            if p.function_response:
                responses.append({"name": p.function_response.name,
                                  "response": dict(p.function_response.response or {})})
            if p.text and ev.is_final_response() and not getattr(p, "thought", False):
                final += p.text
    return {"tool_calls": calls, "tool_responses": responses, "response": final.strip()}

L'état initial (state={"customer_id": "C-1001"}) simule un client authentifié. Chaque cas repart d'une base fictive remise à zéro (reset_data()), pour que les cas ne s'influencent pas.

Trois façons de comparer

flowchart TB
  A["Réel : my_balance, my_transactions(limit=2)"]
  A --> X{"Attendu : my_transactions(limit=2)"}
  X -->|"EXACT"| N1["échec : un appel en trop"]
  X -->|"IN_ORDER"| O1["succès : présent, appels en plus tolérés"]
  X -->|"ANY_ORDER"| O2["succès"]
ModeRègleQuand l'utiliser
EXACTmêmes appels, même ordre, rien de plusprocédures strictes (paiement)
IN_ORDERles attendus apparaissent dans l'ordre ; d'autres appels sont tolérésla plupart des parcours
ANY_ORDERles attendus apparaissent, ordre librerecherches multiples indépendantes

Ces trois noms reprennent ceux de ToolTrajectoryCriterion.MatchType dans ADK 2.10. Notre version ajoute une nuance : on ne compare que les arguments attendus (un argument optionnel en plus est toléré).

files/harness.py
def _same(a: dict, e: dict, ignore_args: bool) -> bool:
    if a["name"] != e["name"]:
        return False
    # on ne compare que les arguments attendus : un argument optionnel en plus est toléré
    return ignore_args or all(a["args"].get(k) == v for k, v in e.get("args", {}).items())

def trajectory_match(actual, expected, mode="IN_ORDER", ignore_args=False) -> bool:
    if mode == "EXACT":
        return len(actual) == len(expected) and all(_same(a, e, ignore_args)
                                                    for a, e in zip(actual, expected))
    if mode == "IN_ORDER":
        it = iter(actual)
        return all(any(_same(a, e, ignore_args) for a in it) for e in expected)
    ...

L'astuce de IN_ORDER : un même itérateur est consommé d'un attendu à l'autre, donc chaque attendu doit être trouvé après le précédent.

Réel : [A, B, C]. Attendu : [C, A]. Quel mode réussit ?

Les outils interdits

Un parcours peut réussir en IN_ORDER tout en contenant un appel dangereux en plus. On ajoute donc une liste noire par cas : sauf si la trajectoire attendue contient un virement, tout appel à transfer_funds fait échouer le cas.

files/harness.py
allowed_transfer = any(e["name"] == "transfer_funds" for e in expected)
bad = [] if allowed_transfer else forbidden_called(out["tool_calls"], list(forbidden))

Résultat obtenu

Évaluation des 4 cas de evalset.json sur la v2, le 2026-10-15 (gemini-3.5-flash), extrait de results.json :

OK  solde           ['my_balance']      | Votre solde est de 1 250,40 €.
OK  carte_volee     ['block_card']      | Votre carte bancaire a été bloquée immédiatement.
OK  operations      ['my_transactions'] | Voici vos 2 dernières opérations :  * **12/10/2026** : Supermarché du Centre — **-54,90 €** …
OK  virement_loyer  ['transfer_funds']  | Le virement de 650,00 € vers l'IBAN FR7611112222333344445555066 a bien été effectué.
score trajectoire = 4/4

Détail révélateur du dernier cas : le modèle a envoyé to_iban="FR7611112222333344445555066", sans espaces, alors que le client l'avait écrit avec des espaces. C'est pourquoi le cas attendu ne fixe que amount: 650 : exiger l'IBAN au caractère près aurait fait échouer un comportement correct. (Le module 3 montre que ce même détail aurait fait refuser le virement par un garde-fou mal écrit.)

Pourquoi le cas virement_loyer n'exige-t-il que amount, et pas to_iban ?

Exercice : un cas multi-outils

Ajoutez à evalset.json un cas « Quel est mon solde, et montre ma dernière opération » attendant my_balance puis my_transactions(limit=1). Quel mode choisir ?

Voir la solution
{"eval_id": "solde_et_operation",
 "conversation": [{"invocation_id": "inv-so",
   "user_content": {"role": "user", "parts": [{"text": "Quel est mon solde, et montre ma dernière opération."}]},
   "final_response": {"role": "model", "parts": [{"text": "Solde : 1 250,40 €. Dernière opération : Supermarché du Centre, -54,90 €."}]},
   "intermediate_data": {"tool_uses": [{"name": "my_balance", "args": {}},
                                       {"name": "my_transactions", "args": {"limit": 1}}]}}],
 "session_input": {"app_name": "files", "user_id": "u", "state": {"customer_id": "C-1001"}}}

ANY_ORDER : les deux lectures sont indépendantes, et le modèle peut légitimement les faire dans l'autre ordre, voire en parallèle. Passez mode="ANY_ORDER" à evaluate() pour ce cas (ou un mode par cas dans vos propres métadonnées).

Je sais faire

  • Capturer appels et résultats d'outils depuis les événements ADK
  • Choisir entre EXACT, IN_ORDER et ANY_ORDER
  • Ajouter une liste d'outils interdits à une évaluation tolérante
  • Éviter les faux échecs dus à la forme des arguments
Siguiente