Évaluation des Systèmes RAG : Métriques et Méthodologies
Guide complet pour mesurer les performances RAG : métriques de récupération, qualité de génération, évaluation de bout en bout et frameworks de tests automatisés.
- Auteur
- Équipe de Recherche Ailog
- Date de publication
- Temps de lecture
- 12 min de lecture
- Niveau
- advanced
Pourquoi l'Évaluation est Importante
Sans mesure, vous ne pouvez pas : • Savoir si les changements améliorent les performances • Identifier les modes de défaillance • Optimiser les hyperparamètres • Justifier les coûts aux parties prenantes • Respecter les SLA de qualité
Point clé : Le RAG a plusieurs composants (récupération, génération), chacun nécessitant une évaluation.
Niveaux d'Évaluation
Au Niveau des Composants
Évaluer les parties individuelles : • Qualité de la récupération • Qualité de la génération • Efficacité du découpage
De Bout en Bout
Évaluer le pipeline complet : • Exactitude de la réponse • Satisfaction utilisateur • Taux d'achèvement de tâche
Les Deux Sont Nécessaires
Les métriques de composants diagnostiquent les problèmes. Les métriques de bout en bout mesurent l'impact business.
Métriques de Récupération
Precision@k
Proportion de documents récupérés qui sont pertinents.
``python def precision_at_k(retrieved, relevant, k): """ retrieved: List of retrieved document IDs relevant: Set of relevant document IDs k: Number of top results to consider """ top_k = set(retrieved[:k]) relevant_retrieved = top_k & relevant
return len(relevant_retrieved) / k if k > 0 else 0 `
Exemple : ` Retrieved top 5: [doc1, doc2, doc3, doc4, doc5] Relevant: {doc1, doc3, doc8}
Precision@5 = 2/5 = 0.4 `
Interprétation : • Plus élevé est mieux • Mesure la précision • Ne tient pas compte du rappel
Recall@k
Proportion de documents pertinents qui ont été récupérés.
`python def recall_at_k(retrieved, relevant, k): """ What fraction of relevant docs did we find? """ top_k = set(retrieved[:k]) relevant_retrieved = top_k & relevant
return len(relevant_retrieved) / len(relevant) if relevant else 0 `
Exemple : ` Retrieved top 5: [doc1, doc2, doc3, doc4, doc5] Relevant: {doc1, doc3, doc8}
Recall@5 = 2/3 ≈ 0.67 `
Interprétation : • Plus élevé est mieux • Mesure la couverture • Plus difficile à optimiser que la précision
F1@k
Moyenne harmonique de la précision et du rappel.
`python def f1_at_k(retrieved, relevant, k): p = precision_at_k(retrieved, relevant, k) r = recall_at_k(retrieved, relevant, k)
if p + r == 0: return 0
return 2 (p r) / (p + r) `
Utiliser quand : • Besoin d'équilibrer précision et rappel • Métrique unique pour l'optimisation
Mean Reciprocal Rank (MRR)
Moyenne des rangs réciproques du premier résultat pertinent.
`python def reciprocal_rank(retrieved, relevant): """ Rank of first relevant document """ for i, doc_id in enumerate(retrieved, 1): if doc_id in relevant: return 1 / i return 0
def mrr(queries_results, queries_relevant): """ Average across multiple queries """ rr_scores = [ reciprocal_rank(retrieved, relevant) for retrieved, relevant in zip(queries_results, queries_relevant) ]
return sum(rr_scores) / len(rr_scores) `
Exemple : ` Query 1: First relevant at position 2 → RR = 1/2 = 0.5 Query 2: First relevant at position 1 → RR = 1/1 = 1.0 Query 3: First relevant at position 5 → RR = 1/5 = 0.2
MRR = (0.5 + 1.0 + 0.2) / 3 = 0.57 `
Interprétation : • Met l'accent sur la qualité du classement • Ne s'intéresse qu'au premier résultat pertinent • Bon pour les questions-réponses
NDCG@k (Normalized Discounted Cumulative Gain)
Prend en compte la pertinence graduée et la position.
`python import numpy as np from sklearn.metrics import ndcg_score
def calculate_ndcg(retrieved, relevance_scores, k): """ relevance_scores: Dict mapping doc_id to relevance (0-3 typical) """ Get scores for retrieved docs scores = [relevance_scores.get(doc_id, 0) for doc_id in retrieved[:k]]
Calculate ideal ranking (best possible) ideal_scores = sorted(relevance_scores.values(), reverse=True)[:k]
NDCG return ndcg_score([ideal_scores], [scores]) `
Exemple : ` Retrieved: [doc1, doc2, doc3] Scores: [2, 3, 1] (votre système) Ideal: [3, 2, 1] (classement parfait)
NDCG calcule à quel point vous êtes proche de l'idéal `
Utiliser quand : • Plusieurs niveaux de pertinence (pas seulement binaire) • La position compte (premier résultat plus important) • Recherche de recherche/entreprise
Hit Rate@k
Avons-nous récupéré au moins un document pertinent ?
`python def hit_rate_at_k(retrieved, relevant, k): top_k = set(retrieved[:k]) return 1 if len(top_k & relevant) > 0 else 0 `
Utiliser pour : • Viabilité minimale (avons-nous obtenu quelque chose d'utile ?) • Agrégation entre requêtes pour le taux de succès global
Métriques de Génération
Fidélité / Ancrage
La réponse est-elle soutenue par le contexte récupéré ?
LLM-as-Judge : `python def evaluate_faithfulness(answer, context, llm): prompt = f"""Cette réponse est-elle fidèle au contexte ? Réponds seulement oui ou non.
Contexte: {context}
Réponse: {answer}
La réponse est-elle soutenue par le contexte ?"""
response = llm.generate(prompt, max_tokens=5) return 1 if 'yes' in response.lower() else 0 `
Pourquoi c'est important : • Détecte les hallucinations • Garantit que les réponses sont ancrées dans les faits • Critique pour les applications à enjeux élevés
Pertinence de la Réponse
La réponse aborde-t-elle la question ?
`python def evaluate_relevance(question, answer, llm): prompt = f"""Cette réponse aborde-t-elle la question ? Note de 1-5.
Question: {question}
Réponse: {answer}
Pertinence (1-5):"""
score = int(llm.generate(prompt, max_tokens=5)) return score / 5 Normaliser à 0-1 `
Précision du Contexte
Le contexte récupéré est-il pertinent ?
`python def context_precision(retrieved_chunks, question, llm): """ Are the retrieved chunks relevant to the question? """ relevant_count = 0
for chunk in retrieved_chunks: prompt = f"""Ce contexte est-il pertinent pour la question ?
Question: {question}
Contexte: {chunk}
Pertinent ? (yes/no)"""
response = llm.generate(prompt, max_tokens=5) if 'yes' in response.lower(): relevant_count += 1
return relevant_count / len(retrieved_chunks) `
Rappel du Contexte
Toutes les informations nécessaires sont-elles dans le contexte récupéré ?
`python def context_recall(ground_truth_answer, retrieved_context, llm): """ Does the context contain all info needed for the ground truth answer? """ prompt = f"""Cette réponse peut-elle être dérivée du contexte ?
Contexte: {retrieved_context}
Réponse: {ground_truth_answer}
Toutes les informations sont-elles présentes ? (yes/no)"""
response = llm.generate(prompt, max_tokens=5) return 1 if 'yes' in response.lower() else 0 `
Frameworks d'Évaluation Automatisés
RAGAS
`python from ragas import evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_precision, context_recall )
Prepare dataset dataset = { 'question': [q1, q2, q3], 'answer': [a1, a2, a3], 'contexts': [c1, c2, c3], List of retrieved chunks 'ground_truth': [gt1, gt2, gt3] }
Evaluate result = evaluate( dataset, metrics=[ faithfulness, answer_relevancy, context_precision, context_recall ] )
print(result) { 'faithfulness': 0.92, 'answer_relevancy': 0.87, 'context_precision': 0.81, 'context_recall': 0.89 } `
TruLens
`python from trulens_eval import TruChain, Feedback, Tru
Initialize tru = Tru()
Define feedback functions f_groundedness = Feedback(groundedness_llm).on_output() f_answer_relevance = Feedback(answer_relevance_llm).on_input_output() f_context_relevance = Feedback(context_relevance_llm).on_input()
Wrap RAG chain tru_rag = TruChain( rag_chain, app_id='my_rag_v1', feedbacks=[f_groundedness, f_answer_relevance, f_context_relevance] )
Use normally - metrics auto-collected result = tru_rag.run(query)
View dashboard tru.run_dashboard() `
DeepEval
`python from deepeval import evaluate from deepeval.metrics import HallucinationMetric, AnswerRelevancyMetric from deepeval.test_case import LLMTestCase
Create test case test_case = LLMTestCase( input="What is the capital of France?", actual_output="The capital of France is Paris.", context=["France is a country in Europe.", "Paris is the capital of France."] )
Define metrics metrics = [ HallucinationMetric(threshold=0.9), AnswerRelevancyMetric(threshold=0.8) ]
Evaluate evaluate([test_case], metrics) `
Créer un Ensemble de Tests
Curation Manuelle
`python test_cases = [ { 'query': 'How do I reset my password?', 'ground_truth_answer': 'Click "Forgot Password" on the login page...', 'relevant_docs': {'doc_123', 'doc_456'}, 'difficulty': 'easy' }, { 'query': 'What are the differences between plans?', 'ground_truth_answer': 'Premium includes...', 'relevant_docs': {'doc_789'}, 'difficulty': 'medium' }, ... more test cases ] `
Bonnes pratiques : • Types de requêtes diversifiés (simple, complexe, ambigu) • Différents niveaux de difficulté • Vraies requêtes utilisateurs • Cas limites • Minimum 50-100 cas de test
Génération Synthétique
`python def generate_test_cases(documents, llm, num_cases=50): test_cases = []
for doc in random.sample(documents, num_cases): prompt = f"""Génère une question à laquelle on peut répondre en utilisant ce document.
Document: {doc}
Question:"""
question = llm.generate(prompt)
prompt_answer = f"""Réponds à cette question en utilisant le document.
Document: {doc}
Question: {question}
Réponse:"""
answer = llm.generate(prompt_answer)
test_cases.append({ 'query': question, 'ground_truth_answer': answer, 'relevant_docs': {doc['id']}, 'source': 'synthetic' })
return test_cases `
Extraction de Requêtes Utilisateurs
`python Extract from logs def extract_queries_from_logs(log_file, sample_size=100): Parse logs queries = parse_query_logs(log_file)
Filter for quality queries = [q for q in queries if len(q.split()) >= 3] Not too short
Sample diverse queries return random.sample(queries, sample_size) `
Tests A/B
Configuration d'Expérience
`python class ABTest: def __init__(self, control_system, treatment_system): self.control = control_system self.treatment = treatment_system self.results = {'control': [], 'treatment': []}
def run_query(self, query, user_id): Assign to variant (50/50 split) variant = 'treatment' if hash(user_id) % 2 else 'control' system = self.treatment if variant == 'treatment' else self.control
Get answer answer = system.query(query)
Log result self.results[variant].append({ 'query': query, 'answer': answer, 'timestamp': time.time() })
return answer, variant
def analyze(self): Compare metrics between variants control_metrics = calculate_metrics(self.results['control']) treatment_metrics = calculate_metrics(self.results['treatment'])
return { 'control': control_metrics, 'treatment': treatment_metrics, 'lift': calculate_lift(control_metrics, treatment_metrics) } `
Métriques à Suivre
Qualité : • Exactitude de la réponse • Notes utilisateurs (pouce en haut/bas) • Taux de questions de suivi
Engagement : • Durée de session • Requêtes par session • Taux d'achèvement de tâche
Business : • Taux de conversion • Déflexion de tickets de support • Satisfaction client (CSAT)
Évaluation Continue
Pipeline de Surveillance
`python class RAGMonitor: def __init__(self, rag_system, test_set): self.system = rag_system self.test_set = test_set self.history = []
def run_evaluation(self): results = []
for test_case in self.test_set: Run RAG answer, contexts = self.system.query(test_case['query'])
Calculate metrics metrics = { 'precision@5': precision_at_k(contexts, test_case['relevant_docs'], 5), 'faithfulness': evaluate_faithfulness(answer, contexts), 'relevance': evaluate_relevance(test_case['query'], answer) }
results.append(metrics)
Aggregate aggregated = aggregate_metrics(results)
Save history self.history.append({ 'timestamp': time.time(), 'metrics': aggregated })
Alert if degradation if self.detect_degradation(aggregated): self.send_alert(aggregated)
return aggregated
def detect_degradation(self, current_metrics, threshold=0.05): if not self.history: return False
previous = self.history[-1]['metrics']
for metric, value in current_metrics.items(): if value < previous[metric] - threshold: return True
return False `
Évaluation Planifiée
`python import schedule
def daily_evaluation(): monitor = RAGMonitor(rag_system, test_set) results = monitor.run_evaluation()
Log to monitoring system metrics_logger.log(results)
Update dashboard update_dashboard(results)
Run daily at 2 AM schedule.every().day.at("02:00").do(daily_evaluation)
while True: schedule.run_pending() time.sleep(60) `
Évaluation Humaine
Interface de Notation
`python def collect_human_ratings(test_cases, rag_system): ratings = []
for test_case in test_cases: Generate answer answer, contexts = rag_system.query(test_case['query'])
Show to human rater print(f"Query: {test_case['query']}") print(f"Answer: {answer}") print(f"Contexts: {contexts}")
Collect ratings correctness = int(input("Correctness (1-5): ")) completeness = int(input("Completeness (1-5): ")) conciseness = int(input("Conciseness (1-5): "))
ratings.append({ 'query': test_case['query'], 'correctness': correctness, 'completeness': completeness, 'conciseness': conciseness })
return ratings `
Fiabilité Inter-Évaluateurs
`python from sklearn.metrics import cohen_kappa_score
def calculate_agreement(rater1_scores, rater2_scores): """ Cohen's Kappa for inter-rater agreement """ kappa = cohen_kappa_score(rater1_scores, rater2_scores)
if kappa > 0.8: return "Strong agreement" elif kappa > 0.6: return "Moderate agreement" else: return "Weak agreement - review rating criteria" `
Coût de l'Évaluation
Coût des Métriques Basées sur LLM
`python def estimate_evaluation_cost(num_test_cases, metrics_per_case=3): GPT-4 pricing (example) cost_per_1k_tokens = 0.03 Input tokens_per_evaluation = 500 Typical
total_evaluations = num_test_cases metrics_per_case total_tokens = total_evaluations tokens_per_evaluation
cost = (total_tokens / 1000) * cost_per_1k_tokens
return cost
Example cost = estimate_evaluation_cost(100) 4,50 $ pour 100 cas de test ``
Optimisation • Mettre en cache les évaluations pour les sorties inchangées • Utiliser des modèles plus petits (GPT-3.5 vs GPT-4) pour certaines métriques • Évaluations par lots • Exécuter moins fréquemment (quotidien vs à chaque PR)
Bonnes Pratiques Ensemble de tests diversifié : Couvrir tous les types de requêtes et niveaux de difficulté Suivre dans le temps : Surveiller les métriques à mesure que le système évolue Composant + E2E : Évaluer à la fois les parties et le tout Vraies requêtes : Inclure les vraies requêtes utilisateurs dans l'ensemble de tests Automatiser : Exécuter l'évaluation à chaque changement Validation humaine : Révision humaine périodique des métriques automatisées Métriques business : Connecter la qualité aux résultats business
Prochaines Étapes
Avec l'évaluation en place, l'accent se déplace vers le déploiement des systèmes RAG en production. Le prochain guide couvre le déploiement en production, la mise à l'échelle, la surveillance et les considérations opérationnelles.