[!NOTE] Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Evaluatie van RAG: Metrieken voor elke fase van een productie RAG-systeem

Deel 1 van de productieserie RAG

Een RAG-systeem met defecte filters kan maandenlang draaien zonder dat er een operationeel alarm wordt uitgegeven. Het levert nog steeds antwoorden en bereikt zijn latency-doelstellingen, maar deze antwoorden zijn gebaseerd op onvolledige bewijsmateriaal. Recall@k tegen het oorspronkelijke gold-set onthult deze fouten; latency- en beschikbaarheidsdashboards doen dit niet.

Een evaluatie kan een fout alleen detecteren wanneer elke pipeline-fase over zijn eigen metriek beschikt. In dit artikel worden veelvoorkomende foutmodi in verband gebracht met deze metrieken, van documentparsing tot productiemonitoring.

[!TIP] Wil je vooruit springen en de code uitvoeren?

De uitvoerbare slavadubrov/rag-evals-demo Het repository past de metrics toe op SciFact. make eval voert het suite uit, en make benchmark Het vergelijkt de configuraties van chunking, embedding en LLM. De notebooks 00–09 zorgen ervoor dat elke metriek apart wordt weergegeven. De demo maakt gebruik van ingebouwde Qdrant, waardoor er geen Docker nodig is.

Kort samengevat

De secties volgen de volgorde van pipeline. Begin met de besluitentabel en gebruik de latere secties daarna als referentie voor elke fase.


RAG evaluatiebeslissings tabel

Gebruik deze tabel als uitgangspunt voordat u een framework kiest. De juiste metriek hangt af van het fallemodel dat u probeert op te sporen, en niet van de naam van het hulpmiddel.

VraagMetricfamilieGebruik dit wanneerLet op voor
Is de bron intact gebleven na het parseren?Volledigheid van de extractie, dekking van tabellen/figurenPDF’s, presentaties, scans en HTML-pagina’s worden opgenomen in het corpus.Zelfs tekst met een schone uitstraling kan nog steeds ondertitelingen, voetnoten of tabelstructuur verliezen.
Heeft retrieval het juiste bewijs gevonden?Recall@k, nDCG@k, MRR, precisie/recall in de contextU kunt relevante chunks of documenten labelen.Een strenge metadata-filter kan het juiste document al verwijderen voordat de rangschikking begint.
Heeft reranking de shortlist verbeterd?Reranker verbetering, Precisie@1, nDCG-deltaCross-encoders of LLM-rankers bevinden zich naast retrieval.Meet latency en de kosten in relatie tot de behaalde kwaliteitsverbetering.
Is het antwoord gebaseerd op de beschikbare bewijsmateriaal?Getrouwheid, grondigheid, ondersteuning voor citatiesHet antwoord vermeldt documenten of baseert feiten op de context.Faithfulness kan geen slechte parsing of foutieve retrieval diagnosticeren.
Is het systeem stabiel in productie?Drift, regeneratie, fallback, p95 latency, kosten per antwoordVerkeerspatronen veranderen na de lanceringProductietelemetrie vereist gemonitorde menselijke evaluatie om gekalibreerd te blijven

Voor een kortere vergelijking van tools, zie De beste RAG evaluatiehulpmiddelen en -metrieken in 2026.

Deel 1: Definieer succes voordat de architectuur wordt ontworpen

Stel de eval-set op vóór het architectuurdiagram. Hierdoor krijgt elke latere keuze voor een component een meetbaar doel.

Je kunt niet kiezen tussen BM25 en dense retrieval, recursieve en semantische chunking, of Cohere Rerank en BGE totdat duidelijk is wat je precies wilt optimaliseren. “Betere antwoorden” is geen meetgrondslag. Een voorbeeld van een dergelijke contractuele specificatie is: “getrouwheid ≥ 0,85 op een gouden set van 200 vragen die onze drie belangrijkste intenties omvat, met een p95 latency van minder dan 1,5 seconden en een foutieve uitsluitingsratio van onder de 2%.” De cijfers hierin zijn slechts placeholders; het belangrijke is dat er duidelijke criteria zijn voor kwaliteit, dekking, latency, en filtering.

Definieer de harness voordat je de retrieval-code schrijft. De eerste harness zal onjuist zijn, waarna je deze moet herzien. Het herzien van een metriek is aanzienlijk goedkoper dan het herzien van een systeem dat al is geleverd.

Drie pipeline lagen en twee uitvoeringsmodi

Moderne RAG is een pipeline, waardoor de evaluatie een pipeline moet zijn. Er bestaat geen enkele enkele waarde die alle mogelijke foutpatronen kan detecteren.

De productie-evaluatie kent drie pipeline lagen. Ingestie-evaluatie onderzoekt of het corpus en de index de oorspronkelijke inhoud behouden. Evaluatie tijdens het opvragen controleert of herschrijving, filtering, retrieval, reranking en samenstelling van context hebben geleid tot het vinden van de juiste bewijzen. Antwoord- en productie-evaluatie bepaalt of het antwoord deze bewijzen heeft gebruikt en of de kwaliteit behouden blijft onder echte gebruiksomstandigheden. Door deze lagen samen te voegen tot één score kan een normalisatiefout verdwijnen binnen een acceptabele antwoordscore.

