Votre RAG fonctionne en local, mais êtes-vous prêt pour la prod ? Ce guide couvre les trois piliers d’un RAG production-ready : sécurité (ACL, isolation), évaluation (métriques, CI/CD) et observabilité (monitoring, alertes).
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Implémenter le filtrage ACL et le multi-tenant
- Ajouter les métadonnées de sécurité aux chunks
- Évaluer avec RAGAS (Recall, Faithfulness, Relevance)
- Intégrer l’évaluation en CI/CD
- Monitorer avec métriques et alertes
Sécurité : ACL et multi-tenant
Section intitulée « Sécurité : ACL et multi-tenant »Un RAG non sécurisé peut exposer des documents confidentiels à des utilisateurs non autorisés.
Le risque
Section intitulée « Le risque »Sans filtrage ACL, le retriever retourne les chunks les plus pertinents sans vérifier les droits. Un utilisateur peut voir des docs auxquels il n’a pas accès.
❌ Utilisateur "dev-junior" pose une question → Le RAG retourne un runbook confidentiel "SRE-only" → Fuite d'informationMétadonnées de sécurité
Section intitulée « Métadonnées de sécurité »Chaque chunk doit porter des métadonnées de gouvernance. Voici la structure complète — vous n’avez pas besoin de tous les champs dès le départ, mais prévoyez-les dans votre schéma :
# Structure d'un chunk avec métadonnées de sécuritéchunk = { "text": "Le contenu du chunk...", "metadata": { # === IDENTIFICATION === # D'où vient ce chunk ? Pour les citations et le debug. "source": "docs/runbook-kubernetes.md", "title": "Runbook Kubernetes", "section": "Déploiement",
# === TRAÇABILITÉ === # Quand a-t-il été indexé ? Pour invalider le cache si le doc change. "date": "2026-01-15", "author": "team-platform", "doc_version": "2026-02-10", # Date de dernière modification "hash": "sha256:a1b2c3d4e5...", # Hash du contenu pour détecter les changements
# === GOUVERNANCE / SÉCURITÉ === # Qui peut voir ce chunk ? Filtrage ACL obligatoire. "tenant": "org-acme", # Isolation multi-tenant "acl": ["team-platform", "sre"], # Groupes autorisés "sensitivity": "internal", # public | internal | confidential | restricted "product": "k8s-prod", "environment": "production" }}Champs prioritaires à implémenter en premier :
tenant— obligatoire en multi-tenantacl— obligatoire si accès différenciésource— pour les citationshash— pour savoir quand réindexer
| Champ | Usage | Exemple |
|---|---|---|
tenant | Isolation multi-tenant | org-acme, org-beta |
acl | Liste des groupes autorisés | ["sre", "team-platform"] |
sensitivity | Niveau de classification | internal, confidential |
doc_version | Invalidation de cache | 2026-02-10 |
hash | Détection de modification | sha256:... |
Filtrage au retrieval
Section intitulée « Filtrage au retrieval »Le filtrage ACL doit être appliqué avant l’injection dans le prompt, au niveau du retriever :
def secure_search(query: str, user: User, top_k: int = 50) -> list[Chunk]: """Recherche avec filtrage ACL."""
# Construire le filtre selon les droits de l'utilisateur acl_filter = { "tenant": user.tenant, # Isolation tenant "acl": {"$in": user.groups}, # Groupes autorisés "sensitivity": {"$in": get_allowed_sensitivity(user)} }
# Recherche filtrée results = retriever.search( query=query, filter=acl_filter, top_k=top_k )
return results
def get_allowed_sensitivity(user: User) -> list[str]: """Détermine les niveaux de sensibilité autorisés.""" if user.role == "admin": return ["public", "internal", "confidential", "restricted"] elif user.role == "employee": return ["public", "internal"] else: return ["public"]Multi-tenant : isolation des données
Section intitulée « Multi-tenant : isolation des données »En multi-tenant, chaque organisation ne doit voir que ses propres documents :
# ❌ DANGEREUX : pas d'isolationresults = retriever.search(query)
# ✅ SÉCURISÉ : isolation par tenantresults = retriever.search( query, filter={"tenant": user.tenant})Pour une isolation forte, utilisez des collections séparées :
# Collection par tenantcollection_name = f"docs_{user.tenant}"retriever = get_retriever(collection_name)Évaluation : mesurer la qualité
Section intitulée « Évaluation : mesurer la qualité »Un RAG sans métriques, c’est piloter à l’aveugle. Vous devez mesurer deux axes :
- Retrieval : est-ce que les bons documents sont retrouvés ?
- Génération : est-ce que la réponse est fidèle au contexte ?
Métriques de retrieval
Section intitulée « Métriques de retrieval »| Métrique | Ce qu’elle mesure | Cible | Formule |
|---|---|---|---|
| Recall@K | % de docs pertinents dans le top-K | > 85% | pertinents_trouvés / total_pertinents |
| Precision@K | % de top-K réellement pertinents | > 70% | pertinents_trouvés / K |
| MRR | Rang moyen du 1er doc pertinent | > 0.8 | 1 / rang_premier_pertinent |
Métriques de génération
Section intitulée « Métriques de génération »| Métrique | Ce qu’elle mesure | Comment |
|---|---|---|
| Faithfulness | Réponse fidèle au contexte (pas d’hallucination) | LLM-as-judge |
| Answer Relevance | Réponse pertinente vs question | LLM-as-judge |
| Groundedness | Chaque affirmation traçable | Citation matching |
Dataset de référence (golden dataset)
Section intitulée « Dataset de référence (golden dataset) »Pour des évaluations fiables, constituez un dataset annoté :
[ { "question": "Comment configurer SSL sur Nginx ?", "expected_sources": ["nginx-ssl.md", "tls-guide.md"], "expected_answer_contains": ["ssl_certificate", "listen 443"], "category": "configuration" }, { "question": "Quelle est la procédure de rollback sur staging ?", "expected_sources": ["runbook-staging.md"], "expected_answer_contains": ["kubectl rollout undo"], "category": "operations" }]Évaluation avec RAGAS
Section intitulée « Évaluation avec RAGAS »RAGAS est le framework standard pour évaluer les RAG. Il calcule automatiquement les métriques de qualité sur votre dataset de référence.
Ce que fait RAGAS :
- Compare les réponses générées aux réponses attendues
- Vérifie que les réponses sont fidèles au contexte (pas d’hallucination)
- Mesure si les bons chunks ont été retrouvés
from ragas import evaluatefrom ragas.metrics import ( faithfulness, # La réponse est-elle fidèle au contexte ? answer_relevancy, # La réponse répond-elle à la question ? context_precision, # Les meilleurs chunks sont-ils en haut du ranking ? context_recall # Tous les chunks pertinents ont-ils été trouvés ?)from datasets import Dataset
# 1. Préparer le dataset d'évaluation# Chaque clé est une liste avec un élément par question testéeeval_data = { "question": ["Comment configurer SSL ?", "Comment rollback staging ?"], "answer": [rag_response_1, rag_response_2], # Réponses générées par votre RAG "contexts": [[chunk1, chunk2], [chunk3]], # Chunks utilisés pour générer "ground_truth": ["Pour SSL...", "Pour rollback..."] # Réponses idéales (manuelles)}
# 2. Convertir en Dataset HuggingFacedataset = Dataset.from_dict(eval_data)
# 3. Exécuter l'évaluation# RAGAS utilise un LLM (GPT-4 par défaut) pour juger la qualitéresults = evaluate( dataset, metrics=[ faithfulness, answer_relevancy, context_precision, context_recall ])
# 4. Afficher les scores (0.0 à 1.0, plus haut = mieux)print(results)# {'faithfulness': 0.92, 'answer_relevancy': 0.88, 'context_precision': 0.75, 'context_recall': 0.85}Interprétation des scores :
| Métrique | < 0.7 | 0.7-0.85 | > 0.85 |
|---|---|---|---|
| faithfulness | ⚠️ Hallucinations fréquentes | ✅ Acceptable | 🌟 Excellent |
| context_recall | ⚠️ Chunks manquants | ✅ Acceptable | 🌟 Excellent |
Intégration CI/CD
Section intitulée « Intégration CI/CD »Automatisez l’évaluation avant chaque déploiement :
name: RAG Quality Gate
on: pull_request: paths: - 'src/rag/**' - 'docs/**'
jobs: evaluate: 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 -r requirements-eval.txt
- name: Run RAG evaluation env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | python scripts/eval_rag.py \ --dataset tests/golden_dataset.json \ --threshold-recall 0.85 \ --threshold-faithfulness 0.90
- name: Upload results uses: actions/upload-artifact@v4 with: name: rag-eval-results path: eval_results.jsonimport argparseimport jsonimport sys
def main(): parser = argparse.ArgumentParser() parser.add_argument("--dataset", required=True) parser.add_argument("--threshold-recall", type=float, default=0.85) parser.add_argument("--threshold-faithfulness", type=float, default=0.90) args = parser.parse_args()
# Charger et évaluer results = run_evaluation(args.dataset)
# Vérifier les seuils if results["context_recall"] < args.threshold_recall: print(f"❌ Recall {results['context_recall']:.2f} < {args.threshold_recall}") sys.exit(1)
if results["faithfulness"] < args.threshold_faithfulness: print(f"❌ Faithfulness {results['faithfulness']:.2f} < {args.threshold_faithfulness}") sys.exit(1)
print("✅ All quality gates passed")
# Sauvegarder les résultats with open("eval_results.json", "w") as f: json.dump(results, f, indent=2)Revue humaine
Section intitulée « Revue humaine »Les métriques automatiques ne suffisent pas. Planifiez une revue humaine régulière :
- Échantillonnage : 10-20 questions/semaine
- Annotateurs : domain experts + utilisateurs finaux
- Critères : utilité, précision, ton, citations correctes
- Feedback loop : enrichir le golden dataset
Observabilité : monitoring en production
Section intitulée « Observabilité : monitoring en production »Un RAG en production nécessite du monitoring continu.
Métriques essentielles
Section intitulée « Métriques essentielles »| Métrique | Ce qu’elle surveille | Seuil d’alerte |
|---|---|---|
| Latence P95 | Temps de réponse total | > 3s |
| Retrieval latency | Temps du retriever seul | > 500ms |
| Rerank latency | Temps du reranker | > 300ms |
| LLM latency | Temps de génération | > 2s |
| Cache hit-rate | Efficacité du prompt caching | < 60% |
| Empty results | Requêtes sans contexte | > 10% |
| User feedback | Thumbs up/down | < 70% positif |
| Error rate | Erreurs API/timeout | > 1% |
Logs structurés
Section intitulée « Logs structurés »Les logs structurés (JSON) permettent de corréler les requêtes, mesurer la latence par étape et détecter les problèmes. Voici un exemple complet avec structlog :
import structlogimport time
logger = structlog.get_logger()
def rag_query(query: str, user: User) -> str: """ Pipeline RAG avec logging structuré.
Chaque étape est chronométrée pour identifier les goulots. """ start_time = time.time()
# === ÉTAPE 1 : RETRIEVAL === # Mesurer la latence du retriever seul retrieval_start = time.time() chunks = retriever.search(query, filter={"tenant": user.tenant}) retrieval_latency = time.time() - retrieval_start
# === ÉTAPE 2 : RERANKING === # Le reranker est souvent le 2e poste de latence rerank_start = time.time() reranked = reranker.rerank(query, chunks) rerank_latency = time.time() - rerank_start
# === ÉTAPE 3 : GÉNÉRATION LLM === # Généralement l'étape la plus longue llm_start = time.time() response = llm.generate(build_prompt(query, reranked[:8])) llm_latency = time.time() - llm_start
total_latency = time.time() - start_time
# === LOG STRUCTURÉ === # Toutes les infos utiles pour le monitoring et le debug logger.info( "rag_request", # Type d'événement (pour filtrer dans Grafana/Kibana)
# Contexte de la requête query=query[:100], # Tronqué pour éviter les logs trop gros user_tenant=user.tenant,
# Métriques de retrieval chunks_retrieved=len(chunks), chunks_injected=min(8, len(reranked)), rerank_top_score=reranked[0].score if reranked else None,
# Latences en millisecondes (plus lisible) retrieval_latency_ms=int(retrieval_latency * 1000), rerank_latency_ms=int(rerank_latency * 1000), llm_latency_ms=int(llm_latency * 1000), total_latency_ms=int(total_latency * 1000),
# Indicateurs de problèmes cache_hit=check_cache_hit(), # Prompt caching utilisé ? empty_context=len(chunks) == 0 # Aucun chunk trouvé = problème )
return responseExemple de log généré :
{ "event": "rag_request", "query": "Comment configurer SSL sur Nginx ?", "user_tenant": "org-acme", "chunks_retrieved": 12, "chunks_injected": 8, "retrieval_latency_ms": 145, "rerank_latency_ms": 280, "llm_latency_ms": 1850, "total_latency_ms": 2275, "empty_context": false, "timestamp": "2026-02-17T10:30:45Z"}Ce format permet de créer des dashboards Grafana avec des percentiles (P95), des alertes sur empty_context=true, etc.
Dashboard Grafana
Section intitulée « Dashboard Grafana »Métriques à visualiser :
# Prometheus metrics (exemple avec prometheus_client)rag_requests_total: type: counter labels: [tenant, status]
rag_latency_seconds: type: histogram labels: [stage] # retrieval, rerank, llm, total buckets: [0.1, 0.25, 0.5, 1, 2, 5]
rag_chunks_retrieved: type: histogram buckets: [0, 5, 10, 20, 50, 100]
rag_empty_results_total: type: counter labels: [tenant]
rag_user_feedback: type: counter labels: [tenant, feedback] # positive, negativeAlertes recommandées
Section intitulée « Alertes recommandées »| Alerte | Condition | Sévérité |
|---|---|---|
| Latence élevée | P95 > 5s pendant 5min | Warning |
| Latence critique | P95 > 10s pendant 2min | Critical |
| Taux d’erreur | > 5% pendant 5min | Critical |
| Empty results | > 20% pendant 10min | Warning |
| Feedback négatif | > 40% sur 1h | Warning |
| Cache miss élevé | < 40% hit-rate | Warning |
Outils d’observabilité
Section intitulée « Outils d’observabilité »| Outil | Usage | Notes |
|---|---|---|
| LangSmith | Tracing LangChain, evals | SaaS, intégré |
| Helicone | Proxy LLM avec analytics | SaaS |
| Langfuse | Tracing open-source | Self-hosted ou cloud |
| Weights & Biases | Expérimentations, comparaisons | ML-focused |
| Prometheus + Grafana | Métriques custom | Self-hosted |
| OpenTelemetry | Tracing distribué | Standard |
Sécurité avancée
Section intitulée « Sécurité avancée »Risques à mitiger
Section intitulée « Risques à mitiger »| Risque | Description | Mitigation |
|---|---|---|
| Prompt injection | L’utilisateur manipule le prompt | Sandboxer le contexte, validation |
| Data exfiltration | Extraire des données via le RAG | Filtrage ACL strict |
| PII leak | Fuites de données personnelles | Anonymisation, classification |
| Jailbreak | Contourner les instructions | Instructions système robustes |
Bonnes pratiques
Section intitulée « Bonnes pratiques »-
Filtrage ACL au retriever : jamais côté LLM
-
Validation des inputs : rejeter les requêtes suspectes
-
Classification des docs : metadata
sensitivity -
Audit trail : logger qui accède à quoi
-
Rate limiting : par utilisateur et tenant
-
Sanitization : nettoyer les outputs avant affichage
Checklist de mise en production
Section intitulée « Checklist de mise en production »- Métadonnées de sécurité sur tous les chunks (tenant, acl, sensitivity)
- Filtrage ACL implémenté au niveau retriever
- Isolation multi-tenant (filtre ou collections séparées)
- Golden dataset constitué (30+ questions)
- Évaluation RAGAS intégrée en CI/CD
- Seuils qualité définis (recall > 85%, faithfulness > 90%)
- Logs structurés avec latences par étape
- Dashboard de monitoring opérationnel
- Alertes configurées (latence, erreurs, empty results)
- Revue humaine planifiée (hebdomadaire)
- Rate limiting en place
- Audit trail activé
À retenir
Section intitulée « À retenir »-
ACL au retriever : filtrer AVANT l’injection, jamais côté LLM
-
Métadonnées obligatoires : tenant, acl, sensitivity, hash, doc_version
-
Golden dataset : 30-50 questions annotées minimum
-
RAGAS : faithfulness + context_recall en CI/CD
-
Seuils : recall > 85%, faithfulness > 90% avant merge
-
Observabilité : latence par étape, empty results, user feedback
-
Revue humaine : 10-20 questions/semaine par des experts