RAG-evaluatiemetrics: retrieval, reranking en generation

Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Artikelupdate

Oorspronkelijk gepubliceerd op 10 mei 2026. Beoordeeld en bijgewerkt op 6 september 2026. De update voegt nieuwere retrieval-benchmarks en reranker-kandidaten toe, herziet de richtlijnen voor evaluation-tools en corrigeert de API-parameters in het judge-voorbeeld.

Een RAG-systeem met defecte relevantiefilters kan maanden draaien zonder een operationele alert te triggeren. Het retourneert nog steeds antwoorden en voldoet aan de latencydoelstelling, maar de antwoorden zijn gebaseerd op onvolledig bewijs. Recall@k tegen de oorspronkelijke eligible gold set maakt het verlies zichtbaar. Dashboards voor latency en availability doen dat niet.

Voor engineers die multi-stage RAG-systemen beheren of evalueren, koppelt deze reference failures in document parsing, filtering, retrieval, reranking en generation aan de metric die elk probleem identificeert. Ook laat de reference zien welke checks vóór release draaien en welke live traffic monitoren.

Wil je meteen naar de code?

De uitvoerbare slavadubrov/rag-evals-demo-repository past geselecteerde metrics toe op SciFact. make eval voert de suite uit en make benchmark vergelijkt chunking-, embedding- en LLM-configuraties. Notebooks 00–09 behandelen retrieval, filtering, generation en systeemvoorbeelden; ze implementeren niet elke check in deze reference. De demo gebruikt embedded Qdrant en vereist daarom geen Docker.

De companion is een teaching harness. De revisie die op 6 september 2026 is beoordeeld heeft nog reparaties nodig voor de verwerking van missing queries, het parsen van judge-resultaten en authorization gold data. De pairwise judge beschikt ook niet over de meegeleverde context die nodig is om support te beoordelen. De gecorrigeerde contracts en inline checks hieronder patchen die repository niet; gebruik de notebooks om de workflow te inspecteren en verifieer deze gevallen voordat je de scores als releasecriteria gebruikt.

TL;DR

  • Een bruikbare evaluation stack omvat ingestion, retrieval, generation grounding, ontology conformance en system signals. RAGAS, TruLens, DeepEval, Arize Phoenix en de TREC 2024 RAG Track bieden libraries of publieke evaluation-protocollen. Ze kiezen je metrics niet voor je.
  • Voor metadata- en ontology-grounded RAG kan een verkeerde tag of brittle hard predicate recall tot nul reduceren. Standard Recall@k detecteert het verlies wanneer de oorspronkelijke eligible gold set behouden blijft. Een filter false-exclusion metric identificeert de oorzaak. Faithfulness kan claims nog steeds scoren tegen incomplete context, maar kan de filter- of retrievaloorzaak niet diagnosticeren. Een lege refusal kan, afhankelijk van de implementatie, geen statements en NaN opleveren.

RAG-evaluation decision table

Gebruik deze tabel als startpunt voordat je een framework kiest. De juiste metric hangt af van de failure mode die je wilt detecteren, niet van de naam van de tool.

VraagMetricfamilieGebruik dit wanneerLet op
Heeft parsing de bron behouden?Extraction completeness, table/figure coveragePDF’s, slides, scans en HTML-pagina’s in de corpus komenTekst die er schoon uitziet kan captions, footnotes of table-structuur missen
Heeft retrieval het juiste bewijs gevonden?Recall@k, nDCG@k, MRR, context precision/recallJe relevante chunks of documenten kunt labelenEen hard metadatafilter kan het juiste document verwijderen voordat ranking begint
Heeft reranking de shortlist verbeterd?Reranker uplift, Precision@1, nDCG deltaCross-encoders of LLM-rankers na retrieval worden gebruiktMeet latency en cost samen met de quality gain
Heeft het antwoord het bewijs gebruikt?Faithfulness, groundedness, citation supportHet antwoord documenten citeert of feiten uit context haaltFaithfulness kan slechte parsing of slechte retrieval niet diagnosticeren
Is het systeem stabiel in productie?Drift, regeneration, fallback, p95 latency, cost per answerTraffic na launch verandertProduction telemetry heeft sampled human review nodig om gekalibreerd te blijven

Zie voor een kortere vergelijking van tools Best RAG Evaluation Tools: Ragas, DeepEval, and TruLens.

Part 1: Definieer succes vóór de architecture

Stel de eval set op vóór het architecture diagram. Daarmee krijgt elke latere componentkeuze een meetbaar doel.

Je kunt niet kiezen tussen BM25 en dense retrieval, recursive en semantic chunking, of Cohere Rerank en BGE voordat je weet wat je optimaliseert. “Betere antwoorden” is geen metric. Een illustratieve release-eis is “faithfulness ≥ 0.85 op een golden set van 200 queries die onze drie belangrijkste intents afdekt, met p95 latency < 1.5 s en een filter false-exclusion rate < 2%.” De getallen zijn placeholders; het belangrijke is dat quality, coverage, latency en filtering expliciete thresholds hebben.

Definieer de harness voordat je retrieval-code schrijft. De eerste harness zal verkeerd zijn en je zult die herzien. Een metric herzien is veel goedkoper dan een systeem herzien dat al is gedeployed.

Drie pipeline-lagen en twee run modes

Production evaluation heeft drie pipeline-lagen. Ingestion evaluation vraagt of de corpus en index de bron behouden. Query-time evaluation vraagt of rewriting, filtering, retrieval, reranking en context assembly het juiste bewijs hebben gevonden. Answer and production evaluation vraagt of de response dat bewijs heeft gebruikt en of de quality onder live traffic behouden blijft. Als je de lagen samenvoegt tot één score, kan een normalization bug verdwijnen in een acceptabele answer score.

De drie plaatsen waar een RAG-systeem bewijs kan verliezen: de corpus en index, het retrievalpad en het antwoord en live trafficDe drie plaatsen waar een RAG-systeem bewijs kan verliezen: de corpus en index, het retrievalpad en het antwoord en live traffic

Deze lagen beschrijven waar een failure optreedt. Offline en online beschrijven wanneer en tegen welke data de check draait. Offline evaluation gebruikt een vaste dataset met bekende ground truth; die is reproduceerbaar en hoort thuis bij componentselectie, A/B-vergelijkingen en CI-checks die een wijziging kunnen blokkeren. Online evaluation scoort sampled live traffic en legt regeneration, dwell time, expliciete feedback en echte query drift vast. Die evaluation is ruisachtiger en moeilijker te instrumenteren.

Gebruik beide modes waar ze waarde toevoegen: vaste corpora en querysets maken regressions reproduceerbaar; sampled live traces leggen freshness failures en drift bloot.

Component-level versus end-to-end

Er zijn twee veelvoorkomende fouten. End-to-end-only evaluation vertelt je dat het systeem defect is, maar niet waar. Component-only evaluation kan laten zien dat elk onderdeel slaagt terwijl het volledige systeem toch faalt. De oplossing bestaat uit enkele headline end-to-end metrics voor go/no-go-beslissingen, plus componentmetrics voor diagnosis. Retrievalmetrics detecteren retriever-regressions. Generationmetrics detecteren generator-regressions. End-to-end answer correctness detecteert integration failures.

De reference frameworks (opinionated tour)

FrameworkBeste inWaar het tekortschiet
RAGASEen gedeelde vocabulary voor faithfulness, answer relevancy en context precision/recall (metrics)Kosten van LLM-judge; ondoorzichtige scorecomponenten bij debugging; version changes
ARESEen task-specific classifier judge, als training en annotation de kosten waard zijn (paper); de gerapporteerde precision is benchmark-boundZwaardere setup; je moet daadwerkelijk models trainen
TruLensTrace-linked feedback functions en OpenTelemetry-integratie (project)Minder out-of-the-box RAG-specifieke metrics dan RAGAS
DeepEvalIntegratie met test-runners en custom metrics (project)Veel gebruik van LLM-judge leidt tot cost spikes
Arize PhoenixTracing, dataset experiments en prebuilt of custom RAG/agent evaluators (evaluation docs)Domain rubrics en judge thresholds hebben nog steeds lokale calibratie nodig
TREC 2024 RAG TrackPublieke benchmark voor nugget evaluation (AutoNuggetizer), support evaluation en fluency op MS MARCO Segment v2.1Geen runtime-tool; een benchmark om tegen te calibreren

Mijn default stack is RAGAS voor de metric vocabulary, DeepEval voor CI-checks, Phoenix voor production tracing en custom code voor ontology-specifieke metrics. Kies het framework dat custom metrics eenvoudig maakt.