De drie situaties waarin een RAG-systeem bewijsmateriaal kan verliezen

Die lagen geven aan waar een fout optreedt. Offline en online geven aan wanneer en tegen welke gegevens de controle wordt uitgevoerd. Offline-evaluatie maakt gebruik van een vaste dataset met bekende referentiewaarden; deze is reproduceerbaar en wordt gebruikt bij componentselectie, A/B-vergelijkingen en CI-gates. Online-evaluatie meet het verkeer dat in werkelijke omstandigheden wordt verwerkt en houdt rekening met procesherstart, verblijfsduur, expliciete feedback en echte query-drift. Deze methode levert meer ruis op en is moeilijker te monitoren.

Elk pipeline-laag kan zowel offline als online controles leveren. Een vast ingesteld corpus helpt bij het opsporen van regressies in de parser vóór publicatie, terwijl monitors voor versheid en parse-fouten live updates in de gaten houden. Een vaste queryset wordt gebruikt om retrieval te meten vóór publicatie, terwijl gemonitorte live traces-gegevens afwijkingen in de productieomgeving aan het licht brengen. Controles die uitsluitend offline worden uitgevoerd, missen live veranderingen; controles die uitsluitend online plaatsvinden, maken het moeilijk om regressies na te bootsen.

Op componentniveau versus end-to-end

Er zijn twee veelvoorkomende fouten. Evaluatie uitsluitend op eind-tot-eind-niveau geeft aan dat het systeem niet werkt, maar niet waar precies. Evaluatie alleen op componentniveau kan aantonen dat alle onderdelen slagen, terwijl het gehele systeem toch faalt. De oplossing bestaat uit enkele belangrijke eind-tot-eind-metingen voor de beslissing of iets goedgekeurd kan worden of niet, gecombineerd met componentmetingen voor diagnostiek. Retrieval-metingen vangen regressies in de retriever op. Generatiemetingen vangen regressies in de generator op. De correctheid van de eind-tot-eind-antwoorden onthult integratiefouten.

De referentie frameworks (opinionated tour)

FrameworkBest in staat omWaar het misgaat
RAGASReferentievrije RAG-metingen (getrouwheid, relevantie van antwoorden, precisie/recall van context); het feitelijke woordenschatLLM-judge kosten; ondoorzichtige componenten van de score bij het oplossen van fouten; standaardinstellingen die gericht zijn op de Engelse gebruikerscultuur
ARESGetrainde classificator judges volgens pipeline; minder annotaties dan aanpakken in de RAGAS-stijl; hoge precisie voor nauwkeurige systemenZwaardere configuratie; je moet models daadwerkelijk trainen.
TruLensComposable feedbackfuncties met hoge explicatierichtheid; OpenTelemetry traces; geschikt voor productieomgevingenEr zitten minder batterijen in de specificaties voor RAG dan in de RAGAS-modellen.
DeepEvalUnittests in Pytest-stijl voor de uitvoer van LLM; G-Eval, aangepaste metrieken, en integratie met CI/CD-pipelinesZware LLM-judge-gebruik = stijgingen in de kosten
Arize PhoenixSterke visualisatie van tracing en embedding; visuele detectie van embedding-afwijkingen; OTEL-natiefJe brengt je eigen definities van metrics mee.
TREC 2024 RAG TrackOpenbare benchmark voor de evaluatie van nuggets (AutoNuggetizer), ondersteuning bij evaluaties en beoordeling van vloeiendheid op MS MARCO Segment v2.1Geen runtime-hulpmiddel; een benchmark om tegen te kalibreren

Mijn standaardstack bestaat uit RAGAS voor het metriekvocabulaire, DeepEval voor CI-gates, Phoenix voor productie tracing, plus aangepaste code voor metrieken die specifiek zijn voor de ontologie. U zult uiteindelijk alles wat u nu begint te gebruiken te klein vinden. Kies de framework die het mogelijk maakt om aangepaste metrieken eenvoudig te genereren.

Voor benchmarks, gebruik dan BEIR (Thakur et al., NeurIPS 2021) voor zero-shot retrieval-generalisatie. MTEB voor een algemene embedding kwaliteit, MIRACL voor meertalige retrieval, en de TREC 2024 RAG Track voor evaluatie op eind-tot-eindniveau van RAG.


Deel 2: Evaluatiepunten koppelen aan pipeline

Een productiesysteem voor RAG is complexer dan louter het embedden van documenten, het ophalen van chunks, en het aanroepen van een LLM. Op elk stadium tussen het verkrijgen van de documenten en het leveren van het antwoord kan er een fout optreden.

Het volledige RAG pipeline met metrische badges op elk stadium

Elk stadium in het diagram beschikt over ten minste één metriek. Een stadium zonder metriek kan falen zonder dat iemand het opmerkt.

De drie kanalen komen overeen met de punten waar bewijsmateriaal verloren kan raken. Het inlaatkanaal omvat parsing, zuivering, chunking, embedding, en indexeren. Het kanaal tijdens het opvragen omvat herformulering, filteren, retrieval, reranking, en samenstelling van context. Het antwoord- en productiekanaal houdt rekening met nauwkeurigheid, controle van citaten, gebruikersignalen, drift, latency, en kosten.

