GuideAvancé

Évaluation des Systèmes RAG : Métriques et Méthodologies

15 février 2025
12 min de lecture
Équipe de Recherche Ailog

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.

DEVELOPERpython
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.

DEVELOPERpython
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.

DEVELOPERpython
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.

DEVELOPERpython
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.

DEVELOPERpython
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 ?

DEVELOPERpython
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 :

DEVELOPERpython
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 ?

DEVELOPERpython
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 ?

DEVELOPERpython
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é ?

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

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

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

DEVELOPERpython
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

  1. Ensemble de tests diversifié : Couvrir tous les types de requêtes et niveaux de difficulté
  2. Suivre dans le temps : Surveiller les métriques à mesure que le système évolue
  3. Composant + E2E : Évaluer à la fois les parties et le tout
  4. Vraies requêtes : Inclure les vraies requêtes utilisateurs dans l'ensemble de tests
  5. Automatiser : Exécuter l'évaluation à chaque changement
  6. Validation humaine : Révision humaine périodique des métriques automatisées
  7. 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

évaluationmétriquestestqualité

Articles connexes

Ailog Assistant

Ici pour vous aider

Salut ! Pose-moi des questions sur Ailog et comment intégrer votre RAG dans vos projets !