Stem bij benchmarkselectie de task af voordat je de leaderboard bekijkt. BEIR, MTEB en MIRACL blijven nuttige retrievalbaselines. Voeg tests toe voor capabilities die deze benchmarks niet aantonen:

  • Huidige end-to-end RAG: de TREC 2026 RAG Track gebruikt narrative queries en ClimbMix-400b, ter vervanging van MS MARCO v2.1, en linkt naar de RAGDoll evaluation toolkit. Op 6 september heeft de trackpagina nog geen datum aangekondigd voor het terugsturen van results en judgments. De vrijgegeven topics zijn beschikbaar voor experiments; ze vormen geen voltooide, beoordeelde 2026-leaderboard. Houd de protocollen voor 2024 en 2025 hieronder gekoppeld aan hun eigen corpora en judgments.
  • Technische vragen over evoluerende code: FreshStack combineert door mensen gestelde Stack Overflow-vragen, repository-corpora en nugget judgments. De vrijgegeven snapshot en de machinery voor het bouwen van nieuwe corpora zijn verschillende zaken; pin de repository-revisie en question date.
  • Afbeeldingen die een deel van de vraag of het bewijs bevatten: MM-BRIGHT splitst text-to-text, multimodal-to-text, multimodal-to-image en multimodal-to-multimodal retrieval. Score de relevante task afzonderlijk. Alleen OCR-tekst kan missen wat een chart of screenshot toevoegt.

Deze benchmarks breiden de coverage uit; ze vervangen niet de eligible, versioned queryset van je applicatie.


Part 2: Breng evaluation points in kaart

De RAG-pipeline gegroepeerd in ingestion-, query-time- en answerpaden, met diagnostische metrics naast elke stageDe RAG-pipeline gegroepeerd in ingestion-, query-time- en answerpaden, met diagnostische metrics naast elke stage

Gebruik het diagram om een symptom naar de eerste diagnostische metric te routeren. Upstream-verliezen begrenzen downstream-quality: slechte parsing begrenst retrieval, en slechte retrieval begrenst reranking en generation. Faithfulness meet het antwoord, nooit de upstream-oorzaak.


Part 3: Ingestion evaluation

Veel production RAG-failures beginnen bij ingestion. Het systeem werkt op schone testdocumenten en faalt vervolgens op echte PDF’s, scans, tabellen en rommelige corpuspagina’s.

Document acquisition en parsing

Wat je meet:

  • Extraction length sanity check: extracted_chars / expected_chars per documentklasse markeert verdachte length changes, maar duplicated of verkeerde tekst kan nog steeds 1.0 scoren. Vergelijk aligned text met een handmatig opgeschoonde reference voor omissions en substitutions en controleer footnotes, captions, table content en reading order afzonderlijk.

  • OCR accuracy: CER (Character Error Rate) en WER (Word Error Rate), de standaardmetrics voor speech/OCR:

    CER=S+D+IN,WER=Sw+Dw+IwNw\text{CER} = \frac{S + D + I}{N}, \qquad \text{WER} = \frac{S_w + D_w + I_w}{N_w}

    waarbij SS, DD, II substitutions, deletions en insertions op characterniveau zijn en NN het aantal reference characters is (subscript ww voor de word-versie). Gebruik niet voor elke corpus dezelfde CER-boundary. Calibreer die per documentklasse en downstream answer loss. Printed text, handwriting en multilingual material hebben verschillende error profiles. Bereken met jiwer (jiwer.cer(refs, hyps), jiwer.wer(refs, hyps)) of Hugging Face evaluate. Voor evaluation corpora zijn FUNSD en SROIE publieke benchmarks.

    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
  • Table extraction fidelity: TEDS (Tree-Edit-Distance-based Similarity) meet hoe dicht een voorspelde HTML-treetabel bij de reference ligt, genormaliseerd op basis van de omvang van de grootste tree. Uit Zhong et al., 2020 (PubTabNet):

    TEDS(Ta,Tb)=1EditDist(Ta,Tb)max(Ta,Tb)\text{TEDS}(T_a, T_b) = 1 - \frac{\text{EditDist}(T_a, T_b)}{\max(|T_a|, |T_b|)}

    TEDS gebruikt zowel structure (rows, columns, spans) als cell content. TEDS-S verwijdert de content en scoort alleen de structure. Reference implementation: PubTabNet’s teds.py (gebruikt intern apted). Zie PubTabNet, FinTabNet en SciTSR voor evaluation corpora. Naive parsers falen vaak op tabellen. Benchmark ze voordat je erop vertrouwt.

  • Layout / structure preservation: heading order, list integrity en reading order in multi-column PDF’s. Gebruik DocLayNet als labeled benchmark. Een off-the-shelf comparison kan een elementparser zoals unstructured, een PDF-library zoals pymupdf en een geselecteerde Docling pipeline omvatten. Docling biedt standard- en VLM-paden; leg vast welke je test.

Vergelijk verschillende parserfamilies, bijvoorbeeld een Tesseract-baseline, een VLM-based OCR-model en je vendor-kandidaat. Gebruik een gestratificeerde sample van echte documentklassen bij een vaste DPI, inclusief clean scans, foto’s, tabellen, multilingual text, math en handwriting. Rapporteer CER of WER per klasse en TEDS voor table pages.

Cleaning en normalization

  • Boilerplate removal accuracy: precision/recall tegen human-labeled boilerplate spans. Aggressive removal verwijdert relevante content; lazy removal vervuilt embeddings. Te vergelijken tools: trafilatura, jusText, Resiliparse. Barbaresi (2021) vergelijkt Trafilatura met baselines waaronder jusText; Resiliparse is een afzonderlijke kandidaat en geen systeem dat in dat paper is geëvalueerd.

  • Unicode normalization: het percentage documenten dat identieke NFC- en NFKC-outputs produceert (berekend met de stdlib unicodedata.normalize) is een bruikbaar signaal voor drift in compatibility forms. Het detecteert geen invisible/default-ignorable code points of cross-script lookalikes: scan expliciet op de eerste categorie en pas een policy of detector voor Unicode confusables toe wanneer de tweede categorie binnen scope valt.

  • Language detection accuracy: F1 op een gelabelde multilingual sample. Dit is cruciaal voor multilingual indexes. Gebruik fasttext-langdetect (Facebook’s lid.176), lingua-py of cld3. FLORES-200 levert evaluation text in 200 talen, maar je production language mix moet de test slice bepalen.

  • Deduplication effectiveness (MinHash / LSH): precision/recall van je near-duplicate detector tegen een handmatig gelabelde set. Het onderliggende idee: schat Jaccard similarity J(A,B)=ABABJ(A, B) = \frac{|A \cap B|}{|A \cup B|} tussen document-shingle sets via kk random permutation hashes (Broder, 1997) en bucket near-duplicates met LSH banding (Indyk & Motwani, 1998). Sweep het aantal hashes en de Jaccard-threshold op je corpus. Track false-merge rate (beschadigt antwoorden) afzonderlijk van missed-merge rate (verspilt index space). datasketch levert de hieronder gebruikte implementation; de parameters zijn illustratief:

    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']
  • PII scrubbing: precision en recall, afzonderlijk berekend per entity type (emails, SSN’s, namen, adressen). Recall errors creëren compliance risk; precision errors schaden answer quality. Bepaal het operating point samen met het legal team. Kandidaattools zijn Microsoft Presidio, scrubadub of een fine-tuned NER-model op een gelabelde set.

Chunking bepaalt retrieval quality

Chunking bepaalt welk bewijs het antwoord bereikt. In NVIDIA’s 2025 vendor benchmark behaalde page-level chunking de hoogste gemiddelde end-to-end answer accuracy in de geteste setup, waarin tabellen en charts als volledige units werden behouden. Dat meet answer accuracy, niet retrieval recall. Beschouw dat resultaat als bewijs voor de geteste corpus, niet als universele winnaar.

Semantic chunking groepeert aangrenzende zinnen op basis van embedding similarity en knipt bij dissimilar boundaries. LangChain’s SemanticChunker en LlamaIndex’s SemanticSplitterNodeParser implementeren deze strategie. Dit kan recall ten opzichte van fixed windows verbeteren wanneer topical boundaries belangrijk zijn.

LangChain’s RecursiveCharacterTextSplitter probeert standaard double newlines, single newlines, spaces en daarna individuele characters: ["\n\n", "\n", " ", ""]. Het detecteert geen sentence boundaries tenzij je geschikte separators configureert of een sentence-aware splitter gebruikt. Kies window- en overlapwaarden die bij je documentstructure passen en vergelijk ze vervolgens op de golden set.