Fouten stapelen zich op langs de keten. Onjuist parseren beperkt chunking. Onjuiste chunking-waarden beperken op hun beurt retrieval. Onjuiste retrieval-waarden beperken vervolgens reranking. Onjuiste reranking-waarden beperken de generatie van resultaten. Betrouwbaarheid meet uitsluitend het eindresultaat, nooit de oorzaken daarboven in de keten.


Deel 3: Evaluatie van de invoer

Veel fouten bij RAG in productie beginnen al tijdens de invoer. Het systeem werkt goed met schone testdocumenten, maar faalt vervolgens bij echte PDF’s, scans, tabellen en ongeorganiseerde pagina’s uit het corpus.

Documentverzameling en parsing

Wat te meten valt:

Vergelijk verschillende families parsers, zoals een Tesseract-baseline, een OCR model gebaseerd op VLM, en de optie van uw leverancier. Gebruik hiervoor een gestrateerde steekproef van echte documenttypen bij een vaste DPI, waarbij ook schone scans, foto’s, tabellen, meertalig tekstmateriaal, wiskundige formules en handschrift worden opgenomen. Rapporteer de CER of WER voor elk type document, evenals de TEDS voor pagina’s met tabellen.

Reiniging en normalisatie

Chunking stelt de kwaliteit van retrieval vast

Chunking kan een meerpuntrige kloof in de recall veroorzaken, zelfs wanneer de embedding model constant blijft. In NVIDIAs leverancier voor 2024 benchmark, Op pagina-niveau leverde chunking de hoogste nauwkeurigheid en de laagste variatie op voor gepagineerde documenten. Beschouw dit resultaat als bewijs voor het geteste corpus, en niet als een universele winnaar.

Semantic chunking groepeert aangrenzende zinnen op basis van embedding-gelijkenis en snijdt bij ongelijke grenzen. LangChain’s SemanticChunker en die van LlamaIndex SemanticSplitterNodeParser Implementeer deze strategie. Hiermee kan de recall worden verbeterd ten opzichte van vaste tijdsvensters wanneer thematische grenzen van belang zijn.

Bij recursieve splitsing van tekens worden eerst paragraafbreuken geprobeerd, gevolgd door zinsbreuken en vervolgens woordbreuken, totdat elke chunk binnen de gewenste grootte past. LangChain’s RecursiveCharacterTextSplitter De sequentie wordt geïmplementeerd door kandidaatvensters en overlappingwaarden te kiezen die passen bij de structuur van uw document, waarna de gouden set de definitieve waarden bepaalt.

Metrics die in de gaten moeten worden gehouden:

Mijn mening: structurele chunking (splitsing op koppen, tabellen en secties — geïmplementeerd door parsers zoals) unstructured.io Of door het AST dat uw parser al heeft gegenereerd te doorlopen, wordt nauwelijks benut. Als uw documenten een structuur hebben, maak u deze eerst gebruik van voordat u similariteitsheuristieken toevoegt. Recursieve karaktersplitsing vormt de basis; semantische chunking is pas de moeite waard vanwege de extra overhead wanneer er sprake is van ongestructureerde tekst.

Extractie en verrijking van metadata

Embedding generatie

Indexopbouw


Deel 4: Evaluatie tijdens het opvragen

De query-tijdbaan bevat de metrics die worden gebruikt om een retrieval-pad te diagnosticeren. Alleen Recall@k kan niet aangeven of het falen is veroorzaakt door herschrijving, filtering, reranking, of contextsamenvoeging.

Vraagbegrip en herschrijving

Retrieval metrics

Dit zijn de basismetrieken. Als u deze niet bijhoudt, kunt u niet bepalen of retrieval zich verbetert.

MetricaWat er wordt gemetenWanneer te gebruiken
Recall@khet deel van de relevante documenten van een query dat in de top k wordt weergegevenGebruik wanneer het ontbreken van een onderdeel uit de relevante set van belang is.
Precision@khet percentage van de top-k elementen dat relevant isHandig wanneer context window de bottleneck is
MRRgemiddelde van 1/rank van het eerste relevante documentwanneer gebruikers alleen naar de top-1 of top-3 kijken
nDCG@kpositiegerelateerde winst, gewogen naar relevantiegradende standaard retrieval-metriek voor geclassificeerde relevantie
MAPgemiddelde precisie voor alle zoekopdrachtenwanneer je rekening houdt met de volledige gerangschikte lijst
Hit Rate@kof er ten minste één relevante document in de top k voorkomtDe binaire uitkomst gemiddelden over meerdere queries om snel een indicatie van de betrouwbaarheid te verkrijgen.
DekkingHet percentage gouden documenten dat ooit is teruggevonden bij alle zoekopdrachtenvindt systematische gaten in de index

De formules, ter referentie (binaire relevantie met het relevante verzameling RqR_q voor de query qq, en reli=1\text{rel}_i = 1 indien het ii-de opgehaalde document zich in RqR_q bevindt):

Recall@k=Rqd1,,dkRq,Precision@k=Rqd1,,dkk\text{Recall@k} = \frac{|R_q ∩ {d_1, …, d_k}|}{|R_q|}, \quad \text{Precision@k} = \frac|R_q ∩ {d_1, …, d_k}|{k} RRq=1rang van het eerste relevante document,MRR=1QqQRRq\text{RR}_q = \frac{1}{\text{rang van het eerste relevante document}}, \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 geclassificeerde relevantie geldt dat reli{0,1,2,}\text{rel}_i \in \{0, 1, 2, \dots\}; binaire nDCG is het speciale geval dat wordt gebruikt in de onderstaande code. MAP is de gemiddelde waarde over alle zoekopdrachten 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, Inleiding tot informatieopvinding, Hoofdstuk 8 voor de afleidingen.

