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.
Pipeline d'Évaluation RAG Automatisé : Détectez les Régressions Avant vos Utilisateurs
Guide complet pour construire un pipeline d'évaluation RAG automatisé : golden datasets, métriques RAGAS, détection de régressions, alerting et intégration CI/CD avec GitHub Actions.
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.