Évaluation et testing des agents IA

Tester un agent IA est radicalement différent de tester du code traditionnel. Les agents sont non-déterministes : même avec le même input, vous pouvez obtenir des réponses différentes à chaque exécution. Cette nature probabiliste rend l’évaluation complexe mais absolument critique.
Dans cet article, nous allons construire un framework d’évaluation complet qui couvre les 4 dimensions essentielles : qualité, performance, coûts et fiabilité. Vous apprendrez à implémenter des tests automatisés, à utiliser des benchmarks standards comme HotpotQA, et à mettre en place de l’A/B testing en production.
Pourquoi l’évaluation est critique
Déployer un agent sans évaluation rigoureuse, c’est comme conduire les yeux bandés. Voici des exemples réels de ce qui peut mal tourner :
L’agent customer support qui coûte une fortune
Contexte : Une startup e-commerce déploie un agent customer support sans tests de coûts.
Problème détecté :
- Budget initial estimé : $1,000/mois
- Coût réel premier mois : $14,000/mois
- Cause : Agent boucle sur questions complexes (8-12 appels LLM/requête au lieu de 2-3)
Impact :
- Budget mensuel explosé de 1,300%
- Urgence de rollback en pleine nuit
- Perte de confiance des investisseurs
Solution : Tests de coûts avec limite stricte à 5 appels LLM/requête avant déploiement.
L’agent juridique qui hallucine
Contexte : Cabinet d’avocats utilise un agent pour résumer des cas juridiques.
Problème détecté :
- Accuracy apparente : 92% (mesurée manuellement sur 50 cas)
- Hallucination rate réelle : 23% (détectée après audit approfondi)
- Agent invente des références de jurisprudence inexistantes
Impact :
- Dossier présenté en cour avec fausses références
- Sanctions professionnelles
- Retrait complet de l’IA
Solution : Tests de factualité systématiques + validation humaine obligatoire sur domaines critiques.
L’agent lent qui frustre les utilisateurs
Contexte : Application mobile avec agent de recommandation.
Problème détecté :
- Latence moyenne tests : 1.2s (acceptable)
- Latence P95 production : 8.4s (inacceptable)
- 35% d’utilisateurs abandonnent avant réponse
Impact :
- Churn rate +15%
- App store rating : 4.2 → 2.8
- Perte de revenus : $50k/mois
Solution : Tests de performance avec P95/P99 obligatoires + load testing avant production.
Défis spécifiques de l’évaluation des agents
Contrairement au code traditionnel où add(2, 3) retourne toujours 5, les agents IA sont fondamentalement non-déterministes. Cette propriété unique crée des défis majeurs :
Non-déterminisme
Même prompt, même modèle, même température = réponses différentes. Exemple concret :
# Même question, 3 exécutions différentes
agent.run("Résume cet article en 2 phrases")
# Réponse 1 : 47 mots, focus sur la méthodologie
# Réponse 2 : 31 mots, focus sur les résultats
# Réponse 3 : 53 mots, focus sur les implications
Implication : Les tests unitaires classiques (assert result == expected) ne fonctionnent pas. Il faut des métriques probabilistes (précision >90%, similarité cosinus >0.85).
Opacité des décisions
Les LLMs sont des boîtes noires. Quand un agent échoue, comprendre pourquoi est complexe :
- Problème dans le prompt système ?
- Données RAG non pertinentes ?
- Function calling mal configuré ?
- Température trop élevée ?
Implication : Besoin de traces détaillées (LangSmith, LangFuse) pour debugger.
Régressions silencieuses
Un changement de prompt peut améliorer 80% des cas et casser 20% sans que vous le sachiez immédiatement.
Exemple réel : Ajouter “Sois concis” au prompt améliore la brièveté mais dégrade la complétude de -15%.
Implication : Regression testing systématique sur un dataset représentatif avant tout changement.
Coûts variables et imprévisibles
Un agent qui boucle infiniment peut générer des milliers de dollars en quelques heures.
Statistique : 60% des équipes IA rapportent avoir eu au moins un incident de coûts non anticipés >$1,000.
Implication : Budgets stricts et monitoring temps réel obligatoires.
Les 4 dimensions d’évaluation
┌─────────────────────────────────────────────────┐
│ ÉVALUATION D'UN AGENT │
├─────────────────────────────────────────────────┤
│ │
│ 1️⃣ QUALITÉ : Précision, pertinence │
│ 2️⃣ PERFORMANCE : Latence, throughput │
│ 3️⃣ COÛTS : $/requête, tokens │
│ 4️⃣ FIABILITÉ : Success rate, erreurs │
│ │
└─────────────────────────────────────────────────┘
Qualité
Métriques de qualité
| Métrique | Description | Cible |
|---|---|---|
| Accuracy | % de réponses correctes | >90% |
| Relevance | Pertinence des réponses | >4/5 |
| Completeness | Réponse complète | >85% |
| Factuality | Exactitude factuelle | >95% |
| CSAT | Satisfaction client | >4.0/5.0 |
Implémentation
import openai
from typing import Dict, List
from dataclasses import dataclass
@dataclass
class EvaluationResult:
"""Résultat d'évaluation"""
accuracy: float
relevance: float
completeness: float
factuality: float
explanation: str
class AgentEvaluator:
"""Évaluateur d'agents IA"""
def __init__(self, evaluator_model="gpt-4"):
self.evaluator_model = evaluator_model
def evaluate_response(
self,
query: str,
agent_response: str,
ground_truth: str = None
) -> EvaluationResult:
"""Évalue une réponse d'agent"""
prompt = f"""Tu es un évaluateur expert d'agents IA.
Question : {query}
Réponse de l'agent : {agent_response}
{f'Réponse attendue : {ground_truth}' if ground_truth else ''}
Évalue cette réponse selon 4 critères (note de 0 à 1) :
1. **Accuracy** : La réponse est-elle correcte ?
2. **Relevance** : La réponse répond-elle à la question ?
3. **Completeness** : La réponse est-elle complète ?
4. **Factuality** : Les faits mentionnés sont-ils exacts ?
Réponds en JSON :
{{
"accuracy": 0.0-1.0,
"relevance": 0.0-1.0,
"completeness": 0.0-1.0,
"factuality": 0.0-1.0,
"explanation": "Explication détaillée"
}}"""
response = openai.chat.completions.create(
model=self.evaluator_model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
import json
result = json.loads(response.choices[0].message.content)
return EvaluationResult(**result)
def evaluate_agent(
self,
agent,
test_cases: List[Dict]
) -> Dict:
"""Évalue un agent sur un ensemble de test cases"""
results = []
for i, test_case in enumerate(test_cases):
print(f"\n🧪 Test {i+1}/{len(test_cases)}: {test_case['query']}")
# Exécuter l'agent
agent_response = agent.run(test_case["query"])
# Évaluer
evaluation = self.evaluate_response(
query=test_case["query"],
agent_response=agent_response,
ground_truth=test_case.get("expected_answer")
)
results.append({
"test_case": test_case,
"response": agent_response,
"evaluation": evaluation
})
print(f" Accuracy: {evaluation.accuracy:.2f}")
print(f" Relevance: {evaluation.relevance:.2f}")
print(f" Completeness: {evaluation.completeness:.2f}")
print(f" Factuality: {evaluation.factuality:.2f}")
# Calculer les moyennes
avg_accuracy = sum(r["evaluation"].accuracy for r in results) / len(results)
avg_relevance = sum(r["evaluation"].relevance for r in results) / len(results)
avg_completeness = sum(r["evaluation"].completeness for r in results) / len(results)
avg_factuality = sum(r["evaluation"].factuality for r in results) / len(results)
return {
"results": results,
"summary": {
"accuracy": avg_accuracy,
"relevance": avg_relevance,
"completeness": avg_completeness,
"factuality": avg_factuality,
"total_tests": len(test_cases)
}
}
# ========== EXEMPLE D'UTILISATION ==========
# Test cases
test_cases = [
{
"query": "Quelle est la capitale de la France ?",
"expected_answer": "Paris"
},
{
"query": "Combien font 15 * 7 ?",
"expected_answer": "105"
},
{
"query": "Qui a écrit Les Misérables ?",
"expected_answer": "Victor Hugo"
}
]
# Évaluer
evaluator = AgentEvaluator()
evaluation_report = evaluator.evaluate_agent(my_agent, test_cases)
# Afficher le résumé
print(f"\n{'='*60}")
print(f"📊 RÉSUMÉ DE L'ÉVALUATION")
print(f"{'='*60}")
summary = evaluation_report["summary"]
print(f"Accuracy : {summary['accuracy']:.1%}")
print(f"Relevance : {summary['relevance']:.1%}")
print(f"Completeness : {summary['completeness']:.1%}")
print(f"Factuality : {summary['factuality']:.1%}")
print(f"Total tests : {summary['total_tests']}")
Performance
Métriques de performance
| Métrique | Description | Cible |
|---|---|---|
| Latence P50 | Temps médian | <1s |
| Latence P95 | 95e percentile | <2s |
| Latence P99 | 99e percentile | <5s |
| Throughput | Requêtes/seconde | >10 req/s |
| TTFB | Time to First Byte | <300ms |
Implémentation
import time
import statistics
from typing import List
class PerformanceTracker:
"""Suivi des performances"""
def __init__(self):
self.latencies = []
self.start_time = None
self.end_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.end_time = time.time()
latency = self.end_time - self.start_time
self.latencies.append(latency)
def get_metrics(self) -> Dict:
"""Calcule les métriques de performance"""
if not self.latencies:
return {}
sorted_latencies = sorted(self.latencies)
n = len(sorted_latencies)
return {
"total_requests": n,
"latency_mean": statistics.mean(self.latencies),
"latency_median": statistics.median(self.latencies),
"latency_p95": sorted_latencies[int(n * 0.95)],
"latency_p99": sorted_latencies[int(n * 0.99)],
"latency_min": min(self.latencies),
"latency_max": max(self.latencies),
"throughput": n / sum(self.latencies) if sum(self.latencies) > 0 else 0
}
def print_report(self):
"""Affiche un rapport de performance"""
metrics = self.get_metrics()
print(f"\n{'='*60}")
print(f"⚡ RAPPORT DE PERFORMANCE")
print(f"{'='*60}")
print(f"Total requêtes : {metrics['total_requests']}")
print(f"Latence moyenne : {metrics['latency_mean']:.3f}s")
print(f"Latence médiane : {metrics['latency_median']:.3f}s")
print(f"Latence P95 : {metrics['latency_p95']:.3f}s")
print(f"Latence P99 : {metrics['latency_p99']:.3f}s")
print(f"Latence min : {metrics['latency_min']:.3f}s")
print(f"Latence max : {metrics['latency_max']:.3f}s")
print(f"Throughput : {metrics['throughput']:.2f} req/s")
# ========== UTILISATION ==========
tracker = PerformanceTracker()
# Tester sur 100 requêtes
for i in range(100):
with tracker:
agent.run(f"Question {i}")
# Rapport
tracker.print_report()
# Vérifier les cibles
metrics = tracker.get_metrics()
assert metrics["latency_p95"] < 2.0, "❌ P95 latency too high"
assert metrics["throughput"] > 1.0, "❌ Throughput too low"
Coûts
Métriques de coûts
| Métrique | Description | Cible |
|---|---|---|
| $/requête | Coût moyen par requête | <0,10$ |
| Tokens/requête | Tokens moyens utilisés | <5000 |
| LLM calls/requête | Nombre d’appels LLM | <5 |
| $/1000 users | Coût pour 1k utilisateurs | <100$ |
Implémentation
class CostTracker:
"""Suivi des coûts"""
PRICING = {
"gpt-4": {"input": 0.03, "output": 0.06}, # $/1k tokens
"gpt-3.5-turbo": {"input": 0.0015, "output": 0.002},
"claude-3-opus": {"input": 0.015, "output": 0.075},
"claude-3-sonnet": {"input": 0.003, "output": 0.015}
}
def __init__(self):
self.calls = []
def track_call(
self,
model: str,
input_tokens: int,
output_tokens: int
):
"""Enregistre un appel LLM"""
pricing = self.PRICING.get(model, {"input": 0, "output": 0})
cost = (
(input_tokens / 1000) * pricing["input"] +
(output_tokens / 1000) * pricing["output"]
)
self.calls.append({
"model": model,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"cost": cost
})
def get_metrics(self) -> Dict:
"""Calcule les métriques de coût"""
if not self.calls:
return {}
total_cost = sum(call["cost"] for call in self.calls)
total_tokens = sum(
call["input_tokens"] + call["output_tokens"]
for call in self.calls
)
return {
"total_calls": len(self.calls),
"total_cost": total_cost,
"cost_per_request": total_cost / len(self.calls),
"total_tokens": total_tokens,
"tokens_per_request": total_tokens / len(self.calls),
"cost_per_1k_users": (total_cost / len(self.calls)) * 1000
}
def print_report(self):
"""Affiche un rapport de coûts"""
metrics = self.get_metrics()
print(f"\n{'='*60}")
print(f"💰 RAPPORT DE COÛTS")
print(f"{'='*60}")
print(f"Total appels : {metrics['total_calls']}")
print(f"Coût total : ${metrics['total_cost']:.4f}")
print(f"Coût/requête : ${metrics['cost_per_request']:.4f}")
print(f"Total tokens : {metrics['total_tokens']:,}")
print(f"Tokens/requête : {metrics['tokens_per_request']:.0f}")
print(f"Coût/1k users : ${metrics['cost_per_1k_users']:.2f}")
# ========== INTÉGRATION AVEC L'AGENT ==========
class CostTrackedAgent:
"""Agent avec tracking des coûts"""
def __init__(self, base_agent):
self.agent = base_agent
self.cost_tracker = CostTracker()
def run(self, query: str) -> str:
"""Exécute l'agent et track les coûts"""
# Wrapper autour des appels OpenAI
original_create = openai.chat.completions.create
def tracked_create(*args, **kwargs):
response = original_create(*args, **kwargs)
# Extraire les métriques
model = kwargs.get("model", "gpt-4")
usage = response.usage
self.cost_tracker.track_call(
model=model,
input_tokens=usage.prompt_tokens,
output_tokens=usage.completion_tokens
)
return response
# Patch temporaire
openai.chat.completions.create = tracked_create
try:
result = self.agent.run(query)
finally:
# Restore
openai.chat.completions.create = original_create
return result
Fiabilité
Métriques de fiabilité
| Métrique | Description | Cible |
|---|---|---|
| Success Rate | % de requêtes réussies | >95% |
| Error Rate | % d’erreurs | <5% |
| Timeout Rate | % de timeouts | <2% |
| Hallucination Rate | % de hallucinations | <10% |
Implémentation
class ReliabilityTracker:
"""Suivi de la fiabilité"""
def __init__(self):
self.results = []
def track_result(
self,
success: bool,
error_type: str = None,
has_hallucination: bool = False
):
"""Enregistre un résultat"""
self.results.append({
"success": success,
"error_type": error_type,
"has_hallucination": has_hallucination
})
def get_metrics(self) -> Dict:
"""Calcule les métriques de fiabilité"""
if not self.results:
return {}
total = len(self.results)
successes = sum(1 for r in self.results if r["success"])
errors = sum(1 for r in self.results if not r["success"])
timeouts = sum(1 for r in self.results if r["error_type"] == "timeout")
hallucinations = sum(1 for r in self.results if r["has_hallucination"])
return {
"total_requests": total,
"success_rate": successes / total,
"error_rate": errors / total,
"timeout_rate": timeouts / total,
"hallucination_rate": hallucinations / total
}
def print_report(self):
"""Affiche un rapport de fiabilité"""
metrics = self.get_metrics()
print(f"\n{'='*60}")
print(f"🔒 RAPPORT DE FIABILITÉ")
print(f"{'='*60}")
print(f"Total requêtes : {metrics['total_requests']}")
print(f"Success rate : {metrics['success_rate']:.1%}")
print(f"Error rate : {metrics['error_rate']:.1%}")
print(f"Timeout rate : {metrics['timeout_rate']:.1%}")
print(f"Hallucination rate : {metrics['hallucination_rate']:.1%}")
Framework de testing complet
import pytest
from typing import Callable
class AgentTestSuite:
"""Suite de tests complète pour agents"""
def __init__(self, agent):
self.agent = agent
self.quality_evaluator = AgentEvaluator()
self.performance_tracker = PerformanceTracker()
self.cost_tracker = CostTracker()
self.reliability_tracker = ReliabilityTracker()
def run_full_evaluation(self, test_cases: List[Dict]) -> Dict:
"""Évaluation complète"""
print(f"\n{'='*60}")
print(f"🧪 ÉVALUATION COMPLÈTE DE L'AGENT")
print(f"{'='*60}")
results = []
for test_case in test_cases:
result = self._run_single_test(test_case)
results.append(result)
# Rapports
self.quality_evaluator.print_report()
self.performance_tracker.print_report()
self.cost_tracker.print_report()
self.reliability_tracker.print_report()
return {
"results": results,
"quality": self.quality_evaluator.get_metrics(),
"performance": self.performance_tracker.get_metrics(),
"cost": self.cost_tracker.get_metrics(),
"reliability": self.reliability_tracker.get_metrics()
}
def _run_single_test(self, test_case: Dict) -> Dict:
"""Exécute un test unique"""
query = test_case["query"]
try:
# Performance tracking
with self.performance_tracker:
response = self.agent.run(query)
# Quality evaluation
evaluation = self.quality_evaluator.evaluate_response(
query=query,
agent_response=response,
ground_truth=test_case.get("expected_answer")
)
# Reliability tracking
self.reliability_tracker.track_result(
success=True,
has_hallucination=evaluation.factuality < 0.7
)
return {
"query": query,
"response": response,
"evaluation": evaluation,
"success": True
}
except TimeoutError:
self.reliability_tracker.track_result(
success=False,
error_type="timeout"
)
return {"query": query, "success": False, "error": "timeout"}
except Exception as e:
self.reliability_tracker.track_result(
success=False,
error_type=type(e).__name__
)
return {"query": query, "success": False, "error": str(e)}
# ========== TESTS PYTEST ==========
@pytest.fixture
def agent():
"""Fixture de l'agent"""
return MyAgent()
@pytest.fixture
def test_suite(agent):
"""Fixture de la suite de tests"""
return AgentTestSuite(agent)
def test_quality(test_suite):
"""Test de qualité"""
test_cases = [
{"query": "Quelle est la capitale de la France ?", "expected_answer": "Paris"},
{"query": "Qui a écrit 1984 ?", "expected_answer": "George Orwell"}
]
report = test_suite.run_full_evaluation(test_cases)
assert report["quality"]["accuracy"] > 0.9, "Accuracy trop basse"
assert report["quality"]["relevance"] > 0.9, "Relevance trop basse"
def test_performance(test_suite):
"""Test de performance"""
test_cases = [{"query": f"Question {i}"} for i in range(100)]
report = test_suite.run_full_evaluation(test_cases)
assert report["performance"]["latency_p95"] < 2.0, "P95 latency trop élevée"
assert report["performance"]["throughput"] > 1.0, "Throughput trop faible"
def test_cost(test_suite):
"""Test de coûts"""
test_cases = [{"query": f"Question {i}"} for i in range(100)]
report = test_suite.run_full_evaluation(test_cases)
assert report["cost"]["cost_per_request"] < 0.10, "Coût par requête trop élevé"
def test_reliability(test_suite):
"""Test de fiabilité"""
test_cases = [{"query": f"Question {i}"} for i in range(100)]
report = test_suite.run_full_evaluation(test_cases)
assert report["reliability"]["success_rate"] > 0.95, "Success rate trop faible"
assert report["reliability"]["hallucination_rate"] < 0.10, "Trop d'hallucinations"
Benchmarks standards
HotpotQA
Benchmark pour questions multi-étapes.
from datasets import load_dataset
def evaluate_on_hotpotqa(agent, num_samples=100):
"""Évalue sur HotpotQA"""
dataset = load_dataset("hotpot_qa", "fullwiki", split="validation")
samples = dataset.shuffle().select(range(num_samples))
correct = 0
for sample in samples:
question = sample["question"]
expected_answer = sample["answer"]
agent_answer = agent.run(question)
# Vérifier si la réponse est correcte
if expected_answer.lower() in agent_answer.lower():
correct += 1
accuracy = correct / num_samples
print(f"\n📊 HotpotQA Results:")
print(f"Accuracy: {accuracy:.1%} ({correct}/{num_samples})")
return accuracy
FEVER (Fact Verification)
def evaluate_on_fever(agent, num_samples=100):
"""Évalue sur FEVER"""
dataset = load_dataset("fever", "v1.0", split="paper_test")
samples = dataset.shuffle().select(range(num_samples))
correct = 0
for sample in samples:
claim = sample["claim"]
expected_label = sample["label"] # SUPPORTS, REFUTES, NOT ENOUGH INFO
prompt = f"Cette affirmation est-elle vraie, fausse, ou impossible à vérifier ? '{claim}'"
agent_response = agent.run(prompt)
# Parser la réponse de l'agent
agent_label = parse_label(agent_response)
if agent_label == expected_label:
correct += 1
accuracy = correct / num_samples
print(f"\n📊 FEVER Results:")
print(f"Accuracy: {accuracy:.1%} ({correct}/{num_samples})")
return accuracy
Stratégies d’évaluation par type d’agent
Tous les agents ne se testent pas de la même façon. Adaptez votre stratégie d’évaluation au type d’agent :
Agents question-réponse (QA)
Métriques prioritaires :
- Exactitude factuelle : >95%
- Temps de réponse : <1s
- Sources citées : >80% des réponses
Datasets recommandés :
- SQuAD 2.0 (questions extractives)
- Natural Questions (questions ouvertes)
- TriviaQA (connaissances générales)
Stratégie :
- Créer un golden dataset de 200-500 Q&A spécifiques à votre domaine
- Évaluation automatique via similarité sémantique (embeddings)
- Revue manuelle mensuelle sur 10% du trafic réel
Agents de support client
Métriques prioritaires :
- CSAT (Customer Satisfaction) : >4.2/5
- Résolution au premier contact : >70%
- Taux d’escalade humain : <30%
Stratégie :
- A/B testing continu (variante baseline vs nouvelle version)
- Feedback utilisateur après chaque interaction
- Analyse sentiment des conversations
- Revue hebdomadaire des escalations
Pièges à éviter :
- Se focaliser uniquement sur l’accuracy technique
- Ignorer la satisfaction utilisateur
- Ne pas mesurer le temps de résolution
Agents de génération de code
Métriques prioritaires :
- Tests unitaires passent : 100% (obligatoire)
- Code compile : 100% (obligatoire)
- Complexité cyclomatique : <10
- Sécurité : 0 vulnérabilité critique
Datasets recommandés :
- HumanEval (benchmark standard)
- MBPP (programmation basique)
- CodeContests (algorithmes complexes)
Stratégie :
- Tests automatisés : compilation + tests unitaires + linting
- Revue sécurité automatique (Bandit, CodeQL)
- Évaluation performance (benchmarks d’exécution)
Agents de recherche/RAG
Métriques prioritaires :
- Précision des sources : >90%
- Pertinence documents récupérés : >0.8 (MRR)
- Complétude de la réponse : >85%
Stratégie :
- Évaluer séparément retrieval et generation
- Métriques RAG spécifiques (faithfulness, answer relevancy, context precision)
- Vérifier citations et hallucinations
Outils recommandés :
- RAGAS (framework évaluation RAG)
- TruLens (monitoring RAG)
Agents autonomes (AutoGPT-like)
Métriques prioritaires :
- Taux de complétion tâche : >80%
- Nombre d’étapes moyennes : <15
- Budget respecté : 100%
- Pas de boucles infinies : 100%
Stratégie :
- Tests avec timeout strict (5 min max)
- Budget max par tâche ($0.50)
- Vérification finale : la tâche est-elle vraiment complétée ?
- Logs détaillés de chaque étape pour post-mortem
A/B testing en production
A/B testing essentiel : Tester 2 versions d’agent en production avec du vrai trafic est la seule façon de savoir laquelle performe vraiment mieux. Les tests en local ne reflètent pas toujours la réalité.
L’A/B testing est la méthode gold standard pour évaluer des agents en conditions réelles. Contrairement aux benchmarks académiques, vous mesurez l’impact sur de vrais utilisateurs avec de vraies questions.
Quand utiliser l’A/B testing
Scénarios idéaux :
- Comparer 2 prompts différents
- Tester GPT-4 vs Claude 3.5 Sonnet
- Évaluer changement d’architecture (ReAct vs Plan-and-Execute)
- Mesurer impact d’une optimisation (caching, routing)
Prérequis :
- Trafic suffisant (>1000 requêtes/semaine minimum)
- Métriques claires (CSAT, latence, coûts)
- Durée suffisante (2-4 semaines pour significativité statistique)
Pièges de l’A/B testing
Erreur 1 : Sample size trop petit
- Besoin de ~385 échantillons/variante pour p-value <0.05
- Avec 50 requêtes/jour → attendre 8 jours minimum
Erreur 2 : Ignorer les facteurs externes
- Lundi vs vendredi : comportement utilisateur différent
- Heures de pointe vs heures creuses
- Solution : randomisation stratifiée par période
Erreur 3 : Tester trop de variantes en même temps
- A/B/C/D testing → besoin de 4× plus de trafic
- Mieux : tests séquentiels (A/B puis gagnant vs C)
Erreur 4 : Arrêter le test trop tôt
- “p-value = 0.04 après 2 jours, on arrête !”
- Risque : faux positif, effet de nouveauté
- Solution : durée minimale prédéfinie (2-4 semaines)
import random
from datetime import datetime, timedelta
class ABTest:
"""Framework d'A/B testing pour agents"""
def __init__(self, variant_a, variant_b, split=0.5):
self.variant_a = variant_a
self.variant_b = variant_b
self.split = split
self.results_a = []
self.results_b = []
def run(self, query: str, user_id: str = None) -> str:
"""Exécute un agent selon l'A/B split"""
# Déterminer la variante
if user_id:
# Consistent assignment basé sur user_id
variant = "a" if hash(user_id) % 2 == 0 else "b"
else:
# Random assignment
variant = "a" if random.random() < self.split else "b"
# Exécuter
start_time = datetime.now()
if variant == "a":
response = self.variant_a.run(query)
self.results_a.append({
"query": query,
"response": response,
"latency": (datetime.now() - start_time).total_seconds()
})
else:
response = self.variant_b.run(query)
self.results_b.append({
"query": query,
"response": response,
"latency": (datetime.now() - start_time).total_seconds()
})
return response
def analyze(self) -> Dict:
"""Analyse les résultats de l'A/B test"""
import scipy.stats as stats
# Latences
latencies_a = [r["latency"] for r in self.results_a]
latencies_b = [r["latency"] for r in self.results_b]
# T-test pour latence
t_stat, p_value = stats.ttest_ind(latencies_a, latencies_b)
print(f"\n{'='*60}")
print(f"📊 RÉSULTATS A/B TEST")
print(f"{'='*60}")
print(f"\n🅰️ Variant A:")
print(f" Samples: {len(self.results_a)}")
print(f" Latence moyenne: {statistics.mean(latencies_a):.3f}s")
print(f" Latence P95: {sorted(latencies_a)[int(len(latencies_a)*0.95)]:.3f}s")
print(f"\n🅱️ Variant B:")
print(f" Samples: {len(self.results_b)}")
print(f" Latence moyenne: {statistics.mean(latencies_b):.3f}s")
print(f" Latence P95: {sorted(latencies_b)[int(len(latencies_b)*0.95)]:.3f}s")
print(f"\n📈 Statistiques:")
print(f" t-statistic: {t_stat:.4f}")
print(f" p-value: {p_value:.4f}")
if p_value < 0.05:
winner = "A" if statistics.mean(latencies_a) < statistics.mean(latencies_b) else "B"
print(f"\n✅ Résultat significatif : Variant {winner} gagne (p < 0.05)")
else:
print(f"\n⚠️ Pas de différence significative (p > 0.05)")
return {
"variant_a": {
"samples": len(self.results_a),
"latency_mean": statistics.mean(latencies_a)
},
"variant_b": {
"samples": len(self.results_b),
"latency_mean": statistics.mean(latencies_b)
},
"p_value": p_value,
"significant": p_value < 0.05
}
# ========== UTILISATION ==========
# Variantes
agent_v1 = MyAgent(model="gpt-3.5-turbo") # Version actuelle
agent_v2 = MyAgent(model="gpt-4") # Nouvelle version
# A/B test
ab_test = ABTest(variant_a=agent_v1, variant_b=agent_v2)
# Simuler du trafic
for i in range(1000):
user_id = f"user_{i % 100}" # 100 utilisateurs
query = f"Question {i}"
ab_test.run(query, user_id=user_id)
# Analyser
ab_test.analyze()
Monitoring continue en production
import prometheus_client as prom
class ProductionMonitor:
"""Monitoring production avec Prometheus"""
def __init__(self):
# Métriques Prometheus
self.request_counter = prom.Counter(
"agent_requests_total",
"Total number of agent requests"
)
self.latency_histogram = prom.Histogram(
"agent_latency_seconds",
"Agent response latency"
)
self.error_counter = prom.Counter(
"agent_errors_total",
"Total number of agent errors",
["error_type"]
)
self.quality_gauge = prom.Gauge(
"agent_quality_score",
"Agent quality score (0-1)"
)
def track_request(self, agent, query: str):
"""Track une requête"""
self.request_counter.inc()
start = time.time()
try:
response = agent.run(query)
latency = time.time() - start
self.latency_histogram.observe(latency)
# Évaluer la qualité (async)
quality_score = self._evaluate_quality_async(query, response)
self.quality_gauge.set(quality_score)
return response
except Exception as e:
self.error_counter.labels(error_type=type(e).__name__).inc()
raise
def _evaluate_quality_async(self, query: str, response: str) -> float:
"""Évalue la qualité de manière asynchrone"""
# Utiliser un evaluator rapide ou sample
# Pour éviter de ralentir les requêtes
return 0.85 # Placeholder
Régression testing
class RegressionTester:
"""Détecte les régressions entre versions"""
def __init__(self, baseline_agent, new_agent):
self.baseline = baseline_agent
self.new_agent = new_agent
def test_regression(self, test_cases: List[Dict]) -> Dict:
"""Compare deux versions d'agent"""
print(f"\n{'='*60}")
print(f"🔄 TEST DE RÉGRESSION")
print(f"{'='*60}")
regressions = []
improvements = []
unchanged = []
for test_case in test_cases:
query = test_case["query"]
# Baseline
baseline_response = self.baseline.run(query)
baseline_score = self._score_response(query, baseline_response)
# New version
new_response = self.new_agent.run(query)
new_score = self._score_response(query, new_response)
# Comparer
diff = new_score - baseline_score
if diff < -0.1:
regressions.append({
"query": query,
"baseline_score": baseline_score,
"new_score": new_score,
"diff": diff
})
elif diff > 0.1:
improvements.append({
"query": query,
"baseline_score": baseline_score,
"new_score": new_score,
"diff": diff
})
else:
unchanged.append({"query": query})
print(f"\n✅ Améliorations : {len(improvements)}")
print(f"⚠️ Régressions : {len(regressions)}")
print(f"➖ Inchangés : {len(unchanged)}")
if regressions:
print(f"\n⚠️ RÉGRESSIONS DÉTECTÉES:")
for reg in regressions[:5]:
print(f" • {reg['query']}")
print(f" Baseline: {reg['baseline_score']:.2f} → New: {reg['new_score']:.2f}")
return {
"regressions": regressions,
"improvements": improvements,
"unchanged": unchanged
}
def _score_response(self, query: str, response: str) -> float:
"""Score une réponse (0-1)"""
# Utiliser un evaluator
evaluator = AgentEvaluator()
result = evaluator.evaluate_response(query, response)
return (result.accuracy + result.relevance) / 2
Outils et plateformes d’évaluation
LangSmith (LangChain)
Forces :
- Intégration native avec LangChain
- Tracing distribué automatique
- Datasets versionnés
- Comparaison de versions
Use cases :
- Debugging d’agents complexes
- Regression testing
- Monitoring production
Pricing : Gratuit jusqu’à 5k traces/mois, puis $39/mois
Exemple :
from langsmith import Client
client = Client()
# Créer un dataset
dataset = client.create_dataset("customer_support_qa")
client.create_examples(
dataset_id=dataset.id,
inputs=[{"question": "..."}],
outputs=[{"answer": "..."}]
)
# Évaluer
results = client.run_on_dataset(
dataset_name="customer_support_qa",
llm_or_chain=my_agent
)
RAGAS (RAG Assessment)
Forces :
- Métriques spécialisées RAG (faithfulness, relevance, context precision)
- Open-source gratuit
- Compatible tous frameworks
Métriques clés :
- Faithfulness : Réponse basée sur les sources ? (>0.9 cible)
- Answer Relevancy : Réponse pertinente ? (>0.8 cible)
- Context Precision : Documents récupérés pertinents ? (>0.7 cible)
- Context Recall : Tous documents pertinents récupérés ? (>0.8 cible)
Exemple :
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy
results = evaluate(
dataset=test_dataset,
metrics=[faithfulness, answer_relevancy]
)
print(results.to_pandas())
TruLens (TruEra)
Forces :
- Monitoring en temps réel
- Détection de dérives
- Feedback loops
- Gratuit open-source
Use cases :
- Monitoring production
- Détection d’hallucinations
- A/B testing
Weights & Biases (W&B)
Forces :
- Suivi expériences ML
- Comparaison de runs
- Dashboards personnalisables
Use case : Comparer 50 variantes de prompts et visualiser l’impact sur toutes les métriques.
PromptLayer
Forces :
- Logging automatique de tous appels LLM
- Analytics coûts
- A/B testing intégré
Pricing : Gratuit jusqu’à 1k requests/mois
Comparaison des outils
| Outil | Meilleur pour | Prix | Courbe apprentissage |
|---|---|---|---|
| LangSmith | Agents LangChain | $39/mois | Moyenne |
| RAGAS | Agents RAG | Gratuit | Faible |
| TruLens | Monitoring production | Gratuit | Moyenne |
| W&B | Expérimentation | Gratuit (basic) | Élevée |
| PromptLayer | Logging simple | Gratuit (basic) | Faible |
Recommandation :
- Débutant : RAGAS + PromptLayer
- Intermédiaire : LangSmith + RAGAS
- Avancé : LangSmith + TruLens + W&B
Exercices pratiques
Exercice 1 : Suite de tests complète
Créez une suite de tests pytest pour votre agent qui couvre :
- Qualité (accuracy >90%)
- Performance (P95 <2s)
- Coûts (<0,10$/requête)
- Fiabilité (success rate >95%)
Objectif : Détecter régressions automatiquement à chaque commit.
Exercice 2 : Benchmark standard
Évaluez votre agent sur HotpotQA et comparez aux résultats académiques :
- GPT-4 baseline : ~67% accuracy
- Votre agent : ? %
Challenge bonus : Atteindre >70% accuracy.
Exercice 3 : A/B test réel
Implémentez un A/B test entre deux prompts différents :
- Variante A : Prompt actuel
- Variante B : Prompt avec “Chain-of-Thought”
Mesurez sur 1000 requêtes :
- Accuracy
- Latence
- Coûts
Déterminez le gagnant avec p-value <0.05.
Exercice 4 : Intégration LangSmith
- Créez un compte LangSmith
- Intégrez le tracing dans votre agent
- Créez un dataset de 50 Q&A
- Lancez une évaluation
- Analysez les échecs
Best practices et lessons learned
Après avoir évalué des centaines d’agents en production, voici les leçons essentielles :
Commencez simple, itérez vite
❌ Erreur : Construire un framework d’évaluation parfait pendant 2 mois avant de tester quoi que ce soit.
✅ Bon réflexe : Commencer avec 20 test cases manuels et un script Python de 50 lignes. Enrichir progressivement.
Golden dataset = actif stratégique
Votre dataset de test est aussi important que votre code. Investissez dedans :
- 200-500 exemples minimum
- Cas limites (edge cases) : 30%
- Diversité : couvrir tous les use cases
- Versioning : Git pour le dataset
Astuce : Extraire des vraies questions utilisateur (production) pour alimenter le dataset.
Automatisez tout, mais revoyez manuellement
- Automatisé : 95% de l’évaluation (tests pytest, benchmarks)
- Manuel : 5% revue qualitative (1 fois/mois, 50 exemples aléatoires)
Pourquoi ? Les métriques automatiques manquent des nuances (ton, empathie, créativité).
P95/P99 > moyenne
Mauvais : “Latence moyenne = 800ms, excellent !” Bon : “Latence P95 = 3.2s, 5% d’utilisateurs frustrés”
Optimisez pour le pire cas, pas la moyenne.
Coûts = métrique de premier ordre
Ne traitez pas les coûts comme une après-pensée. Intégrez-les dès le jour 1 :
- Budget max par requête : $0.10
- Alerte si >$0.15
- Killswitch si >$0.50
A/B testing > benchmarks académiques
Un agent avec 85% sur HotpotQA mais CSAT 4.8/5 en production bat un agent avec 92% sur HotpotQA mais CSAT 3.9/5.
Les utilisateurs réels > benchmarks académiques.
Monitoring ≠ évaluation
- Évaluation : Tests pré-déploiement (offline)
- Monitoring : Suivi post-déploiement (online)
Vous avez besoin des deux :
- Évaluation attrape 80% des bugs
- Monitoring attrape les 20% restants (edge cases production)
Ressources et liens
Outils d’évaluation
- LangSmith : Plateforme d’évaluation LangChain
- RAGAS : Framework évaluation RAG
- TruLens : Monitoring agents
- PromptLayer : Logging et analytics LLM
Benchmarks et datasets
- HotpotQA : Questions multi-hop
- FEVER : Fact verification
- AgentBench : Suite complète
- SQuAD 2.0 : Question answering
- HumanEval : Code generation
Documentation technique
- Testing AI Systems : Google guide
- LLM Evaluation : OpenAI cookbook
- A/B Testing : Statsig guide
Articles connexes
Série Agents IA :
- Introduction aux Agents - Concepts de base
- Architecture des Agents - Les 4 piliers
- LangChain - Framework pratique
- Multi-Agents - Systèmes collaboratifs
- Production - Déploiement scalable
Conclusion
L’évaluation des agents IA n’est pas optionnelle - c’est la différence entre un prototype qui impressionne en démo et un système production qui crée de la valeur réelle.
Les 3 piliers d’une stratégie d’évaluation réussie :
Tests automatisés systématiques : Framework pytest avec les 4 dimensions (qualité, performance, coûts, fiabilité). Exécuté à chaque commit, bloque le merge si échec.
Benchmarks standards + Golden Dataset : HotpotQA pour la comparaison académique + 200-500 exemples spécifiques à votre domaine pour la validation business.
A/B testing en production : La seule vraie mesure de performance. Déployez progressivement (10% → 50% → 100%), mesurez sur 2-4 semaines, décidez avec p-value <0.05.
Erreurs fatales à éviter :
- Se concentrer uniquement sur l’accuracy (ignorer latence, coûts, satisfaction)
- Tester en local mais pas en production (edge cases manquants)
- Pas de regression testing (chaque amélioration casse autre chose)
- Dataset de test trop petit (<50 exemples)
- Ignorer les coûts jusqu’au déploiement
ROI de l’évaluation : Investir 20% de votre temps dev en évaluation vous fait économiser 80% du temps de debugging production. C’est le meilleur investissement possible.
Prochaine étape : Dans l’article suivant Déploiement en Production, nous verrons comment déployer votre agent évalué dans une architecture scalable avec CI/CD, monitoring et sécurité.
Points clés à retenir :
- Évaluation = 4 dimensions obligatoires (qualité, performance, coûts, fiabilité)
- Framework pytest + golden dataset = baseline
- A/B testing en production = gold standard
- Outils : LangSmith (LangChain), RAGAS (RAG), TruLens (monitoring)
- Benchmarks : HotpotQA, FEVER, HumanEval
- P95/P99 > moyenne, coûts = métrique critique, monitoring ≠ évaluation
Les agents qui réussissent en production sont ceux qui sont rigoureusement évalués à chaque étape. Commencez simple (20 test cases + pytest), puis enrichissez progressivement. L’évaluation n’est pas un sprint, c’est un marathon.
Navigation
← Retour à la série Agents IA | Module suivant : Déploiement en Production →