Voor productiecode moet u deze gebruiken. ranx, pytrec_eval, of ir_measures — Zij implementeren de volledige TREC-metriekfamilie en verwerken gegradeerde relevantie correct. Stel release-doelstellingen vast op basis van een realistisch gouden dataset, de kwaliteit van de downstream-antwoorden en de kosten van een fout. Neem geen drempelwaarden over uit een handleiding.

De test harness voor deze componenten is kort. U kunt hem al uitvoeren vanuit een notebook, nog voordat u een vector database hebt geselecteerd.

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 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):
    # 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):
    # 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[q], gold[q], K) for q in gold):.3f}")
print(f"MRR:       {mean(reciprocal_rank(runs[q], gold[q]) for q in gold):.3f}")
print(f"nDCG@{K}:  {mean(ndcg_at_k(runs[q], gold[q], K) for q in gold):.3f}")
# Recall@5: 0.750
# MRR:       0.625
# nDCG@5:    0.627

Dat is uw retrieval CI-gate. Koppel het aan een snel subset dat is gebaseerd op codecoverage bij elke PR, en voer de volledige ‘golden set’ uit bij het langzamere release-gate. Blokkeer een merge wanneer een van tevoren geregistreerde metric zijn regressiebudget overschrijdt.

Het bijbehorende repository pinnt de exacte getallen hierboven vast.Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) als een eenheidstest in tests/test_retrieval_metrics.py; Notitieboek 01 voert sweeps uit voor Recall@k / MRR / nDCG op een echte SciFact-index, en de in productie gebruikte harness bevindt zich in evaluation/retrieval.py.

Hybride fusing van retrieval-ranken en wederzijdse rangen

BM25 is een spaarzame lexicaal scoreerder die exacte-termovereenkomsten, termgewichting en lengte-normalisatie combineert. Hij is beschikbaar in rank_bm25Elasticsearch, OpenSearch en de meeste zoekmachines.

Reciproque rangfusie (Cormack, Clarke, en Buettcher, SIGIR 2009) combineert BM25 met dichte rangschikkingen op basis van positie. Het originele k=60 Een dergelijke instelling vormt een nuttige uitgangspunt. RRF is score-onafhankelijk, waardoor de normalisatie tussen verschillende rijbanen die bij lineaire interpolatie nodig is, wordt vermeden. Met een voldoende grote gemarkeerde dataset om een stabiele delta te kunnen bepalen, dient men ook een convexe combinatie te testen en de waarde van α af te stemmen.

Een hybridmodel retrieval in combinatie met een cross-encoder reranker verbetert vaak technische, loggen-gebaseerde en codecorpora. De voordelen zijn mogelijk beperkt bij corpora waarin semantiek een grote rol speelt. Meet de prestaties tegenover configuraties die uitsluitend gebruikmaken van dichte of spaarzame data, aangezien een slechte fusing-configuratie de prestaties van beide invoerbronnen kan verminderen.

De implementatie past in een paar 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)
    for ranking in rankings:
        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

Let op wat RRF niet doet: het kijkt nooit naar de ruwe similariteitscores. Een dense retriever die een cosinuswaarde van 0,98 retourneert en een BM25-algoritme dat een score van 17,4 geeft, zijn niet direct vergelijkbaar. Als je ze normaliseert met z-scores of min-max-scaling, kan dit ertoe leiden dat de methode met de hoogste variatie in die batch wordt bevoordeeld.

RRF maakt uitsluitend gebruik van rang. Als een retriever een document op positie 2 plaatst, is die stem waard 1 / (60 + 2)ongeacht het ruwe scoreniveau dat dit heeft opgeleverd.

Hybride + RRF op SciFact: Notitieboek 02 vergelijkt dense met BM25 en RRF aan de hand van per-query verschillen. De voor productie gebruikte fuser bevindt zich in retrieval/hybrid_rrf.py; tests/test_rrf.py pinnt de canonieke versie vast d3 / d2 / d1 Bestelling plaatsen bij k=60.

Reranking

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

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

Een reranker is een veelbelovend kandidaat voor een eenvoudige RAG pipeline, maar dit garandeert geen succes. Meet de waarden ΔPrecision@1 en ΔnDCG op uw gouden dataset, en houd het alleen bij als het behaalde voordeel boven de latency en de kostenbegroting uitkomt. Vergelijk dit gemeten voordeel met kleinere retrieval-aanpassingen voordat u voor de volgende optimalisatie kiest.

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

Contextconstructie en het ‘verloren in het midden’-probleem

Dit is waar veel fouten van het type “goede retrieval, slechte antwoord” vandaan komen.


Deel 5: Het false-exclusion-ratio van de filter

Deze metriek krijgt een eigen sectie omdat samengestelde retrieval-scores geen fout kunnen worden toegeschreven aan de filter.

