Aller au contenu principal

Évaluer vos agents : métriques et benchmarks

Mis à jour le 29 juillet 2026

Évaluer vos agents : métriques et benchmarks

Un agent qui fonctionne en développement peut échouer en production. L’évaluation systématique est la seule façon de garantir la qualité avant et après le déploiement. Dans cette leçon, vous apprendrez à définir des métriques, créer des jeux de test, et automatiser l’évaluation.

Pourquoi évaluer ?

Sans évaluation formelle, vous volez à l’aveugle. Vous reformulez une phrase des instructions parce qu’un utilisateur s’est plaint, et rien ne vous dit si l’agent s’est amélioré ou si vous venez de casser trois autres scénarios. Vous hésitez entre Terra et Luna pour votre cas d’usage — ou entre deux niveaux de raisonnement sur le même modèle —, et faute de mesure la décision se prend sur une impression après cinq essais manuels. Vous modifiez le schéma d’un tool, l’agent cesse de l’appeler dans un cas particulier, et personne ne s’en aperçoit avant que le volume de tickets ne remonte. Une suite d’évaluation transforme chacune de ces situations en un chiffre comparable d’une version à l’autre.

Les métriques fondamentales

Exactitude (Accuracy)

La première question est la plus directe : l’agent donne-t-il la bonne réponse ? Vous rejouez un jeu de questions dont vous connaissez la réponse et vous vérifiez sa présence dans la sortie. La comparaison par sous-chaîne insensible à la casse est volontairement grossière — elle tolère « Le laptop Pro coûte 1299 € TTC » là où une égalité stricte échouerait — mais elle suffit pour les faits vérifiables et se calcule en une seconde.

from agents import Agent, Runner

async def evaluer_exactitude(agent, cas_de_test):
    resultats = []
    for cas in cas_de_test:
        result = await Runner.run(agent, cas["question"])
        # Vérification automatique ou par LLM
        est_correct = cas["reponse_attendue"].lower() in result.final_output.lower()
        resultats.append({
            "question": cas["question"],
            "reponse": result.final_output,
            "attendu": cas["reponse_attendue"],
            "correct": est_correct,
        })

    taux = sum(1 for r in resultats if r["correct"]) / len(resultats)
    print(f"Exactitude : {taux:.1%} ({sum(1 for r in resultats if r['correct'])}/{len(resultats)})")
    return resultats

# Jeu de test
cas_de_test = [
    {"question": "Quel est le prix du laptop pro ?", "reponse_attendue": "1299"},
    {"question": "Combien de laptops en stock ?", "reponse_attendue": "45"},
    {"question": "Quel est le produit le moins cher ?", "reponse_attendue": "tablet"},
]

Utilisation correcte des tools

Une bonne réponse obtenue par hasard reste un problème. Un agent qui récite un prix mémorisé au lieu d’interroger le catalogue passera le test d’exactitude aujourd’hui et donnera un tarif périmé demain. La deuxième métrique regarde donc le chemin et non le résultat : l’agent appelle-t-il les bons tools avec les bons arguments ? Les appels se lisent dans raw_responses, en filtrant les sorties de type function_call, et la comparaison porte sur des ensembles pour rester indifférente à l’ordre.

async def evaluer_tools(agent, cas_de_test):
    resultats = []
    for cas in cas_de_test:
        result = await Runner.run(agent, cas["question"])

        # Extraire les tools appelés depuis les raw responses
        tools_appeles = []
        for response in result.raw_responses:
            for output in getattr(response, "output", []):
                if hasattr(output, "type") and output.type == "function_call":
                    tools_appeles.append(output.name)

        tools_corrects = set(cas["tools_attendus"]) == set(tools_appeles)
        resultats.append({
            "question": cas["question"],
            "tools_appeles": tools_appeles,
            "tools_attendus": cas["tools_attendus"],
            "correct": tools_corrects,
        })

    taux = sum(1 for r in resultats if r["correct"]) / len(resultats)
    print(f"Tools corrects : {taux:.1%}")
    return resultats

cas_tools = [
    {
        "question": "Quel est le prix du laptop ?",
        "tools_attendus": ["rechercher_produit"],
    },
    {
        "question": "Prix du laptop avec 10% de remise",
        "tools_attendus": ["rechercher_produit", "calculer_remise"],
    },
]

Le second cas de test est le plus instructif : la question exige un enchaînement de deux tools, et c’est exactement le genre de composition qu’un changement de description fait silencieusement disparaître.

Latence et coût

Vient enfin la dimension opérationnelle. Une moyenne ne dit rien de l’expérience réelle : c’est la queue de distribution qui fait fuir les utilisateurs, quand une requête sur cent met vingt secondes. En répétant chaque message plusieurs fois, vous obtenez une distribution exploitable et vous lisez la médiane comme le comportement typique, le P99 comme votre pire scénario courant.