Te tracken metrics:

  • Chunk coherence: coherence=cos(si,sj)withincos(si,sj)across boundary\text{coherence} = \overline{\cos(s_i, s_j)}_{\text{within}} - \overline{\cos(s_i, s_j)}_{\text{across boundary}}, waarbij sis_i sentence embeddings zijn. Gezonde chunks zijn intern similar en aan de boundary dissimilar. Bereken met sentence-transformers plus cosine_similarity van scikit-learn.
  • Boundary quality: human-labeled “is dit een sensible cut?” op een sample, plus een structural check dat chunks geen tabellen, lijsten of genummerde secties splitsen.
  • Optimal chunk size: sweep token sizes (128, 256, 512, 1024) en plot Recall@k tegen size op je golden set. Kies het knikpunt. Kies niet simpelweg wat de tutorial aangaf.
  • Overlap effectiveness: ablate verschillende overlap fractions en meet Recall@k. Verhoog overlap niet verder wanneer de lokale recallcurve afvlakt of duplication cost zwaarder weegt dan de gain.
  • Chunk attribution fidelity: het percentage chunks dat een verifieerbare source pointer behoudt (paginanummer, section anchor, doc ID). Dit is vereist voor auditability.
  • Late versus early chunking: late chunking (Günther et al., 2024) embedt het volledige document en segmenteert daarna, waardoor global context behouden blijft (reference implementation in jina-embeddings-v3). Contextual Retrieval (Anthropic, 2024) prependt door een LLM gegenereerde context aan elke chunk. Beide voegen cost toe. Benchmark ze op je corpus voordat je een van beide adopteert.

Mijn mening: structural chunking (splitsen op headings, tabellen en secties — geïmplementeerd door parsers zoals unstructured.io of door de AST te doorlopen die je parser al heeft geproduceerd) wordt te weinig gebruikt. Als je documenten structure hebben, gebruik die dan voordat je similarity heuristics toevoegt. Recursive character splitting is de baseline; semantic chunking is vooral op unstructured prose de overhead waard.

Metadata extraction en enrichment

  • NER precision/recall/F1: per entity type, op een gelabelde subset. Standard CoNLL/MUC-style. Bereken met seqeval (from seqeval.metrics import f1_score) voor de BIO/IOB-tag-aware versie, of met scikit-learn voor span-set comparisons. CoNLL-2003 en OntoNotes 5.0 zijn de canonical reference corpora.
  • Relation extraction F1: nog belangrijker voor ontology-grounded systems. Label handmatig een set die is gestratificeerd op relation type en documentklasse. TACRED en DocRED zijn publieke benchmarks; kandidaatimplementaties zijn opennre en spaCy relation pipelines.
  • Title / heading extraction accuracy: exact match plus genormaliseerde Levenshtein-similarity (1edit_dist(a,b)max(a,b)1 - \frac{\text{edit\_dist}(a, b)}{\max(|a|, |b|)}) tegen ground truth — python-Levenshtein of rapidfuzz leveren beide in één call.
  • Hierarchical metadata preservation: percentage chunks dat de parent section, het parent document en het ancestry path correct behoudt. Dit is de metric die bepaalt of je RAG vragen kan beantwoorden als “wat zegt het child van policy X?”

Embedding generation

  • Model selection benchmarks: gebruik de retrieval-taskresultaten van MTEB, BEIR voor zero-shot generalization en MIRACL voor multilingual retrieval als comparison points. MTEB retrieval rapporteert meestal nDCG@10; andere task families gebruiken andere metrics. Het MTEB Python package voert de benchmarks lokaal uit. Beschouw transfer van English MTEB naar een lower-resource language als een hypothesis die je op de gelabelde set van die taal moet testen.
  • Domain-specific evaluation: behandel een algemene benchmarkrank niet als domain result. Stel een domain golden set samen op basis van de coverage matrix en de uncertainty die je besluit kan verdragen. Rank candidate models vervolgens opnieuw met ranx of pytrec_eval. Een domain set kan de leaderboard-volgorde omdraaien, dus publiceer de dataset slice, retrieval protocol en confidence interval bij het resultaat.
  • Embedding drift detection: vergelijk een vaste reference window met rolling embeddings met MMD of een gevalideerde reference-versus-current classifier. KL vereist een expliciete probability-distribution estimator en kan niet rechtstreeks op raw embedding coordinates worden toegepast. Meet ook nearest-neighbor stability voor een vaste probe set. evidently en alibi-detect implementeren model-based en statistical detectors. Evidently’s comparative study is één vendor evaluation; vergelijk methods op bekende shifts in je eigen embeddings.
  • Multi-vector versus single-vector: late interaction behoudt token-level representations in plaats van elk document tot één vector te reduceren; ColBERT is het canonical design, met reference implementations in RAGatouille en PyLate. Die rijkere representation verhoogt index- en retrievalkosten. Vergelijk quality, storage en latency met een single-vector baseline op dezelfde domain set voordat je dit adopteert.

Index construction

  • Recall@k under approximation: vergelijk de approximate-nearest-neighbour (ANN)-index met een exact brute-force baseline bij dezelfde k — in FAISS is dat IndexHNSWFlat (of IndexIVFFlat) versus IndexFlatIP/IndexFlatL2. Stel het aanvaardbare recall loss vast op basis van je downstream quality budget. Het ann-benchmarks-project volgt recall–QPS Pareto-curves over libraries.
  • HNSW tuning: HNSW (Hierarchical Navigable Small World) is een layered proximity graph; zie Malkov & Yashunin, 2018. Het is geïmplementeerd in hnswlib, FAISS’s IndexHNSWFlat en de meeste vector DB’s. HNSW heeft drie knobs: M (graph fan-out), efConstruction (candidate width tijdens build) en efSearch (candidate width tijdens queries). Begin met de gedocumenteerde defaults van de library en sweep de parameters vervolgens tot de recall–latency curve aan de requirements van je evaluation set voldoet.
  • IVF tuning: IVF (Inverted File index — partitioneer vectors met k-means in nlist cells en scan bij de query de nprobe nearest cells; zie FAISS’s IndexIVFFlat en IndexIVFPQ). Sweep nlist en nprobe tegen exact-search recall en latency. Benchmark filtered queries afzonderlijk omdat indexfamilies en vector databases filter traversal verschillend implementeren.
  • Update freshness lag: tijd van doc commit tot retrievability. Track p50 en p99. Track voor systemen met regulatory requirements ook het percentage queries dat tegen stale indexes wordt geserved.

Part 4: Query-time evaluation

De query-time lane bevat de metrics die een retrievalpad diagnosticeren. Recall@k alleen kan niet laten zien of rewriting, filtering, reranking of context assembly de failure veroorzaakte.

Query understanding en rewriting

  • Query expansion quality: Recall@k uplift op je golden set, expanded query versus raw. Definieer minimum useful gain en de onzekerheid daarvan vóór je test. Als expansion niet aan die requirement voldoet, rechtvaardigt die de latency en cost niet. Classical PRF (pseudo-relevance feedback)-baselines zoals RM3 en Bo1 blijven nuttige sanity checks; LLM-based expansion moet ze overtreffen.
  • HyDE evaluation: HyDE (Gao et al., 2022) genereert met de LLM een hypothetical answer, embedt die en voert daarop retrieval uit. Het voegt generation latency en een nieuw failure surface toe. Meet Recall@10 afzonderlijk op in-domain, out-of-domain en low-confidence slices en bepaal vervolgens of het in het default path, als fallback of nergens thuishoort.
  • Multi-query generation: Recall@k van de union van N rewrites versus één query. Sweep N en kies een punt op je recall–latency frontier. Implementaties: LangChain’s MultiQueryRetriever, LlamaIndex’s QueryFusionRetriever.
  • Intent classification accuracy: standaard precision/recall/F1 per intent (bereken met sklearn.metrics.classification_report), maar de operationele metric is routing correctness — wordt de juiste downstream pipeline aangeroepen?
  • Adaptive routing: Adaptive-RAG (Jeong et al., NAACL 2024) beargumenteert dat niet elke query dezelfde retrieval strategy nodig heeft. Track router accuracy als classification problem tegen een gelabelde set van “needs no retrieval / one-shot / iterative.”

Iterative search vereist een budgeted end-to-end test

Een agent kan zoeken, een result inspecteren en opnieuw zoeken in plaats van één vaste top-k-lijst op te halen. Het huidige tau3 knowledge domain biedt configureerbare RAG en agentic shell-based search, waardoor dit een concrete evaluation path is en niet alleen een architecture sketch. Geef one-shot en iterative retrieval voor een lokale comparison dezelfde eligible corpus en expliciete limieten voor tijd, model tokens en tool calls. Leg elke query en het bij elke turn geziene bewijs vast; score final evidence coverage, answer correctness, citation support en exhausted-budget failures. Meer search calls zijn alleen nuttig wanneer het extra bewijs binnen die limieten het antwoord verbetert.

Retrievalmetrics

Dit zijn de baselinemetrics. Als je ze niet trackt, kun je niet vaststellen of retrieval verbetert.

MetricWat het meetWanneer gebruiken
Recall@kfractie van de relevante documenten van een query die in de top k worden geretourneerdgebruik wanneer het missen van enig deel van de relevante set belangrijk is
Precision@kpercentage van de top-k dat relevant isnuttig wanneer de context window de bottleneck is
MRRgemiddelde van 1/rank van het eerste relevante documentwanneer users alleen naar top-1 of top-3 kijken
nDCG@kposition-discounted gain, gewogen met relevance gradesstandard retrieval metric voor graded relevance
MAPgemiddelde over queries van average precisionwanneer de volledige ranked list belangrijk is
Hit Rate@kof ten minste één relevant document in top k verschijntaverage het binaire resultaat over queries voor een snelle sanity metric
Coveragepercentage golden docs dat ooit over alle queries wordt opgehaalddetecteert systematische gaps in de index