Een strikte metadata-filter zoals tenant_id = X AND product = Y AND locale = en-US Dit kan de effectieve recall tot nul doen dalen. Een correct geïmplementeerde Recall@k herkent dit verlies, aangezien de noemer nog steeds bestaat uit de oorspronkelijke set relevante documenten. Het geeft echter geen indicatie of de filter, de retriever of de ranker de oorzaak is van het gemiste resultaat. De faithfulness kan er nog steeds goed uitzien, omdat deze de antwoord kwaliteiten evalueert op basis van de onvolledige, geraadpleegde context; model heeft immers trouw gezegd: “Ik weet het niet.”

De rode tak in de boom vertegenwoordigt de meest voorkomende fout: het juiste document bestaat wel, maar de filter verwijdert het al vóór retrieval.

Taxonomie van stille fouten met de metric die elke modus detecteert

De metriek

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

Bij deze definitie op query-niveau worden catastrofale uitsluitingen meegeteld: er blijft geen relevante document over. Bij meervoudige ‘gold’-queries leidt standaard Recall@k nog steeds tot een gedeeltelijke verlies van informatie; voeg een uitsluitingsratio per document toe als die grens belangrijk is. Om een van deze ratios te berekenen, heb je (a) de echte document-ID’s voor elke eval query en (b) instrumentatie die de toepaste filterpredicaten logt, en niet alleen de uiteindelijke resultaten. Stel het doel vast op basis van de kosten van het uitsluiten van een geldig antwoord en de betrouwbaarheidsinterval van je productiesample.

Hier is een werkende implementatie. Deze vergelijkt de juiste standaard-recall met een ongeldige evaluator die de relevantie na het filteren opnieuw definiërt.

# A small worked example where hard filters remove relevant 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"},
]

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 is in a different tenant and gets dropped
    {"qid": "q3", "gold": {"d3"}, "filter": lambda d: d["tenant"] == "acme"},
    # 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, docs):
    n_with_gold, n_excluded = 0, 0
    for q in queries:
        if not q["gold"]:
            continue
        n_with_gold += 1
        survivors = {d["id"] for d in docs if q["filter"](d)}
        if not (q["gold"] & survivors):
            n_excluded += 1
    return n_excluded / n_with_gold if n_with_gold else 0.0

rate = filter_false_exclusion_rate(queries, docs)
print(f"filter_false_exclusion_rate = {rate:.2%}")
# filter_false_exclusion_rate = 50.00%

# Correct Recall@k keeps the original gold set as its denominator.
def standard_recall_at_k(queries, docs, k=10):
    recalls = []
    for q in queries:
        survivors = [d for d in docs if q["filter"](d)][:k]
        survivor_ids = {d["id"] for d in survivors}
        recalls.append(len(q["gold"] & survivor_ids) / len(q["gold"]))
    return sum(recalls) / len(recalls) if recalls else 0.0

print(f"standard recall@10 = {standard_recall_at_k(queries, 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, docs, k=10):
    recalls = []
    all_doc_ids = {d["id"] for d in docs}
    for q in queries:
        all_survivors = {d["id"] for d in 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, 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, docs) == 0.5
assert invalid == 1.0

De helft van de zoekopdrachten verliest het gouden document door de filter, waardoor de correcte Recall@10 daalt tot 50%. Deze score geeft wel aan dat er een probleem is, maar kan dit niet specifiek toeschrijven aan een oorzaak. Het percentage valse uitsluitingen laat zien dat het predicaat al twee antwoorden heeft verwijderd voordat de retriever kon worden uitgevoerd. De opzettelijk ongeldige evaluator rapporteert 100% alleen omdat deze fouten worden buitengesloten uit zijn gouden dataset. Geen model kan een document herstellen dat al is gefilterd.

Het hierboven genoemde percentage van 50% wordt als een eenheidstest weergegeven in het bijbehorende repo: tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. Notitieboek 04 het wordt uitgevoerd op SciFact met synthetische metadata, zodat je kunt zien hoe een echte filter de recall tot nul brengt; de runtime-meting (gecombineerd met de bijbehorende predicaten-precisie/recall-waarden) bevindt zich in evaluation/filter_exclusion.py.

Bijbehorende metric: predicatenprecisie en herinneringsratio

Wanneer het filteren dynamisch is (bijvoorbeeld wanneer een LLM filterpredicaten uit de query haalt), moet de predicatenextractor worden beschouwd als een classificatie model en op die manier worden geëvalueerd. Meet de precisie en recall van de predicaten aan de hand van een gelabeld dataset. (query, correct predicate) paren. Een foutkans van een predicaat komt niet direct overeen met dezelfde puntverlieswaarde bij retrieval recall; meet in plaats daarvan hoe vaak dergelijke fouten een gouden document uitsluiten. Zodra een harde filter het gouden document al heeft verwijderd, helpt geen enkele vorm van reranking meer.

Zachte versterking versus harde filtering

Deze metriek dwingt tot een ontwerpprioriteit. Gebruik harde filters wanneer de correctheid binair is: juridische jurisdictie, grenzen van ACL’s, en het onderscheid tussen gepubliceerde en conceptversies. Gebruik zachte versterkingen wanneer de relevantie op een schaal wordt beoordeeld: voorkeur voor locatie, recentheid en versie. Zonder meting van het uitsluitingspercentage is het moeilijk om de verkeerde keuze te herkennen.

De beslissingsregel is meetbaar:

For each filter predicate 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
  exclusion_rate = % of queries where the gold doc was filtered out (hard)

Use hard filter only if exclusion_rate < ε AND hard_precision >> soft_precision.
Otherwise prefer soft boost.

Kies ε op basis van de schade die voortvloeit uit een valse uitsluiting, het voordeel van meer precisie en de omvang van het evaluatiemonster. Een apart artikel in deze serie gaat dieper in op deze afweging.


Deel 6: Evaluatie van generatie

Retrieval-metrieken geven aan dat het systeem kan correct antwoorden. Ze geven echter niet aan dat dit daadwerkelijk is gebeurd. Generatiemetrieken vullen deze lacune op.

Getrouwheid en gefundeerdheid

RAGAS-getrouwheid het antwoord wordt opgesplitst in atomaire beweringen (korte, zelfstandige feitelijke uitspraken), waarna elke bewering wordt geverifieerd tegen de opgehaalde context met behulp van LLM judge:

\text{getrouwheid} = \frac|\text{Claimen ondersteund door de context}|}{|\text{totale claims}|}

