
Un RAG qui marche sur votre poste n'est pas un RAG en production. Trois exigences l'en séparent. La sécurité : un utilisateur ne doit récupérer que les documents auxquels il a droit. L'évaluation : sans mesure, impossible de savoir si le RAG s'améliore ou se dégrade. L'observabilité : en production, il faut voir ce qui se passe à chaque requête. Ce guide traite les trois — le filtrage par droits au moment de la recherche, une évaluation par recall et exactitude sur un jeu de test, et la journalisation structurée des requêtes. Public visé : développeur qui veut exploiter un RAG, pas seulement le démontrer.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Filtrer la recherche par droits d'accès — le multi-tenant.
- Construire un jeu de test et mesurer un RAG.
- Distinguer recall du retrieval et exactitude de la réponse.
- Journaliser chaque requête pour l'observabilité.
- Intégrer l'évaluation dans une démarche d'amélioration continue.
Prérequis
Section intitulée « Prérequis »- Avoir un RAG fonctionnel.
- Une instance Ollama avec
qwen2.5etnomic-embed-text. - Python 3.10+.
Sécurité : filtrer par droits d'accès
Section intitulée « Sécurité : filtrer par droits d'accès »Un RAG d'entreprise indexe des documents de sensibilités différentes : des notes publiques, des documents d'équipe, des données confidentielles. Un RAG naïf cherche dans tout — et peut donc faire fuiter, dans une réponse, un passage qu'un utilisateur n'avait pas le droit de voir.
La parade ne se joue pas dans le prompt — espérer que le modèle « ne révèle pas » est illusoire. Elle se joue à la recherche : on ne récupère, dès le départ, que les documents autorisés.
Le mécanisme est le filtrage par métadonnée, vu avec Chroma. À l'indexation, chaque document reçoit une métadonnée d'accès — une équipe, un niveau, un identifiant de client. À la recherche, on filtre sur cette métadonnée.
# L'utilisateur appartient à l'équipe « infra » :# la recherche sémantique ne s'applique qu'à ses documents.filtre = {"equipe": "infra"}resultats = collection.query( query_embeddings=vectoriser([question]), n_results=k, where=filtre,)C'est le principe du multi-tenant : plusieurs « locataires » — équipes, clients — partagent la même base vectorielle, mais chaque recherche est cloisonnée à un périmètre. Le document hors périmètre n'est jamais récupéré, donc jamais injecté dans le prompt, donc jamais dans la réponse.
Évaluation : mesurer pour améliorer
Section intitulée « Évaluation : mesurer pour améliorer »« Le RAG a l'air de mieux marcher » n'est pas un constat exploitable. Pour piloter un RAG — décider si un nouveau chunking aide, si un changement de modèle régresse —, il faut des chiffres.
L'évaluation repose sur un jeu de test : une liste de questions de référence, chacune avec ce qu'on attend — la source qui devrait être trouvée, un fait qui devrait figurer dans la réponse.
JEU_TEST = [ {"question": "Comment conserver les données d'un conteneur ?", "source": "volumes.md", "terme": "volume"}, {"question": "Comment des conteneurs communiquent par leur nom ?", "source": "reseau.md", "terme": "réseau"},]Ce jeu se construit à la main, à partir de vraies questions d'utilisateurs. C'est un investissement — mais c'est lui qui rend toute mesure possible. Sans jeu de test, on pilote à l'aveugle.
Recall et exactitude : deux problèmes distincts
Section intitulée « Recall et exactitude : deux problèmes distincts »Un RAG peut échouer à deux endroits : la recherche ne remonte pas le bon document, ou la réponse déforme un document pourtant correct. Deux métriques séparent ces deux causes.
Le recall du retrieval mesure la recherche : la source attendue figure-t-elle parmi les chunks récupérés ?
def recall_at_k(source_attendue: str, sources_trouvees: list[str]) -> bool: """Vrai si la source attendue figure parmi les sources récupérées.""" return source_attendue in sources_trouveesL'exactitude de la réponse mesure la génération : le fait attendu est-il présent dans la réponse finale ?
def contient_terme(reponse: str, terme: str) -> bool: """Vrai si la réponse contient le terme factuel attendu.""" return terme.lower() in reponse.lower()La distinction est opérationnelle. Un recall bas pointe vers le retrieval : chunking à revoir, recherche hybride à ajouter. Une exactitude basse malgré un bon recall pointe vers la génération : le prompt à durcir, le modèle à changer. Mesurer les deux séparément dit où agir.
def evaluer(index, jeu_test, k=3): """Évalue le RAG sur le jeu de test : recall et exactitude.""" recalls, exactitudes = [], [] for cas in jeu_test: resultat = repondre(index, cas["question"], k) recalls.append(recall_at_k(cas["source"], resultat["sources"])) exactitudes.append(contient_terme(resultat["reponse"], cas["terme"])) n = len(jeu_test) return {"recall_at_k": sum(recalls) / n, "exactitude": sum(exactitudes) / n}Intégrer l'évaluation à l'amélioration continue
Section intitulée « Intégrer l'évaluation à l'amélioration continue »Mesurer une fois ne sert à rien : la valeur vient de la répétition. L'évaluation devient utile quand elle tourne à chaque changement — nouveau chunking, autre modèle d'embedding, prompt modifié.
Le réflexe : faire de l'évaluation une étape automatisée. Avant de déployer une modification du RAG, on relance le jeu de test ; si le recall ou l'exactitude baissent, le changement est une régression — on ne déploie pas. C'est exactement le rôle d'un test en intégration continue, appliqué à la qualité du RAG et non plus seulement à son code.
Cette boucle — modifier, mesurer, comparer — est ce qui transforme un RAG figé en un RAG qui progresse.
Observabilité : journaliser chaque requête
Section intitulée « Observabilité : journaliser chaque requête »En production, le RAG répond à des questions qu'on n'a pas anticipées. Pour comprendre son comportement réel — et repérer les dérives —, il faut journaliser chaque requête sous une forme exploitable.
def journaliser(question: str, resultat: dict) -> dict: """Construit un enregistrement structuré d'une requête.""" return { "question": question, "sources": resultat["sources"], "latence_ms": resultat["latence_ms"], "longueur_reponse": len(resultat["reponse"]), }Un journal structuré — un dictionnaire, sérialisable en JSON — vaut infiniment mieux qu'une ligne de texte libre : il s'agrège, se filtre, s'analyse. Trois informations méritent d'y figurer systématiquement : les sources récupérées (pour repérer un document jamais utile, ou au contraire surreprésenté), la latence (pour suivre les performances), et de quoi relier la requête à un éventuel retour utilisateur.
Agrégés, ces journaux révèlent ce qu'aucun test ne montre : les questions fréquentes mal couvertes, les pics de latence, les documents morts. C'est la matière première de l'amélioration en production.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
| Une réponse cite un document interdit | Filtrage absent ou fait dans le prompt | Filtrer par métadonnée à la recherche |
| Impossible de comparer deux versions | Pas de jeu de test | Construire un jeu de questions de référence |
| Recall bas | Retrieval défaillant | Revoir le chunking, ajouter la recherche hybride |
| Exactitude basse, recall correct | Génération défaillante | Durcir le prompt, changer de modèle |
| Dérive invisible en production | Pas de journalisation | Journaliser chaque requête en structuré |
À retenir
Section intitulée « À retenir »- La sécurité d'un RAG se joue à la recherche : filtrer par droits, jamais dans le prompt.
- Le multi-tenant cloisonne chaque recherche à un périmètre, sur une base partagée.
- L'évaluation exige un jeu de test : des questions de référence aux réponses connues.
- Le recall mesure le retrieval, l'exactitude mesure la réponse — deux problèmes distincts.
- L'évaluation prend sa valeur répétée : à chaque changement, pour détecter les régressions.
- La journalisation structurée des requêtes est la base de l'observabilité en production.