Accueil / Articles

Évaluations pour les GenAI et les agents : tests en une seule étape vs tests en plusieurs étapes dans le CI

Comparez les évaluations en une seule itération basées sur des traces avec les évaluations en plusieurs itérations pilotées par un simulateur, en abordant les référents d’or, les juges basés sur des LLM, les métriques d’agent, les portes de contrôle continue et sept pratiques de travail.

Éditeur : REACTAPP.TOP8 min de lecture1800 mots

Sources: End-to-End Evals for Gen AI and Agentic AI: A Practitioner’s Guide to Single-Turn and Multi-Turn Evaluation

« Ça fonctionne avec mon prompt » n’est pas une norme d’ingénierie. Un test unitaire conventionnel réussit ou échoue de manière déterministe, mais les systèmes génératifs fournissent des réponses ouvertes et probabilistes. Un pipeline RAG peut être globalement correct, et un agent peut accomplir une tâche de manière à sembler réussie sur le papier tout en étant dangereux dans la pratique. C’est pourquoi les évaluations, contrairement au jugement subjectif ou à des canaux de discussion remplis d’avis du type « ça me semble bien », servent désormais de base à une ingénierie ML sérieuse pour l’IA générative et les systèmes agents. Ce qui suit aborde ce qu’il convient de mesurer, en quoi les évaluations à un tour et à plusieurs tours diffèrent, les compromis associés à chacune, ainsi que des modèles en Python adaptés aux pipelines CI.

Pourquoi les évaluations ne sont pas optionnelles

Trois caractéristiques des applications de LLM en font la nécessité :

  1. Non-déterminisme. Des prompts identiques peuvent produire des résultats différents selon les exécutions, les versions du modèle ou les valeurs de température.
  2. Faillite cumulative chez les agents. Alors qu’un chatbot classique ne peut se tromper qu’une seule fois, un agent enchaîne de 10 à 50 appels au modèle, appels d’outils et décisions. Un taux d’erreur modeste à chaque étape peut entraîner des échecs fréquents de bout en bout.
  3. Régressions silencieuses. Un léger ajustement du prompt, une mise à jour du modèle ou un réindexage d’un stock de vecteurs peut dégrader la qualité sans qu’aucune exception ne soit levée.

L’évaluation transforme l’expression « ça semble mieux » en un chiffre qui peut être suivi dans le temps, utilisé pour décider des déploiements et comparé entre les versions.

Deux formes d’évaluation

Presque tous les frameworks, parmi lesquels DeepEval, Ragas, LangSmith, Braintrust et OpenAI Evals, suivent l’un de ces deux schémas.

Un seul tour, basé sur les traces

Le processus est simple : prendre chaque trace, appeler l’application une fois, puis évaluer cette trace.

Open a test run with trace-level metrics
  for each golden:
    yield golden
    call app with golden.input
    capture trace
    build a test case from the trace
    score test case with metrics
  upload test run (traces + scores)

Un golden est un échantillon étiqueté : l’entrée, la réponse qu’il doit produire, ainsi que souvent le contexte ou les appels d’outils qu’il doit impliquer. Le trace enregistre l’ensemble du déroulement d’une exécution, y compris quels fragments ont été récupérés, quels outils ont été utilisés, le raisonnement intermédiaire, la latence et le nombre de tokens, et pas seulement le texte final. Évaluer ce enregistrement plutôt que uniquement la réponse permet d’identifier l’étape où a eu lieu l’échec : la récupération des données, la génération ou un appel d’outil en cours de route ?

Multi-turn, basé sur un simulateur

C’est le modèle plus difficile et plus réaliste pour les agents et les assistants de conversation.

def simulate(conversational goldens)
  for each golden:
    loop until expected outcome OR max turns:
      simulator model generates a user turn
      ask app for next reply
      capture assistant turn
    build a ConversationalTestCase from the full transcript
  evaluate(test cases, metrics)   # metrics run concurrently
    -> pass/fail + reason per metric