Het percentage ondersteunde claims vormt de score. Deze structuur is nuttiger dan één enkel getal, omdat deze aangeeft welke claims niet ondersteund worden. De productiecode bevindt zich in de ragas package — gebruik ziet er als volgt uit:

from datasets import Dataset
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision

samples = Dataset.from_dict({
    "question": ["How many moons does Mars have?"],
    "answer":   ["Mars has two moons, Phobos and Deimos."],
    "contexts": [["Mars has two moons named Phobos and Deimos."]],
    "ground_truth": ["Mars has two moons."],
})

result = evaluate(samples, metrics=[faithfulness, answer_relevancy, context_precision])
print(result)

Hieronder staat dezelfde lus uitgewerkt met een deterministische vervanger judge, zodat u de volledige structuur van begin tot eind 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)
for c, ok in verdicts:
    print(f"  [{'✓' if ok else '✗'}] {c}")
print(f"faithfulness = {faithfulness:.2f}")
# faithfulness = 0.75   (one unsupported claim about the atmosphere)

De structuur is van cruciaal belang. In productieomgevingen, verify_claim Het wordt een NLI model of een LLM aanroep. De overige stappen van harness blijven onveranderd: extraheren, verifiëren en aggregeren.

Einde-tot-einde extractie en verificatie van claims in gegenereerde SciFact-antwoorden: Notitieboek 05; module: evaluation/faithfulness.py. In dezelfde lus draait het repository ook een verificator in HHEM-stijl die verschillende families controleert, zodat u kunt zien welke judge-familie overeenkomt met welke andere.

Een speciaal ontworpen alternatief voor LLM als judge HHEM-2.1-Open (Hughes Hallucination Evaluatie Model, Vectara), een classifier die is gefine-tuned voor hallucination-detectie. De model-kaart documenteert de checkpoint, de standaard beslissingsgrens, evenals de resultaten op AggreFact en RAGTruth. Beschouw deze gegevens als bewijsmateriaal op een model-kaart, en niet als een garantie voor uw eigen corpus: kalibreer de drempelwaarde aan de hand van lokale labels en vergelijk deze met de door u gekozen judge vóór deployment.

Evaluatie van atoomfeiten

FActScore (Min et al., EMNLP 2023) splitst lange vormen van generatieprocesen op in atomaire feiten, haalt bewijsmateriaal op voor elk feit en geeft elk feit een label toe supported / not-supporteden geeft het ondersteunde fractiegedeelte weer:

FActScore=ondersteunde atomaire feitentotale atomaire feiten\text{FActScore} = \frac{|\text{ondersteunde atomaire feiten}|}{|\text{totale atomaire feiten}|}

Referentieimplementatie: shmsw25/FActScore. Het werkt uitstekend voor biografieën, samenvattingen en andere lange tekstuitvoerformaten. Wees echter voorzichtig: herhalende, triviale feiten kunnen de score verhogen, en “MontageLie”-aanvallen (waarbij echte feiten in een misleidende volgorde worden gepresenteerd) kunnen het systeem misleiden. VeriScore verwerkt claims met de benodigde modificatoren; de Kernel Een filter helpt om het toevoegen van onnodige feiten te voorkomen.

Precisie van citaties

Houd de precisie van citaties in de gaten (de geciteerde spans ondersteunen daadwerkelijk de bewering) en de herinneringsratio van citaties (de beweringen die geciteerd zouden moeten worden, zijn):

\text{cite\_precision} = \frac|\text{de geciteerde spans die een bewering ondersteunen}|}{|\text{geciteerd spans}|}, \quad \text{cite\_recall} = \frac|\text{Claimen met ten minste één onderbouwende geciteerde span}|}{|\text{claimen die geciteerd moeten worden}|}

De TREC 2024 RAG Track definieert een reproduceerbaar protocol voor support evaluatie. Upadhyay et al. (SIGIR 2025) rapport dat GPT-4o het met mensen eens is judges 56% van de tijd bij een handmatige evaluatie vanaf nul, met een stijging tot 72% na post-editing. LLM Voorspellingen. Dit is nuttig als krachtvermeerker onder hun specifieke omstandigheden, maar niet als vervanging voor menselijke beoordeling in risicovolle situaties. Voor een geautomatiseerde benadering, ALCE (Gao et al., EMNLP 2023) implementeert citatieprecisie/citatierecall met verificatie gebaseerd op NLI.