import time

async def evaluer_performance(agent, messages, iterations=10):
    latences = []
    for message in messages:
        for _ in range(iterations):
            debut = time.perf_counter()
            result = await Runner.run(agent, message)
            duree = time.perf_counter() - debut
            latences.append(duree)

    import statistics
    print(f"Latence moyenne : {statistics.mean(latences):.2f}s")
    print(f"Latence P50 : {statistics.median(latences):.2f}s")
    print(f"Latence P99 : {sorted(latences)[int(len(latences) * 0.99)]:.2f}s")

Évaluation par LLM (LLM-as-Judge)

Ces trois métriques laissent de côté tout ce qui n’a pas de réponse unique : une explication de politique de retour, une reformulation commerciale, un message d’excuse. Pour ces réponses ouvertes, utilisez un modèle pour évaluer la qualité. Le juge reçoit la question et la réponse, applique des critères que vous avez écrits — exactitude, pertinence, professionnalisme — et retourne une note structurée grâce à un output_type Pydantic. La contrainte de format est ce qui rend le procédé exploitable : vous obtenez un entier agrégeable, pas un commentaire à relire.

from pydantic import BaseModel

class Evaluation(BaseModel):
    score: int  # 1-5
    raison: str
    est_correct: bool
    est_pertinent: bool
    est_professionnel: bool

agent_evaluateur = Agent(
    name="Évaluateur",
    instructions="""Évaluez la qualité de la réponse de l'agent.
    Critères :
    - Exactitude : la réponse est-elle factuelle et correcte ?
    - Pertinence : la réponse répond-elle à la question posée ?
    - Professionnalisme : le ton est-il approprié ?
    Score de 1 (très mauvais) à 5 (excellent).""",
    model="gpt-5.6-sol",
    output_type=Evaluation,
)

async def evaluer_qualite(question, reponse_agent):
    prompt = f"""Question posée : {question}
    Réponse de l'agent : {reponse_agent}
    Évaluez cette réponse."""

    result = await Runner.run(agent_evaluateur, prompt)
    return result.final_output

Le champ raison mérite votre attention lors des revues : c’est là que le juge explique pourquoi il a mis 3 sur 5, et c’est souvent la formulation d’un défaut que vous n’aviez pas su nommer.

Jeux de test structurés

Une évaluation qui n’est pas reproductible ne sert à rien. Organisez vos cas de test dans un format stable où chaque entrée porte un identifiant, une catégorie, la question, la réponse attendue, les tools attendus et les critères qualitatifs. La catégorie fait ici tout le travail : un score global de 4,2 sur 5 rassure à tort, alors qu’un score détaillé révèle que la catégorie « stock » s’effondre à 2,8 pendant que « prix » tire la moyenne vers le haut.

import json

# evaluation_dataset.json
DATASET = [
    {
        "id": "test_001",
        "categorie": "prix",
        "question": "Quel est le prix du laptop pro ?",
        "reponse_attendue": "1299€",
        "tools_attendus": ["rechercher_produit"],
        "critères": ["contient_prix", "ton_professionnel"],
    },
    {
        "id": "test_002",
        "categorie": "stock",
        "question": "Le tablet air est-il en stock ?",
        "reponse_attendue": "oui",
        "tools_attendus": ["rechercher_produit"],
        "critères": ["reponse_claire", "information_stock"],
    },
]

async def run_evaluation_complete(agent):
    resultats = []
    for cas in DATASET:
        result = await Runner.run(agent, cas["question"])
        eval_result = await evaluer_qualite(cas["question"], result.final_output)
        resultats.append({
            **cas,
            "reponse_agent": result.final_output,
            "score": eval_result.score,
            "évaluation": eval_result.raison,
        })

    # Rapport
    score_moyen = sum(r["score"] for r in resultats) / len(resultats)
    print(f"\nScore moyen : {score_moyen:.1f}/5")
    for cat in set(r["categorie"] for r in resultats):
        scores_cat = [r["score"] for r in resultats if r["categorie"] == cat]
        print(f"  {cat} : {sum(scores_cat)/len(scores_cat):.1f}/5")

    return resultats

Faites tourner cette évaluation dans votre chaîne d’intégration continue, au même titre que vos tests unitaires, et fixez un seuil sous lequel la livraison est bloquée. À partir de là, chaque modification — nouveau modèle, instructions retouchées, tool ajouté — se compare à la précédente sur les mêmes cas, et la discussion cesse de porter sur des impressions.

Points clés à retenir

  • Évaluez l’exactitude, l’utilisation des tools, la latence et le coût
  • L’évaluation par LLM (LLM-as-Judge) gère les réponses ouvertes
  • Créez des jeux de test structurés et reproductibles
  • Automatisez l’évaluation dans votre CI/CD pour détecter les régressions
  • Comparez systématiquement les changements (modèle, instructions, tools)