
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.