De formules ter referentie (binary relevance met relevante set RqR_q voor query qq, en reli=1\text{rel}_i = 1 als het iie retrieved doc in RqR_q zit):

Recall@k=Rq{d1,,dk}Rq,Precision@k=Rq{d1,,dk}k\text{Recall@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{|R_q|}, \quad \text{Precision@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{k} RRq=1rank of first relevant doc,MRR=1QqQRRq\text{RR}_q = \frac{1}{\text{rank of first relevant doc}}, \quad \text{MRR} = \frac{1}{|Q|} \sum_{q \in Q} \text{RR}_q DCG@k=i=1k2reli1log2(i+1),nDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{\text{rel}_i} - 1}{\log_2(i + 1)}, \quad \text{nDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Voor graded relevance is reli{0,1,2,}\text{rel}_i \in \{0, 1, 2, \dots\}; binary nDCG is het special case die in de code hieronder wordt gebruikt. MAP is het gemiddelde over queries van APq=1Rqi:reli=1Precision@i\text{AP}_q = \frac{1}{|R_q|}\sum_{i: \text{rel}_i = 1} \text{Precision@}i. Zie Manning, Raghavan, Schütze, Introduction to Information Retrieval, hoofdstuk 8 voor derivations.

Gebruik voor production code ranx, pytrec_eval of ir_measures — ze implementeren de volledige TREC metric family en behandelen graded relevance correct. Stel release targets vast op basis van een realistische golden set, downstream answer quality en de cost van een miss. Neem thresholds niet over uit een tutorial.

Hier telt k unieke document IDs. Reject duplicates in plaats van een repeated document extra gain te geven. Declareer voor chunking-experimenten of k chunks of deduplicated parent documents telt en vergelijk ook het bewijs dat binnen een fixed token budget wordt aangeleverd. Dezelfde document recall kan zeer verschillende context quality verbergen.

De test harness hiervoor is kort. Je kunt hem vanuit een notebook uitvoeren voordat je zelfs een vector database hebt gekozen.

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")

De aggregate loopt over alle expected queries en behandelt een ontbrekende run als empty. Dit voorbeeld kent zero toe aan empty-gold queries; markeer die in een echte suite als een afzonderlijke answerability slice met een eigen denominator in plaats van zero als measured recall te behandelen. Rapporteer naast scores ook timeouts en missing runs.

Voer op elke PR een coverage-driven fast subset uit en vóór release de volledige golden set. Blokkeer een merge wanneer een preregistered metric zijn regression budget overschrijdt.

De companion repo pint de exacte getallen hierboven (Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) als unit test in tests/test_retrieval_metrics.py; notebook 01 sweept Recall@k / MRR / nDCG over een echte SciFact-index en de production-shaped harness staat in evaluation/retrieval.py.

Hybrid retrieval en reciprocal rank fusion

BM25 is een sparse lexical scorer die exact-term matching, term weighting en length normalization combineert. Het is beschikbaar in rank_bm25, Elasticsearch, OpenSearch en de meeste search engines.

Reciprocal Rank Fusion (Cormack, Clarke en Buettcher, SIGIR 2009) combineert BM25- en dense rankings op basis van position. De oorspronkelijke k=60-setting is een nuttige baseline. RRF is score-agnostic, waardoor cross-lane normalization voor linear interpolation niet nodig is. Test bij een gelabelde set die groot genoeg is om een stabiele delta te schatten ook een convex combination en tune α.

Mijn hypothesis is dat hybrid retrieval plus een cross-encoder reranker kan helpen voor technical, log-style en code corpora. De gain kan klein zijn op sterk semantic corpora. Meet tegen de dense-only en sparse-only lanes omdat een slechte fusion configuration slechter kan presteren dan beide inputs. De companion SciFact notebook is één bounded test en geen algemeen resultaat.

De implementation past in enkele regels.

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")

Let op wat RRF niet doet: het kijkt nooit naar de raw similarity scores. Een dense retriever die cosine 0.98 retourneert en een BM25-lane met score 17.4 zijn niet rechtstreeks vergelijkbaar. Z-score- en min-max-normalization verwijderen affine scale differences, maar geen van beide kalibreert een score als relevance. Outliers, distribution shape en de candidate set beïnvloeden de normalized values en dus het fusionresultaat nog steeds. Valideer elke score-based combination op gelabelde queries.

RRF gebruikt alleen rank. Als een retriever een document op positie 2 plaatst, is die vote 1 / (60 + 2) waard, ongeacht de raw score die dit opleverde.

Hybrid + RRF op SciFact: notebook 02 vergelijkt dense versus BM25 versus RRF met per-query deltas. De production-shaped fuser staat in retrieval/hybrid_rrf.py; tests/test_rrf.py pint de canonical d3 / d2 / d1-ordering op k=60.

Reranking

  • ΔnDCG / ΔMRR: uplift ten opzichte van no-rerank, op je golden set en op de depth die je applicatie werkelijk gebruikt. Bereken je retrievalmetrics met en zonder reranker op identieke candidate sets.
  • Cross-encoder versus bi-encoder: een bi-encoder embedt query en document onafhankelijk (één vector per zijde) en scoort met dot product; een cross-encoder concateneert query+doc en voert één forward pass uit die gezamenlijk over beide attendt. Cross-encoders ruilen een forward pass per candidate in voor rijkere query–document interaction. Reference implementation: sentence-transformers CrossEncoder. Benchmark relevance en latency op benoemde hardware, batch size en candidate depth; transfer het resultaat van één model of managed service niet naar een andere environment.
  • Listwise versus pointwise: pointwise scoort elk (query, doc)-paar onafhankelijk; listwise scoort de volledige candidate list jointly zodat het model candidates kan vergelijken. Evalueer beide op dezelfde candidate sets. Calibreer een score threshold per model en corpus in plaats van een gepubliceerd voorbeeld als portable te behandelen.
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}")

De BGE-code hierboven is een kleine baseline. Voeg voor een actuele comparison Cohere Rerank 4.0 fast/pro toe voor managed multilingual text ranking, of Qwen3-VL-Reranker 2B/8B wanneer queries of documenten images, screenshots of video bevatten. Houd task modalities, candidate depth, input limits, instructions en hardware expliciet. Een algemene Qwen3.8 generation checkpoint is niet hetzelfde model als de gespecialiseerde Qwen3-VL-reranker.

Een reranker helpt vaak een basic RAG-pipeline, maar is geen gegarandeerde win. Meet de ΔPrecision@1 en ΔnDCG op je golden set en behoud de reranker alleen wanneer de gain zijn latency- en costbudget overschrijdt. Vergelijk die gemeten gain met kleinere retrievalwijzigingen voordat je de volgende optimization kiest.

ΔnDCG en ΔPrecision@1 van een cross-encoder op SciFact: notebook 03; module: retrieval/reranker.py.

Context construction en lost-in-the-middle

Veel failures van het type “good retrieval, bad answer” beginnen bij context construction.

  • Context relevance: Ragas ContextRelevance beoordeelt de aangeleverde context list met twee prompts en genormaliseerde ratings; dit is geen score per chunk. Score voor chunkdiagnostics elk query–chunk-paar expliciet, bijvoorbeeld met een cross-encoder, en rapporteer de distribution onder een lokaal gekalibreerde threshold.
  • Supplied-context citation coverage: distinct supplied chunks cited gedeeld door distinct supplied chunks. Rapporteer empty-context cases afzonderlijk. Deze observable proxy zegt welke chunks citations ontvingen, niet welke chunks het model intern gebruikte of of de citations de claims ondersteunen. Controleer citation support afzonderlijk en vergelijk coverage met answer quality en token cost.
  • Lost-in-the-middle detection: synthetic eval waarin je de gold chunk op posities {first, middle, last} in een long context plaatst en answer correctness meet. De geciteerde studie van Liu et al. (TACL 2024) rapporteert U-shaped degradation onder de eigen long-context conditions. Behandel hetzelfde patroon in een current model als een hypothesis die je moet testen. Mitigations: rerank en reorder vervolgens de top-k zodat de highest-scored chunk first of last staat (LangChain’s LongContextReorder doet precies dit), of comprimeer middle chunks agressief. Meet met een position-stratified eval, niet alleen met een aggregate score. Een uitgewerkte, uitvoerbare position-stratified eval staat in notebook 06 (module: evaluation/lost_in_middle.py).
  • Context compression: rapporteer compression ratio (input tokens / output tokens) naast answer correctness. Tools zijn onder andere LangChain’s ContextualCompressionRetriever en LongLLMLingua. Definieer vooraf het grootste aanvaardbare correctness loss op basis van het risico en token budget van de applicatie en reject configuraties die deze grens overschrijden.

Part 5: De filter false-exclusion rate

Deze metric krijgt een eigen sectie omdat aggregate retrieval scores een miss niet kunnen toeschrijven aan een relevance filter. Evalueer eligibility filters afzonderlijk tegen caller entitlements; een document buiten die set moet excluded blijven.

Een hard relevance predicate zoals product = Y AND locale = en-US kan effective recall tot nul reduceren onder documenten die de caller mag zien. Correct geïmplementeerde Recall@k detecteert het verlies omdat de denominator de oorspronkelijke eligible relevant-document set blijft. De metric vertelt niet of de filter, retriever of ranker de miss veroorzaakte. Faithfulness beoordeelt claims tegen de retrieved context. Faithfulness kan claims die door die incomplete context worden ondersteund nog steeds scoren, maar kan de filter- of retrievaloorzaak niet diagnosticeren. Een lege refusal kan, afhankelijk van de implementatie, geen statements en NaN produceren; behandel dit niet als bewijs dat faithfulness de refusal heeft goedgekeurd.

De gemarkeerde branch is de meest voorkomende failure: het juiste document bestaat, maar de filter verwijdert het voordat retrieval start. Recall@k registreert de daling; alleen de exclusion rate schrijft die toe aan de predicate.

Stille RAG-failures, van de source corpus via filtering, ranking en generation naar de metric die elke bron identificeertStille RAG-failures, van de source corpus via filtering, ranking en generation naar de metric die elke bron identificeert

De metric

filter_false_exclusion_rate =
    (# queries where all gold docs were excluded by metadata filter) /
    (# queries with at least one entitlement-eligible gold doc)

Deze query-level definitie telt catastrophic exclusions: geen enkel eligible relevant document overleeft. Intersect de gold set van elke query met de entitlement set van de caller vóór scoring; een ineligible document is geen false exclusion. Voor multi-gold queries legt standard Recall@k partial loss nog steeds bloot; voeg een per-document exclusion rate toe als die boundary relevant is. Voor beide rates heb je (a) ground-truth doc IDs voor elke eval query en (b) instrumentation nodig die de toegepaste filter predicates logt, niet alleen de final results. Bepaal de target op basis van de cost van het uitsluiten van een geldig antwoord en het confidence interval van je production sample.

Hier volgt een werkende implementation. Die vergelijkt correcte standard recall met een invalid evaluator die relevance na filtering opnieuw definieert.

# 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")

De function retourneert de rate en het aantal queries met eligible gold. Als die denominator empty is, wordt een exception opgegooid in plaats van een geruststellende zero gerapporteerd; een verplichte release check moet dit als ontbrekend bewijs behandelen. Queries met alleen unauthorized gold blijven in afzonderlijke entitlement tests.

Bij de helft van de queries gaat het gold doc door de filter verloren, waardoor correcte Recall@10 tot 50% daalt. Die score detecteert het symptom, maar kan het niet attribueren. De false-exclusion rate toont dat de predicate twee antwoorden verwijderde voordat de retriever draaide. De opzettelijk invalid evaluator rapporteert 100% alleen omdat hij die failures uit zijn gold set verwijdert. Geen enkel model kan een document terughalen dat is weggefilterd.

De rate van 50% hierboven wordt als unit test gereproduceerd in de companion repo: tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. Notebook 04 voert dit uit op SciFact met synthetic metadata, zodat je kunt zien hoe een echte filter recall tot nul reduceert; de runtime metric (met predicate-precision/recall companion) staat in evaluation/filter_exclusion.py.

Companion metric: predicate precision en recall

Wanneer filtering dynamic is (bijvoorbeeld wanneer een LLM filter predicates uit de query extraheert), behandel de predicate extractor als een classification model en evalueer hem ook zo. Meet predicate precision en recall tegen een gelabelde set van (query, correct predicate)-paren. Een predicate error rate vertaalt zich niet rechtstreeks naar hetzelfde puntverlies in retrieval recall; meet hoe vaak die errors een gold document uitsluiten. Zodra een hard filter het gold document verwijdert, helpt geen enkele reranking.

Eligibility filters versus relevance preferences

Authorization, tenant isolation, legal jurisdiction en publication state bepalen of een document de candidate set mag binnengaan. Houd deze als hard filters en valideer ze afzonderlijk; Recall@k, false-exclusion rate en relevance precision autoriseren niet dat je ze versoepelt.

Vergelijk voor een relevance preference zoals locale, recency of version een hard predicate met een soft boost op dezelfde held-out, eligible queries:

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.

Kies minimum useful precision gain, uncertainty interval, ε en recall-loss bound op basis van de harm van het uitsluiten van een anders eligible answer, het voordeel van extra precision en de omvang van de held-out sample. Dit zijn lokale releasecriteria, geen universele thresholds. Een dedicated post over deze trade-off staat gepland; zie de follow-ups aan het einde.


Part 6: Generation evaluation

Retrievalmetrics vertellen je dat het systeem correct zou kunnen antwoorden. Ze vertellen niet dat het dat ook heeft gedaan. Generationmetrics vullen dat gat.

Faithfulness en groundedness

RAGAS faithfulness splitst het antwoord op in atomic claims (korte, zelfstandige factual statements) en verifieert vervolgens elke claim tegen de retrieved context via een LLM-judge:

faithfulness=claims supported by contexttotal claims\text{faithfulness} = \frac{|\text{claims supported by context}|}{|\text{total claims}|}

Faithfulness controleert support in het supplied evidence; het stelt niet vast dat het bewijs correct of voldoende is om de vraag te beantwoorden. Registreer no-claim responses afzonderlijk en rapporteer hun aantal in plaats van perfecte faithfulness aan een leeg antwoord toe te kennen.

De current Ragas documentation raadt de collections API hieronder aan. Installeer in een uv-project ragas en openai met uv add ragas openai, stel OPENAI_API_KEY in en sla dit op als script om het uit te voeren met uv run. Het maakt provider calls en brengt kosten met zich mee; de score is een judge-resultaat, geen deterministische expected constant. Pin de resolved dependencies in je 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())

Dit voorbeeld gebruikt GPT-5.6 Terra als actuele kandidaat met structured outputs; de Ragas factory forwardt model arguments. Ragas 0.4.3 herkent dotted GPT generation names niet in zijn token-limit mapper. Het voorbeeld laat de legacy max_tokens en sampling defaults expliciet weg en levert max_completion_tokens; verifieer het outgoing request wanneer je de adapter upgradet. Reasoning uitschakelen maakt de configuratie expliciet, niet gevalideerd. Vergelijk judge false passes, false failures en cost met human labels voordat je een goedkopere calibrated judge vervangt.

Hieronder staat dezelfde loop uitgeschreven met een deterministic stand-in judge, zodat je de end-to-end vorm kunt zien.

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("") == []

De structure is belangrijk. In productie wordt verify_claim een NLI-model of een LLM-call. Behoud de extract–verify–aggregate-structure, maar valideer extraction en entailment afzonderlijk en registreer no-claim of failed judgments. De offline stand-in hierboven is hard-coded voor deze voorbeelden; het is geen factuality detector.

End-to-end claim extraction + verification op gegenereerde SciFact-antwoorden: notebook 05; module: evaluation/faithfulness.py. De repo voert dezelfde loop uit via twee judge families — het eigen model van de generator en een cross-family judge (RAG_EVALS_JUDGE_MODEL) — plus een deterministic lexical baseline, zodat je ziet waar de families het oneens zijn.

Een purpose-built alternatief voor LLM-as-judge is HHEM-2.1-Open (Hughes Hallucination Evaluation Model, Vectara), een classifier die is fine-tuned voor hallucination detection. De model card documenteert de checkpoint, de raw 0–1-score die wordt uitgegeven en balanced-accuracy-resultaten op AggreFact en RAGTruth. Er wordt geen default decision boundary gepubliceerd, dus die keuze ligt bij jou. Beschouw dit als model-card evidence, niet als garantie voor je corpus: calibreer de threshold op lokale labels en vergelijk hem vóór deployment met je gekozen judge.

Atomic-fact evaluation

FActScore (Min et al., EMNLP 2023) splitst long-form generations op in atomic facts, haalt per fact evidence op, labelt elke fact als supported / not-supported en rapporteert de supported fraction:

FActScore=supported atomic factstotal atomic facts\text{FActScore} = \frac{|\text{supported atomic facts}|}{|\text{total atomic facts}|}

Reference implementation: shmsw25/FActScore. Dit werkt goed voor biographies, summaries en andere long-form outputs. Let op: repetitive trivial facts kunnen de score opblazen en individueel ware statements kunnen samen een misleidend antwoord vormen. MontageLie (EMNLP 2025) test deze zwakte via deceptive relationships en ordering tussen true statements. VeriScore behandelt claims met noodzakelijke modifiers; de Core-filter helpt fact-padding te voorkomen.

Citation accuracy

Track citation precision (cited spans ondersteunen de claim daadwerkelijk) en citation recall (claims die geciteerd zouden moeten worden, zijn geciteerd):

cite_precision=cited spans that support a claimcited spans,cite_recall=claims with at least one supporting cited spanclaims that should be cited\text{cite\_precision} = \frac{|\text{cited spans that support a claim}|}{|\text{cited spans}|}, \quad \text{cite\_recall} = \frac{|\text{claims with at least one supporting cited span}|}{|\text{claims that should be cited}|}

De TREC 2024 RAG Track definieert een reproduceerbaar support evaluation-protocol. Thakur et al. (SIGIR 2025) rapporteren ongeveer 56% agreement met human judgments die from scratch zijn gemaakt en 72% onder een andere condition waarin mensen LLM predictions achteraf bewerkten. Dit laatste is assisted annotation en geen onafhankelijk bewijs van betere judge accuracy. Houd de annotation condition aan het getal gekoppeld. Voor een geautomatiseerde approximation implementeert ALCE (Gao et al., EMNLP 2023) citation precision/recall met NLI-based verification.

Answer correctness, completeness, refusal

  • Answer correctness tegen een reference: exact match of token-F1 kan geschikt zijn voor korte antwoorden. Controleer voor langere antwoorden factual relations, entities, quantities, negation en required information tegen reviewed references. BERTScore en embedding cosine meten similarity; een verkeerd getal of negation kan toch een hoge score behouden. Ragas AnswerCorrectness combineert factual comparison met similarity in plaats van beide gelijk te stellen.
  • Completeness via nuggets: een nugget is een relevante information unit, waarbij vital nuggets worden onderscheiden van optionele useful ones. Voor een vraag naar een founding date kan het year vereist zijn; de naam van de founder is niet automatisch vereist. AutoNuggetizer construeert en verfijnt nuggets uit judged document pools en controleert vervolgens hun aanwezigheid in generated answers. Het eerste TREC 2024-report omvatte 21 topics en 45 runs. Het TREC 2025 overview, gepubliceerd in maart 2026, breidt het protocol uit met narrative queries en evalueert retrieval relevance, answer completeness en attribution. Dit zijn publieke evaluation-protocollen en geen bewijs dat elke production RAG-systeem dezelfde nugget rubric nodig heeft.
  • Refusal behavior: label of het supplied evidence een antwoord toestaat en meet vervolgens correcte refusals onder alle refusals en refusals onder gevallen waarin abstention hoort plaats te vinden. NoMIRACL (Findings of EMNLP 2024) test robustness tegen relevante en niet-relevante supplied passages; het bewijst niet dat de volledige corpus geen antwoord bevat. Scheid retrieval misses van werkelijk out-of-scope queries in je eigen suite.

Post-generation verification

De goedkoopste reliability gains komen vaak uit deterministic post-checks, niet uit grotere models.

  • Unseen-entity flag: registreer named-entity strings in het antwoord die ontbreken in een normalized context string (bijvoorbeeld spaCy’s ents plus exact matching). Dit is een goedkope, domain-specific flag voor unseen entity strings, geen grounding check: het kan identity, relation, negation, time of provenance niet vaststellen. Meet precision en recall op lokale labels voordat je het gebruikt om een release goed te keuren en behoud claim-level entailment of human review voor verification.

    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
  • Claim verification: extraheer claims, voer NLI tegen context uit en fail of flag alles onder de threshold. NLI-as-faithfulness models: cross-encoder/nli-deberta-v3-large, MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli. Dit voegt latency toe. Voor high-stakes domains is het de moeite waard.

  • Self-consistency (Wang et al., ICLR 2023): sample multiple generations bij temperature > 0; rapporteer agreement rate (bijvoorbeeld het aandeel generations dat overeenkomt met het modal answer, of pairwise BERTScore); kies het aantal samples op basis van de stability–cost curve en flag low-agreement answers voor human review.

  • Confidence calibration: verzamel verbalized confidence (“Hoe zeker ben je, 0–1?”) en vergelijk die met actual correctness op de eval set. Plot een calibration curve en rapporteer Expected Calibration Error: ECE=m=1MBmnacc(Bm)conf(Bm)\text{ECE} = \sum_{m=1}^{M} \frac{|B_m|}{n} |\text{acc}(B_m) - \text{conf}(B_m)|, waarbij BmB_m confidence bins zijn. Implementaties: netcal, torchmetrics.CalibrationError. Een model dat 0.9 confidence rapporteert, zou in vergelijkbare gevallen ongeveer 90% correct moeten zijn; meet de gap in plaats van calibration aan te nemen.


Part 7: Ontology-grounded RAG evaluation

De standard metrics hierboven dekken open-corpus RAG. Als je RAG retrieval uitvoert tegen een structured ontology, taxonomy of knowledge graph, zijn die metrics noodzakelijk maar niet voldoende. Voorbeelden zijn products in een catalog, conditions in SNOMED, components in een BOM en security techniques in MITRE ATT&CK. Je moet ook de ontologielaag meten.

Entity linking accuracy

De eerste task is het mappen van een query mention naar een ontology entity (“Aspirin” → wikidata:Q18216, “the 737” → aircraft:Boeing_737).

  • Mention-level precision/recall/F1: standard, tegen gold mention spans (bereken met seqeval of een span-set comparator).
  • Disambiguation accuracy: welk deel van de correct gedetecteerde mentions wordt naar de juiste entity ID gemapt? Publieke referenties zijn ReFinED, REL en GENRE; benchmarks zoals AIDA-CoNLL en BELB laten zien dat resultaten per systeem en domain verschillen.
  • NIL handling: precision/recall voor “entity not in ontology.” Meet over-linking naar near-but-wrong entities afzonderlijk van correct abstention.

Hierarchy-aware evaluation

Plain accuracy behandelt “Sedan voorspellen terwijl de truth Hatchback is” hetzelfde als “Sedan voorspellen terwijl de truth Submarine is.” Die errors zijn niet gelijk.

  • Hierarchical precision/recall/F1 (Kosmopoulos et al., 2015): geef credit voor gedeelde ancestors in de ontology DAG. Met P^q\hat{P}_q het predicted node plus al zijn ancestors en TqT_q het true node plus al zijn ancestors:

    hP=qP^qTqqP^q,hR=qP^qTqqTq,hF1=2hPhRhP+hRhP = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |\hat{P}_q|}, \quad hR = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |T_q|}, \quad hF1 = \frac{2 \cdot hP \cdot hR}{hP + hR}

    Implementeer dit met networkx op de ontology graph: verrijk elke prediction en elk label met zijn ancestors en neem vervolgens de set overlaps hierboven.

  • Wu-Palmer similarity tussen predicted en gold entity in de taxonomy (Wu & Palmer, 1994):

    WuP(c1,c2)=2depth(LCA(c1,c2))depth(c1)+depth(c2)\text{WuP}(c_1, c_2) = \frac{2 \cdot \text{depth}(\text{LCA}(c_1, c_2))}{\text{depth}(c_1) + \text{depth}(c_2)}

    waarbij LCA de lowest common ancestor in de taxonomy is. Out of the box beschikbaar in NLTK voor WordNet (from nltk.corpus import wordnet as wn; wn.synset("car.n.01").wup_similarity(wn.synset("truck.n.01"))); bereken voor custom taxonomies LCA met networkx.

  • Sibling/parent confusion rate: track confusions naar siblings, parents en children afzonderlijk — count_sibling / total_errors, count_parent / total_errors, count_descendant / total_errors. Gebruik reviewed examples om te testen of sibling errors uit ambiguous mentions komen of parent errors uit over-generalization.