Ici, il n’existe pas de paire d’entrée et de sortie fixe. Un modèle de simulateur simule un utilisateur ayant un objectif, comme annuler une abonnement ou obtenir un remboursement pour une commande spécifique, et adapte chaque message aux réponses de l’assistant, tout comme le ferait une personne. Le système est évalué sur l’ensemble du parcours : reste-t-il concentré sur sa tâche ? Utilise-t-il correctement les outils au fil des échanges ? Résout-il finalement son objectif ? Invente-t-il des détails de politique dès le troisième échange ?

Les agents, les assistants à plusieurs étapes et tout système disposant de mémoire ou d’état ont besoin de ce type d’approche. Une évaluation sur un seul échange ne permet pas de détecter si un agent se contredit au quatrième échange ou utilise un outil cinq fois de suite en boucle.

Choix des métriques

Les métriques se divisent en quatre catégories, et il est judicieux de les combiner, car aucune ne donne une image complète.

Un modèle mental utile : les métriques à un seul tour donnent une image instantanée, tandis que les métriques agentes évaluent une trajectoire.

L’LLM en tant que juge

La plupart des métriques actuelles sans référence fonctionnent selon le principe de « LLM en tant que juge ». Un modèle puissant, souvent différent et plus performant que le modèle testé, évalue une sortie selon des critères prédéfinis et explique son score.

from dataclasses import dataclass

JUDGE_PROMPT = """
You are evaluating an AI assistant's response for FAITHFULNESS.
A response is faithful if every factual claim it makes is supported
by the provided context. It is unfaithful if it adds unsupported
claims, contradicts the context, or fabricates details.
Context:
{context}
Response:
{response}
Return JSON: {{"score": <0.0-1.0>, "reason": "<short explanation>"}}
"""

@dataclass
class TestCase:
    input: str
    actual_output: str
    retrieval_context: list[str]

def faithfulness_metric(test_case: TestCase, judge_llm) -> dict:
    prompt = JUDGE_PROMPT.format(
        context="\n".join(test_case.retrieval_context),
        response=test_case.actual_output,
    )
    result = judge_llm.generate(prompt, response_format="json")
    return {"score": result["score"], "reason": result["reason"], "passed": result["score"] >= 0.7}

Les juges présentent des défis :

  • Biais de préférence personnelle. Les modèles évaluent plus favorablement leurs propres résultats ou ceux produits dans leur propre style. Utilisez autant que possible un juge provenant d’une autre famille de modèles.
  • Sensibilité aux instructions. De légères modifications dans la formulation des critères influencent les scores. Mettez les instructions destinées aux juges sous contrôle de version, tout comme du code.
  • Costs et latence. L’utilisation de juges coûte également de l’argent. Lorsque le budget est limité, testez d’abord avec du trafic réel et ne jugez pas tout.
  • Biais de longueur et de précision. Les juges ont tendance à privilégier des réponses plus longues et plus assurées, qu’elles soient justes ou non. Calibrez régulièrement leurs évaluations à l’aide d’un petit ensemble de données étiquetées par des humains.

L’usage en une seule étape dans la pratique

Un ensemble d’exemples idéal peut être aussi simple qu’une liste de dictionnaires contenant une entrée et une sortie attendue.

goldens = [
    {
        "input": "What's our refund policy for digital products?",
        "expected_output": "Digital products are refundable within 14 days if unused.",
        "expected_tools": [],
    },
    {
        "input": "Cancel order #48213",
        "expected_output": None,  # open-ended, judged not matched
        "expected_tools": ["lookup_order", "cancel_order"],
    },
]

Le lanceur suit le cycle décrit ci-dessus : lancer un essai, parcourir chaque cas de test, appeler l’application, enregistrer les traces, évaluer et télécharger le résultat.

