É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.
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.
DEVELOPERpythondef 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.
DEVELOPERpythondef 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.
DEVELOPERpythondef 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.
DEVELOPERpythondef 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.
DEVELOPERpythonimport 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 ?
DEVELOPERpythondef 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 :
DEVELOPERpythondef 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 ?
DEVELOPERpythondef 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 ?
DEVELOPERpythondef 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é ?
DEVELOPERpythondef 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
DEVELOPERpythonfrom 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
DEVELOPERpythonfrom 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
DEVELOPERpythonfrom 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
DEVELOPERpythontest_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
DEVELOPERpythondef 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
DEVELOPERpython# 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
DEVELOPERpythonclass 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
DEVELOPERpythonclass 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
DEVELOPERpythonimport 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
DEVELOPERpythondef 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
DEVELOPERpythonfrom 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
DEVELOPERpythondef 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.
Tags
Articles connexes
Évaluation automatique du RAG : nouveau framework atteint 95% de corrélation avec les jugements humains
Google Research introduit AutoRAGEval, un framework d'évaluation automatisé qui évalue fiablement la qualité du RAG sans annotation humaine.
Réduire la Latence RAG : De 2000ms à 200ms
RAG 10x Plus Rapide : Récupération Parallèle, Réponses en Streaming et Optimisations Architecturales pour une Latence Inférieure à 200ms.
Qdrant : Fonctionnalités Avancées de Recherche Vectorielle
Exploitez les fonctionnalités puissantes de Qdrant : indexation de payload, quantization, déploiement distribué pour des systèmes RAG haute performance.