Correctheid, volledigheid en weigering van antwoorden

Verificatie na generatie

De goedkoopste verbeteringen op het gebied van betrouwbaarheid komen meestal voort uit deterministische post-checks, en niet uit grotere models-systemen.


Deel 7: Evaluatie gebaseerd op ontologieën voor RAG

De hierboven genoemde standaardmetrieken dekken open corpora RAG. Systemen die zijn gebaseerd op een ontologie hebben meer nodig. Als uw RAG gegevens ophaalt uit een gestructureerde ontologie, taxonomie of kennisgrafiek (producten in een catalogus, condities in SNOMED, componenten in een BOM, beveiligingstechnieken in MITRE ATT&CK), zijn standaard RAG-metrieken noodzakelijk, maar voldoende niet. U moet ook de ontologielayer meten.

Precisie van entiteitenkoppeling

De eerste taak is het koppelen van een vermelding in een query aan een entiteit in de ontologie (“Aspirin” → wikidata:Q18216, “de 737” → aircraft:Boeing_737).

Evaluatie met rekening houding met hiërarchie

Eenvoudige nauwkeurigheid behandelt het geval ‘voorspeld Sedan terwijl de werkelijkheid Hatchback is’ op dezelfde manier als ‘voorspeld Sedan terwijl de werkelijkheid Submarine is’. Deze fouten zijn echter niet gelijkwaardig.

Foutieve uitsluitingsratio van filters (herhaling, nu kritiek)

In ontologie-gebaseerde systemen komen harde filters vaak voort uit de ontologie zelf (“haal alleen documenten op die zijn gemarkeerd met categorie X”). De exclusieratio-metriek (gedefinieerd in Deel 5) Dit wordt een belangrijk signaal voor correctheid. Een verkeerde categorievoorspelling kan de recall tot nul brengen; de uitsluitingsgraad wijst aan dat deze verlies op het filter terug te voeren is.

Conformiteit van beperkte generatie

Wanneer uw uitvoer moet voldoen aan een ontologie (elk entiteitsnaam in het antwoord moet een geldig lid van de ontologie zijn; elke predicaat moet afkomstig zijn uit een gesloten woordenschat), meet dan:

Constrained decoding frameworks (Overzichten, XGrammar, Leidraad, OpenAI Structured Outputs) zijn ontworpen om de validiteit van het schema af te dwingen. JSONSchemaBench Het vergelijkt de efficiëntie, de dekking en de kwaliteit tussen verschillende implementaties. Voer opnieuw de testgevallen uit die overeenkomen met uw schema’s en serving backend, aangezien zowel de dekking als latency afhankelijk zijn van beide.

Auditbaarheid

Voor ontologie-gebaseerde systemen waarbij antwoorden worden gecontroleerd:


Deel 8: Evaluatie op systeemniveau

Algemene kwaliteit van het antwoord

LLM-as-judge vertoont echte vooringenomenheden:

Praktische aanpak: kies een judge uit calibration data-geëtiketteerde gegevens, randomiseer de volgorde van de antwoorden, maskereer de identiteiten van model en specificeer de lengtebeperking in de beoordelingscriteria. Herhaal gevallen alleen wanneer de toegevoegde voorbeelden de onzekerheid aanzienlijk verminderen. Bij evaluaties met hoge stakes moet je judges uit verschillende model-families vergelijken en de meningsverschillen analyseren ten opzichte van de menselijke labels.

Schema-gestuurde Reasoning voor judges

Vrije vorm van uitvoer is een van de oorzaken van variatie bij uitvoeringen van judge. Twee uitvoeringen op dezelfde vraag kunnen de beoordelingscriteria op verschillende manieren toepassen, waardoor er verschillende scores ontstaan. Schema-gestuurde Reasoning (SGR) Maak deze richtlijnen expliciet: definieer de evaluatiefasen als een Pydantic-schema, en gebruik vervolgens geconstrueerde uitvoer via Outlines, XGrammar, vLLM structured outputs, of OpenAI. response_format Dus elke uitvoering retourneert dezelfde velden in dezelfde volgorde.

Voor RAG eval splitst het schema het oordeel op in expliciete, controleerbare velden, in plaats van de model direct naar een getal te laten gaan:

from pydantic import BaseModel, Field
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."
    )
    failure_mode: Literal[
        "none", "fabrication", "overgeneralization", "wrong_entity", "stale_fact"
    ]
    score: float = Field(ge=0.0, le=1.0)
    rationale: str

De gestructureerde velden maken het mogelijk om de score weer te herstellen. len(supported) / len(extracted) En toon precies over welke beweringen twee judges het oneens waren. De Pydantic model maakt ook een verandering in de rubriek zichtbaar als een code‑diff. Door de uitvoer te beperken wordt alleen de vorm gegarandeerd, niet een onbevooroordeeld oordeel; daarom blijven positierandomisatie, cross‑family judges en menselijke calibration van toepassing.