from deepeval import evaluate
from deepeval.metrics import FaithfulnessMetric, AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase

def run_eval_suite(goldens, app_callback, metrics):
    test_cases = []
    for golden in goldens:
        trace = app_callback(golden["input"])   # your traced LLM/RAG app
        test_case = LLMTestCase(
            input=golden["input"],
            actual_output=trace.final_output,
            retrieval_context=trace.retrieved_chunks,
            expected_output=golden.get("expected_output"),
        )
        test_cases.append(test_case)
    results = evaluate(test_cases, metrics)  # runs metrics concurrently
    return results
metrics = [FaithfulnessMetric(threshold=0.7), AnswerRelevancyMetric(threshold=0.7)]
results = run_eval_suite(goldens, my_rag_app, metrics)

Une affirmation transforme les scores en un seuil.

def assert_eval_gate(results, min_pass_rate=0.9):
    passed = sum(1 for r in results if r.success)
    pass_rate = passed / len(results)
    assert pass_rate >= min_pass_rate, f"Eval pass rate {pass_rate:.2%}
    below gate {min_pass_rate:.0%}"

Intégrez-le dans le pipeline de PR de la même manière que pytest est intégré. Si un changement dans les prompts ou la récupération de données fait chuter le niveau de fidélité en dessous du seuil, la fusion doit être bloquée et rien ne doit être déployé discrètement.

En pratique, les conversations multiples

Plutôt que d’une entrée fixe, le simulateur reçoit un profil d’utilisateur et un objectif, et il guide la conversation.

class ConversationSimulator:
    def __init__(self, simulator_model, max_turns=8):
        self.simulator_model = simulator_model
        self.max_turns = max_turns

    def simulate(self, golden, chatbot_callback):
        transcript = []
        user_turn = self.simulator_model.generate_opening(golden["scenario"])
        for _ in range(self.max_turns):
            transcript.append({"role": "user", "content": user_turn})
            assistant_turn = chatbot_callback(transcript)
            transcript.append({"role": "assistant", "content": assistant_turn})
            if self.simulator_model.is_goal_met(golden["expected_outcome"], transcript):
                break
            user_turn = self.simulator_model.generate_next_turn(transcript, golden)
        return transcript

La conversation résultante est ensuite évaluée à l’aide de métriques d’agentivité.

from deepeval.test_case import ConversationalTestCase, Turn
from deepeval.metrics import (
    TaskCompletionMetric,
    ToolCorrectnessMetric,
    ConversationCompletenessMetric,
)

def build_conversational_test_case(transcript, tools_called):
    turns = [Turn(role=t["role"], content=t["content"]) for t in transcript]
    return ConversationalTestCase(turns=turns, tools_called=tools_called)
    metrics = [
    TaskCompletionMetric(threshold=0.8),          # did it accomplish the user's goal?
    ToolCorrectnessMetric(threshold=1.0),          # right tools, right args, right order
    ConversationCompletenessMetric(threshold=0.7), # did it address everything raised?
    ]
    test_case = build_conversational_test_case(transcript, tools_called=agent_trace.tool_calls)
    result = evaluate([test_case], metrics)

Les vérifications typiques au niveau de l’agent comprennent :

  • Correctitude de l’appel à l’outil : l’outil approprié, avec les arguments justes, dans le bon ordre, ce qui va au-delà du simple fait que « quelque outil a été appelé ».
  • Efficacité des étapes et détection des boucles : appels répétés à un même outil sans aucun progrès.
  • Respect du rôle et des politiques définies : maintien du personnage et des contraintes définis tout au long de la conversation, et non seulement lors du premier échange.
  • Récupération en cas d’erreur : tentative de réessai judicieuse après un appel à l’outil infructueux, plutôt que la production d’un résultat inventé.
  • Transmission du contexte : mémorisation de ce que l’utilisateur a dit trois échanges auparavant.

Évaluation des compromis