Filter false-exclusion rate (opnieuw, nu cruciaal)

In ontology-grounded systems komen hard filters vaak uit de ontology zelf (“haal alleen docs op die met category X zijn getagd”). De exclusion-rate metric (gedefinieerd in Part 5) wordt dan een primary correctness signal. Een verkeerde category prediction kan recall tot nul reduceren; de exclusion rate schrijft dat verlies toe aan de filter.

Constrained generation conformance

Wanneer je output aan een ontology moet voldoen (elke entity name in het antwoord moet een geldige ontology member zijn; elke predicate moet uit een closed vocabulary komen), meet je:

  • Schema validity rate: percentage outputs die parsen en valideren tegen het ontology schema. Valideer met jsonschema of pydantic. JSONSchemaBench is de publieke benchmark voor algemene structured output; bouw voor ontology-specific schemas je eigen validator.
  • Vocabulary conformance: percentage named entities in de output dat geldige ontology IDs zijn — een one-line set-membership check tegen de closed vocabulary.
  • Semantic conformance: een syntactisch geldige output kan nog steeds de verkeerde, maar geldige entity kiezen. Combineer conformance met downstream answer correctness.

Constrained decoding frameworks (Outlines, XGrammar, Guidance, OpenAI Structured Outputs) zijn ontworpen om schema validity af te dwingen. JSONSchemaBench vergelijkt efficiency, coverage en quality over implementaties. Voer de cases die bij je schemas passen opnieuw uit met jouw serving backend, omdat coverage en latency van beide afhangen.