Dit werkt voor elke op een rubriek gebaseerde judge, niet alleen voor de beoordeling van nauwkeurigheid. Zowel de paarwaardige voorkeursbeoordeling, de ondersteuning voor citaten als de correctheid bij weigeringen profiteren allemaal van dezelfde behandeling.

Een G-Eval / paarsgewijs / positiebias / interfamiliale judge harness komt voor in Notitieboek 07; module: evaluation/llm_judge.py. De benchmark-scanmake benchmark in de repo) verbindt drie frontier-tier models — gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash — omgezet naar een rotatievolle judge paarvoor-paar A/B zodat elke model judges de andere twee, waardoor de zelfvoorkeur numeriek zichtbaar wordt.

Latency en kosten

Een per-fase p50/p95/p99 met een gedetailleerde fasesplitsing is ingebouwd Notitieboek 08 en de runner bij evaluation/latency.py; Het rapport benchmark combineert latency met nauwkeurigheid in één enkele matrix die u opnieuw kunt uitvoeren. make benchmark.

A/B-testen


Deel 9: Opbouw van de testset

Een metriek is alleen zo goed als de testset waarop deze wordt uitgevoerd. Als uw gouden dataset drie intenties omvat en het productietraffic spans twaalf, meet Recall@10 alleen die drie intenties. Erger nog: een testset die te goed past bij eenvoudige vragen (“Wat is het restitutiebeleid van het bedrijf?”) kan een systeem goedkeuren dat faalt bij moeilijkere gevallen (“Voldoet een gedeeltelijke annulering onder de EU Digital Services Act van 2023, gefactureerd in EUR en afkomstig uit Ierland, aan de voorwaarden voor restitutie?”). De totale score stijgt, terwijl het systeem nog steeds een belangrijk deel van het productietraffic niet kan verwerken.

Hetzelfde probleem doet zich voor bij de ground truth. Als KMO’s de voor de hand liggende documenten hebben gelaagd maar de minder voorkomende, relevante documenten over het hoofd hebben gezien, zal Recall@k een retriever die ze wel heeft gevonden onterecht lage scores geven. Men optimaliseert dan richting de labels, en niet richting de waarheid.

Bouw eerst het testset op basis van de werkelijke verdeling en moeilijkheidsgraad van de vragen. Kies daarna metrics die reageren op de gewenste foutmodi, en stem het systeem hierop af.

Generatie van synthetische queries

Gebruik een LLM om vragen te genereren uit uw corpus:

RAGAS bevat een ingebouwde verdeling van vragensoorten (reasoning, conditioneel, meervoudig context). DataMorgana Het genereert configureerbare synthetische benchmarks-gegevens voor verschillende categorieën gebruikers en vragen. Synthetische gegevens zijn nuttig voor ‘cold start’-situaties en voor het uitvoeren van coverage-tests. Ze kunnen echter geen vervanging zijn voor echte gebruikersvragen.

Gouden dataset-constructie

Gedegenereerde gegevens vormen de basis voor de gouden set.

  1. Voorbeelden van echte gebruikersvragen (of gesimuleerde vragen indien nog voor de lancering), gegroepeerd naar intentie.
  2. Laat SME’s elke vraag beantwoorden en bepalen welk(ke) document(en) het antwoord bevatten.
  3. Bepaal de omvang van de set op basis van de dekkingsmatrix en het vertrouwensinterval dat nodig is voor beslissingen over de lancering; dekking is belangrijker dan het aantal gebruikte vragen.
  4. Herstel de dataset wanneer de lanceringssnelheid, signalen van afwijkingen, domeinrisico’s of annotatiecapaciteit dit rechtvaardigen.

Adversariale testsets

Dekking en continue evaluatie


Deel 10: Productiemonitoring

Het eval-pakket dat u distribueert, beschrijft het systeem bij de lancering. Het verkeer in productie verandert daarna.

Impliciete en expliciete feedback

Detectie van drift

Schaduwevaluatie en human-in-the-loop

Voer het kandidaatstelsel parallel aan het productiesysteem uit, vergelijk de uitvoer offline en serveer deze niet aan gebruikers. Op deze manier worden regressies al vóór de lancering opgespoord. Dit kost extra inference, maar heeft geen invloed op de klanten.

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

Het minimale guardrail-set

Geef alarm voor deze items, in volgorde van prioriteit:

  1. Faithfulness/HHEM-score lager dan de drempelwaarde in een rollende productiesample.
  2. p95 latency hoger dan de SLO.
  3. Foutloze uitsluitingsratio van filters boven de drempelwaarde (op basis van een sample).
  4. Regeneratieratio buiten een lokaal gekalibreerde controleband die rekening houdt met de venstergrootte, het verkeer, seizoensinvloeden en het budget voor valse alarmen.
  5. Kosten/per query boven het budget.

Wanneer een alarm wordt afgevuurd zonder een overeenkomstige code of een model-wijziging, wijst dit waarschijnlijk op drift. Als het alarm na een wijziging wordt afgevuurd, duidt dit meestal op een regressie. In beide gevallen krijg je al een signaal voordat er supportverzoeken binnenkomen.


Aandachtspunten


Wat komt eraan in deze serie

Dit was de index. De vervolgstukken zijn planning:


Referenties

Frameworks en benchmarks

Retrieval en rangschikking

Generatie, getrouwheid, judges

Drift en productieomgeving

Bijbehorende code