Tester un Systeme RAG : La Methodologie en 5 Etapes que Google Utilise (Et Vous Devriez Aussi)
Methodologie complete pour tester un systeme RAG en 5 etapes : golden dataset, tests unitaires retrieval, tests generation, evaluation end-to-end RAGAS, A/B testing production.
TL;DR
Tester un systeme RAG ne se resume pas a "est-ce que la reponse a l'air correcte". Google, Anthropic et les meilleures equipes IA suivent une methodologie en 5 etapes : (1) construire un golden dataset, (2) tester le retrieval unitairement, (3) tester la generation unitairement, (4) evaluation end-to-end avec RAGAS, (5) A/B testing en production. Ce guide detaille chaque etape avec du code, des metriques concretes et les pieges a eviter.
Pourquoi 90% des systemes RAG echouent en production
La plupart des equipes testent leur RAG en posant quelques questions a la main. C'est comme tester une voiture en regardant si elle demarre. Les vrais problemes apparaissent apres le deploiement :
| Probleme | Frequence | Impact |
|---|---|---|
| Hallucinations non detectees | 15-30% des reponses | Perte de confiance utilisateur |
| Retrieval hors-sujet | 20-40% des requetes | Reponses non pertinentes |
| Regression apres mise a jour | A chaque deploiement | Degradation silencieuse |
| Biais de confirmation | Permanent | Faux sentiment de qualite |
La solution : une methodologie de test systematique, automatisee et reproductible.
Etape 1 : Construire un Golden Dataset
Le golden dataset est votre source de verite. C'est un ensemble de paires question-reponse-contexte valide par des experts humains.
Structure du golden dataset
DEVELOPERjson{ "id": "GD-001", "question": "Quels sont les delais de livraison en France ?", "expected_answer": "Les delais de livraison en France metropolitaine sont de 2 a 5 jours ouvrables pour la livraison standard et 24h pour la livraison express.", "expected_contexts": [ "doc_livraison_france.md#section-delais", "faq_expedition.md#question-12" ], "category": "logistics", "difficulty": "easy", "metadata": { "created_by": "support_team", "created_at": "2026-01-15", "last_validated": "2026-03-01" } }
Combien de paires faut-il ?
| Taille du corpus | Golden dataset recommande | Couverture |
|---|---|---|
| < 100 documents | 50 paires | 1 paire / 2 docs |
| 100-1000 docs | 100-200 paires | Principales categories |
| 1000-10000 docs | 200-500 paires | Echantillon stratifie |
| > 10000 docs | 500-1000 paires | Categories + edge cases |
Generer un golden dataset semi-automatiquement
DEVELOPERpythonfrom openai import OpenAI import json client = OpenAI() def generate_golden_pairs(documents: list[dict], n_pairs: int = 5) -> list[dict]: """Genere des paires Q&A a partir de documents reels.""" golden_pairs = [] for doc in documents: prompt = f"""A partir du document suivant, genere {n_pairs} paires question-reponse. Document: {doc['content'][:3000]} Regles: - Questions variees (factuelles, comparatives, oui/non) - Reponses basees UNIQUEMENT sur le document - Inclure la section source exacte Format JSON: [{{"question": "...", "answer": "...", "source_section": "..."}}]""" response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], response_format={"type": "json_object"} ) pairs = json.loads(response.choices[0].message.content) for pair in pairs.get("pairs", []): golden_pairs.append({ "question": pair["question"], "expected_answer": pair["answer"], "expected_contexts": [f"{doc['id']}#{pair['source_section']}"], "category": doc.get("category", "general"), "auto_generated": True, "validated": False # A valider par un humain }) return golden_pairs # Utilisation documents = [ {"id": "doc_001", "content": "...", "category": "product"}, {"id": "doc_002", "content": "...", "category": "support"}, ] golden = generate_golden_pairs(documents, n_pairs=5) print(f"Generated {len(golden)} pairs - REVIEW REQUIRED")
Validation humaine : la regle des 3
Chaque paire du golden dataset doit etre validee par au moins 1 personne. Pour les systemes critiques (medical, juridique), utilisez la regle des 3 : 3 validateurs independants, majorite requise.
Etape 2 : Tester le Retrieval unitairement
Le retrieval est le composant le plus critique. Si les mauvais documents sont recuperes, aucun LLM ne pourra generer une bonne reponse.
Metriques cles
| Metrique | Formule | Objectif | Interpretation |
|---|---|---|---|
| Recall@k | Documents pertinents dans top-k / Total pertinents | > 0.85 | "On trouve bien les bons docs" |
| Precision@k | Documents pertinents dans top-k / k | > 0.60 | "Les docs trouves sont bons" |
| MRR | 1 / rang du premier doc pertinent | > 0.70 | "Le bon doc est en haut" |
| nDCG@k | Score DCG normalise | > 0.75 | "Le classement est bon" |
| Hit Rate | Requetes avec au moins 1 doc pertinent / Total | > 0.90 | "On trouve toujours quelque chose" |
Code de test retrieval
DEVELOPERpythonimport numpy as np from dataclasses import dataclass @dataclass class RetrievalResult: query: str retrieved_docs: list[str] relevant_docs: list[str] def recall_at_k(result: RetrievalResult, k: int = 10) -> float: """Recall@k - proportion de documents pertinents retrouves.""" retrieved_set = set(result.retrieved_docs[:k]) relevant_set = set(result.relevant_docs) if not relevant_set: return 1.0 return len(retrieved_set & relevant_set) / len(relevant_set) def precision_at_k(result: RetrievalResult, k: int = 10) -> float: """Precision@k - proportion de documents retrouves qui sont pertinents.""" retrieved = result.retrieved_docs[:k] relevant_set = set(result.relevant_docs) if not retrieved: return 0.0 return sum(1 for doc in retrieved if doc in relevant_set) / len(retrieved) def mrr(result: RetrievalResult) -> float: """Mean Reciprocal Rank - inverse du rang du premier resultat pertinent.""" relevant_set = set(result.relevant_docs) for i, doc in enumerate(result.retrieved_docs): if doc in relevant_set: return 1.0 / (i + 1) return 0.0 def evaluate_retrieval(golden_dataset: list[dict], retriever) -> dict: """Evalue le retrieval sur le golden dataset complet.""" metrics = {"recall@5": [], "recall@10": [], "precision@5": [], "mrr": [], "hit_rate": []} for item in golden_dataset: retrieved = retriever.search(item["question"], top_k=10) result = RetrievalResult( query=item["question"], retrieved_docs=[doc.id for doc in retrieved], relevant_docs=item["expected_contexts"] ) metrics["recall@5"].append(recall_at_k(result, 5)) metrics["recall@10"].append(recall_at_k(result, 10)) metrics["precision@5"].append(precision_at_k(result, 5)) metrics["mrr"].append(mrr(result)) metrics["hit_rate"].append(1.0 if recall_at_k(result, 10) > 0 else 0.0) return {k: np.mean(v) for k, v in metrics.items()}
Seuils de qualite
| Metrique | Minimum acceptable | Bon | Excellent |
|---|---|---|---|
| Recall@10 | 0.75 | 0.85 | 0.95+ |
| Precision@5 | 0.50 | 0.65 | 0.80+ |
| MRR | 0.60 | 0.75 | 0.85+ |
| Hit Rate | 0.80 | 0.90 | 0.95+ |
Etape 3 : Tester la Generation unitairement
Meme avec un retrieval parfait, le LLM peut halluciner, ignorer le contexte, ou generer des reponses hors-sujet.
Metriques de generation
| Metrique | Mesure | Methode |
|---|---|---|
| Faithfulness | La reponse est fidele au contexte | LLM-as-judge |
| Answer Relevancy | La reponse repond a la question | LLM-as-judge |
| Answer Correctness | La reponse correspond au golden | Cosine similarity + LLM |
| Harmfulness | La reponse est-elle dangereuse | Classification |
Test de faithfulness avec LLM-as-judge
DEVELOPERpythonfrom openai import OpenAI client = OpenAI() def evaluate_faithfulness(context: str, answer: str) -> dict: """Evalue si la reponse est fidele au contexte fourni.""" prompt = f"""Evalue si la reponse suivante est fidele au contexte fourni. CONTEXTE: {context} REPONSE: {answer} Analyse chaque affirmation de la reponse : 1. L'affirmation est-elle supportee par le contexte ? (supported/unsupported) 2. Y a-t-il des informations inventees ? (hallucination: yes/no) Score de fidelite (0.0 a 1.0) = affirmations supportees / total affirmations Reponds en JSON : {{"claims": [{{"text": "...", "supported": true/false}}], "faithfulness_score": 0.X, "hallucinations": ["..."]}}""" response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], response_format={"type": "json_object"} ) return json.loads(response.choices[0].message.content) # Exemple result = evaluate_faithfulness( context="Ailog est une plateforme RAG-as-a-Service francaise, hebergee en France.", answer="Ailog est une plateforme RAG americaine hebergee aux Etats-Unis." ) # faithfulness_score: 0.0 (hallucination detectee)
Test de relevance
DEVELOPERpythondef evaluate_relevancy(question: str, answer: str) -> dict: """Evalue si la reponse est pertinente par rapport a la question.""" prompt = f"""Evalue la pertinence de cette reponse par rapport a la question. QUESTION: {question} REPONSE: {answer} Criteres : 1. La reponse repond-elle directement a la question ? (0-1) 2. La reponse contient-elle des informations hors-sujet ? (0-1) 3. La reponse est-elle complete ? (0-1) Score de pertinence (0.0 a 1.0) = moyenne des criteres. Reponds en JSON : {{"directness": 0.X, "focus": 0.X, "completeness": 0.X, "relevancy_score": 0.X, "explanation": "..."}}""" response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], response_format={"type": "json_object"} ) return json.loads(response.choices[0].message.content)
Etape 4 : Evaluation End-to-End avec RAGAS
RAGAS (Retrieval Augmented Generation Assessment) est le framework de reference pour evaluer un pipeline RAG complet.
Installation et utilisation
DEVELOPERpythonfrom ragas import evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_precision, context_recall, answer_correctness ) from datasets import Dataset # Preparer les donnees data = { "question": [], "answer": [], "contexts": [], "ground_truth": [] } for item in golden_dataset: # Executer le pipeline RAG rag_result = rag_pipeline.query(item["question"]) data["question"].append(item["question"]) data["answer"].append(rag_result.answer) data["contexts"].append(rag_result.contexts) data["ground_truth"].append(item["expected_answer"]) dataset = Dataset.from_dict(data) # Evaluation RAGAS results = evaluate( dataset, metrics=[ faithfulness, answer_relevancy, context_precision, context_recall, answer_correctness ] ) print(results) # {'faithfulness': 0.87, 'answer_relevancy': 0.91, # 'context_precision': 0.78, 'context_recall': 0.85, # 'answer_correctness': 0.82}
Comparaison des frameworks d'evaluation
| Framework | Metriques | LLM-as-Judge | Open-Source | Facilite | Production-ready |
|---|---|---|---|---|---|
| RAGAS | 8+ | Oui | Oui | Facile | Oui |
| DeepEval | 14+ | Oui | Oui | Moyen | Oui |
| TruLens | 6+ | Oui | Oui | Facile | Oui |
| Phoenix (Arize) | 10+ | Oui | Oui | Moyen | Oui |
| LangSmith | Custom | Oui | Non | Facile | Oui |
| Braintrust | Custom | Oui | Non | Facile | Oui |
RAGAS vs DeepEval : comparaison detaillee
| Critere | RAGAS | DeepEval |
|---|---|---|
| Metriques RAG | Excellentes | Excellentes |
| Metriques custom | Limitees | Flexibles |
| CI/CD integration | Via Python | Plugin pytest natif |
| Dashboard | Non (export JSON) | Oui (Confident AI) |
| Cout LLM | ~$0.05/evaluation | ~$0.08/evaluation |
| Communaute | Large | Croissante |
DeepEval : alternative solide
DEVELOPERpythonfrom deepeval import evaluate from deepeval.metrics import ( FaithfulnessMetric, AnswerRelevancyMetric, ContextualRelevancyMetric ) from deepeval.test_case import LLMTestCase # Creer les cas de test test_cases = [] for item in golden_dataset: rag_result = rag_pipeline.query(item["question"]) test_cases.append( LLMTestCase( input=item["question"], actual_output=rag_result.answer, expected_output=item["expected_answer"], retrieval_context=rag_result.contexts ) ) # Metriques faithfulness = FaithfulnessMetric(threshold=0.7) relevancy = AnswerRelevancyMetric(threshold=0.7) context = ContextualRelevancyMetric(threshold=0.7) # Evaluation results = evaluate(test_cases, [faithfulness, relevancy, context])
Etape 5 : A/B Testing en Production
Les metriques offline ne suffisent pas. Le vrai test, c'est la production.
Metriques de production
| Metrique | Source | Objectif |
|---|---|---|
| Taux de clic source | Frontend | > 30% |
| Thumbs up/down ratio | Frontend | > 80% up |
| Taux d'escalade humain | Backend | < 20% |
| Temps de session moyen | Analytics | Stable ou croissant |
| Taux de retour | Analytics | > 40% |
| Requetes sans reponse | Backend | < 5% |
Implementation A/B testing
DEVELOPERpythonimport random import hashlib from datetime import datetime class RAGABTest: def __init__(self, variant_a, variant_b, traffic_split: float = 0.5): self.variant_a = variant_a # Pipeline RAG v1 self.variant_b = variant_b # Pipeline RAG v2 self.traffic_split = traffic_split self.results = {"a": [], "b": []} def get_variant(self, user_id: str) -> str: """Attribution deterministe basee sur le user_id.""" hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16) return "a" if (hash_val % 100) < (self.traffic_split * 100) else "b" async def query(self, question: str, user_id: str) -> dict: variant = self.get_variant(user_id) pipeline = self.variant_a if variant == "a" else self.variant_b start_time = datetime.now() result = await pipeline.query(question) latency = (datetime.now() - start_time).total_seconds() # Log pour analyse self.results[variant].append({ "question": question, "answer": result.answer, "latency": latency, "variant": variant, "timestamp": datetime.now().isoformat() }) return { "answer": result.answer, "sources": result.sources, "variant": variant # Pour le tracking frontend } def get_stats(self) -> dict: """Statistiques comparatives.""" stats = {} for variant in ["a", "b"]: data = self.results[variant] if data: latencies = [d["latency"] for d in data] stats[variant] = { "count": len(data), "avg_latency": sum(latencies) / len(latencies), "p95_latency": sorted(latencies)[int(len(latencies) * 0.95)] } return stats
Duree minimale d'un A/B test
| Volume de requetes/jour | Duree minimale | Echantillon necessaire |
|---|---|---|
| < 100 | 4 semaines | ~2800 requetes |
| 100-1000 | 2 semaines | ~7000 requetes |
| 1000-10000 | 1 semaine | ~7000 requetes |
| > 10000 | 3-5 jours | ~15000 requetes |
Integration CI/CD
GitHub Actions pour RAG testing
DEVELOPERyaml# .github/workflows/rag-tests.yml name: RAG Quality Tests on: push: branches: [develop, main] paths: - 'backend/rag/**' - 'backend/prompts/**' - 'tests/golden_dataset.json' jobs: rag-evaluation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: pip install ragas deepeval openai - name: Run retrieval tests run: python tests/test_retrieval.py env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} QDRANT_URL: ${{ secrets.QDRANT_TEST_URL }} - name: Run RAGAS evaluation run: python tests/test_ragas.py --threshold 0.80 - name: Upload results uses: actions/upload-artifact@v4 with: name: rag-evaluation-results path: tests/results/ - name: Check thresholds run: | python -c " import json with open('tests/results/ragas_scores.json') as f: scores = json.load(f) assert scores['faithfulness'] >= 0.80, f'Faithfulness too low: {scores[\"faithfulness\"]}' assert scores['answer_relevancy'] >= 0.75, f'Relevancy too low: {scores[\"answer_relevancy\"]}' assert scores['context_recall'] >= 0.80, f'Context recall too low: {scores[\"context_recall\"]}' print('All RAG quality thresholds passed') "
Alertes de regression
DEVELOPERpythondef check_regression(current_scores: dict, baseline_scores: dict, tolerance: float = 0.05) -> list[str]: """Detecte les regressions par rapport au baseline.""" alerts = [] for metric, current in current_scores.items(): baseline = baseline_scores.get(metric, 0) if current < baseline - tolerance: drop = baseline - current alerts.append( f"REGRESSION: {metric} dropped by {drop:.3f} " f"(baseline: {baseline:.3f}, current: {current:.3f})" ) return alerts
Les 7 pieges les plus courants
| Piege | Impact | Solution |
|---|---|---|
| Golden dataset trop petit | Faux sentiment de qualite | Minimum 100 paires, categories equilibrees |
| Pas de test de regression | Degradation silencieuse | CI/CD avec seuils automatiques |
| Evaluer uniquement la generation | Manque le retrieval defaillant | Tester retrieval ET generation |
| Utiliser le meme LLM pour evaluer | Biais de confirmation | Utiliser un LLM different pour le judge |
| Ignorer les edge cases | Echecs en production | Inclure 20% d'edge cases dans le golden |
| Metriques sans contexte business | Optimiser le mauvais objectif | Lier aux KPI business (satisfaction, escalade) |
| Test one-shot | Variabilite ignoree | Executer 3x, prendre la mediane |
Notre approche chez Ailog
Chez Ailog, chaque pipeline RAG client est teste automatiquement :
- Golden dataset : genere semi-automatiquement a partir des documents du client, valide par notre equipe
- Tests retrieval : recall@10 > 0.85 obligatoire avant deploiement
- Tests RAGAS : faithfulness > 0.80, relevancy > 0.75
- Monitoring production : alertes automatiques si thumbs-down > 20%
Decouvrez comment nous evaluons nos systemes RAG et comment nous monitorons en production.
FAQ
Conclusion
Tester un systeme RAG n'est pas optionnel - c'est ce qui separe un prototype d'un produit. La methodologie en 5 etapes fonctionne quelle que soit l'echelle :
- Golden dataset : votre source de verite
- Tests retrieval : le composant le plus critique
- Tests generation : faithfulness et relevance
- RAGAS end-to-end : le bilan complet
- A/B testing : la validation finale
Commencez petit (50 paires, RAGAS basique), puis automatisez. L'investissement se rentabilise des le premier bug detecte avant la production.
Vous voulez un systeme RAG teste et monitore automatiquement ? Essayez Ailog - nos pipelines incluent l'evaluation automatique et le monitoring en temps reel.
Tags
Articles connexes
Évaluer un système RAG : Métriques et méthodologies
Guide complet pour mesurer la performance de votre RAG : faithfulness, relevancy, recall, et frameworks d'évaluation automatisée.
Latence RAG < 500ms : Le Guide Ultime pour des Réponses à la Vitesse de l'Éclair
Guide technique exhaustif pour optimiser la latence de votre pipeline RAG sous les 500ms. Decomposition du temps, techniques d'optimisation, caching, streaming et benchmarks.
Optimisation de la Fenêtre de Contexte : Gérer les Limites de Tokens
Stratégies pour Intégrer Plus d'Informations dans des Fenêtres de Contexte Limitées : Compression, Résumé, Sélection Intelligente et Techniques de Gestion de Fenêtre.