Auditability

Voor ontology-grounded systems waarvan antwoorden worden gereviewd:

  • Citation completeness: percentage factual claims met ten minste één verifieerbare citation.
  • Provenance depth: percentage citations dat helemaal terugverwijst naar een source document met een stabiele ID, niet alleen naar een chunk hash.
  • Reproducibility rate: bij opnieuw uitvoeren van dezelfde query op een vaste snapshot wordt hetzelfde antwoord teruggegeven. Pin model version, runtime, decoding configuration en seed en bepaal de vereiste repeat rate op basis van de auditability needs van de workflow. Temperature zero alleen garandeert geen determinism. Een miss kan uit generation, de serving runtime of een upstream-stage komen.

Part 8: System-level evaluation

Holistic answer quality

  • LLM-as-judge (Zheng et al., NeurIPS 2023): een scalable model-based evaluation approach. G-Eval (Liu et al., EMNLP 2023) genereert evaluation steps uit de task en criteria en weegt rating levels vervolgens met hun token probabilities: score=ip(si)si\text{score} = \sum_i p(s_i)\,s_i. Dit zijn probabilities, geen log probabilities. Agreement hangt af van judge, task, prompt en calibration set.
  • Pairwise preference: geef de judge answer A versus answer B en registreer de preference. Dit vervangt een absolute rating door een comparative decision, maar vereist nog steeds calibratie tegen human preferences. MT-Bench rapporteerde GPT-4 judge agreement boven 80% met zowel human preferences als human–human agreement onder zijn benchmarkconditions; draag die rate niet over naar een ander domain zonder calibratie.

