Métriques d’évaluation du RAG : retrieval, reranking et génération
Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.
Mise à jour de l’article
Publié initialement le 10 mai 2026. Relu et mis à jour le 6 septembre 2026. Cette mise à jour ajoute de nouveaux benchmarks de retrieval et des candidats rerankers, révise les recommandations concernant les outils d’évaluation et corrige les paramètres d’API de l’exemple de judge.
Un système RAG dont les filtres de pertinence sont défaillants peut fonctionner pendant des mois sans déclencher d’alerte opérationnelle. Il renvoie toujours des réponses et respecte son objectif de latence, mais ces réponses reposent sur des éléments de preuve incomplets. Le Recall@k calculé sur l’ensemble gold initial éligible révèle cette perte. Les dashboards de latence et de disponibilité, eux, ne la détectent pas.
Pour les ingénieurs qui exploitent ou évaluent des systèmes RAG multi-étapes, cette référence relie les défaillances du parsing documentaire, du filtrage, du retrieval, du reranking et de la génération à la métrique qui permet d’identifier chacune d’elles. Elle indique également quels contrôles exécuter avant une mise en production et lesquels utiliser pour surveiller le trafic réel.
Vous voulez passer directement au code ?
Le dépôt exécutable
slavadubrov/rag-evals-demoapplique certaines métriques à SciFact.make evalexécute la suite, etmake benchmarkcompare les configurations de chunking, d’embeddings et de LLM. Les notebooks 00 à 09 couvrent le retrieval, le filtrage, la génération et des exemples de systèmes ; ils n’implémentent pas tous les contrôles décrits dans cette référence. La démonstration utilise Qdrant embarqué et ne nécessite donc pas Docker.
Le projet associé est un harness pédagogique. La révision relue le 6 septembre 2026 nécessite encore des corrections concernant le suivi des requêtes manquantes, le parsing des résultats du judge et les données gold d’autorisation. Son judge pairwise ne dispose pas non plus du contexte fourni nécessaire pour évaluer le support. Les contrats corrigés et les contrôles inline ci-dessous ne corrigent pas ce dépôt ; utilisez ses notebooks pour examiner le workflow et vérifiez ces cas avant d’adopter ses scores comme critères de release.
TL;DR
- Une stack d’évaluation utile couvre l’ingestion, le retrieval, le grounding de la génération, la conformité à l’ontologie et les signaux système. RAGAS, TruLens, DeepEval, Arize Phoenix et le TREC 2024 RAG Track proposent des bibliothèques ou des protocoles publics d’évaluation. Ils ne choisissent pas vos métriques à votre place.
- Pour un RAG fondé sur des métadonnées et une ontologie, un tag erroné ou un prédicat strict fragile peut faire tomber le recall à zéro. Le Recall@k standard détecte cette perte lorsqu’il conserve l’ensemble gold éligible initial. Une métrique de fausse exclusion par le filtre identifie la cause. La faithfulness peut encore évaluer les claims par rapport à un contexte incomplet, mais elle ne peut pas diagnostiquer la cause liée au filtre ou au retrieval. Un refus vide peut ne contenir aucune statement et
NaN, selon l’implémentation.
Tableau de décision pour l’évaluation du RAG
Utilisez ce tableau comme point de départ avant de choisir un framework. La bonne métrique dépend du mode de défaillance que vous cherchez à détecter, et non du nom de l’outil.
| Question | Famille de métriques | À utiliser lorsque | Points de vigilance |
|---|---|---|---|
| Le parsing a-t-il préservé la source ? | Complétude de l’extraction, couverture des tableaux/figures | Des PDF, slides, scans et pages HTML entrent dans le corpus | Un texte d’apparence propre peut perdre des légendes, notes de bas de page ou structures de tableaux |
| Le retrieval a-t-il trouvé les bons éléments de preuve ? | Recall@k, nDCG@k, MRR, précision/rappel du contexte | Vous pouvez annoter les chunks ou documents pertinents | Un filtre de métadonnées strict peut supprimer le bon document avant le début du ranking |
| Le reranking a-t-il amélioré la shortlist ? | Gain du reranker, Precision@1, delta de nDCG | Des cross-encoders ou des LLM rankers interviennent après le retrieval | Mesurez la latence et le coût avec le gain de qualité |
| La réponse a-t-elle utilisé les éléments de preuve ? | Faithfulness, groundedness, support des citations | La réponse cite des documents ou affirme des faits issus du contexte | La faithfulness ne peut pas diagnostiquer un mauvais parsing ou un mauvais retrieval |
| Le système est-il stable en production ? | Drift, régénération, fallback, latence p95, coût par réponse | Le trafic évolue après le lancement | La télémétrie de production nécessite une revue humaine échantillonnée pour rester calibrée |
Pour une comparaison plus courte des outils, consultez Meilleurs outils d’évaluation du RAG : Ragas, DeepEval et TruLens.
Partie 1 : définir le succès avant l’architecture
Préparez le jeu d’évaluation avant le schéma d’architecture. Il donne à chaque choix de composant ultérieur une cible mesurable.
Vous ne pouvez pas choisir entre BM25 et le retrieval dense, le chunking récursif et sémantique, ou Cohere Rerank et BGE tant que vous ne savez pas ce que vous optimisez. « De meilleures réponses » n’est pas une métrique. Une exigence de release illustrative serait : « faithfulness ≥ 0,85 sur un golden set de 200 requêtes couvrant nos trois principales intentions, avec une latence p95 < 1,5 s et un taux de fausse exclusion par le filtre < 2 %. » Les chiffres sont des valeurs d’exemple ; l’important est que la qualité, la couverture, la latence et le filtrage disposent de seuils explicites.
Définissez le harness avant d’écrire le code de retrieval. Le premier harness sera erroné et vous le réviserez. Réviser une métrique coûte bien moins cher que réviser un système déjà livré.
Trois couches de pipeline et deux modes d’exécution
L’évaluation en production comporte trois couches de pipeline. L’évaluation de l’ingestion vérifie si le corpus et l’index préservent la source. L’évaluation au moment de la requête vérifie si la réécriture, le filtrage, le retrieval, le reranking et l’assemblage du contexte ont trouvé les bons éléments de preuve. L’évaluation de la réponse et de la production vérifie si la réponse a utilisé ces éléments et si la qualité se maintient sur le trafic réel. Fusionner ces couches en un seul score permet à un bug de normalisation de disparaître dans un score de réponse acceptable.
Ces couches décrivent l’endroit où survient une défaillance. Offline et online décrivent le moment où le contrôle est exécuté et les données utilisées. L’évaluation offline s’appuie sur un dataset fixe dont le ground truth est connu ; elle est reproductible et convient à la sélection de composants, aux comparaisons A/B et aux contrôles CI capables de bloquer une modification. L’évaluation online évalue un échantillon du trafic réel et capture les régénérations, le temps passé, les retours explicites et le drift réel des requêtes. Elle est plus bruitée et plus difficile à instrumenter.
Utilisez les deux modes là où ils sont utiles : les corpus et jeux de requêtes fixes rendent les régressions reproductibles ; les traces réelles échantillonnées exposent les problèmes de fraîcheur et de drift.
Évaluation par composant contre évaluation end-to-end
Deux erreurs sont fréquentes. Une évaluation uniquement end-to-end indique que le système est défaillant, mais pas où. Une évaluation uniquement par composant peut montrer que chaque partie passe les contrôles alors que le système complet échoue. La solution consiste à utiliser quelques métriques end-to-end principales pour les décisions go/no-go, complétées par des métriques par composant pour le diagnostic. Les métriques de retrieval détectent les régressions du retriever. Les métriques de génération détectent celles du générateur. La correction end-to-end de la réponse détecte les problèmes d’intégration.
Les frameworks de référence (vue d’ensemble assumée)
| Framework | Points forts | Limites |
|---|---|---|
| RAGAS | Un vocabulaire commun pour la faithfulness, la pertinence de la réponse et la précision/le recall du contexte (métriques) | Coût du LLM judge ; composants du score opaques lors du debugging ; changements de version |
| ARES | Un judge classifier spécialisé par tâche, si l’entraînement et l’annotation justifient le coût (article) ; sa précision publiée dépend du benchmark | Mise en place plus lourde ; vous devez réellement entraîner des modèles |
| TruLens | Fonctions de feedback liées aux traces et intégration OpenTelemetry (projet) | Moins de métriques RAG natives que RAGAS |
| DeepEval | Intégration aux test runners et métriques personnalisées (projet) | Un usage intensif des LLM judges entraîne des pics de coût |
| Arize Phoenix | Tracing, expériences sur datasets et évaluateurs RAG/agentic prédéfinis ou personnalisés (documentation d’évaluation) | Les rubriques métier et les seuils du judge doivent encore être calibrés localement |
| TREC 2024 RAG Track | Benchmark public pour l’évaluation par nuggets (AutoNuggetizer), le support et la fluidité sur MS MARCO Segment v2.1 | Pas un outil runtime ; un benchmark auquel se comparer |
Ma stack par défaut est RAGAS pour le vocabulaire des métriques, DeepEval pour les contrôles CI, Phoenix pour le tracing en production, ainsi que du code personnalisé pour les métriques propres à l’ontologie. Choisissez le framework qui facilite la création de métriques personnalisées.
Pour choisir un benchmark, faites correspondre la tâche avant de consulter le leaderboard. BEIR, MTEB et MIRACL restent des baselines utiles pour le retrieval. Ajoutez des tests pour les capacités qu’ils n’établissent pas :
- RAG end-to-end actuel : le TREC 2026 RAG Track utilise des requêtes narratives et ClimbMix-400b, en remplacement de MS MARCO v2.1, et référence le toolkit d’évaluation RAGDoll. Au 6 septembre, la page du track n’a pas annoncé de date de publication des résultats et des jugements. Ses topics publiés sont disponibles pour des expériences ; ils ne constituent pas encore un leaderboard 2026 complet et évalué. Conservez les protocoles 2024 et 2025 ci-dessous liés à leurs propres corpus et jugements.
- Questions techniques sur du code évolutif : FreshStack combine des questions Stack Overflow posées par des humains, des corpus de dépôts et des jugements par nuggets. Son snapshot publié et ses mécanismes de construction de nouveaux corpus sont deux choses différentes ; épinglez la révision du dépôt et la date des questions.
- Images portant une partie de la question ou des éléments de preuve : MM-BRIGHT sépare le retrieval texte-vers-texte, multimodal-vers-texte, multimodal-vers-image et multimodal-vers-multimodal. Évaluez séparément la tâche concernée. Le texte issu de l’OCR peut à lui seul omettre ce qu’apporte un graphique ou une capture d’écran.
Ces benchmarks étendent la couverture ; ils ne remplacent pas le jeu de requêtes éligibles, versionné, de votre application.
Partie 2 : cartographier les points d’évaluation
Utilisez le schéma pour relier un symptôme à sa première métrique de diagnostic. Les pertes en amont plafonnent la qualité en aval : un mauvais parsing plafonne le retrieval, et un mauvais retrieval plafonne le reranking et la génération. La faithfulness mesure la réponse, jamais la cause en amont.
Partie 3 : évaluation de l’ingestion
De nombreuses défaillances RAG en production commencent lors de l’ingestion. Le système fonctionne sur des documents de test propres, puis échoue sur de vrais PDF, scans, tableaux et pages de corpus désordonnées.
Acquisition et parsing des documents
Mesures à suivre :
-
Contrôle de cohérence de la longueur extraite :
extracted_chars / expected_charspar classe de document signale les variations de longueur suspectes, mais du texte dupliqué ou incorrect peut tout de même obtenir 1,0. Comparez le texte aligné à une référence nettoyée manuellement pour détecter les omissions et substitutions, puis vérifiez séparément les notes de bas de page, légendes, contenus de tableaux et ordre de lecture. -
Précision de l’OCR : CER (Character Error Rate) et WER (Word Error Rate), les métriques standard de la reconnaissance vocale et de l’OCR :
où , , sont les substitutions, suppressions et insertions au niveau des caractères, et le nombre de caractères de référence (indice pour la version mots). N’appliquez pas un même seuil CER à tout le corpus. Calibrez-le par classe de document et selon la perte de qualité de réponse en aval. Les textes imprimés, l’écriture manuscrite et les contenus multilingues ont des profils d’erreur différents. Calculez-le avec
jiwer(jiwer.cer(refs, hyps),jiwer.wer(refs, hyps)) ou avecevaluatede HuggingFace. Pour les corpus d’évaluation, FUNSD et SROIE sont des benchmarks publics.from jiwer import cer, wer refs = ["Mars has two moons, Phobos and Deimos."] hyps = ["Mars has two m00ns, Phobos and Deirnos."] print(f"CER = {cer(refs, hyps):.3f}") # CER = 0.105 print(f"WER = {wer(refs, hyps):.3f}") # WER = 0.286 -
Fidélité de l’extraction des tableaux : TEDS (Tree-Edit-Distance-based Similarity) mesure la proximité entre l’arbre d’un tableau HTML prédit et celui de la référence, normalisée par la taille du plus grand arbre. D’après Zhong et al., 2020 (PubTabNet) :
TEDS utilise à la fois la structure (lignes, colonnes, spans) et le contenu des cellules. TEDS-S retire le contenu et évalue uniquement la structure. Implémentation de référence :
teds.pyde PubTabNet (qui utiliseapteden interne). Pour les corpus d’évaluation, consultez PubTabNet, FinTabNet et SciTSR. Les parsers naïfs échouent souvent sur les tableaux. Faites un benchmark avant de leur faire confiance. -
Préservation de la mise en page et de la structure : ordre des titres, intégrité des listes et ordre de lecture dans les PDF à plusieurs colonnes. Utilisez DocLayNet comme benchmark annoté. Une comparaison prête à l’emploi peut inclure un parser d’éléments tel que
unstructured, une bibliothèque PDF telle quepymupdfet un pipeline Docling sélectionné. Docling propose des parcours standard et VLM ; indiquez celui que vous testez.
Comparez des familles de parsers distinctes, par exemple une baseline Tesseract, un modèle OCR fondé sur un VLM et le candidat de votre fournisseur. Utilisez un échantillon stratifié de classes de documents réels à DPI fixe, incluant des scans propres, des photos, des tableaux, du texte multilingue, des formules mathématiques et de l’écriture manuscrite. Rapportez le CER ou le WER pour chaque classe et le TEDS pour les pages contenant des tableaux.
Nettoyage et normalisation
-
Précision de la suppression du boilerplate : précision et recall par rapport à des spans de boilerplate annotés par des humains. Une suppression agressive élimine du contenu pertinent ; une suppression trop prudente pollue les embeddings. Outils à comparer :
trafilatura,jusText,Resiliparse. Barbaresi (2021) compare Trafilatura à des baselines dont jusText ; Resiliparse est un autre candidat, mais n’est pas un système évalué dans cet article. -
Normalisation Unicode : le pourcentage de documents produisant des sorties NFC et NFKC identiques (calculé avec
unicodedata.normalizede la bibliothèque standard) constitue un signal utile de dérive liée aux formes de compatibilité. Il ne détecte pas les points de code invisibles ou ignorables par défaut, ni les caractères visuellement similaires entre scripts : recherchez explicitement les premiers et appliquez une politique ou un détecteur de confusables Unicode lorsque les seconds sont concernés. -
Précision de la détection de langue : F1 sur un échantillon multilingue annoté. Elle est essentielle pour les index multilingues. Utilisez
fasttext-langdetect(lelid.176de Facebook),lingua-pyoucld3. FLORES-200 fournit du texte d’évaluation dans 200 langues, mais c’est la composition linguistique de votre production qui doit déterminer la tranche de test. -
Efficacité de la déduplication (MinHash / LSH) : précision et recall de votre détecteur de quasi-doublons par rapport à un jeu annoté manuellement. L’idée sous-jacente consiste à estimer la similarité de Jaccard entre les ensembles de shingles de documents au moyen de fonctions de hachage par permutation aléatoire (Broder, 1997), puis à regrouper les quasi-doublons par banding LSH (Indyk & Motwani, 1998). Faites varier le nombre de hash et le seuil de Jaccard sur votre corpus. Suivez séparément le taux de fusion erronée (qui corrompt les réponses) et le taux de fusion manquée (qui gaspille de l’espace d’index).
datasketchfournit l’implémentation utilisée ci-dessous ; ses paramètres sont donnés à titre d’exemple :from datasketch import MinHash, MinHashLSH def shingles(text: str, k: int = 5) -> set[str]: text = text.lower() return {text[i:i + k] for i in range(len(text) - k + 1)} def to_minhash(text: str, num_perm: int = 128) -> MinHash: m = MinHash(num_perm=num_perm) for s in shingles(text): m.update(s.encode("utf-8")) return m docs = { "d1": "Mars has two moons, Phobos and Deimos.", "d2": "Mars has two moons, Phobos and Deimos!", # near-dup "d3": "Curiosity rover landed on Mars in 2012.", } lsh = MinHashLSH(threshold=0.8, num_perm=128) for did, text in docs.items(): lsh.insert(did, to_minhash(text)) print(sorted(lsh.query(to_minhash(docs["d1"])))) # ['d1', 'd2'] -
Purge des données personnelles (PII) : précision et recall, calculés séparément pour chaque type d’entité (e-mails, SSN, noms, adresses). Les erreurs de recall créent un risque de conformité ; les erreurs de précision dégradent la qualité des réponses. Définissez le point de fonctionnement avec l’équipe juridique. Les outils candidats incluent Microsoft Presidio,
scrubadubou un modèle NER fine-tuné sur un jeu annoté.
Le chunking contrôle la qualité du retrieval
Le chunking détermine quels éléments de preuve parviennent à la réponse. Dans le benchmark fournisseur 2025 de NVIDIA, le chunking au niveau des pages a obtenu la meilleure précision moyenne des réponses end-to-end dans la configuration testée, qui conservait les tableaux et graphiques comme unités complètes. Cela mesure la précision des réponses, et non le recall du retrieval. Considérez ce résultat comme un élément probant pour le corpus testé, pas comme un vainqueur universel.
Le chunking sémantique regroupe les phrases adjacentes selon la similarité de leurs embeddings et coupe aux frontières dissemblables. SemanticChunker de LangChain et SemanticSplitterNodeParser de LlamaIndex implémentent cette stratégie. Elle peut améliorer le recall par rapport aux fenêtres fixes lorsque les frontières thématiques sont importantes.
Le RecursiveCharacterTextSplitter de LangChain essaie par défaut les doubles sauts de ligne, les sauts de ligne simples, les espaces, puis les caractères individuels : ["\n\n", "\n", " ", ""]. Il ne détecte pas les frontières de phrases, sauf si vous configurez des séparateurs adaptés ou utilisez un splitter sensible aux phrases. Choisissez des valeurs de fenêtre et d’overlap adaptées à la structure de vos documents, puis comparez-les sur le golden set.
Métriques à suivre :
- Cohérence des chunks : , où sont les embeddings de phrases. Des chunks sains sont similaires en interne et dissemblables aux frontières. Calculez cette métrique avec
sentence-transformerset lecosine_similaritydescikit-learn. - Qualité des frontières : annotation humaine de « cette coupure est-elle pertinente ? » sur un échantillon, complétée par un contrôle structurel vérifiant que les chunks ne coupent pas les tableaux, listes ou sections numérotées.
- Taille optimale des chunks : faites varier les tailles en tokens (128, 256, 512, 1024) et tracez Recall@k en fonction de la taille sur votre golden set. Choisissez le coude de la courbe. Ne prenez pas la valeur indiquée par un tutoriel.
- Efficacité de l’overlap : supprimez alternativement plusieurs fractions d’overlap et mesurez le Recall@k. Arrêtez d’augmenter l’overlap lorsque la courbe de recall local s’aplatit ou que le coût de duplication dépasse le gain.
- Fidélité de l’attribution des chunks : pourcentage de chunks conservant un pointeur vers une source vérifiable (numéro de page, ancre de section, ID de document). C’est indispensable à l’auditabilité.
- Chunking late contre early : le late chunking (Günther et al., 2024) encode le document complet avant de le segmenter, ce qui préserve le contexte global (implémentation de référence dans
jina-embeddings-v3). Le Contextual Retrieval (Anthropic, 2024) préfixe chaque chunk avec du contexte généré par un LLM. Les deux ajoutent du coût. Faites un benchmark sur votre corpus avant d’adopter l’une ou l’autre approche.
À mon avis, le chunking structurel (découpage sur les titres, tableaux et sections — implémenté par des parsers comme unstructured.io ou par parcours de l’AST déjà produit par votre parser) est sous-utilisé. Si vos documents ont une structure, exploitez-la avant d’ajouter des heuristiques de similarité. Le découpage récursif par caractères est la baseline ; le chunking sémantique justifie principalement son coût sur de la prose non structurée.
Extraction et enrichissement des métadonnées
- Précision/recall/F1 du NER : par type d’entité, sur un sous-ensemble annoté. Utilisez le format standard de type CoNLL/MUC. Calculez-les avec
seqeval(from seqeval.metrics import f1_score) pour la version compatible avec les tags BIO/IOB, ou avec scikit-learn pour des comparaisons d’ensembles de spans. CoNLL-2003 et OntoNotes 5.0 sont les corpus de référence canoniques. - F1 de l’extraction de relations : encore plus important pour les systèmes fondés sur une ontologie. Annotez manuellement un ensemble stratifié par type de relation et classe de document. TACRED et DocRED sont des benchmarks publics ; les implémentations candidates comprennent les pipelines de relations
opennreetspaCy. - Précision de l’extraction des titres et en-têtes : exact match complété par la similarité de Levenshtein normalisée () par rapport au ground truth —
python-Levenshteinourapidfuzzfournissent les deux en un seul appel. - Préservation des métadonnées hiérarchiques : pourcentage de chunks conservant correctement leur section parente, leur document parent et leur chemin d’ascendance. C’est cette métrique qui détermine si votre RAG peut répondre à des questions du type « que dit l’enfant de la politique X ? ».
Génération des embeddings
- Benchmarks de sélection de modèle : utilisez les résultats des tâches de retrieval de MTEB, BEIR pour la généralisation zero-shot et MIRACL pour le retrieval multilingue comme points de comparaison. Le retrieval MTEB rapporte souvent le nDCG@10 ; les autres familles de tâches utilisent d’autres métriques. Le package Python MTEB exécute les benchmarks localement. Considérez le transfert des résultats MTEB en anglais vers une langue moins dotée comme une hypothèse à tester sur un jeu annoté dans cette langue.
- Évaluation spécifique au domaine : ne considérez pas le rang d’un benchmark général comme un résultat dans votre domaine. Dimensionnez un golden set de domaine à partir de sa matrice de couverture et du niveau d’incertitude acceptable pour votre décision. Reclassez ensuite les modèles candidats avec
ranxoupytrec_eval. Un jeu de domaine peut inverser l’ordre d’un leaderboard ; publiez donc la tranche de dataset, le protocole de retrieval et l’intervalle de confiance avec le résultat. - Détection du drift des embeddings : comparez une fenêtre de référence fixe aux embeddings glissants avec MMD ou un classifier de référence contre courant validé. KL nécessite un estimateur explicite de distribution de probabilité et ne peut pas être appliqué directement aux coordonnées brutes des embeddings. Mesurez également la stabilité des plus proches voisins pour un jeu fixe de probes.
evidentlyetalibi-detectimplémentent des détecteurs statistiques et fondés sur un modèle. L’étude comparative d’Evidently constitue une évaluation fournisseur ; comparez les méthodes sur des shifts connus dans vos propres embeddings. - Multi-vector contre single-vector : le late interaction conserve des représentations au niveau des tokens au lieu de réduire chaque document à un seul vecteur ; ColBERT est l’architecture canonique, avec des implémentations de référence dans RAGatouille et PyLate. Cette représentation plus riche augmente le coût de l’index et du retrieval. Comparez la qualité, le stockage et la latence à une baseline single-vector sur le même jeu de domaine avant de l’adopter.
Construction de l’index
- Recall@k sous approximation : comparez l’index approximate-nearest-neighbour (ANN) à une baseline exacte brute-force pour le même k — dans FAISS, il s’agit de
IndexHNSWFlat(ouIndexIVFFlat) contreIndexFlatIP/IndexFlatL2. Définissez la perte de recall acceptable à partir de votre budget de qualité en aval. Le projetann-benchmarkssuit les courbes de Pareto recall–QPS entre bibliothèques. - Réglage de HNSW : HNSW (Hierarchical Navigable Small World) est un graphe de proximité hiérarchisé ; voir Malkov & Yashunin, 2018. Il est implémenté dans
hnswlib, dansIndexHNSWFlatde FAISS et dans la plupart des bases vectorielles. HNSW expose trois paramètres :M(fan-out du graphe),efConstruction(largeur des candidats lors de la construction) etefSearch(largeur des candidats à la requête). Partez des valeurs par défaut documentées par la bibliothèque, puis faites varier les paramètres jusqu’à ce que la courbe recall–latence réponde aux exigences de votre jeu d’évaluation. - Réglage de l’IVF : IVF (Inverted File index — partitionnement des vecteurs par k-means en
nlistcellules, puis, lors de la requête, parcours desnprobecellules les plus proches ; voirIndexIVFFlatetIndexIVFPQde FAISS). Faites variernlistetnprobeen fonction du recall et de la latence de la recherche exacte. Évaluez séparément les requêtes filtrées, car les familles d’index et les bases vectorielles implémentent le parcours des filtres différemment. - Retard de fraîcheur des mises à jour : temps écoulé entre le commit d’un document et sa disponibilité dans le retrieval. Suivez p50 et p99. Pour les systèmes soumis à des exigences réglementaires, suivez également le pourcentage de requêtes servies par des index obsolètes.
Partie 4 : évaluation au moment de la requête
Le parcours au moment de la requête contient les métriques qui diagnostiquent le chemin de retrieval. Le Recall@k seul ne permet pas de savoir si la réécriture, le filtrage, le reranking ou l’assemblage du contexte est à l’origine de la défaillance.
Compréhension et réécriture de la requête
- Qualité de l’expansion de requête : gain de Recall@k sur votre golden set, requête étendue contre requête brute. Définissez à l’avance le gain minimal utile et son incertitude. Si l’expansion ne satisfait pas cette exigence, elle ne justifie ni sa latence ni son coût. Les baselines classiques de PRF (pseudo-relevance feedback), comme RM3 et Bo1, restent des contrôles de cohérence utiles ; l’expansion fondée sur un LLM doit les dépasser.
- Évaluation de HyDE : HyDE (Gao et al., 2022) génère une réponse hypothétique avec le LLM, l’encode et effectue le retrieval à partir de celle-ci. Il ajoute de la latence de génération et une nouvelle surface de défaillance. Mesurez séparément le Recall@10 sur des tranches in-domain, out-of-domain et à faible confiance, puis choisissez s’il doit appartenir au parcours par défaut, à un fallback ou à aucun des deux.
- Génération multi-query : union du Recall@k de N réécritures contre une requête unique. Faites varier N et choisissez un point sur votre frontière recall–latence. Implémentations :
MultiQueryRetrieverde LangChain etQueryFusionRetrieverde LlamaIndex. - Précision de la classification d’intention : précision/recall/F1 standard par intention (à calculer avec
sklearn.metrics.classification_report), mais la métrique opérationnelle est la correction du routage : le bon pipeline aval est-il appelé ? - Routage adaptatif : Adaptive-RAG (Jeong et al., NAACL 2024) montre que toutes les requêtes ne méritent pas la même stratégie de retrieval. Suivez la précision du routeur comme un problème de classification sur un jeu annoté en « aucun retrieval nécessaire / one-shot / itératif ».
La recherche itérative nécessite un test end-to-end budgété
Un agent peut effectuer une recherche, inspecter un résultat, puis rechercher à nouveau au lieu de récupérer une liste top-k fixe. Le domaine de connaissances tau3 actuel expose une recherche configurable fondée sur le RAG et un shell agentic, ce qui en fait un parcours d’évaluation concret plutôt qu’un simple schéma d’architecture. Pour une comparaison locale, donnez au retrieval one-shot et itératif le même corpus éligible et les mêmes limites explicites de temps, de tokens de modèle et de tool calls. Enregistrez chaque requête et les éléments de preuve vus à chaque tour ; évaluez la couverture finale des preuves, la correction de la réponse, le support des citations et les échecs liés à l’épuisement du budget. Des appels de recherche supplémentaires ne sont utiles que si les preuves ajoutées améliorent la réponse dans ces limites.
Métriques de retrieval
Ce sont les métriques de base. Si vous ne les suivez pas, vous ne pouvez pas savoir si le retrieval s’améliore.
| Métrique | Ce qu’elle mesure | Quand l’utiliser |
|---|---|---|
| Recall@k | fraction des documents pertinents d’une requête renvoyés dans les k premiers | lorsque l’absence d’une partie quelconque de l’ensemble pertinent est importante |
| Precision@k | pourcentage des k premiers documents qui sont pertinents | utile lorsque la fenêtre de contexte est le goulot d’étranglement |
| MRR | moyenne de 1/rang du premier document pertinent | lorsque les utilisateurs ne consultent que le top-1 ou le top-3 |
| nDCG@k | gain pondéré par le degré de pertinence et actualisé selon la position | métrique standard de retrieval pour une pertinence graduée |
| MAP | moyenne, sur les requêtes, de la précision moyenne | lorsque toute la liste classée vous importe |
| Hit Rate@k | présence d’au moins un document pertinent dans les k premiers | moyenne du résultat binaire sur les requêtes, comme contrôle rapide |
| Coverage | pourcentage des documents gold récupérés au moins une fois sur l’ensemble des requêtes | détecte les lacunes systématiques de l’index |
Les formules, pour référence (pertinence binaire avec l’ensemble pertinent pour la requête , et si le document récupéré en position appartient à ) :
Pour la pertinence graduée, ; le nDCG binaire est le cas particulier utilisé dans le code ci-dessous. MAP est la moyenne sur les requêtes de . Voir Manning, Raghavan, Schütze, Introduction to Information Retrieval, chapitre 8, pour les dérivations.
Pour le code de production, utilisez ranx, pytrec_eval ou ir_measures : ils implémentent toute la famille de métriques TREC et gèrent correctement la pertinence graduée. Définissez les objectifs de release à partir d’un golden set réaliste, de la qualité des réponses en aval et du coût d’un manque. N’héritez pas des seuils d’un tutoriel.
Ici, k compte les IDs de documents uniques. Rejetez les doublons au lieu d’accorder un gain supplémentaire à un document répété. Pour les expériences de chunking, indiquez si k compte les chunks ou les documents parents dédupliqués, et comparez également les éléments de preuve fournis dans une enveloppe fixe de tokens. Un même recall documentaire peut masquer des qualités de contexte très différentes.
Le harness de test est court. Vous pouvez l’exécuter dans un notebook avant même d’avoir choisi une base vectorielle.
from math import log2
from statistics import mean
# synthetic gold set: query_id -> set of relevant doc ids
gold = {
"q1": {"d3"},
"q2": {"d7", "d2"},
"q3": {"d11"},
"q4": {"d5"},
}
# ranked retrieval results: query_id -> ranked list of doc ids (top-10)
runs = {
"q1": ["d8", "d3", "d1", "d4", "d2", "d9", "d6", "d10", "d12", "d13"],
"q2": ["d2", "d6", "d4", "d7", "d1", "d3", "d8", "d11", "d5", "d9"],
"q3": ["d11", "d2", "d3", "d4", "d1", "d6", "d7", "d8", "d10", "d12"],
"q4": ["d1", "d2", "d3", "d6", "d8", "d9", "d10", "d12", "d13", "d14"],
}
def recall_at_k(ranked, gold_set, k):
if k <= 0 or len(ranked) != len(set(ranked)):
raise ValueError("require positive k and unique document IDs")
if not gold_set:
return 0.0
hit = sum(1 for d in ranked[:k] if d in gold_set)
return hit / len(gold_set)
def reciprocal_rank(ranked, gold_set):
if len(ranked) != len(set(ranked)):
raise ValueError("require unique document IDs")
# MRR contribution per query: 1/rank of the first relevant doc.
for rank, d in enumerate(ranked, start=1):
if d in gold_set:
return 1.0 / rank
return 0.0
def ndcg_at_k(ranked, gold_set, k):
if k <= 0 or len(ranked) != len(set(ranked)):
raise ValueError("require positive k and unique document IDs")
# binary relevance: rel ∈ {0, 1}
gains = [1.0 if d in gold_set else 0.0 for d in ranked[:k]]
dcg = sum(g / log2(i + 2) for i, g in enumerate(gains))
# ideal DCG: all gold docs ranked first, capped by k
n_gold_in_topk = min(k, len(gold_set))
idcg = sum(1.0 / log2(i + 2) for i in range(n_gold_in_topk))
return dcg / idcg if idcg else 0.0
K = 5
print(f"Recall@{K}: {mean(recall_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
print(f"MRR: {mean(reciprocal_rank(runs.get(q, []), gold[q]) for q in gold):.3f}")
print(f"nDCG@{K}: {mean(ndcg_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
# Recall@5: 0.750
# MRR: 0.625
# nDCG@5: 0.627
assert recall_at_k([], {"d1"}, 5) == 0.0
for score in (lambda ids: recall_at_k(ids, {"d1"}, 2),
lambda ids: reciprocal_rank(ids, {"d1"}),
lambda ids: ndcg_at_k(ids, {"d1"}, 2)):
try:
score(["d1", "d1"])
except ValueError:
pass
else:
raise AssertionError("duplicate IDs must fail")
L’agrégat itère sur toutes les requêtes attendues et traite une exécution manquante comme vide. Cet exemple attribue zéro aux requêtes dont le gold est vide ; dans une vraie suite, marquez-les comme une tranche d’answerability distincte avec son propre dénominateur, au lieu de traiter ce zéro comme un recall mesuré. Rapportez les timeouts et les exécutions manquantes en plus des scores.
Exécutez un sous-ensemble rapide piloté par la couverture à chaque PR, puis le golden set complet avant une release. Bloquez une fusion lorsqu’une métrique préenregistrée dépasse son budget de régression.
Le dépôt associé épingle les valeurs exactes ci-dessus (Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) comme test unitaire dans tests/test_retrieval_metrics.py ; le notebook 01 fait varier Recall@k / MRR / nDCG sur un véritable index SciFact, et le harness de forme production se trouve dans evaluation/retrieval.py.
Retrieval hybride et reciprocal rank fusion
BM25 est un score lexical sparse qui combine la correspondance exacte des termes, leur pondération et la normalisation par longueur. Il est disponible dans rank_bm25, Elasticsearch, OpenSearch et la plupart des moteurs de recherche.
La Reciprocal Rank Fusion (Cormack, Clarke et Buettcher, SIGIR 2009) combine les rankings BM25 et dense selon leur position. Le réglage original k=60 constitue une baseline utile. RRF est agnostique aux scores, ce qui évite la normalisation inter-parcours requise par l’interpolation linéaire. Avec un jeu annoté suffisamment grand pour estimer un delta stable, testez également une combinaison convexe et ajustez α.
Mon hypothèse est que le retrieval hybride associé à un reranker cross-encoder peut aider sur les corpus techniques, de logs et de code. Le gain peut être faible sur des corpus fortement sémantiques. Mesurez-le par rapport aux parcours dense-only et sparse-only, car une mauvaise configuration de fusion peut être moins performante que l’une ou l’autre entrée. Le notebook SciFact associé constitue un test limité, pas un résultat général.
L’implémentation tient en quelques lignes.
from collections import defaultdict
# two retrieval lanes: dense embeddings and BM25.
dense = ["d3", "d7", "d1", "d4", "d2", "d9", "d10"]
sparse = ["d2", "d3", "d8", "d1", "d11", "d4", "d6"]
def rrf(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
"""Reciprocal Rank Fusion (Cormack et al., SIGIR 2009).
score(d) = sum over rankings of 1 / (k + rank(d))
Score-agnostic: only rank position matters. k=60 is the canonical default.
"""
scores: dict[str, float] = defaultdict(float)
if k <= 0:
raise ValueError("k must be positive")
for ranking in rankings:
if len(ranking) != len(set(ranking)):
raise ValueError("each ranking must contain unique document IDs")
for rank, doc in enumerate(ranking, start=1):
scores[doc] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
fused = rrf([dense, sparse], k=60)
for doc, score in fused[:5]:
print(f"{doc} score={score:.5f}")
# d3 score=0.03252 <- rank 1 dense, rank 2 sparse
# d2 score=0.03178 <- rank 5 dense, rank 1 sparse
# d1 score=0.03150
assert [doc for doc, _ in fused[:3]] == ["d3", "d2", "d1"]
try:
rrf([["d1", "d1"]])
except ValueError:
pass
else:
raise AssertionError("one lane must not vote twice for a document")
Notez ce que RRF ne fait pas : il ne regarde jamais les scores de similarité bruts. Un retriever dense renvoyant un cosinus de 0,98 et un parcours BM25 renvoyant un score de 17,4 ne sont pas directement comparables. La normalisation par z-score et min-max supprime les différences d’échelle affines, mais aucune des deux ne calibre un score comme une mesure de pertinence. Les valeurs normalisées restent influencées par les outliers, la forme de la distribution et l’ensemble des candidats, et donc le résultat de la fusion. Validez toute combinaison fondée sur les scores avec des requêtes annotées.
RRF utilise uniquement le rang. Si un retriever place un document en deuxième position, ce vote vaut 1 / (60 + 2), quel que soit le score brut qui l’a produit.
Hybrid + RRF sur SciFact : le notebook 02 compare dense, BM25 et RRF avec des deltas par requête. Le fuser de forme production se trouve dans retrieval/hybrid_rrf.py ; tests/test_rrf.py épingle l’ordre canonique d3 / d2 / d1 à k=60.
Reranking
- ΔnDCG / ΔMRR : gain par rapport à l’absence de reranking, sur votre golden set et à la profondeur réellement utilisée par votre application. Calculez vos métriques de retrieval avec et sans reranker sur des ensembles de candidats identiques.
- Cross-encoder contre bi-encoder : un bi-encoder encode indépendamment la requête et le document (un vecteur par côté) et calcule un score par produit scalaire ; un cross-encoder concatène la requête et le document et exécute un seul forward pass qui porte conjointement son attention sur les deux. Les cross-encoders échangent un forward pass par candidat contre une interaction requête-document plus riche. Implémentation de référence :
sentence-transformersCrossEncoder. Faites le benchmark de la pertinence et de la latence sur un matériel, une taille de batch et une profondeur de candidats explicitement nommés ; ne transférez pas le résultat d’un modèle ou service managé vers un autre environnement. - Listwise contre pointwise : pointwise évalue chaque paire (requête, document) indépendamment ; listwise évalue conjointement toute la liste de candidats afin que le modèle puisse les comparer. Évaluez les deux approches sur les mêmes ensembles de candidats. Calibrez tout seuil de score par modèle et par corpus au lieu de considérer un exemple publié comme portable.
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3") # Reproducible baseline, not a latest-model ranking
query = "How do I rotate database credentials in production?"
candidates = [
"Production database credentials are rotated via Vault every 30 days.",
"The new logo was unveiled at the all-hands meeting.",
"To rotate prod DB creds, run the `rotate-secrets` GitHub Action.",
]
scores = reranker.predict([(query, c) for c in candidates])
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
for doc, score in ranked:
print(f"{score:+.3f} {doc}")
Le code BGE ci-dessus est une petite baseline. Pour une comparaison actuelle, incluez Cohere Rerank 4.0 fast/pro pour le ranking de texte multilingue managé, ou Qwen3-VL-Reranker 2B/8B lorsque les requêtes ou documents contiennent des images, captures d’écran ou vidéos. Rendez explicites les modalités de tâche, la profondeur de candidats, les limites d’entrée, les instructions et le matériel. Un checkpoint de génération général Qwen3.8 n’est pas le même modèle que le reranker spécialisé Qwen3-VL.
Un reranker aide souvent un pipeline RAG basique, mais ce n’est pas une amélioration garantie. Mesurez son ΔPrecision@1 et son ΔnDCG sur votre golden set, puis ne le conservez que si le gain respecte son budget de latence et de coût. Comparez ce gain mesuré à celui de modifications plus petites du retrieval avant de choisir la prochaine optimisation.
ΔnDCG et ΔPrecision@1 d’un cross-encoder sur SciFact : notebook 03 ; module : retrieval/reranker.py.
Construction du contexte et lost-in-the-middle
De nombreuses défaillances « bon retrieval, mauvaise réponse » commencent lors de la construction du contexte.
- Pertinence du contexte :
ContextRelevancede Ragas évalue la liste de contexte fournie au moyen de deux prompts et de notes normalisées ; il ne s’agit pas d’un score par chunk. Pour diagnostiquer les chunks, évaluez explicitement chaque paire requête–chunk, par exemple avec un cross-encoder, et rapportez la distribution avec un seuil calibré localement. - Couverture des citations du contexte fourni : chunks fournis distincts cités divisés par chunks fournis distincts. Rapportez séparément les cas où le contexte est vide. Ce proxy observable indique quels chunks ont reçu des citations, pas ceux que le modèle a utilisés en interne ni si les citations étayent ses claims. Vérifiez séparément le support des citations et comparez la couverture à la qualité de la réponse et au coût en tokens.
- Détection du lost-in-the-middle : évaluation synthétique où le chunk gold est placé aux positions {première, centrale, dernière} d’un contexte long et où l’on mesure la correction de la réponse. L’étude citée de Liu et al. (TACL 2024) rapporte une dégradation en U dans ses conditions de contexte long. Considérez l’observation du même schéma sur un modèle actuel comme une hypothèse à tester. Atténuations : effectuez le reranking, puis réordonnez le top-k afin de placer le chunk au score le plus élevé au début ou à la fin (le
LongContextReorderde LangChain fait exactement cela), ou compressez fortement les chunks centraux. Mesurez avec une évaluation stratifiée par position, pas uniquement avec un score agrégé. Une évaluation exécutable stratifiée par position se trouve dans le notebook 06 (module :evaluation/lost_in_middle.py). - Compression du contexte : rapportez le ratio de compression (tokens en entrée / tokens en sortie) avec la correction de la réponse. Les outils comprennent le
ContextualCompressionRetrieverde LangChain et LongLLMLingua. Définissez à l’avance la perte maximale de correction acceptable selon le risque de l’application et le budget de tokens, puis rejetez les configurations qui la dépassent.
Partie 5 : taux de fausse exclusion par le filtre
Cette métrique dispose de sa propre section, car les scores agrégés de retrieval ne peuvent pas attribuer un manque à un filtre de pertinence. Évaluez séparément les filtres d’éligibilité par rapport aux droits du demandeur ; un document situé hors de cet ensemble doit rester exclu.
Un prédicat de pertinence strict tel que product = Y AND locale = en-US peut faire tomber le recall effectif à zéro parmi les documents que le demandeur est autorisé à consulter. Un Recall@k correctement implémenté détecte cette perte, car son dénominateur reste l’ensemble initial des documents pertinents et éligibles. Il ne permet pas de savoir si le filtre, le retriever ou le ranker a causé le manque. La faithfulness évalue les claims par rapport au contexte récupéré. Elle peut encore attribuer un bon score à des claims étayés par ce contexte incomplet, mais elle ne peut pas diagnostiquer la cause liée au filtre ou au retrieval. Un refus vide peut ne contenir aucune statement et NaN, selon l’implémentation ; ne le considérez pas comme une preuve que la faithfulness a validé le refus.
La branche mise en évidence correspond à la défaillance courante : le bon document existe, mais le filtre le supprime avant le retrieval. Le Recall@k enregistre la baisse ; seul le taux d’exclusion attribue celle-ci au prédicat.
La métrique
filter_false_exclusion_rate =
(# queries where all gold docs were excluded by metadata filter) /
(# queries with at least one entitlement-eligible gold doc)
Cette définition au niveau de la requête compte les exclusions catastrophiques : aucun document pertinent éligible ne survit. Intersectez l’ensemble gold de chaque requête avec l’ensemble des documents autorisés pour le demandeur avant le scoring ; un document non éligible n’est pas une fausse exclusion. Pour les requêtes ayant plusieurs golds, le Recall@k standard expose tout de même la perte partielle ; ajoutez un taux d’exclusion par document si cette frontière est importante. Pour calculer l’un ou l’autre taux, il vous faut (a) les IDs des documents ground truth pour chaque requête d’évaluation et (b) une instrumentation qui journalise les prédicats de filtre appliqués, et pas uniquement les résultats finaux. Définissez la cible à partir du coût d’exclusion d’une réponse valide et de l’intervalle de confiance de votre échantillon de production.
Voici une implémentation fonctionnelle. Elle compare le recall standard correct avec un évaluateur invalide qui redéfinit la pertinence après filtrage.
# A small worked example where relevance predicates remove eligible documents.
docs = [
{"id": "d1", "tenant": "acme", "locale": "en-US"},
{"id": "d2", "tenant": "acme", "locale": "en-GB"},
{"id": "d3", "tenant": "globex", "locale": "en-US"},
{"id": "d4", "tenant": "acme", "locale": "en-US"},
{"id": "d5", "tenant": "acme", "locale": "de-DE"},
{"id": "d6", "tenant": "acme", "locale": "fr-FR"},
]
# Tenant isolation is an eligibility invariant, not a relevance experiment.
# Verify it independently before evaluating relevance preferences.
eligible_docs = [d for d in docs if d["tenant"] == "acme"]
assert {d["id"] for d in eligible_docs} == {"d1", "d2", "d4", "d5", "d6"}
queries = [
# the gold doc lives in en-GB but the dynamic filter forced en-US
{"qid": "q1", "gold": {"d2"}, "filter": lambda d: d["locale"] == "en-US"},
# the gold doc is correctly within the tenant filter
{"qid": "q2", "gold": {"d4"}, "filter": lambda d: d["tenant"] == "acme"},
# the gold doc lives in fr-FR but the dynamic filter forced en-US
{"qid": "q3", "gold": {"d6"}, "filter": lambda d: d["locale"] == "en-US"},
# the gold doc passes the filter (de-DE locale match)
{"qid": "q4", "gold": {"d5"}, "filter": lambda d: d["locale"] == "de-DE"},
]
def filter_false_exclusion_rate(queries, eligible_docs):
n_with_gold, n_excluded = 0, 0
eligible_ids = {d["id"] for d in eligible_docs}
for q in queries:
eligible_gold = q["gold"] & eligible_ids
if not eligible_gold:
continue
n_with_gold += 1
survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
if not (eligible_gold & survivors):
n_excluded += 1
if not n_with_gold:
raise ValueError("false-exclusion rate needs queries with eligible gold")
return n_excluded / n_with_gold, n_with_gold
rate, eligible_query_count = filter_false_exclusion_rate(queries, eligible_docs)
print(f"filter_false_exclusion_rate = {rate:.2%}")
# filter_false_exclusion_rate = 50.00%
print(f"eligible queries = {eligible_query_count} / {len(queries)}")
# eligible queries = 4 / 4
# Correct Recall@k keeps the original eligible gold set as its denominator.
def standard_recall_at_k(queries, eligible_docs, k=10):
recalls = []
eligible_ids = {d["id"] for d in eligible_docs}
for q in queries:
eligible_gold = q["gold"] & eligible_ids
if not eligible_gold:
continue
# demo only: the survivor set stands in for a ranked run.
# A real harness ranks the survivors first, then slices to k.
survivors = [d for d in eligible_docs if q["filter"](d)][:k]
survivor_ids = {d["id"] for d in survivors}
recalls.append(len(eligible_gold & survivor_ids) / len(eligible_gold))
return sum(recalls) / len(recalls) if recalls else 0.0
print(f"standard recall@10 = {standard_recall_at_k(queries, eligible_docs):.2%}")
# standard recall@10 = 50.00%
# INVALID: rebuilding the gold set after filtering changes the question.
# It drops queries whose relevant documents did not survive, then scores 100%.
def invalid_recall_over_filtered_gold(queries, eligible_docs, k=10):
recalls = []
all_doc_ids = {d["id"] for d in eligible_docs}
for q in queries:
all_survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
filtered_gold = q["gold"] & all_doc_ids & all_survivors
if not filtered_gold:
continue
top_k_ids = set(list(all_survivors)[:k])
recalls.append(len(filtered_gold & top_k_ids) / len(filtered_gold))
return sum(recalls) / len(recalls) if recalls else 0.0
invalid = invalid_recall_over_filtered_gold(queries, eligible_docs)
print(f"INVALID recall (filtered gold) = {invalid:.2%}")
# INVALID recall (filtered gold) = 100.00%
assert rate == 0.5
assert standard_recall_at_k(queries, eligible_docs) == 0.5
assert invalid == 1.0
# An unauthorized-only gold document belongs in an entitlement test, not this metric.
unauthorized_only = [
{"qid": "q5", "gold": {"d3"}, "filter": lambda d: d["locale"] == "en-US"},
]
assert eligible_query_count == 4
assert filter_false_exclusion_rate(queries + unauthorized_only, eligible_docs) == (rate, 4)
for unscorable in ([], unauthorized_only):
try:
filter_false_exclusion_rate(unscorable, eligible_docs)
except ValueError:
pass
else:
raise AssertionError("an unmeasured exclusion rate must not pass as zero")
La fonction renvoie le taux et le nombre de requêtes disposant d’un gold éligible. Si ce dénominateur est vide, elle lève une exception au lieu de rapporter un zéro rassurant ; un contrôle de release obligatoire doit traiter ce cas comme une absence de preuve. Les requêtes dont les golds sont tous non autorisés restent dans des tests d’habilitation séparés.
La moitié des requêtes perd son document gold à cause du filtre, le Recall@10 correct tombe donc à 50 %. Ce score détecte le symptôme, mais ne peut pas l’attribuer. Le taux de fausse exclusion montre que le prédicat a supprimé deux réponses avant l’exécution du retriever. L’évaluateur volontairement invalide rapporte 100 % uniquement parce qu’il retire ces échecs de son ensemble gold. Aucun modèle ne peut récupérer un document qui a été filtré.
Le taux de 50 % ci-dessus est reproduit dans un test unitaire du dépôt associé : tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. Le Notebook 04 l’exécute sur SciFact avec des métadonnées synthétiques afin de montrer un filtre réel qui annule le recall ; la métrique runtime (avec son équivalent en précision/recall du prédicat) se trouve dans evaluation/filter_exclusion.py.
Métrique complémentaire : précision et recall du prédicat
Lorsque le filtrage est dynamique (par exemple, lorsqu’un LLM extrait des prédicats de filtre à partir de la requête), traitez l’extracteur de prédicats comme un modèle de classification et évaluez-le comme tel. Mesurez la précision et le recall des prédicats sur un ensemble annoté de paires (query, correct predicate). Un taux d’erreur des prédicats ne se traduit pas directement par une perte équivalente du recall de retrieval ; mesurez la fréquence à laquelle ces erreurs excluent un document gold. Une fois que le filtre strict a supprimé le document gold, aucun reranking ne peut aider.
Filtres d’éligibilité contre préférences de pertinence
L’autorisation, l’isolation des tenants, la juridiction légale et l’état de publication déterminent si un document peut entrer dans l’ensemble de candidats. Conservez-les comme filtres stricts et validez-les indépendamment ; le Recall@k, le taux de fausse exclusion et la précision de pertinence n’autorisent pas leur assouplissement.
Pour une préférence de pertinence telle que la locale, la récence ou la version, comparez un prédicat strict à un boost soft sur les mêmes requêtes de test éligibles :
For each relevance preference F:
hard_recall_F = retrieval_recall@k with F as a hard filter
soft_recall_F = retrieval_recall@k with F as a +0.X rerank boost
hard_precision = relevant_in_top_k / k under hard filter
soft_precision = relevant_in_top_k / k under soft boost
precision_gain = hard_precision - soft_precision
precision_gain_CI = paired-query uncertainty interval for precision_gain
exclusion_rate = % of queries where the gold doc was filtered out (hard)
Pre-register Δ_min, ε, and recall_loss_max for this workflow.
Use a hard predicate only if:
lower_bound(precision_gain_CI) >= Δ_min
AND exclusion_rate <= ε
AND hard_recall - soft_recall >= -recall_loss_max.
Otherwise prefer a soft boost.
Choisissez le gain minimal de précision utile, l’intervalle d’incertitude, ε et la limite de perte de recall à partir du préjudice lié à l’exclusion d’une réponse autrement éligible, du bénéfice d’une précision accrue et de la taille de l’échantillon de test. Ce sont des critères de release locaux, pas des seuils universels. Un article dédié à cet arbitrage est prévu ; consultez les suivis listés à la fin.
Partie 6 : évaluation de la génération
Les métriques de retrieval indiquent que le système pourrait répondre correctement. Elles n’indiquent pas qu’il l’a fait. Les métriques de génération comblent cette lacune.
Faithfulness et groundedness
La faithfulness de RAGAS décompose la réponse en claims atomiques (énoncés factuels courts et autonomes), puis vérifie chacun d’eux par rapport au contexte récupéré au moyen d’un LLM judge :
La faithfulness vérifie le support dans les éléments fournis ; elle n’établit pas que ces éléments sont corrects ou suffisants pour répondre à la question. Enregistrez séparément les réponses sans claim et rapportez leur nombre au lieu d’attribuer une faithfulness parfaite à une réponse vide.
La documentation actuelle de Ragas recommande l’API collections ci-dessous. Dans un projet uv, installez ragas et openai avec uv add ragas openai, définissez OPENAI_API_KEY et enregistrez ceci dans un script à exécuter avec uv run. Le script effectue des appels au fournisseur et entraîne leur facturation ; le score est le résultat d’un judge, pas une constante attendue déterministe. Épinglez les dépendances résolues dans votre lockfile.
import asyncio
from openai import AsyncOpenAI, omit
from ragas.llms import llm_factory
from ragas.metrics.collections import Faithfulness
async def main():
async with AsyncOpenAI() as client:
llm = llm_factory(
"gpt-5.6-terra", client=client, reasoning_effort="none",
max_tokens=omit, max_completion_tokens=1024,
temperature=omit, top_p=omit,
)
scorer = Faithfulness(llm=llm)
result = await scorer.ascore(
user_input="How many moons does Mars have?",
response="Mars has two moons, Phobos and Deimos.",
retrieved_contexts=["Mars has two moons named Phobos and Deimos."],
)
print(result.value)
if __name__ == "__main__":
asyncio.run(main())
Cet exemple utilise GPT-5.6 Terra comme candidat actuel avec des structured outputs ; la factory Ragas transmet les arguments du modèle. Ragas 0.4.3 ne reconnaît pas les noms de génération GPT avec points dans son mapper de limites de tokens. L’exemple omet explicitement son ancien max_tokens et les valeurs par défaut d’échantillonnage, et fournit max_completion_tokens ; vérifiez la requête sortante lors de la mise à niveau de l’adapter. Désactiver le reasoning rend la configuration explicite, mais ne la valide pas. Comparez les faux positifs, faux négatifs et coûts du judge à des annotations humaines avant de remplacer un judge moins coûteux et calibré.
Voici la même boucle dépliée avec un judge de substitution déterministe afin d’en voir la forme end-to-end.
def extract_claims(answer: str) -> list[str]:
# Production: an LLM call that decomposes the answer.
# Demo: split on sentence-final punctuation.
return [c.strip() for c in answer.replace("?", ".").replace("!", ".").split(".") if c.strip()]
def verify_claim(claim: str, context: str) -> bool:
# Production: an NLI (natural-language inference) model or LLM judge.
# Demo: a deterministic stand-in so the example runs offline.
entailed_pairs = {
"Mars has two moons": True,
"Phobos and Deimos orbit Mars": True,
"Mars has a thick atmosphere": False, # unsupported by context
"Curiosity landed in 2012": True,
}
for k, v in entailed_pairs.items():
if k.lower() in claim.lower() or claim.lower() in k.lower():
return v
words = [w.lower() for w in claim.split() if len(w) > 3]
return all(w in context.lower() for w in words) if words else False
context = (
"Mars has two moons, Phobos and Deimos. NASA's Curiosity rover "
"landed on Mars in 2012."
)
answer = (
"Mars has two moons. Phobos and Deimos orbit Mars. "
"Mars has a thick atmosphere. Curiosity landed in 2012."
)
claims = extract_claims(answer)
verdicts = [(c, verify_claim(c, context)) for c in claims]
faithfulness = sum(1 for _, ok in verdicts if ok) / len(verdicts) if verdicts else None
for c, ok in verdicts:
print(f" [{'✓' if ok else '✗'}] {c}")
print(f"faithfulness = {faithfulness:.2f}" if faithfulness is not None else "no_claims")
# faithfulness = 0.75 (one unsupported claim about the atmosphere)
assert faithfulness == 0.75
assert extract_claims("") == []
La structure est importante. En production, verify_claim devient un modèle NLI ou un appel à un LLM. Conservez la structure extraction–vérification–agrégation, mais validez séparément l’extraction et l’entailment, et enregistrez les claims absents ou les jugements échoués. Le substitut offline ci-dessus est codé en dur pour ces exemples ; ce n’est pas un détecteur de factualité.
Extraction et vérification end-to-end des claims sur des réponses SciFact générées : notebook 05 ; module : evaluation/faithfulness.py. Le dépôt exécute la même boucle avec deux familles de judges — le modèle du générateur lui-même et un judge d’une autre famille (RAG_EVALS_JUDGE_MODEL) — ainsi qu’une baseline lexicale déterministe, afin de montrer où les familles divergent.
Une alternative spécialisée au LLM-as-judge est HHEM-2.1-Open (Hughes Hallucination Evaluation Model, Vectara), un classifier fine-tuné pour la détection des hallucinations. Sa model card documente le checkpoint, le score brut de 0 à 1 qu’il émet et les résultats de balanced accuracy sur AggreFact et RAGTruth. Aucun seuil de décision par défaut n’est publié ; c’est à vous de le choisir. Considérez ces éléments comme des preuves issues de la model card, et non comme une garantie sur votre corpus : calibrez le seuil sur des annotations locales et comparez-le à votre judge choisi avant le déploiement.
Évaluation par faits atomiques
FActScore (Min et al., EMNLP 2023) décompose les générations longues en faits atomiques, récupère des éléments de preuve pour chaque fait, annote chacun comme supported / not-supported et rapporte la fraction étayée :
Implémentation de référence : shmsw25/FActScore. Cette méthode fonctionne bien pour les biographies, les résumés et autres sorties longues. Attention : des faits triviaux et répétitifs peuvent gonfler le score, et des statements vrais individuellement peuvent former une réponse trompeuse. MontageLie (EMNLP 2025) teste cette faiblesse via des relations et un ordre trompeurs entre statements vrais. VeriScore gère les claims comportant des modificateurs nécessaires ; le filtre Core aide à prévenir le fact-padding.
Exactitude des citations
Suivez la précision des citations (les spans cités étayent effectivement le claim) et le recall des citations (les claims qui devraient être cités le sont) :
Le TREC 2024 RAG Track définit un protocole reproductible d’évaluation du support. Thakur et al. (SIGIR 2025) rapportent environ 56 % d’accord avec des jugements humains réalisés from scratch et 72 % dans une autre condition où les humains éditaient ensuite les prédictions du LLM. Ce dernier chiffre correspond à une annotation assistée, et non à une preuve indépendante d’une amélioration de la précision du judge. Conservez la condition d’annotation associée au chiffre. Pour une approximation automatisée, ALCE (Gao et al., EMNLP 2023) implémente la précision et le recall des citations avec une vérification fondée sur NLI.
Correction, complétude et refus
- Correction de la réponse par rapport à une référence : l’exact match ou le token-F1 peuvent convenir aux réponses courtes. Pour les réponses plus longues, vérifiez les relations factuelles, entités, quantités, négations et informations requises par rapport à des références relues. BERTScore et le cosinus des embeddings mesurent la similarité ; un nombre ou une négation erroné peut conserver un score élevé.
AnswerCorrectnessde Ragas combine la comparaison factuelle et la similarité au lieu de les assimiler. - Complétude par nuggets : un nugget est une unité d’information pertinente, les nuggets vitaux étant distingués des nuggets optionnels utiles. Une question sur une date de fondation peut exiger l’année ; le nom du fondateur n’est pas automatiquement requis. AutoNuggetizer construit et affine des nuggets à partir de pools de documents évalués, puis vérifie leur présence dans les réponses générées. Son premier rapport TREC 2024 couvrait 21 topics et 45 runs. La synthèse TREC 2025, publiée en mars 2026, étend le protocole aux requêtes narratives et évalue la pertinence du retrieval, la complétude des réponses et l’attribution. Il s’agit de protocoles publics d’évaluation, pas d’une preuve que chaque système RAG en production nécessite la même rubrique de nuggets.
- Comportement de refus : annotez si les éléments fournis permettent une réponse, puis mesurez les refus corrects parmi tous les refus et les refus parmi les cas où une abstention est attendue. NoMIRACL (Findings of EMNLP 2024) teste la robustesse face à des passages fournis pertinents et non pertinents ; il ne prouve pas que le corpus entier ne contient aucune réponse. Séparez les échecs de retrieval des requêtes véritablement hors périmètre dans votre propre suite.
Vérification post-génération
Les gains de fiabilité les moins coûteux proviennent souvent de contrôles postérieurs déterministes plutôt que de modèles plus grands.
-
Signalement des entités absentes : enregistrez les chaînes d’entités nommées de la réponse absentes d’une chaîne de contexte normalisée (par exemple,
spaCyetentsde celui-ci, plus une correspondance exacte). Il s’agit d’un signal peu coûteux et spécifique au domaine pour les chaînes d’entités absentes, pas d’un contrôle de grounding : il ne peut pas établir l’identité, la relation, la négation, le temps ou la provenance. Mesurez sa précision et son recall sur des annotations locales avant de l’utiliser pour approuver une release, et conservez l’entailment au niveau des claims ou la revue humaine pour la vérification.def unseen_entity_flag(answer: str, context: str, entities: list[str]) -> bool: return any(entity.lower() not in context.lower() for entity in entities) context = "Paris was not founded in 1994." answer = "Paris was founded in 1994." assert not unseen_entity_flag(answer, context, ["Paris"]) claim_supported = False # human label or an NLI verdict, not the lexical flag assert not claim_supported -
Vérification des claims : extrayez les claims, exécutez un NLI par rapport au contexte, puis échouez ou signalez ceux situés sous le seuil. Modèles NLI pour la faithfulness :
cross-encoder/nli-deberta-v3-large,MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli. Cette approche ajoute de la latence. Elle vaut la peine dans les domaines à forts enjeux. -
Self-consistency (Wang et al., ICLR 2023) : échantillonnez plusieurs générations à une température > 0 ; rapportez le taux d’accord (par exemple, la proportion de générations correspondant à la réponse modale ou le BERTScore pairwise) ; choisissez le nombre d’échantillons à partir de la courbe stabilité–coût et signalez les réponses à faible accord pour une revue humaine.
-
Calibration de la confiance : collectez une confiance verbalisée (« À quel point êtes-vous sûr, de 0 à 1 ? ») et comparez-la à la correction réelle sur le jeu d’évaluation. Tracez une courbe de calibration et rapportez l’Expected Calibration Error : , où sont les intervalles de confiance. Implémentations :
netcal,torchmetrics.CalibrationError. Un modèle qui annonce une confiance de 0,9 devrait être correct dans environ 90 % des cas comparables ; mesurez l’écart au lieu de supposer la calibration.
Partie 7 : évaluation du RAG fondé sur une ontologie
Les métriques standard ci-dessus couvrent le RAG sur corpus ouvert. Si votre RAG effectue le retrieval sur une ontologie structurée, une taxonomie ou un knowledge graph, ces métriques sont nécessaires mais insuffisantes. Exemples : produits d’un catalogue, pathologies de SNOMED, composants d’une BOM et techniques de sécurité de MITRE ATT&CK. Vous devez également mesurer la couche ontologique.
Précision du entity linking
La première tâche consiste à associer une mention de la requête à une entité de l’ontologie (« Aspirin » → wikidata:Q18216, « the 737 » → aircraft:Boeing_737).
- Précision/recall/F1 au niveau des mentions : métriques standard, par rapport aux spans de mentions gold (à calculer avec
seqevalou un comparateur d’ensembles de spans). - Précision de la désambiguïsation : parmi les mentions correctement détectées, quelle fraction est associée au bon ID d’entité ? Les références publiques comprennent ReFinED, REL et GENRE ; des benchmarks comme AIDA-CoNLL et BELB montrent que les résultats varient selon le système et le domaine.
- Gestion de NIL : précision/recall sur les cas « entité absente de l’ontologie ». Mesurez séparément les associations abusives avec des entités proches mais incorrectes et les abstentions correctes.
Évaluation tenant compte de la hiérarchie
La précision brute traite « prédire Sedan quand la vérité est Hatchback » de la même manière que « prédire Sedan quand la vérité est Submarine ». Ces erreurs ne sont pas équivalentes.
-
Précision/recall/F1 hiérarchiques (Kosmopoulos et al., 2015) : accordez un crédit aux ancêtres communs dans le DAG de l’ontologie. Avec le nœud prédit et tous ses ancêtres, et le nœud réel et tous ses ancêtres :
Implémentez ceci avec
networkxsur le graphe ontologique : enrichissez chaque prédiction et chaque label avec ses ancêtres, puis calculez les intersections d’ensembles ci-dessus. -
Similarité de Wu-Palmer entre l’entité prédite et l’entité gold dans la taxonomie (Wu & Palmer, 1994) :
où LCA est le plus bas ancêtre commun dans la taxonomie. Disponible directement dans NLTK pour WordNet (
from nltk.corpus import wordnet as wn; wn.synset("car.n.01").wup_similarity(wn.synset("truck.n.01"))) ; pour les taxonomies personnalisées, calculez le LCA avecnetworkx. -
Taux de confusion entre frères et parents : suivez séparément les confusions avec des frères, des parents et des enfants —
count_sibling / total_errors,count_parent / total_errors,count_descendant / total_errors. Utilisez des exemples relus pour vérifier si les erreurs entre frères proviennent de mentions ambiguës ou si les erreurs vers les parents sont dues à une surgénéralisation.
Taux de fausse exclusion par le filtre (rappel, désormais critique)
Dans les systèmes fondés sur une ontologie, les filtres stricts proviennent souvent de l’ontologie elle-même (« récupérer uniquement les documents tagués avec la catégorie X »). La métrique de taux d’exclusion (définie dans la Partie 5) devient un signal de correction primaire. Une prédiction de catégorie erronée peut annuler le recall ; le taux d’exclusion attribue cette perte au filtre.
Conformité de la génération contrainte
Lorsque votre sortie doit respecter une ontologie (chaque nom d’entité de la réponse doit être un membre valide de l’ontologie ; chaque prédicat doit provenir d’un vocabulaire fermé), mesurez :
- Taux de validité du schéma : pourcentage des sorties qui sont parsées et validées par rapport au schéma de l’ontologie. Validez-les avec
jsonschemaoupydantic. JSONSchemaBench est le benchmark public des structured outputs en général ; pour les schémas spécifiques à une ontologie, construisez votre propre validator. - Conformité au vocabulaire : pourcentage des entités nommées de la sortie qui sont des IDs valides de l’ontologie — un simple contrôle d’appartenance à l’ensemble du vocabulaire fermé.
- Conformité sémantique : une sortie syntaxiquement valide peut tout de même sélectionner la mauvaise entité parmi des entités valides. Associez la conformité à la correction de la réponse en aval.
Les frameworks de constrained decoding (Outlines, XGrammar, Guidance, OpenAI Structured Outputs) sont conçus pour imposer la validité du schéma. JSONSchemaBench compare l’efficacité, la couverture et la qualité entre implémentations. Relancez ses cas correspondant à vos schémas et à votre backend de serving, car la couverture et la latence dépendent des deux.
Auditabilité
Pour les systèmes fondés sur une ontologie dont les réponses sont soumises à revue :
- Complétude des citations : pourcentage des claims factuels disposant d’au moins une citation vérifiable.
- Profondeur de provenance : pourcentage des citations qui remontent jusqu’à un document source doté d’un ID stable, et pas seulement à un hash de chunk.
- Taux de reproductibilité : réexécuter la même requête sur un snapshot fixe renvoie la même réponse. Épinglez la version du modèle, le runtime, la configuration de décodage et le seed, puis définissez le taux de répétition requis selon les besoins d’auditabilité du workflow. Une température nulle ne garantit pas à elle seule le déterminisme. Un échec peut provenir de la génération, du runtime de serving ou de n’importe quelle étape en amont.
Partie 8 : évaluation au niveau système
Qualité globale des réponses
- LLM-as-judge (Zheng et al., NeurIPS 2023) : approche d’évaluation fondée sur un modèle et capable de passer à l’échelle. G-Eval (Liu et al., EMNLP 2023) génère des étapes d’évaluation à partir de la tâche et des critères, puis pondère les niveaux de notation par leurs probabilités de tokens : . Ce sont des probabilités, pas des log probabilities. L’accord dépend du judge, de la tâche, du prompt et du jeu de calibration.
- Préférence pairwise : présentez au judge la réponse A contre la réponse B et enregistrez sa préférence. Cette méthode remplace une note absolue par une décision comparative, mais nécessite toujours une calibration par rapport aux préférences humaines. MT-Bench a rapporté un accord du judge GPT-4 supérieur à 80 % avec les préférences humaines et l’accord entre humains dans ses conditions de benchmark ; ne transposez pas ce taux à un autre domaine sans calibration.
Le LLM-as-judge présente de vrais biais :
- Biais de position : mesurez la sensibilité à l’ordre sur des cas annotés par des humains pour le judge et la tâche sélectionnés. La randomisation ou l’agrégation après inversion de l’ordre peut aider certains couples modèle/tâche, mais ne les conservez que si elles améliorent l’accord humain local ; l’étude contrôlée de 2026 a trouvé que l’inversion de position nuisait aux résultats sur ses cas adversariaux.
- Biais de verbosité : les judges peuvent confondre longueur et qualité. L’étude contrôlée citée de 2026, version 2 a constaté des comportements hétérogènes sur les paires d’expansion : trois judges préféraient les réponses plus longues, Claude préférait les réponses concises et GPT-4o était approximativement neutre. Les cinq judges ont bien fonctionné sur les contrôles par troncature. Ces résultats dépendent du benchmark ; indiquez à votre judge comment traiter la complétude et le remplissage, puis rapportez les performances contrôlées par la longueur selon votre propre rubrique.
- Risque d’auto-préférence : Zheng et al. ont observé dans leurs données un taux de victoire de GPT-4 supérieur de 10 % lorsqu’il s’évaluait lui-même et un taux supérieur de 25 % pour Claude-v1 dans la même situation, mais ont conclu que les données limitées et les faibles différences ne permettaient pas d’établir un biais d’auto-amélioration. Comparez les judges de même famille et de familles différentes à des annotations humaines locales ; choisissez le judge le mieux calibré au lieu de supposer qu’une configuration est sûre.
Recette pratique : sélectionnez un judge sur des données de calibration annotées par des humains, masquez l’identité des modèles, mesurez la sensibilité à l’ordre et explicitez la politique de longueur dans la rubrique. Ne répétez les cas que lorsque les échantillons supplémentaires réduisent sensiblement l’incertitude. Pour les évaluations à forts enjeux, comparez les judges de même famille et de familles différentes, puis analysez les désaccords par rapport aux annotations humaines.
Schema-Guided Reasoning pour les judges
Les sorties libres sont une source de variation dans les runs des judges. Deux runs sur la même réponse peuvent organiser la rubrique différemment et produire des scores différents. Le Schema-Guided Reasoning (SGR) rend cette rubrique explicite : définissez les étapes de l’évaluation comme un schéma Pydantic, puis utilisez une sortie contrainte via Outlines, XGrammar, les structured outputs de vLLM ou OpenAI response_format pour imposer les contraintes de schéma prises en charge. L’ordre des champs peut aider un reviewer à inspecter l’enregistrement, mais ne prouve pas que le modèle a raisonné dans cet ordre.
Pour l’évaluation RAG, le schéma décompose le jugement en champs explicites et auditables au lieu de laisser le modèle passer directement à un nombre :
from pydantic import BaseModel, Field, ValidationError, model_validator
from typing import Literal
class FaithfulnessJudgment(BaseModel):
extracted_claims: list[str] = Field(
description="Atomic factual claims in the answer, one per item."
)
supported_claims: list[str] = Field(
description="Subset of extracted_claims that are entailed by the context."
)
unsupported_claims: list[str] = Field(
description="Subset that is NOT entailed by the context."
)
outcome: Literal["scored", "no_claims"]
failure_mode: Literal[
"none", "fabrication", "overgeneralization", "wrong_entity", "stale_fact"
]
rationale: str
@model_validator(mode="after")
def require_claim_partition(self):
for claims in (self.extracted_claims, self.supported_claims, self.unsupported_claims):
if any(not claim.strip() for claim in claims):
raise ValueError("claims must contain non-whitespace text")
extracted = set(self.extracted_claims)
supported = set(self.supported_claims)
unsupported = set(self.unsupported_claims)
if not extracted:
if supported or unsupported or self.outcome != "no_claims":
raise ValueError("an empty extraction must be a no_claims outcome")
return self
if (
len(extracted) != len(self.extracted_claims)
or len(supported) != len(self.supported_claims)
or len(unsupported) != len(self.unsupported_claims)
or supported & unsupported
or supported | unsupported != extracted
or self.outcome != "scored"
):
raise ValueError("verdict lists must be a complete, disjoint claim partition")
return self
@property
def score(self) -> float | None:
if not self.extracted_claims:
return None
return len(self.supported_claims) / len(self.extracted_claims)
valid = FaithfulnessJudgment(
extracted_claims=["Mars has two moons", "Mars has a thick atmosphere"],
supported_claims=["Mars has two moons"],
unsupported_claims=["Mars has a thick atmosphere"],
outcome="scored",
failure_mode="fabrication",
rationale="The atmosphere claim lacks support.",
)
assert valid.score == 0.5
empty = FaithfulnessJudgment(
extracted_claims=[],
supported_claims=[],
unsupported_claims=[],
outcome="no_claims",
failure_mode="none",
rationale="Score refusal correctness separately.",
)
assert empty.score is None
try:
FaithfulnessJudgment(
extracted_claims=["Mars has two moons"],
supported_claims=["Mars has two moons"],
unsupported_claims=["Mars has two moons"],
outcome="scored",
failure_mode="none",
rationale="Contradictory verdict.",
)
except ValidationError:
pass
else:
raise AssertionError("overlapping verdicts must fail validation")
for blank in ("", " ", "\t\n"):
for verdict_field in ("supported_claims", "unsupported_claims"):
fields = {"extracted_claims": [blank], "supported_claims": [], "unsupported_claims": []}
fields[verdict_field] = [blank]
try:
FaithfulnessJudgment(
**fields, outcome="scored", failure_mode="none", rationale="Blank claim."
)
except ValidationError:
pass
else:
raise AssertionError("blank claims must fail validation")
Ce contrat illustratif ne calcule le score qu’après que les listes de verdicts forment une partition complète et disjointe des claims extraits. Un résultat sans claim n’a pas de score de faithfulness et doit rester exclu de l’agrégat de faithfulness ; rapportez son nombre et évaluez séparément la correction des refus afin que les abstentions ne puissent pas améliorer silencieusement la moyenne. Préservez ces contrôles sémantiques en production ; la sortie contrainte garantit la forme, pas l’absence de biais du verdict. Le modèle Pydantic rend également une modification de rubrique visible comme un diff de code, tandis que la calibration humaine évalue le jugement lui-même.
Cette approche fonctionne pour toute évaluation fondée sur une rubrique, pas uniquement pour la faithfulness. La préférence pairwise, le support des citations et la correction des refus bénéficient tous du même traitement.
Un judge de rubrique simplifié, accompagné d’exemples pairwise, de biais de position et de comparaison inter-familles, se trouve dans le notebook 07 ; module : evaluation/llm_judge.py. Dans la révision relue, la fonction nommée g_eval demande une note entière unique ; elle ne génère pas d’étapes d’évaluation et ne calcule pas de scores pondérés par les probabilités, et ne reproduit donc pas le protocole G-Eval. Le sweep du benchmark (make benchmark dans le dépôt) connecte trois modèles (gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash) à un A/B pairwise avec judges tournants : chaque judge évalue les réponses issues des deux autres familles de modèles. Cette topologie permet d’obtenir des résultats pairwise inter-familles ; mesurer l’auto-préférence nécessite également des conditions de même famille et de familles différentes comparées à des annotations humaines locales.
Latence et coût
- p50, p95, p99 à chaque étape du pipeline. Choisissez le percentile SLO et le seuil d’alerte selon le parcours utilisateur, le volume de trafic et le budget d’erreur.
- Time-to-first-token contre temps total de génération. Pour une UX en streaming, les utilisateurs se soucient du TTFT.
- Décomposition par étape : retrieval, reranking, génération, post-traitement. Utilisez la trace pour localiser la queue au lieu de supposer quelle étape en est responsable ; indiquez le device et la taille de batch du reranker lors de la comparaison des runs.
- Total $/requête = embeddings + retrieval + rerank + génération + stockage amorti. Suivez p50 et p99 ; c’est dans la longue traîne que le budget disparaît.
- Taux de hit des caches au niveau du cache d’embeddings, du cache de retrieval et des niveaux de KV-cache. Définissez des objectifs distincts à partir de la répétition observée, de la politique d’invalidation et du coût évité à chaque couche.
Les p50/p95/p99 par étape avec une décomposition sont intégrés au notebook 08 et au runner dans evaluation/latency.py ; le rapport de benchmark combine latence et faithfulness dans une matrice unique que vous pouvez relancer avec make benchmark.
Tests A/B
- Unité de randomisation : choisissez l’unité selon l’estimand, les effets de report et les interférences. Utilisez une affectation par utilisateur ou par session lorsque des expositions répétées peuvent modifier le comportement ou créer une UX incohérente. Une affectation par requête n’est défendable que lorsque ces effets sont négligeables et que l’analyse modélise les observations répétées.
- Métriques primaires, guardrails et exploratoires : préenregistrez-les. Choisissez la mesure primaire à partir du résultat produit ; les proxys de satisfaction comprennent les pouces, les régénérations et le temps passé. Traitez la latence et le coût comme des guardrails lorsqu’ils contraignent l’expérience.
- Taille de l’échantillon : effectuez une analyse de puissance avant le lancement à partir de l’effet minimal à détecter, de la variance de référence, de l’unité d’affectation et de la règle d’arrêt.
Partie 9 : construction du jeu de test
Une métrique n’est jamais meilleure que le jeu de test sur lequel elle est exécutée. Si votre golden set couvre trois intentions et que le trafic de production en couvre douze, le Recall@10 ne mesure que ces trois intentions. Pire encore, un jeu de test surajusté à des questions faciles (« Quelle est la politique de remboursement de l’entreprise ? ») peut approuver un système qui échoue sur les questions difficiles (« Éligibilité au remboursement pour une annulation partielle au titre du Digital Services Act européen de 2023, facturée en EUR et effectuée depuis l’Irlande ? »). Le score agrégé augmente alors que le système continue d’échouer sur une partie importante du trafic de production.
Des labels de pertinence incomplets peuvent fausser le recall dans les deux directions. Si l’ensemble réellement pertinent est {a, b} mais que les labels ne contiennent que {a}, récupérer {a} obtient 1,0 au lieu de 0,5 ; récupérer {b} obtient zéro au lieu de 0,5. Versionnez les jugements et examinez les éléments de preuve nouvellement récupérés et non jugés avant d’interpréter un delta.
Construisez d’abord le jeu de test autour de la distribution et de la difficulté réelles des requêtes. Choisissez ensuite les métriques qui réagissent aux modes de défaillance ciblés et ajustez le système à partir de celles-ci.
Génération de requêtes synthétiques
Utilisez un LLM pour générer des questions à partir de votre corpus :
- Par chunk : « Générez 3 questions qu’un utilisateur pourrait poser et auxquelles ce chunk répond. »
- Multi-hop : échantillonnez deux chunks et générez une question nécessitant les deux.
- Adversarial : générez des questions contenant des entités distractrices, des formulations quasi-dupliquées et des mentions ambiguës.
La génération de tests Ragas utilise des scénarios fondés sur des graphes, avec des requêtes single-hop et multi-hop et des besoins d’information spécifiques ou abstraits. DataMorgana génère des benchmarks synthétiques configurables sur plusieurs catégories d’utilisateurs et de questions. Les données synthétiques sont utiles pour démarrer à froid et tester la couverture. Elles ne peuvent pas remplacer les requêtes réelles des utilisateurs.
Construction du golden dataset
Les données sélectionnées par des humains ancrent le golden set.
- Échantillonnez les requêtes réelles des utilisateurs (ou des requêtes simulées avant le lancement), en les stratifiant par intention.
- Demandez à des SMEs de répondre à chaque question et d’identifier le ou les documents contenant la réponse.
- Dimensionnez l’ensemble à partir de la matrice de couverture et de l’intervalle de confiance requis pour les décisions de release ; la couverture compte davantage qu’un nombre de requêtes emprunté.
- Refaites la curation lorsque la cadence de release, les signaux de drift, le risque métier et la capacité d’annotation le justifient.
Séparez les requêtes de développement, la calibration du judge, la mesure de release en held-out et les échantillons de monitoring. Regroupez les documents sources et les sessions partagés avant le split. Dès qu’une requête ou un label guide l’ajustement, il s’agit de données de développement. Réservez des cas intacts pour valider la configuration et le judge choisis.
Enregistrez le corpus, la requête, les versions des labels de pertinence et des politiques, l’unité de k, le budget du contexte fourni, les révisions du scorer et du judge et les règles d’agrégation. Comparez les deltas par requête appariés sur la même population, avec l’incertitude et le nombre de cas par tranche. Incluez chaque requête attendue : un timeout, un résultat manquant ou une réponse du judge impossible à scorer doit rester visible dans le suivi des complétions et des erreurs. Ne laissez pas un score de release s’améliorer en supprimant silencieusement les échecs. ARES propose une approche de recherche utilisant des judges automatisés, une validation humaine et une inférence assistée par les prédictions pour estimer le système lorsque les annotations sont rares ; une suite locale relue peut commencer plus simplement.
Jeux de tests adversariaux
- Contrefactuels : remplacez des entités clés dans la requête. Le système récupère-t-il les bons chunks pour la requête modifiée ?
- Distracteurs : requêtes pour lesquelles le corpus contient une réponse plausible mais erronée qui ne devrait pas être récupérée. C’est ce que RGB (Chen et al., AAAI 2024) teste sous contrainte : robustesse au bruit, rejet des négatifs, intégration de l’information et robustesse contrefactuelle.
- Négation et quantificateurs : requêtes contenant « ne… pas », « sauf » et « uniquement ». Les retrievers denses ont souvent des difficultés avec ces formulations.
- Hors périmètre : requêtes auxquelles le corpus ne répond pas. Le système devrait dire « Je ne sais pas », et non halluciner. NoMIRACL fournit des tests de pertinence et d’answerability au niveau des passages ; ajoutez des labels séparés de hors périmètre au niveau du corpus. Évaluez explicitement l’abstention sur vos types de requêtes de production.
Couverture et évaluation continue
- Construisez une matrice de couverture : intention de requête × type de document × branche de l’ontologie. Une requête par cellule constitue un inventaire initial de couverture, mais ne fournit pas une puissance statistique suffisante pour une décision de release. Les cellules vides révèlent les lacunes de couverture ; dimensionnez les tranches remplies selon l’incertitude tolérable.
- Exécutez un sous-ensemble de régression borné et rapide à chaque PR, puis la suite complète selon une cadence plus lente.
- Planifiez l’évaluation complète du golden set en fonction de la cadence de release et du coût d’évaluation ; exécutez-la sur les release candidates.
- Planifiez l’évaluation du drift en fonction du volume de trafic, des changements attendus et du risque. Utilisez un échantillon de production glissant et stratifiez-le selon le feedback au lieu de modifier silencieusement la distribution cible.
Partie 10 : monitoring en production
La suite d’évaluation que vous livrez décrit le système au lancement. Le trafic de production évolue ensuite.
Feedback implicite et explicite
- Traitez les événements implicites comme des signaux candidats, et non comme des KPI de qualité positifs ou négatifs, tant qu’ils ne sont pas corrélés à une revue en aveugle ou à un feedback explicite sur un échantillon local.
- Click-through / taux d’ouverture des sources citées (si votre UI les expose).
- Temps passé sur la réponse.
- Taux de régénération : pourcentage de réponses que l’utilisateur demande à nouveau ou fait refaire au système. Considérez-le comme un signal d’insatisfaction parmi d’autres et calibrez-le par rapport à des conversations relues.
- Taux de copie / partage / export : signaux implicites candidats pouvant représenter l’utilité, la vérification, le handoff ou l’insatisfaction. Mesurez leur association et leur intervalle de confiance avant de leur attribuer une direction.
- Patterns de suivi : utilisez « Vous êtes sûr ? » ou « Et concernant X ? » comme strates de revue, puis annotez leur association avec la méfiance ou un besoin non résolu.
- Pouces vers le haut/bas avec des catégories de motifs facultatives (incorrect, incomplet, hors sujet, nuisible, lent). Les modifications inline peuvent préserver davantage de contexte diagnostique ; évaluez cette valeur sur des échantillons relus.
Détection du drift
- Drift des requêtes : comparez les embeddings des requêtes à une fenêtre de référence au moyen de MMD ou d’un classifier de référence contre courant validé. KL nécessite un estimateur de probabilité défini, comme des histogrammes choisis ; les coordonnées brutes des embeddings ne sont pas des probabilités. Calibrez les alertes sur des shifts connus, puis inspectez les tranches touchées.
- Drift des embeddings : épinglez une représentation et un jeu de probes, puis mesurez la stabilité des voisins et la qualité du retrieval. Des versions différentes de modèle ne partagent pas nécessairement les mêmes dimensions ou la même base de coordonnées ; leur cosinus inter-version peut donc être dépourvu de sens. Migrez ensemble les encodeurs de requêtes et de documents, évaluez le nouvel index et conservez des snapshots versionnés pour permettre un rollback.
- Drift des performances : suivez dans le temps des métriques équivalentes à celles de la production (taux de régénération par intention). Des variations soudaines et progressives suggèrent des hypothèses différentes, mais leur forme n’établit pas la cause ; inspectez les changements de données, de trafic, de fournisseur, de politique et de déploiement.
Évaluation shadow et human-in-the-loop
Exécutez le système candidat en parallèle de la production, comparez les sorties offline et ne les servez pas aux utilisateurs. Cette approche peut révéler des régressions avant le lancement. L’inférence shadow consomme tout de même des ressources et peut appeler des outils : isolez les ressources, supprimez les écritures et vérifiez que la comparaison ne dégrade pas la latence de production.
Pour une revue human-in-the-loop (HITL) :
- Échantillonnez les sorties à faible confiance dans une file de revue.
- Incluez un échantillon aléatoire du trafic de production pour une revue en aveugle ; définissez son taux selon le volume de trafic, le risque et la capacité des reviewers.
- Suréchantillonnez les sorties marquées d’un pouce vers le bas en complément de l’échantillon aléatoire.
- Utilisez les sorties relues pour enrichir le golden set.
Le jeu minimal de guardrails
Choisissez les priorités et les seuils d’alerte selon le préjudice utilisateur, les SLO et les performances validées des détecteurs. Les signaux candidats comprennent :
- Score Faithfulness/HHEM inférieur au seuil sur un échantillon de production glissant.
- Latence p95 supérieure au SLO.
- Taux de fausse exclusion par le filtre supérieur au seuil (sur échantillon).
- Taux de régénération hors d’une bande de contrôle calibrée localement, tenant compte de la taille de fenêtre, du volume de trafic, de la saisonnalité et du budget de fausses alertes.
- Coût/requête supérieur au budget.
Utilisez la chronologie des releases pour guider le diagnostic, puis vérifiez-la avec les traces et les tranches affectées. Un déploiement peut coïncider avec un drift du trafic, et un changement de fournisseur ou de données peut se produire sans release applicative. Les alertes sont des éléments à investiguer ; mesurez le délai entre leur déclenchement et les retours des utilisateurs.
Réserves
- Les cibles sont locales, pas universelles. Tout chiffre qualifié d’illustratif dans ce guide correspond à une configuration ou à un résultat présenté en exemple, pas à un seuil de release. Calibrez les seuils selon votre domaine, les enjeux, l’incertitude du jeu d’évaluation et les attentes des utilisateurs.
- L’espace des frameworks évolue rapidement. Les versions de HHEM, les noms des métriques RAGAS, les model cards et l’ordre des leaderboards peuvent changer après la publication. Revérifiez les sources liées et refaites les benchmarks avant de vous engager.
- Les chiffres d’accord des LLM judges comportent des astérisques. Le chiffre de 80 % entre GPT-4 et les humains provient des conditions de MT-Bench / Chatbot Arena. Il n’établit pas un accord dans un domaine de niche ou une tranche adversariale. Utilisez les judges comme multiplicateurs de force, pas comme remplacement des contrôles ponctuels.
- Les gains des benchmarks fournisseurs sont souvent difficiles à reproduire indépendamment. Reproduisez-les sur vos propres données avant de croire un chiffre, en particulier pour les rerankers et systèmes OCR récents.
- Aucune métrique ne remplace l’examen des sorties. Planifiez une revue en aveugle d’un échantillon aléatoire de production selon le trafic, le risque et la capacité des reviewers. Les métriques permettent de faire passer cette habitude à l’échelle ; elles ne la remplacent pas.
À venir dans cette série
Ceci était l’index. Voici les articles suivants prévus :
- Soft Boosts contre Hard Filters : analyse approfondie du taux de fausse exclusion par le filtre, avec du code, de vrais exemples de production et un framework de décision.
- Le chunking est la variable cachée : expérience contrôlée comparant le chunking récursif, sémantique, late et structurel sur trois corpus.
- Sélection d’un reranker en 2026 : BGE contre Cohere contre ZeRank contre les cross-encoders actuels, comparés sur le coût, la latence et le gain.
- RAG fondé sur une ontologie : walkthrough end-to-end : construction du harness d’évaluation complet pour un système de retrieval fondé sur les entités.
- LLM-as-judge sans le piège de l’auto-préférence : recettes pratiques pour une évaluation automatisée non biaisée.
- Évaluation online en production : patterns d’instrumentation, politiques d’alerte et dashboards qui détectent les vraies régressions.
Références
Frameworks et benchmarks
- Es et al., Ragas: Automated Evaluation of Retrieval Augmented Generation, 2023.
- Documentation RAGAS et GitHub.
- Saad-Falcon et al., ARES: An Automated Evaluation Framework for Retrieval-Augmented Generation Systems, NAACL 2024.
- TruLens, DeepEval, Arize Phoenix.
- Thakur et al., BEIR: A Heterogenous Benchmark for Zero-shot Evaluation of Information Retrieval Models, NeurIPS 2021.
- MTEB Leaderboard.
- TREC 2024 RAG Track.
- Pradeep et al., Initial Nugget Evaluation Results for the TREC 2024 RAG Track with the AutoNuggetizer Framework, 2024.
Retrieval et ranking
- Cormack, Clarke, Buettcher, Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods, SIGIR 2009.
- Gao et al., Precise Zero-Shot Dense Retrieval Without Relevance Labels (HyDE), 2022.
- Jeong et al., Adaptive-RAG: Learning to Adapt Retrieval-Augmented Large Language Models through Question Complexity, NAACL 2024.
- Anthropic, Introducing Contextual Retrieval, septembre 2024.
- Günther et al., Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models, 2024.
Génération, faithfulness et judges
- Min et al., FActScore: Fine-grained Atomic Evaluation of Factual Precision in Long Form Text Generation, EMNLP 2023.
- Liu et al., Lost in the Middle: How Language Models Use Long Contexts, TACL 2024.
- Chen et al., Benchmarking Large Language Models in Retrieval-Augmented Generation (RGB), AAAI 2024.
- Vectara, HHEM-2.1-Open hallucination evaluation model.
- Zheng et al., Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena, NeurIPS 2023.
- Thakur et al., Support Evaluation for the TREC 2024 RAG Track: Comparing Human versus LLM Judges, SIGIR 2025.
- Thakur et al., Knowing When You Don’t Know: A Multilingual Relevance Assessment Dataset for Robust Retrieval-Augmented Generation, Findings of EMNLP 2024.
- Geng et al., JSONSchemaBench: A Rigorous Benchmark of Structured Outputs for Language Models, 2025.
- Kosmopoulos et al., Evaluation Measures for Hierarchical Classification: a unified view and novel approaches, 2015.
Drift et production
- Evidently, Embedding drift detection methods compared.
Code associé
slavadubrov/rag-evals-demo— harness exécutable pour certaines métriques de cet article sur le corpus SciFact, ainsi qu’un sweep de benchmark chunking × embeddings × LLM. Notebooks 00 à 09, tests unitaires épinglant les exemples présentés ci-dessus et index Qdrant embarqué pour une exécution sans Docker.