Un seul échange Plusieurs échanges
Coût Un appel par message, peu coûteux et rapide
Chaque exécution correspond à une conversation multi-appels, multipliée par les tours et les métriques Ensemble de données Les ensembles d’entraînement importants sont faciles à obtenir, que ce soit à partir de journaux ou de génération synthétique Les simulateurs permettent d’atteindre des cas limites et des utilisateurs hostiles que personne ne scripterait manuellement Signal Sépare clairement les bugs de récupération des bugs de génération, offrant un comportement quasi-déterministe pour les contrôles CI Détecte les échecs cumulatifs qui provoquent des incidents réels, et teste l’ensemble du système y compris les outils, l’orchestration, l’état et les mécanismes de contrôle Ponts faibles Néglige le dérive, les contradictions et la perte de mémoire au fil des tours, et favorise le surajustement à l’ensemble d’entraînement Variance plus élevée car le simulateur ajoute de la randomisation, débogage plus difficile puisqu’un échec au tour 6 peut commencer au tour 2, et jugement difficile avec de longs transcrits

Puisque les résultats obtenus après plusieurs tours sont bruités, interprétez-les de manière statistique. Exécutez l’ensemble d’épreuves N fois et examinez les distributions plutôt qu’un seul score.

Méthodes fiables

  1. Commencez par un petit ensemble d’entraînement propre de 30 à 100 exemples. Un ensemble bruité de 1 000 exemples est pire qu’un ensemble bien organisé de 50, car des étiquettes incorrectes altèrent le signal.
  2. Évaluez séparément la récupération et la génération dans RAG. La fidélité et le rappel contextuel répondent à des questions différentes, et les combiner masque la véritable cause du problème.
  3. Jugez à l’aide d’un modèle plus robuste et indépendant, et vérifiez de temps en temps ses scores par rapport aux évaluateurs humains. Même 20 à 30 exemples par mois permettent de détecter d’éventuelles déviations dans le jugement.
  • Enregistrez tout. Prenez en compte les données issues des traces enregistrées (documents récupérés, appels d’outils, étapes intermédiaires), et pas seulement le texte final. C’est pourquoi l’enregistrement des traces doit précéder la création du cas de test.
  • Simulez les agents avant déploiement. Exécutez des conversations avec des profils réalistes et des objectifs adversaires, tels qu’un utilisateur en colère, une demande ambiguë ou un cas limite lié à la politique, avant de livrer le produit et non après un incident.
  • Considérez les évaluations comme des actifs vivants. Réexécutez l’ensemble complet des tests après chaque modification de prompt, mise à jour du modèle ou reconstruction de l’index, et fixez des seuils de taux de réussite dans les processus CI tout comme pour les seuils de couverture.
  • Surveillez les distributions au fil du temps. Une seule exécution d’évaluation n’est qu’un échantillon, pas la vérité absolue. Répétez les suites de tests critiques et suivez l’évolution des tendances.
  • En résumé

    Les évaluations pour l’IA générative ont commencé comme une note en bas de page, une cellule de notebook exécutée avant une démonstration. Les systèmes agents ont dépassé ce stade. Lorsque le logiciel peut agir à plusieurs reprises, appeler des outils et conserver un état d’une manche à l’autre, l’évaluation n’est plus une simple vérification de qualité. Celle-ci devient le principal outil pour comprendre le comportement réel du système.

    Les évaluations en une seule manche basées sur le suivi offrent des contrôles de régression rapides et peu coûteux sur la qualité des résultats. Les évaluations en plusieurs manches utilisant des simulateurs fournissent l’analyse fiable pour déterminer si un agent se comporte comme prévu tout au long d’une interaction, et pour la plupart des agents en production, c’est cette analyse qui compte le plus. Concevez les deux types d’évaluations et automatisez-les tous deux. Ne faites confiance à aucun d’eux aveuglément, et laissez toujours un humain vérifier les résultats.