LLM-as-judge heeft reële biases:

  • Position bias: meet order sensitivity op human-labeled cases voor de geselecteerde judge en task. Randomization of swapped-order aggregation kan voor sommige model/task-paren helpen, maar behoud dit alleen als het de lokale human agreement verbetert; de 2026 controlled study vond position swapping schadelijk op zijn adversarial cases.
  • Verbosity bias: judges kunnen lengte verwarren met quality. De geciteerde 2026 controlled study, version 2 vond heterogeneous expansion-pair behavior: drie judges prefereerden langere antwoorden, Claude prefereerde beknopte antwoorden en GPT-4o was ongeveer neutraal. Alle vijf presteerden goed op truncation controls. Die resultaten zijn benchmark-bound, dus geef je judge aan hoe hij completeness en filler moet behandelen en rapporteer length-controlled performance op je eigen rubric.
  • Self-preference risk: Zheng et al. observeerden een 10% hogere GPT-4 self-win rate en een 25% hogere Claude-v1 self-win rate in hun data, maar concludeerden dat beperkte data en kleine verschillen geen self-enhancement bias konden aantonen. Vergelijk same- en cross-family judges tegen lokale human labels; kies de beter gekalibreerde judge in plaats van aan te nemen dat een van beide arrangements veilig is.

Praktisch recept: selecteer een judge op human-labeled calibration data, mask model identities, meet order sensitivity en leg het length policy vast in de rubric. Herhaal cases alleen wanneer de extra samples uncertainty materieel reduceren. Vergelijk voor high-stakes evaluations same- en cross-family judges en analyseer disagreements tegen human labels.

Schema-Guided Reasoning voor judges

Free-form output is een bron van variation in judge runs. Twee runs tegen hetzelfde antwoord kunnen de rubric anders organiseren en verschillende scores opleveren. Schema-Guided Reasoning (SGR) maakt die rubric expliciet: definieer de evaluation stages als een Pydantic schema en gebruik vervolgens constrained output via Outlines, XGrammar, vLLM structured outputs of OpenAI response_format om de ondersteunde schema constraints af te dwingen. Field order kan een reviewer helpen het record te inspecteren, maar bewijst niet dat het model de stages in die volgorde heeft doorlopen.

Voor RAG eval splitst het schema de judgment op in expliciete, auditable fields in plaats van het model direct naar een getal te laten springen:

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")

Dit illustrative contract berekent de score pas nadat de verdict lists een complete, disjoint partition van de extracted claims vormen. Een no-claim outcome heeft geen faithfulness score en moet buiten de faithfulness aggregate blijven; rapporteer het aantal en score refusal correctness afzonderlijk, zodat abstentions het gemiddelde niet stilletjes verbeteren. Behoud deze semantic checks in productie; constrained output garandeert de shape, niet een unbiased verdict. Het Pydantic-model maakt een rubric change bovendien zichtbaar als code diff, terwijl human calibration tests de judgment zelf testen.

Dit werkt voor elke rubric-based judge, niet alleen voor faithfulness. Pairwise preference, citation support en refusal correctness hebben allemaal baat bij dezelfde behandeling.

Een simplified rubric judge plus pairwise-, position-bias- en cross-family-voorbeelden staat in notebook 07; module: evaluation/llm_judge.py. In de reviewed revision vraagt de function met naam g_eval om één integer rating; hij genereert geen evaluation steps en berekent geen probability-weighted scores, en reproduceert dus niet het G-Eval-protocol. De benchmark sweep (make benchmark in de repo) koppelt drie models (gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash) aan een rotating-judge pairwise A/B: elke judge evalueert antwoorden uit de andere twee model families. Die topology ondersteunt cross-family pairwise resultaten; het meten van self-preference vereist ook same-family- en cross-family-conditions die tegen lokale human labels worden vergeleken.

Latency en cost

  • p50, p95, p99 op elke pipeline-stage. Kies de SLO-percentile en alert threshold op basis van de user journey, traffic volume en error budget.
  • Time-to-first-token versus total generation time. Users geven voor streaming UX om TTFT.
  • Stage breakdown: retrieval, reranking, generation, post-processing. Gebruik de trace om de tail te lokaliseren in plaats van aan te nemen welke stage de oorzaak is; leg reranker device en batch size vast bij het vergelijken van runs.
  • Total $/query = embedding + retrieval + rerank + generation + storage amortized. Track p50 en p99; de long tail is waar het budget verdwijnt.
  • Cache hit rates op embedding cache-, retrieval cache- en KV-cache-niveau. Stel afzonderlijke targets in op basis van observed repetition, invalidation policy en de cost avoided op elke layer.

Per-stage p50/p95/p99 met stage breakdown is ingebouwd in notebook 08 en de runner op evaluation/latency.py; het benchmark report combineert latency met faithfulness in één matrix die je met make benchmark opnieuw kunt uitvoeren.

A/B-testing

  • Unit of randomization: kies de unit op basis van estimand, carryover en interference. Gebruik per-user of per-session assignment wanneer repeated exposure behavior kan veranderen of inconsistente UX kan creëren. Per-query assignment is alleen verdedigbaar wanneer die effecten verwaarloosbaar zijn en de analysis repeated observations modelleert.
  • Primary, guardrails, exploratory metrics: preregistreer deze. Kies de primary measure op basis van de product outcome; satisfaction proxies omvatten thumbs, regenerations en dwell. Behandel latency en cost als guardrails wanneer ze de experience beperken.
  • Sample size: doe vóór launch een power analysis op basis van het minimum effect worth detecting, baseline variance, assignment unit en stopping rule.

Part 9: Testset construction

Een metric is slechts zo goed als de testset waarop hij draait. Als je golden set drie intents dekt en production traffic twaalf intents omvat, meet Recall@10 slechts die drie intents. Erger nog: een testset die overfit op eenvoudige vragen (“Wat is het refundbeleid van het bedrijf?”) kan een systeem goedkeuren dat faalt op moeilijke vragen (“Komt een gedeeltelijke annulering uit 2023 in aanmerking voor een refund onder de EU Digital Services Act, gefactureerd in EUR en afkomstig uit Ierland?”). De aggregate score stijgt terwijl het systeem nog steeds faalt op een belangrijk deel van production traffic.

Incomplete relevance labels kunnen recall in beide richtingen vertekenen. Als de echte relevante set {a, b} is maar de labels alleen {a} bevatten, scoort retrieval van {a} 1.0 in plaats van 0.5; retrieval van {b} scoort zero in plaats van 0.5. Versioneer de judgments en review nieuw opgehaald, ongejudeerd evidence voordat je een delta interpreteert.

Bouw de testset eerst rond de echte querydistribution en difficulty. Kies daarna metrics die op de target failure modes reageren en tune het systeem daarop.

Synthetic query generation

Gebruik een LLM om vragen uit je corpus te genereren:

  • Per-chunk: “Genereer 3 vragen die een user zou kunnen stellen en die deze chunk beantwoordt.”
  • Multi-hop: sample twee chunks en genereer een vraag die beide vereist.
  • Adversarial: genereer vragen met distractor entities, near-duplicate phrasing en ambiguous mentions.

Ragas test generation gebruikt graph-based scenarios met single-hop- en multi-hop-queries en specifieke of abstracte information needs. DataMorgana genereert configureerbare synthetic benchmarks over user- en question categories. Synthetic data is nuttig voor cold starts en coverage testing. Het kan real user queries niet vervangen.

Golden dataset construction

Human-curated data verankert de golden set.

  1. Sample echte user queries (of simulated queries als je nog vóór launch bent), gestratificeerd per intent.
  2. Laat SMEs elke vraag beantwoorden en identificeren in welk(e) doc(s) het antwoord staat.
  3. Bepaal de omvang van de set op basis van de coverage matrix en het confidence interval dat nodig is voor releasebeslissingen; coverage is belangrijker dan een geleend query count.
  4. Re-curate wanneer release cadence, drift signals, domain risk en annotation capacity dat rechtvaardigen.

Houd development queries, judge calibration, held-out release measurement en monitoring samples gescheiden. Group shared source documents en sessions voordat je splitst. Zodra een query of label tuning stuurt, is het development data. Reserveer untouched cases om de gekozen configuratie en judge te valideren.

Leg corpus-, query-, relevance-label- en policy versions vast, evenals de unit van k, supplied-context budget, scorer- en judge-revisions en aggregation rules. Vergelijk paired per-query deltas op dezelfde population, met uncertainty en slice counts. Neem elke expected query op: een timeout, missing result of unscorable judge response moet zichtbaar blijven in completion/error accounting. Laat een release score niet verbeteren door failures stilzwijgend te verwijderen. ARES biedt een research approach met automated judges, human validation en prediction-powered inference voor system estimates wanneer annotation schaars is; een reviewed local suite kan eenvoudiger starten.

Adversarial testsets

  • Counterfactuals: verwissel belangrijke entities in de query. Haalt het systeem de juiste chunks op voor de gewijzigde query?
  • Distractors: queries waarbij de corpus een plausible-but-wrong answer bevat die niet moet worden opgehaald. Dit is wat RGB (Chen et al., AAAI 2024) stresstest: noise robustness, negative rejection, information integration en counterfactual robustness.
  • Negation en quantifiers: queries met “not”, “except” en “only”. Dense retrievers hebben hier vaak moeite mee.
  • Out-of-scope: queries zonder antwoord in de corpus. Het systeem moet “I don’t know” zeggen en niet hallucineren. NoMIRACL levert passage-level relevance/answerability tests; voeg afzonderlijke corpus-level out-of-scope labels toe. Evalueer abstention expliciet op je production querytypes.

Coverage en continuous evaluation

  • Bouw een coverage matrix: query intent × document type × ontology branch. Eén query per cell is een startpunt voor de coverage inventory, niet genoeg statistical power voor een releasebeslissing. Empty cells leggen ontbrekende coverage bloot; dimensioneer populated slices op basis van de uncertainty die je kunt verdragen.
  • Voer op elke PR een bounded, fast regression subset uit en de volledige suite volgens een trager schedule.
  • Plan de volledige golden-set eval op basis van release cadence en evaluation cost; voer die uit op release candidates.
  • Plan drift evaluation op basis van traffic volume, expected change en risk. Gebruik een rolling production sample en stratify op feedback in plaats van de target distribution stilzwijgend te veranderen.

Part 10: Production monitoring

De eval suite die je shipt beschrijft het systeem bij launch. Production traffic verandert daarna.

Implicit en expliciete feedback

  • Behandel implicit events als candidate signals en niet als positive of negative quality KPI’s totdat ze op een lokale sample correleren met blinded review of expliciete feedback.
  • Click-through / open rate op cited sources (als je UI die toont).
  • Dwell time op het antwoord.
  • Regeneration rate: percentage antwoorden dat de user opnieuw vraagt of laat redo’en. Behandel dit als één dissatisfaction signal en calibreer het tegen reviewed conversations.
  • Copy / share / export rates: candidate implicit signals die usefulness, verification, handoff of dissatisfaction kunnen vertegenwoordigen. Meet hun association en confidence interval voordat je een richting toekent.
  • Follow-up patterns: gebruik “Are you sure?” of “But what about X?” als review strata en label hun association met distrust of unresolved need.
  • Thumbs up/down met optionele reason categories (wrong, incomplete, off-topic, harmful, slow). Inline edits kunnen meer diagnostic context behouden; beoordeel die waarde op reviewed samples.

Drift detection

  • Query drift: vergelijk query embeddings met een reference window via MMD of een gevalideerde reference-versus-current classifier. KL vereist een gedefinieerde probability estimator, zoals gekozen histograms; raw embedding coordinates zijn geen probabilities. Calibreer alarms op bekende shifts en inspecteer vervolgens de affected slices.
  • Embedding drift: pin een representation en een probe set en meet neighbor stability en retrieval quality. Verschillende model versions hoeven niet dezelfde dimensions of coordinate basis te delen, dus cross-version cosine kan betekenisloos zijn. Migreer query- en documentencoders samen, evalueer de nieuwe index en bewaar versioned snapshots voor rollback.
  • Performance drift: track production-equivalent metrics (regeneration rate per intent) in de tijd. Abrupte en geleidelijke shifts suggereren verschillende hypotheses, maar hun vorm bewijst de oorzaak niet; inspecteer data-, traffic-, provider-, policy- en deploymentwijzigingen.

Shadow evaluation en human-in-the-loop

Draai het candidate system parallel aan productie, vergelijk outputs offline en serveer ze niet aan users. Hiermee kunnen regressions vóór launch zichtbaar worden. Shadow inference verbruikt nog steeds capacity en kan tools aanroepen: isoleer resources, onderdruk writes en controleer dat de comparison production latency niet verslechtert.

Voor human-in-the-loop (HITL) review:

  • Sample low-confidence outputs naar een review queue.
  • Neem een random sample van production traffic op voor blind review; bepaal de rate op basis van traffic volume, risk en reviewer capacity.
  • Oversample thumbs-down outputs voor review naast de random sample.
  • Gebruik reviewed outputs om de golden set uit te breiden.

De minimale guardrail set

Kies alert priorities en thresholds op basis van user harm, SLOs en gevalideerde detector performance. Candidate signals zijn:

  1. Faithfulness/HHEM score onder threshold op een rolling production sample.
  2. p95 latency boven SLO.
  3. Filter false-exclusion rate boven threshold (sample-based).
  4. Regeneration rate buiten een lokaal gekalibreerde control band die rekening houdt met window size, traffic, seasonality en false-alert budget.
  5. Cost/query boven budget.

Gebruik release timing om diagnosis te sturen en verifieer dit vervolgens met traces en affected slices. Een deployment kan samenvallen met traffic drift en een provider- of datawijziging kan optreden zonder application release. Alerts zijn evidence om te onderzoeken; de lead time ten opzichte van user reports moet je meten.


Caveats

  • Targets zijn lokaal, niet universeel. Elk getal dat in deze guide als illustrative is aangeduid, is een example configuration of worked result en geen release threshold. Calibreer thresholds voor je domain, stakes, evaluation-set uncertainty en user expectations.
  • De framework space beweegt snel. HHEM versions, RAGAS metric names, model cards en leaderboard order kunnen na publicatie veranderen. Controleer de gelinkte source opnieuw en benchmark opnieuw voordat je je vastlegt.
  • LLM-as-judge agreement numbers hebben voetnoten. Het percentage van 80% voor GPT-4 versus humans komt uit MT-Bench / Chatbot Arena conditions. Dat resultaat bewijst geen agreement op een niche domain of adversarial slice. Gebruik judges als force multiplier en niet als vervanging voor spot-checking.
  • Vendor benchmark uplifts zijn vaak niet onafhankelijk reproduceerbaar. Reproduceer op je eigen data voordat je een getal gelooft, vooral voor nieuwere rerankers en OCR-systemen.
  • Geen enkele metric vervangt het bekijken van outputs. Plan blind review van een random production sample op basis van traffic, risk en reviewer capacity. De metrics schalen die gewoonte; ze vervangen haar niet.

Coming up in deze serie

Dit was de index. De follow-ups die ik plan:

  • Soft Boosts versus Hard Filters: een deep dive in filter false-exclusion rate, met code, echte production examples en een decision framework.
  • Chunking Is the Hidden Variable: een controlled experiment over recursive, semantic, late en structural chunking op drie corpora.
  • Reranker Selection in 2026: BGE versus Cohere versus ZeRank versus current cross-encoder models, head-to-head op cost, latency en uplift.
  • Ontology-Grounded RAG: An End-to-End Walkthrough: de volledige evaluation harness bouwen voor een entity-grounded retrieval system.
  • LLM-as-Judge Without the Self-Preference Trap: praktische recepten voor unbiased automated evaluation.
  • Online Evaluation in Production: instrumentation patterns, alerting policies en de dashboards die echte regressions detecteren.

References

Frameworks en benchmarks

Retrieval en ranking

Generation, faithfulness, judges

Drift en productie

Companion code

  • slavadubrov/rag-evals-demo — uitvoerbare harness voor geselecteerde metrics uit dit artikel op de SciFact-corpus, plus een chunking × embedding × LLM benchmark sweep. Notebooks 00–09, unit tests die de bovenstaande worked examples pinnen en een embedded-Qdrant-index zodat dit zonder Docker draait.