RAG-Evaluationsmetriken: Retrieval, Reranking, Generation

Automatische Übersetzung Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.

Artikel-Update

Ursprünglich am 10. Mai 2026 veröffentlicht. Am 6. September 2026 geprüft und aktualisiert. Das Update ergänzt neuere Retrieval-Benchmarks und Reranker-Kandidaten, überarbeitet die Hinweise zu Evaluation-Tools und korrigiert die API-Parameter im Judge-Beispiel.

Ein RAG-System mit defekten Relevanzfiltern kann monatelang laufen, ohne einen operativen Alarm auszulösen. Es liefert weiterhin Antworten und erreicht sein Latency-Ziel, aber die Antworten basieren auf unvollständiger Evidenz. Recall@k gegen das ursprüngliche zulässige Gold-Set macht diesen Verlust sichtbar. Dashboards für Latency und Verfügbarkeit tun das nicht.

Für Engineers, die Multi-Stage-RAG-Systeme betreiben oder evaluieren, ordnet diese Referenz Fehler beim Document Parsing, Filtern, Retrieval, Reranking und bei der Generation der Metrik zu, die sie jeweils identifiziert. Außerdem zeigt sie, welche Checks vor einem Release laufen und welche den Live-Traffic überwachen.

Direkt zum Code?

Das ausführbare slavadubrov/rag-evals-demo-Repository wendet ausgewählte Metriken auf SciFact an. make eval führt die Suite aus, und make benchmark vergleicht Chunking-, Embedding- und LLM-Konfigurationen. Die Notebooks 00–09 behandeln Retrieval, Filterung, Generation und Systembeispiele; sie implementieren nicht jeden Check dieser Referenz. Die Demo verwendet eingebettetes Qdrant und benötigt daher kein Docker.

Das Companion-Projekt ist ein Lehr-Harness. Die am 6. September 2026 geprüfte Revision benötigt weiterhin Reparaturen bei der Erfassung fehlender Queries, beim Parsing von Judge-Ergebnissen und bei den Authorization-Golddaten. Dem Pairwise Judge fehlt außerdem der bereitgestellte Context, der für die Bewertung der Unterstützung erforderlich ist. Die korrigierten Contracts und Inline-Checks weiter unten patchen dieses Repository nicht. Verwende seine Notebooks, um den Workflow zu untersuchen, und prüfe diese Fälle, bevor du seine Scores als Release-Kriterien übernimmst.

TL;DR

  • Ein brauchbarer Evaluationsstack deckt Ingestion, Retrieval, Grounding der Generation, Ontologie-Konformität und Systemsignale ab. RAGAS, TruLens, DeepEval, Arize Phoenix und der TREC 2024 RAG Track bieten Libraries oder öffentliche Evaluationsprotokolle. Sie wählen die Metriken jedoch nicht für dich aus.
  • Bei metadata- und ontologie-basiertem RAG kann ein falscher Tag oder ein fragiles hartes Predicate den Recall auf null reduzieren. Standard-Recall@k erkennt den Verlust, wenn das ursprüngliche zulässige Gold-Set beibehalten wird. Eine Metrik für False Exclusions durch Filter identifiziert die Ursache. Faithfulness kann Claims weiterhin gegen einen unvollständigen Context bewerten, aber die Ursache im Filter oder Retrieval nicht diagnostizieren. Eine leere Verweigerung kann je nach Implementierung keine Statements und NaN erzeugen.

Entscheidungstabelle für RAG-Evaluation

Verwende diese Tabelle als Ausgangspunkt, bevor du ein Framework auswählst. Die richtige Metrik hängt vom Failure Mode ab, den du erkennen willst – nicht vom Namen des Tools.

FrageMetrikfamilieVerwenden, wennAchtung
Hat das Parsing die Quelle erhalten?Extraction Completeness, Table-/Figure-CoveragePDFs, Slides, Scans und HTML-Seiten in den Corpus gelangenSauber aussehender Text kann Captions, Fußnoten oder Tabellenstruktur verlieren
Hat Retrieval die richtige Evidenz gefunden?Recall@k, nDCG@k, MRR, Context Precision/RecallRelevante Chunks oder Dokumente gelabelt werden könnenEin harter Metadata-Filter kann das richtige Dokument vor dem Ranking entfernen
Hat Reranking die Shortlist verbessert?Reranker-Uplift, Precision@1, nDCG-DeltaCross-Encoder oder LLM-Ranker nach dem Retrieval eingesetzt werdenLatency und Kosten zusammen mit dem Quality Gain messen
Hat die Antwort die Evidenz verwendet?Faithfulness, Groundedness, Citation SupportDie Antwort Dokumente zitiert oder Fakten aus dem Context übernimmtFaithfulness kann schlechtes Parsing oder schlechtes Retrieval nicht diagnostizieren
Ist das System in Production stabil?Drift, Regeneration, Fallback, p95 Latency, Cost per AnswerSich der Traffic nach dem Launch verändertProduction-Telemetrie braucht gesampeltes Human Review zur Kalibrierung

Einen kürzeren Tool-Vergleich findest du unter Best RAG Evaluation Tools: Ragas, DeepEval, and TruLens.

Teil 1: Erfolg vor der Architektur definieren

Erstelle das Eval-Set vor dem Architekturdiagramm. Es gibt jeder späteren Komponentenentscheidung ein messbares Ziel.

Du kannst dich nicht zwischen BM25 und Dense Retrieval, Recursive und Semantic Chunking oder Cohere Rerank und BGE entscheiden, bevor du weißt, was du optimierst. „Bessere Antworten“ ist keine Metrik. Eine beispielhafte Release-Anforderung wäre: „Faithfulness ≥ 0,85 auf einem Golden Set mit 200 Queries, das unsere drei wichtigsten Intents abdeckt, bei p95 Latency < 1,5 s und einer False-Exclusion-Rate des Filters < 2 %.“ Die Zahlen sind Platzhalter; entscheidend ist, dass Quality, Coverage, Latency und Filtering explizite Schwellenwerte haben.

Definiere das Harness, bevor du den Retrieval-Code schreibst. Das erste Harness wird falsch sein, und du wirst es überarbeiten. Eine Metrik zu ändern ist wesentlich günstiger, als ein bereits ausgeliefertes System zu überarbeiten.

Drei Pipeline-Layer und zwei Run-Modi

Production-Evaluation hat drei Pipeline-Layer. Ingestion-Evaluation fragt, ob Corpus und Index die Quelle erhalten. Query-Time-Evaluation fragt, ob Rewriting, Filtering, Retrieval, Reranking und Context Assembly die richtige Evidenz gefunden haben. Answer- und Production-Evaluation fragt, ob die Response diese Evidenz verwendet hat und ob die Quality unter Live-Traffic stabil bleibt. Wenn du die Layer zu einem Score zusammenfasst, kann ein Normalisierungs-Bug in einem akzeptablen Answer-Score verschwinden.

Die drei Stellen, an denen ein RAG-System Evidenz verlieren kann: Corpus und Index, Retrieval-Pfad sowie Antwort und Live-TrafficDie drei Stellen, an denen ein RAG-System Evidenz verlieren kann: Corpus und Index, Retrieval-Pfad sowie Antwort und Live-Traffic

Diese Layer beschreiben, wo ein Fehler auftritt. Offline und Online beschreiben, wann und gegen welche Daten der Check läuft. Offline-Evaluation verwendet ein fixes Dataset mit bekannter Ground Truth; sie ist reproduzierbar und gehört in die Auswahl von Komponenten, A/B-Vergleiche und CI-Checks, die eine Änderung blockieren können. Online-Evaluation bewertet gesampelten Live-Traffic und erfasst Regeneration, Verweildauer, explizites Feedback und echten Query Drift. Sie ist verrauschter und schwieriger zu instrumentieren.

Verwende beide Modi, wo sie helfen: Fixe Corpora und Query-Sets machen Regressionen reproduzierbar; gesampelte Live-Traces zeigen Freshness-Probleme und Drift.

Komponentenebene vs. End-to-End

Es gibt zwei häufige Fehler. Eine reine End-to-End-Evaluation zeigt, dass das System defekt ist, aber nicht wo. Eine reine Komponenten-Evaluation kann zeigen, dass jedes Teil besteht, während das Gesamtsystem trotzdem scheitert. Die Lösung sind einige zentrale End-to-End-Metriken für Go/No-Go-Entscheidungen plus Komponentenmetriken für die Diagnose. Retrieval-Metriken erkennen Retriever-Regressionen. Generation-Metriken erkennen Generator-Regressionen. End-to-End-Answer-Correctness erkennt Integrationsfehler.

Die Referenz-Frameworks (meinungsstarker Überblick)

FrameworkBesonders gut beiSchwächen
RAGASEiner gemeinsamen Terminologie für Faithfulness, Answer Relevancy und Context Precision/Recall (Metriken)Kosten des LLM-Judges; intransparente Score-Komponenten beim Debugging; Versionsänderungen
ARESEinem Task-spezifischen Classifier-Judge, wenn Training und Annotation die Kosten rechtfertigen (Paper); die gemeldete Precision ist an den Benchmark gebundenSchwereres Setup; du musst Modelle tatsächlich trainieren
TruLensTrace-verknüpften Feedback Functions und OpenTelemetry-Integration (Projekt)Weniger RAG-spezifische Metriken out of the box als RAGAS
DeepEvalTest-Runner-Integration und Custom Metrics (Projekt)Starke Nutzung von LLM-Judges führt zu Kostenspitzen
Arize PhoenixTracing, Dataset-Experimente sowie vorgefertigten oder benutzerdefinierten RAG-/Agent-Evaluatoren (Evaluationsdokumentation)Domain-Rubriken und Judge-Schwellenwerte müssen weiterhin lokal kalibriert werden
TREC 2024 RAG TrackÖffentlichem Benchmark für Nugget-Evaluation (AutoNuggetizer), Support-Evaluation und Fluency auf MS MARCO Segment v2.1Kein Runtime-Tool, sondern ein Benchmark zur Kalibrierung

Mein Default-Stack ist RAGAS für die Metrikterminologie, DeepEval für CI-Checks, Phoenix für Production-Tracing und Custom Code für ontologiespezifische Metriken. Wähle das Framework, das Custom Metrics einfach macht.

Bei der Benchmark-Auswahl solltest du den Task abgleichen, bevor du das Leaderboard liest. BEIR, MTEB und MIRACL bleiben nützliche Retrieval-Baselines. Ergänze Tests für Fähigkeiten, die sie nicht abdecken:

  • Aktuelles End-to-End-RAG: Der TREC 2026 RAG Track verwendet narrative Queries und ClimbMix-400b, ersetzt MS MARCO v2.1 und verlinkt das RAGDoll-Evaluation-Toolkit. Am 6. September hat die Track-Seite noch kein Datum für die Rückgabe von Ergebnissen und Judgments angekündigt. Die veröffentlichten Topics stehen für Experimente zur Verfügung; sie sind kein abgeschlossener, bewerteter 2026-Leaderboard. Halte die Protokolle von 2024 und 2025 an ihre jeweiligen Corpora und Judgments gebunden.
  • Technische Fragen gegen sich weiterentwickelnden Code: FreshStack kombiniert von Menschen gestellte Stack-Overflow-Fragen, Repository-Corpora und Nugget-Judgments. Sein veröffentlichter Snapshot und seine Mechanismen zum Erstellen neuer Corpora sind verschiedene Dinge; pinne die Repository-Revision und das Fragendatum.
  • Bilder, die einen Teil der Frage oder Evidenz tragen: MM-BRIGHT trennt Text-to-Text-, Multimodal-to-Text-, Multimodal-to-Image- und Multimodal-to-Multimodal-Retrieval. Bewerte den relevanten Task separat. OCR-Text allein kann auslassen, was ein Chart oder Screenshot beiträgt.

Diese Benchmarks erweitern die Coverage; sie ersetzen nicht das zulässige, versionierte Query-Set deiner Anwendung.


Teil 2: Evaluationspunkte zuordnen

Die RAG-Pipeline gruppiert in Ingestion-, Query-Time- und Answer-Pfade, mit diagnostischen Metriken neben jeder StageDie RAG-Pipeline gruppiert in Ingestion-, Query-Time- und Answer-Pfade, mit diagnostischen Metriken neben jeder Stage

Verwende das Diagramm, um ein Symptom zu seiner ersten diagnostischen Metrik zuzuordnen. Upstream-Verluste begrenzen die Downstream-Quality: Schlechtes Parsing begrenzt Retrieval, und schlechtes Retrieval begrenzt Reranking und Generation. Faithfulness misst die Antwort, niemals die Upstream-Ursache.


Teil 3: Ingestion-Evaluation

Viele Production-RAG-Fehler beginnen bei der Ingestion. Das System funktioniert mit sauberen Testdokumenten und scheitert dann an echten PDFs, Scans, Tabellen und unordentlichen Corpus-Seiten.

Dokumentbeschaffung und Parsing

Was du messen solltest:

  • Sanity Check für die Extraktionslänge: extracted_chars / expected_chars pro Dokumentklasse markiert verdächtige Längenänderungen, aber doppelter oder falscher Text kann trotzdem 1,0 erreichen. Vergleiche ausgerichteten Text mit einer manuell bereinigten Referenz auf Auslassungen und Ersetzungen und prüfe Fußnoten, Captions, Tabelleninhalt und Lesereihenfolge separat.

  • OCR-Genauigkeit: CER (Character Error Rate) und WER (Word Error Rate), die Standardmetriken für 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}

    wobei SS, DD, II Substitutionen, Löschungen und Einfügungen auf Zeichenebene sind und NN die Anzahl der Referenzzeichen angibt (Subscript ww für die Wortversion). Verwende nicht für jeden Corpus dieselbe CER-Grenze. Kalibriere sie nach Dokumentklasse und nachgelagertem Answer Loss. Gedruckter Text, Handschrift und mehrsprachiges Material haben unterschiedliche Fehlerprofile. Berechne sie mit jiwer (jiwer.cer(refs, hyps), jiwer.wer(refs, hyps)) oder HuggingFace evaluate. Für Evaluationscorpora sind FUNSD und SROIE öffentliche 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
  • Fidelity der Tabellenextraktion: TEDS (Tree-Edit-Distance-based Similarity) misst, wie nahe ein vorhergesagter HTML-Tabellenbaum am Referenzbaum liegt, normalisiert anhand der Größe des größeren Baums. Aus 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 verwendet sowohl Struktur (Zeilen, Spalten, Spans) als auch Zellinhalt. TEDS-S entfernt den Inhalt und bewertet nur die Struktur. Referenzimplementierung: teds.py von PubTabNet (verwendet intern apted). Für Evaluationscorpora siehe PubTabNet, FinTabNet und SciTSR. Naive Parser scheitern häufig an Tabellen. Benchmarke sie, bevor du ihnen vertraust.

  • Erhalt von Layout und Struktur: Überschriftenreihenfolge, Listenintegrität und Lesereihenfolge in mehrspaltigen PDFs. Verwende DocLayNet als gelabelten Benchmark. Ein direkter Vergleich kann einen Element-Parser wie unstructured, eine PDF-Library wie pymupdf und eine ausgewählte Docling-Pipeline umfassen. Docling bietet Standard- und VLM-Pfade; dokumentiere, welchen du testest.

Vergleiche unterschiedliche Parserfamilien, zum Beispiel eine Tesseract-Baseline, ein VLM-basiertes OCR-Modell und deinen Vendor-Kandidaten. Verwende ein stratifiziertes Sample realer Dokumentklassen bei fixer DPI, darunter saubere Scans, Fotos, Tabellen, mehrsprachiger Text, Mathematik und Handschrift. Berichte CER oder WER für jede Klasse sowie TEDS für Tabellenseiten.

Bereinigung und Normalisierung

  • Genauigkeit der Boilerplate-Entfernung: Precision/Recall gegen von Menschen gelabelte Boilerplate-Spans. Aggressive Entfernung löscht relevante Inhalte; zu wenig Entfernung verunreinigt Embeddings. Vergleichbare Tools: trafilatura, jusText, Resiliparse. Barbaresi (2021) vergleicht Trafilatura mit Baselines einschließlich jusText; Resiliparse ist ein separater Kandidat und wurde in diesem Paper nicht evaluiert.

  • Unicode-Normalisierung: Der Prozentsatz der Dokumente, die identische NFC- und NFKC-Ausgaben erzeugen (berechnet mit dem stdlib-unicodedata.normalize), ist ein nützliches Signal für Drift in der Compatibility Form. Unsichtbare oder Default-Ignorable Code Points sowie Lookalikes aus anderen Scripts werden dadurch nicht erkannt: Scanne erstere explizit und wende eine Unicode-Confusables-Policy oder einen Detector an, wenn letztere relevant sind.

  • Genauigkeit der Spracherkennung: F1 auf einem gelabelten multilingualen Sample. Für multilinguale Indizes kritisch. Verwende fasttext-langdetect (Facebooks lid.176), lingua-py oder cld3. FLORES-200 stellt Evaluationstext für 200 Sprachen bereit, aber dein Production-Sprachenmix sollte den Test-Slice bestimmen.

  • Wirksamkeit der Deduplizierung (MinHash / LSH): Precision/Recall deines Near-Duplicate-Detectors gegen ein manuell gelabeltes Set. Die zugrunde liegende Idee: Schätze die Jaccard-Ähnlichkeit J(A,B)=ABABJ(A, B) = \frac{|A \cap B|}{|A \cup B|} zwischen Dokument-Shingle-Sets mithilfe von kk Hashes zufälliger Permutationen (Broder, 1997) und bilde Near-Duplicates mit LSH-Banding in Buckets (Indyk & Motwani, 1998). Sweep Hash-Anzahl und Jaccard-Schwelle auf deinem Corpus. Verfolge False-Merge-Rate (beschädigt Antworten) getrennt von Missed-Merge-Rate (verschwendet Indexspeicher). datasketch stellt die unten verwendete Implementierung bereit; die Parameter dienen nur als Beispiel:

    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 und Recall, jeweils getrennt nach Entitätstyp (E-Mails, SSNs, Namen, Adressen). Recall-Fehler erzeugen Compliance-Risiken; Precision-Fehler verschlechtern die Answer-Quality. Lege den Operating Point gemeinsam mit dem Legal-Team fest. Kandidaten sind Microsoft Presidio, scrubadub oder ein Fine-Tuned-NER-Modell auf einem gelabelten Set.

Chunking steuert die Retrieval-Quality

Chunking bestimmt, welche Evidenz die Antwort erreicht. Im Vendor-Benchmark von NVIDIA aus dem Jahr 2025 erzielte Page-Level-Chunking im getesteten Setup die höchste durchschnittliche End-to-End-Answer-Accuracy, wobei Tabellen und Charts als vollständige Einheiten erhalten blieben. Das misst Answer Accuracy, nicht Retrieval Recall. Interpretiere dieses Ergebnis als Evidenz für den getesteten Corpus, nicht als universellen Sieger.

Semantic Chunking gruppiert benachbarte Sätze anhand der Embedding-Ähnlichkeit und trennt an unähnlichen Grenzen. LangChains SemanticChunker und LlamaIndex’ SemanticSplitterNodeParser implementieren diese Strategie. Sie kann gegenüber fixen Windows den Recall verbessern, wenn thematische Grenzen wichtig sind.

LangChains RecursiveCharacterTextSplitter versucht standardmäßig zuerst doppelte Newlines, einzelne Newlines, Leerzeichen und anschließend einzelne Zeichen: ["\n\n", "\n", " ", ""]. Satzgrenzen erkennt es nicht, sofern du keine geeigneten Separatoren konfigurierst oder einen satzbewussten Splitter verwendest. Wähle Window- und Overlap-Werte, die zu deiner Dokumentstruktur passen, und vergleiche sie auf dem Golden Set.

Zu verfolgende Metriken:

  • 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}}, wobei sis_i Satz-Embeddings sind. Gesunde Chunks sind intern ähnlich und an ihren Grenzen unähnlich. Berechne dies mit sentence-transformers plus cosine_similarity von scikit-learn.
  • Boundary Quality: Von Menschen gelabelte Antwort auf „Ist dies ein sinnvoller Schnitt?“ auf einem Sample sowie ein struktureller Check, dass Chunks keine Tabellen, Listen oder nummerierten Abschnitte trennen.
  • Optimale Chunk-Größe: Sweep über Token-Größen (128, 256, 512, 1024) und zeichne Recall@k gegen die Größe auf deinem Golden Set auf. Wähle den Knickpunkt. Übernimm nicht einfach, was das Tutorial vorgibt.
  • Wirksamkeit des Overlaps: Ablation mehrerer Overlap-Anteile und Messung des Recall@k. Erhöhe den Overlap nicht weiter, sobald die lokale Recall-Kurve abflacht oder die Duplikationskosten den Gewinn übersteigen.
  • Fidelity der Chunk-Attribution: Prozentsatz der Chunks mit verifizierbarem Source Pointer (Seitennummer, Section Anchor, Doc ID). Auditierbarkeit erfordert dies.
  • Late vs. Early Chunking: Late Chunking (Günther et al., 2024) bettet zunächst das vollständige Dokument ein und segmentiert anschließend, wodurch globaler Context erhalten bleibt (Referenzimplementierung in jina-embeddings-v3). Contextual Retrieval (Anthropic, 2024) stellt jedem Chunk LLM-generierten Context voran. Beides verursacht zusätzliche Kosten. Benchmarke auf deinem Corpus, bevor du eine der beiden Varianten übernimmst.

Meine Einschätzung: Strukturelles Chunking (Aufteilung an Überschriften, Tabellen und Abschnitten – implementiert durch Parser wie unstructured.io oder durch Traversieren des AST, den dein Parser bereits erzeugt) wird zu selten eingesetzt. Wenn deine Dokumente Struktur haben, nutze sie, bevor du Similarity-Heuristiken hinzufügst. Recursive Character Splitting ist die Baseline; Semantic Chunking lohnt den Overhead hauptsächlich bei unstrukturierter Prosa.

Metadata-Extraktion und Enrichment

  • NER Precision/Recall/F1: pro Entitätstyp auf einem gelabelten Subset. Standard nach CoNLL/MUC. Berechne dies mit seqeval (from seqeval.metrics import f1_score) für die BIO-/IOB-Tag-bewusste Variante oder mit scikit-learn für Span-Set-Vergleiche. CoNLL-2003 und OntoNotes 5.0 sind die kanonischen Referenzcorpora.
  • F1 der Relation Extraction: Für ontologie-basierte Systeme noch wichtiger. Label von Hand ein nach Relationstyp und Dokumentklasse stratifiziertes Set. TACRED und DocRED sind öffentliche Benchmarks; mögliche Implementierungen sind opennre und spaCy-Relation-Pipelines.
  • Genauigkeit der Titel-/Heading-Extraktion: Exact Match plus normalisierte Levenshtein-Ähnlichkeit (1edit_dist(a,b)max(a,b)1 - \frac{\text{edit\_dist}(a, b)}{\max(|a|, |b|)}) gegen Ground Truth – python-Levenshtein oder rapidfuzz liefern beides in einem Aufruf.
  • Erhalt hierarchischer Metadata: Prozentsatz der Chunks, die ihren Parent-Abschnitt, ihr Parent-Dokument und ihren Ancestry Path korrekt beibehalten. Diese Metrik entscheidet, ob dein RAG Fragen wie „Was sagt das Child von Policy X?“ beantworten kann.

Embedding-Generation

  • Benchmarks zur Modellauswahl: Verwende MTEB-Ergebnisse für Retrieval-Tasks, BEIR für Zero-Shot-Generalization und MIRACL für multilinguales Retrieval als Vergleichspunkte. MTEB Retrieval berichtet typischerweise nDCG@10; andere Task-Familien verwenden andere Metriken. Das MTEB-Python-Package führt die Benchmarks lokal aus. Betrachte die Übertragung von englischem MTEB auf eine Low-Resource-Sprache als Hypothese, die du auf dem gelabelten Set dieser Sprache testen musst.
  • Domänenspezifische Evaluation: Interpretiere einen Rang in einem allgemeinen Benchmark nicht als Ergebnis für deine Domäne. Dimensioniere ein Domain Golden Set anhand seiner Coverage-Matrix und der Unsicherheit, die deine Entscheidung tolerieren kann. Reranke anschließend Kandidatenmodelle darauf mit ranx oder pytrec_eval. Ein Domain-Set kann die Leaderboard-Reihenfolge umkehren; veröffentliche daher Dataset-Slice, Retrieval-Protokoll und Konfidenzintervall zusammen mit dem Ergebnis.
  • Erkennung von Embedding Drift: Vergleiche ein fixes Referenzfenster mit rollenden Embeddings anhand von MMD oder einem validierten Reference-vs.-Current-Classifier. KL benötigt einen expliziten Probability-Distribution-Estimator und kann nicht direkt auf rohe Embedding-Koordinaten angewandt werden. Miss außerdem die Nearest-Neighbor-Stabilität für ein fixes Probe-Set. evidently und alibi-detect implementieren modellbasierte und statistische Detectoren. Evidentlys vergleichende Studie ist eine Vendor-Evaluation; vergleiche die Methoden anhand bekannter Shifts in deinen eigenen Embeddings.
  • Multi-Vector vs. Single-Vector: Late Interaction bewahrt Repräsentationen auf Token-Ebene, statt jedes Dokument in einen einzelnen Vector zu reduzieren; ColBERT ist das kanonische Design, mit Referenzimplementierungen in RAGatouille und PyLate. Diese reichhaltigere Repräsentation erhöht Index- und Retrieval-Kosten. Vergleiche Quality, Speicherbedarf und Latency vor der Einführung mit einer Single-Vector-Baseline auf derselben Domain.

Index-Konstruktion

  • Recall@k unter Approximation: Vergleiche den Approximate-Nearest-Neighbour-(ANN-)Index bei gleichem k mit einer exakten Brute-Force-Baseline – in FAISS ist das IndexHNSWFlat (oder IndexIVFFlat) gegenüber IndexFlatIP/IndexFlatL2. Lege den akzeptablen Recall-Verlust anhand deines Downstream-Quality-Budgets fest. Das ann-benchmarks-Projekt verfolgt Recall–QPS-Pareto-Kurven über mehrere Libraries hinweg.
  • HNSW-Tuning: HNSW (Hierarchical Navigable Small World) ist ein geschichteter Proximity Graph; siehe Malkov & Yashunin, 2018. Implementiert ist er in hnswlib, in FAISS’ IndexHNSWFlat und in den meisten Vector DBs. HNSW bietet drei Stellschrauben: M (Graph Fan-Out), efConstruction (Candidate Width zur Build-Zeit) und efSearch (Candidate Width zur Query-Zeit). Starte mit den dokumentierten Defaults der Library und sweepe die Parameter, bis die Recall–Latency-Kurve die Anforderungen deines Eval-Sets erfüllt.
  • IVF-Tuning: IVF (Inverted File Index – Partitionierung der Vectors mit k-means in nlist Zellen und Scan der nprobe nächsten Zellen zur Query-Zeit; siehe FAISS’ IndexIVFFlat und IndexIVFPQ). Sweep nlist und nprobe gegen Exact-Search-Recall und Latency. Benchmarke gefilterte Queries separat, da Indexfamilien und Vector Databases die Filter-Traversierung unterschiedlich implementieren.
  • Freshness Lag bei Updates: Zeit vom Dokument-Commit bis zur Abrufbarkeit. Verfolge p50 und p99. Bei Systemen mit regulatorischen Anforderungen solltest du außerdem den Prozentsatz der Queries messen, die gegen veraltete Indizes bedient werden.

Teil 4: Query-Time-Evaluation

Die Query-Time-Spur enthält die Metriken zur Diagnose des Retrieval-Pfads. Recall@k allein zeigt nicht, ob Rewriting, Filtering, Reranking oder Context Assembly den Fehler verursacht hat.

Query Understanding und Rewriting

  • Quality der Query Expansion: Recall@k-Uplift auf deinem Golden Set, Expanded Query gegenüber Raw Query. Definiere den minimalen nützlichen Gain und seine Unsicherheit vor dem Test. Wenn die Expansion diese Anforderung nicht erfüllt, rechtfertigt sie ihre Latency und Kosten nicht. Klassische PRF-(Pseudo-Relevance-Feedback-)Baselines wie RM3 und Bo1 bleiben nützliche Sanity Checks; LLM-basierte Expansion muss sie übertreffen.
  • HyDE-Evaluation: HyDE (Gao et al., 2022) erzeugt mit dem LLM eine hypothetische Antwort, bettet sie ein und führt Retrieval dagegen aus. Dadurch entstehen zusätzliche Generation-Latency und eine neue Failure Surface. Miss Recall@10 separat für In-Domain-, Out-of-Domain- und Low-Confidence-Slices und entscheide dann, ob HyDE in den Default-Pfad, einen Fallback oder gar nicht gehört.
  • Multi-Query-Generation: Recall@k der Union von N Rewrites gegenüber einer einzelnen Query. Sweep N und wähle einen Punkt auf deiner Recall–Latency-Frontier. Implementierungen: LangChains MultiQueryRetriever, LlamaIndex’ QueryFusionRetriever.
  • Genauigkeit der Intent-Klassifikation: Standard-Precision/Recall/F1 pro Intent (berechne mit sklearn.metrics.classification_report), aber die operative Metrik ist Routing Correctness – wird die richtige nachgelagerte Pipeline aufgerufen?
  • Adaptive Routing: Adaptive-RAG (Jeong et al., NAACL 2024) argumentiert, dass nicht jede Query dieselbe Retrieval-Strategie benötigt. Verfolge die Router Accuracy als Klassifikationsproblem gegen ein gelabeltes Set aus „kein Retrieval nötig / One-Shot / iterativ“.

Iterative Search benötigt einen budgetierten End-to-End-Test

Ein Agent kann suchen, ein Ergebnis prüfen und erneut suchen, statt eine einzige fixe Top-k-Liste abzurufen. Die aktuelle tau3 Knowledge Domain bietet konfigurierbares RAG und agentic Shell-basiertes Search und macht daraus einen konkreten Evaluationspfad statt nur eines Architekturentwurfs. Für einen lokalen Vergleich gib One-Shot- und iterativem Retrieval denselben zulässigen Corpus und explizite Limits für Zeit, Model Tokens und Tool Calls. Zeichne jede Query und die in jedem Turn gesehene Evidenz auf; bewerte finale Evidence Coverage, Answer Correctness, Citation Support und Fälle mit erschöpftem Budget. Mehr Search Calls sind nur dann nützlich, wenn die zusätzliche Evidenz die Antwort innerhalb dieser Grenzen verbessert.

Retrieval-Metriken

Dies sind die Baseline-Metriken. Wenn du sie nicht verfolgst, kannst du nicht feststellen, ob sich Retrieval verbessert.

MetrikWas sie misstWann verwenden
Recall@kAnteil der relevanten Dokumente einer Query, die in den Top k zurückgegeben werdenwenn das Fehlen irgendeines Teils des relevanten Sets wichtig ist
Precision@kProzentsatz der Top-k-Dokumente, die relevant sindnützlich, wenn das Context Window der Bottleneck ist
MRRDurchschnitt von 1/Rang des ersten relevanten Dokumentswenn Nutzer nur Top-1 oder Top-3 betrachten
nDCG@kPositionsdiskontierter Gain, gewichtet nach Relevance GradesStandard-Retrieval-Metrik für graded Relevance
MAPMittelwert der Average Precision über Querieswenn die gesamte gerankte Liste wichtig ist
Hit Rate@kob mindestens ein relevantes Dokument in den Top k erscheintbinäres Ergebnis über Queries mitteln – als schneller Sanity Check
CoverageProzentsatz der Golden Docs, die über alle Queries hinweg jemals abgerufen werdenerkennt systematische Lücken im Index

Die Formeln zur Referenz (binäre Relevance mit relevantem Set RqR_q für Query qq und reli=1\text{rel}_i = 1, falls das ii-te abgerufene Dokument in RqR_q liegt):

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

Für graded Relevance gilt reli{0,1,2,}\text{rel}_i \in \{0, 1, 2, \dots\}; binäres nDCG ist der unten im Code verwendete Spezialfall. MAP ist der Mittelwert über Queries von APq=1Rqi:reli=1Precision@i\text{AP}_q = \frac{1}{|R_q|}\sum_{i: \text{rel}_i = 1} \text{Precision@}i. Siehe Manning, Raghavan, Schütze, Introduction to Information Retrieval, Kapitel 8, für die Herleitungen.

Für Production-Code verwende ranx, pytrec_eval oder ir_measures – sie implementieren die gesamte TREC-Metrikfamilie und behandeln graded Relevance korrekt. Lege Release-Ziele anhand eines realistischen Golden Sets, der Downstream-Answer-Quality und der Kosten eines Fehlers fest. Übernimm keine Schwellenwerte aus einem Tutorial.

Hier zählt k eindeutige Dokument-IDs. Weise Duplikate zurück, statt einem wiederholten Dokument zusätzlichen Gain zu geben. Deklariere bei Chunking-Experimenten, ob k Chunks oder deduplizierte Parent Documents zählt, und vergleiche außerdem die innerhalb eines fixen Token-Budgets gelieferte Evidenz. Derselbe Document Recall kann sehr unterschiedliche Context-Quality verbergen.

Das Test-Harness dafür ist kurz. Du kannst es in einem Notebook ausführen, bevor du überhaupt eine Vector Database ausgewählt hast.

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

Die Aggregation iteriert über alle erwarteten Queries und behandelt einen fehlenden Run als leer. Dieses Beispiel weist Queries mit leerem Gold-Set null zu; in einer echten Suite solltest du sie als separaten Answerability-Slice mit eigenem Nenner markieren, statt die Null als gemessenen Recall zu behandeln. Berichte Timeouts und fehlende Runs zusätzlich zu den Scores.

Führe bei jedem PR ein schnelles, Coverage-getriebenes Subset und vor dem Release das vollständige Golden Set aus. Blockiere einen Merge, wenn eine vorab registrierte Metrik ihr Regression Budget überschreitet.

Das Companion-Repository pinnt die exakten Zahlen oben (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 über einen echten SciFact-Index, und das Production-nahe Harness befindet sich in evaluation/retrieval.py.

Hybrid Retrieval und Reciprocal Rank Fusion

BM25 ist ein Sparse-Lexical-Scorer, der Exact-Term-Matching, Term Weighting und Length Normalization kombiniert. Es ist in rank_bm25, Elasticsearch, OpenSearch und den meisten Search Engines verfügbar.

Reciprocal Rank Fusion (Cormack, Clarke und Buettcher, SIGIR 2009) kombiniert BM25- und Dense-Rankings anhand ihrer Position. Die ursprüngliche Einstellung k=60 ist eine nützliche Baseline. RRF ist Score-agnostisch und vermeidet dadurch die Cross-Lane-Normalisierung, die bei linearer Interpolation erforderlich ist. Mit einem ausreichend großen gelabelten Set, um ein stabiles Delta zu schätzen, solltest du außerdem eine konvexe Kombination testen und α tunen.

Meine Hypothese ist, dass Hybrid Retrieval plus Cross-Encoder-Reranker bei technischen, logartigen und Code-Corpora helfen kann. Bei stark semantischen Corpora kann der Gain klein sein. Miss gegen die Dense-Only- und Sparse-Only-Spuren, weil eine schlechte Fusion-Konfiguration schlechter abschneiden kann als jeder ihrer Inputs. Das begleitende SciFact-Notebook ist ein begrenzter Test, kein allgemeines Ergebnis.

Die Implementierung passt in wenige Zeilen.

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

Beachte, was RRF nicht tut: Es betrachtet niemals die Rohwerte der Similarity Scores. Ein Dense Retriever mit Cosine 0,98 und eine BM25-Spur mit Score 17,4 sind nicht direkt vergleichbar. Z-Score- und Min-Max-Normalisierung entfernen affine Skalierungsunterschiede, aber keine der beiden Methoden kalibriert einen Score als Relevance. Ausreißer, Verteilungsform und Candidate Set beeinflussen weiterhin die normalisierten Werte und damit das Fusion-Ergebnis. Validiere jede Score-basierte Kombination auf gelabelten Queries.

RRF verwendet ausschließlich den Rang. Wenn ein Retriever ein Dokument auf Position 2 platziert, ist diese Stimme 1 / (60 + 2) wert – unabhängig vom Roh-Score, der dazu geführt hat.

Hybrid + RRF auf SciFact: Notebook 02 vergleicht Dense vs. BM25 vs. RRF mit Deltas pro Query. Der Production-nahe Fuser befindet sich in retrieval/hybrid_rrf.py; tests/test_rrf.py pinnt die kanonische Reihenfolge d3 / d2 / d1 bei k=60.

Reranking

  • ΔnDCG / ΔMRR: Uplift gegenüber No-Rerank auf deinem Golden Set und bei der Tiefe, die deine Anwendung tatsächlich verwendet. Berechne deine Retrieval-Metriken mit und ohne Reranker auf identischen Candidate Sets.
  • Cross-Encoder vs. Bi-Encoder: Ein Bi-Encoder bettet Query und Dokument unabhängig voneinander ein (ein Vector pro Seite) und bewertet per Dot Product; ein Cross-Encoder konkateniert Query und Dokument und führt einen einzelnen Forward Pass aus, der beide gemeinsam per Attention verarbeitet. Cross-Encoder tauschen einen Forward Pass pro Kandidat gegen reichhaltigere Query–Document-Interaction. Referenzimplementierung: sentence-transformers CrossEncoder. Benchmarke Relevance und Latency auf benannter Hardware, mit Batch Size und Candidate Depth; übertrage das Ergebnis eines Modells oder Managed Services nicht einfach auf eine andere Umgebung.
  • Listwise vs. Pointwise: Pointwise bewertet jedes (Query, Document)-Paar unabhängig; Listwise bewertet die gesamte Candidate List gemeinsam, sodass das Modell Kandidaten vergleichen kann. Evaluiere beide auf denselben Candidate Sets. Kalibriere Score-Schwellenwerte pro Modell und Corpus, statt ein veröffentlichtes Beispiel als übertragbar zu behandeln.
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}")

Der obige BGE-Code ist eine kleine Baseline. Für einen aktuellen Vergleich solltest du Cohere Rerank 4.0 fast/pro für Managed Multilingual Text Ranking oder Qwen3-VL-Reranker 2B/8B einbeziehen, wenn Queries oder Dokumente Bilder, Screenshots oder Video enthalten. Halte Task-Modalitäten, Candidate Depth, Input Limits, Instructions und Hardware explizit fest. Ein allgemeiner Qwen3.8-Generation-Checkpoint ist nicht dasselbe Modell wie der spezialisierte Qwen3-VL-Reranker.

Ein Reranker hilft einer einfachen RAG-Pipeline oft, aber ein Gewinn ist nicht garantiert. Miss ΔPrecision@1 und ΔnDCG auf deinem Golden Set und behalte ihn nur, wenn der Gain sein Latency- und Kostenbudget erfüllt. Vergleiche diesen gemessenen Gain mit kleineren Retrieval-Änderungen, bevor du die nächste Optimierung auswählst.

ΔnDCG und ΔPrecision@1 eines Cross-Encoders auf SciFact: Notebook 03; Modul: retrieval/reranker.py.

Context Construction und Lost-in-the-Middle

Viele „gutes Retrieval, schlechte Antwort“-Fehler beginnen bei der Context Construction.

  • Context Relevance: Ragas ContextRelevance bewertet die bereitgestellte Context-Liste anhand zweier Prompts und normalisierter Ratings; es ist kein Score pro Chunk. Für Chunk-Diagnostik solltest du jedes Query–Chunk-Paar explizit bewerten, zum Beispiel mit einem Cross-Encoder, und die Verteilung unter einer lokal kalibrierten Schwelle berichten.
  • Citation Coverage des bereitgestellten Contexts: Anzahl der unterschiedlichen zitierten bereitgestellten Chunks geteilt durch die Anzahl der unterschiedlichen bereitgestellten Chunks. Berichte Fälle mit leerem Context separat. Dieses beobachtbare Proxy-Signal zeigt, welche Chunks Zitate erhalten haben, nicht welche das Modell intern verwendet hat oder ob die Zitate seine Claims stützen. Prüfe Citation Support separat und vergleiche Coverage mit Answer-Quality und Token-Kosten.
  • Erkennung von Lost-in-the-Middle: Synthetische Eval, bei der du den Gold-Chunk an Positionen {first, middle, last} eines langen Contexts platzierst und Answer Correctness misst. Die zitierte Studie von Liu et al. (TACL 2024) berichtet unter ihren Long-Context-Bedingungen eine U-förmige Verschlechterung. Behandle dasselbe Muster in einem aktuellen Modell als Hypothese, die du testen musst. Mögliche Mitigations: Reranke und ordne anschließend die Top-k so um, dass der Chunk mit dem höchsten Score zuerst oder zuletzt steht (LangChains LongContextReorder tut genau das), oder komprimiere mittlere Chunks aggressiv. Messe mit einer positionsstratifizierten Eval statt nur mit einem aggregierten Score. Eine ausführbare positionsstratifizierte Eval findest du in Notebook 06 (Modul: evaluation/lost_in_middle.py).
  • Context Compression: Berichte die Compression Ratio (Input Tokens / Output Tokens) zusammen mit Answer Correctness. Zu den Tools gehören LangChains ContextualCompressionRetriever und LongLLMLingua. Definiere vorab den maximal akzeptablen Correctness-Verlust anhand des Risikos und Token-Budgets der Anwendung und verwerfe Konfigurationen, die ihn überschreiten.

Teil 5: False-Exclusion-Rate des Filters

Diese Metrik erhält einen eigenen Abschnitt, weil aggregierte Retrieval-Scores einen Miss nicht einem Relevance Filter zuordnen können. Evaluiere Eligibility Filter separat gegen die Berechtigungen des Aufrufers; ein Dokument außerhalb dieses Sets muss ausgeschlossen bleiben.

Ein hartes Relevance Predicate wie product = Y AND locale = en-US kann den effektiven Recall unter den Dokumenten, die der Aufrufer sehen darf, auf null reduzieren. Korrekt implementierter Recall@k erkennt den Verlust, weil sein Nenner das ursprüngliche Set relevanter zulässiger Dokumente bleibt. Er zeigt nicht, ob Filter, Retriever oder Ranker den Miss verursacht haben. Faithfulness bewertet Claims gegen den abgerufenen Context. Sie kann Claims, die von diesem unvollständigen Context gestützt werden, weiterhin positiv bewerten, aber die Ursache im Filter oder Retrieval nicht diagnostizieren. Eine leere Verweigerung kann je nach Implementierung keine Statements und NaN erzeugen; behandle sie nicht als Beleg dafür, dass Faithfulness die Verweigerung bestätigt hat.

Der hervorgehobene Zweig ist der häufige Fehler: Das richtige Dokument existiert, aber der Filter entfernt es vor dem Retrieval. Recall@k registriert den Rückgang; nur die Exclusion Rate ordnet ihn dem Predicate zu.

Stille RAG-Fehler, vom Source Corpus über Filtering, Ranking und Generation bis zur Metrik abgebildet, die jede Quelle identifiziertStille RAG-Fehler, vom Source Corpus über Filtering, Ranking und Generation bis zur Metrik abgebildet, die jede Quelle identifiziert

Die Metrik

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

Diese Query-Level-Definition zählt katastrophale Exclusions: Kein relevantes zulässiges Dokument überlebt. Schneide vor dem Scoring den Gold-Set jeder Query mit dem Entitlement-Set des Aufrufers; ein nicht zulässiges Dokument ist keine False Exclusion. Bei Queries mit mehreren Gold-Dokumenten zeigt Standard-Recall@k weiterhin einen partiellen Verlust; ergänze eine Exclusion Rate pro Dokument, wenn diese Grenze relevant ist. Für beide Raten benötigst du (a) Ground-Truth-Dokument-IDs für jede Eval-Query und (b) Instrumentation, die die angewandten Filter-Predicates protokolliert, nicht nur die finalen Ergebnisse. Lege das Ziel anhand der Kosten des Ausschlusses einer gültigen Antwort und des Konfidenzintervalls deines Production-Samples fest.

Hier ist eine funktionierende Implementierung. Sie vergleicht korrekten Standard-Recall mit einem ungültigen Evaluator, der Relevance nach dem Filtering neu definiert.

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

Die Funktion gibt die Rate und die Anzahl der Queries mit zulässigem Gold zurück. Wenn dieser Nenner leer ist, löst sie einen Fehler aus, statt eine beruhigende Null zu melden; ein erforderlicher Release-Check muss dies als fehlende Evidenz behandeln. Queries, deren Gold ausschließlich nicht autorisiert ist, bleiben in separaten Entitlement-Tests.

Bei der Hälfte der Queries verliert der Filter das Gold-Dokument, daher fällt korrekter Recall@10 auf 50 %. Dieser Score erkennt das Symptom, kann es aber nicht zuordnen. Die False-Exclusion-Rate zeigt, dass das Predicate zwei Antworten entfernt hat, bevor der Retriever lief. Der absichtlich ungültige Evaluator meldet 100 %, weil er diese Fehler aus seinem Gold-Set entfernt. Kein Modell kann ein herausgefiltertes Dokument wiederherstellen.

Die obige Rate von 50 % wird im Companion-Repository als Unit Test reproduziert: tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. Notebook 04 führt den Test auf SciFact mit synthetischer Metadata aus, sodass du beobachten kannst, wie ein echter Filter den Recall auf null setzt; die Runtime-Metrik (mit Predicate-Precision-/Recall-Ergänzung) befindet sich in evaluation/filter_exclusion.py.

Begleitmetrik: Predicate Precision und Recall

Wenn Filtering dynamisch ist (zum Beispiel wenn ein LLM Filter-Predicates aus der Query extrahiert), behandle den Predicate Extractor als Klassifikationsmodell und evaluiere ihn entsprechend. Miss Predicate Precision und Recall gegen ein gelabeltes Set von (query, correct predicate)-Paaren. Eine Predicate Error Rate entspricht nicht direkt demselben Punktverlust im Retrieval Recall; miss, wie häufig diese Fehler ein Gold-Dokument ausschließen. Sobald ein harter Filter das Gold-Dokument entfernt, hilft kein Reranking mehr.

Eligibility Filter vs. Relevance Preferences

Authorization, Tenant Isolation, Legal Jurisdiction und Publication State bestimmen, ob ein Dokument in das Candidate Set gelangen darf. Halte sie als Hard Filters und validiere sie unabhängig; Recall@k, False-Exclusion-Rate und Relevance Precision autorisieren keine Lockerung.

Bei einer Relevance Preference wie Locale, Recency oder Version solltest du ein hartes Predicate mit einem Soft Boost auf denselben zurückgehaltenen, zulässigen Queries vergleichen:

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.

Wähle den minimal nützlichen Precision Gain, das Unsicherheitsintervall, ε und die Recall-Loss-Grenze anhand des Schadens durch den Ausschluss einer ansonsten zulässigen Antwort, des Nutzens zusätzlicher Precision und der Größe des Holdout-Samples. Das sind lokale Release-Kriterien, keine universellen Schwellenwerte. Ein eigener Beitrag zu diesem Trade-off ist geplant; siehe die am Ende aufgeführten Follow-ups.


Teil 6: Generation-Evaluation

Retrieval-Metriken zeigen, dass das System korrekt antworten könnte. Sie zeigen nicht, dass es das auch getan hat. Generation-Metriken schließen diese Lücke.

Faithfulness und Groundedness

RAGAS Faithfulness zerlegt die Antwort in atomare Claims (kurze, in sich geschlossene faktische Aussagen) und verifiziert anschließend jeden Claim anhand des abgerufenen Contexts über einen LLM Judge:

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

Faithfulness prüft die Unterstützung durch die bereitgestellte Evidenz; sie stellt nicht fest, dass die Evidenz korrekt oder für die Beantwortung der Frage ausreichend ist. Erfasse Responses ohne Claims separat und berichte ihre Anzahl, statt einer leeren Antwort perfekte Faithfulness zuzuweisen.

Die aktuelle Ragas-Dokumentation empfiehlt die unten stehende Collections API. Installiere in einem uv-Projekt ragas und openai mit uv add ragas openai, setze OPENAI_API_KEY und speichere dies als Script, das du mit uv run ausführst. Das Script führt Provider Calls aus und verursacht deren Kosten; der Score ist ein Judge-Ergebnis und keine deterministische Expected Constant. Pinne die aufgelösten Dependencies in deiner 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())

Dieses Beispiel verwendet GPT-5.6 Terra als aktuellen Kandidaten mit Structured Outputs; die Ragas Factory leitet Model Arguments weiter. Ragas 0.4.3 erkennt gepunktete GPT-Generationsnamen in seinem Token-Limit-Mapper nicht. Das Beispiel lässt sein Legacy-max_tokens und Sampling-Defaults explizit weg und setzt max_completion_tokens; prüfe beim Upgrade des Adapters den ausgehenden Request. Reasoning zu deaktivieren macht die Konfiguration explizit, validiert sie aber nicht. Vergleiche False Passes, False Failures und Kosten des Judges mit Human Labels, bevor du einen günstigeren kalibrierten Judge ersetzt.

Unten ist derselbe Loop mit einem deterministischen Stand-in-Judge ausgerollt, damit du die End-to-End-Struktur sehen kannst.

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

Die Struktur ist entscheidend. In Production wird verify_claim zu einem NLI-Modell oder einem LLM Call. Behalte die Struktur aus Extract–Verify–Aggregate bei, validiere aber Extraction und Entailment separat und erfasse No-Claim- oder Failed-Judgment-Fälle. Der Offline-Stand-in oben ist auf diese Beispiele hardcodiert; er ist kein Factuality Detector.

End-to-End Claim Extraction + Verification auf generierten SciFact-Antworten: Notebook 05; Modul: evaluation/faithfulness.py. Das Repository führt denselben Loop über zwei Judge-Familien aus – das eigene Modell des Generators und einen Cross-Family-Judge (RAG_EVALS_JUDGE_MODEL) – plus eine deterministische lexikalische Baseline, sodass du sehen kannst, wo die Familien voneinander abweichen.

Eine speziell entwickelte Alternative zu LLM-as-Judge ist HHEM-2.1-Open (Hughes Hallucination Evaluation Model, Vectara), ein auf Hallucination Detection Fine-Tuned Classifier. Seine Model Card dokumentiert den Checkpoint, den ausgegebenen Raw-Score von 0–1 und Balanced-Accuracy-Ergebnisse auf AggreFact und RAGTruth. Es wird keine Default Decision Boundary veröffentlicht; deren Auswahl ist deine Aufgabe. Betrachte diese Werte als Evidenz aus der Model Card, nicht als Garantie für deinen Corpus: Kalibriere die Schwelle mit lokalen Labels und vergleiche sie vor dem Deployment mit deinem gewählten Judge.

Atomic-Fact-Evaluation

FActScore (Min et al., EMNLP 2023) zerlegt Long-Form-Generationen in atomare Fakten, ruft Evidenz pro Fakt ab, labelt jeden mit supported / not-supported und berichtet den unterstützten Anteil:

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

Referenzimplementierung: shmsw25/FActScore. Sie funktioniert gut für Biografien, Zusammenfassungen und andere Long-Form-Outputs. Achtung: Wiederholte triviale Fakten können den Score künstlich erhöhen, und einzeln wahre Statements können eine irreführende Antwort bilden. MontageLie (EMNLP 2025) untersucht diese Schwäche anhand täuschender Beziehungen und Anordnungen zwischen wahren Statements. VeriScore behandelt Claims mit erforderlichen Modifikatoren; der Core-Filter hilft, Fact Padding zu verhindern.

Citation Accuracy

Verfolge Citation Precision (zitierte Spans stützen den Claim tatsächlich) und Citation Recall (Claims, die zitiert werden sollten, werden zitiert):

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

Der TREC 2024 RAG Track definiert ein reproduzierbares Support-Evaluation-Protokoll. Thakur et al. (SIGIR 2025) berichten ungefähr 56 % Übereinstimmung mit von Menschen erstellten Judgments aus dem Stand und 72 % unter einer anderen Bedingung, bei der Menschen LLM-Vorhersagen nachbearbeiteten. Letzteres ist unterstützte Annotation und kein unabhängiger Beleg für eine höhere Judge Accuracy. Halte die Annotationsbedingung bei der Zahl fest. Für eine automatisierte Näherung implementiert ALCE (Gao et al., EMNLP 2023) Citation Precision/Recall mit NLI-basierter Verification.

Answer Correctness, Completeness und Refusal

  • Answer Correctness gegen eine Referenz: Exact Match oder Token-F1 können für kurze Antworten geeignet sein. Prüfe bei längeren Antworten faktische Relationen, Entitäten, Mengen, Negation und erforderliche Informationen gegen geprüfte Referenzen. BERTScore und Embedding Cosine messen Similarity; eine falsche Zahl oder Negation kann trotzdem einen hohen Score ergeben. Ragas AnswerCorrectness kombiniert den Faktenvergleich mit Similarity, statt beides gleichzusetzen.
  • Completeness über Nuggets: Ein Nugget ist eine relevante Informationseinheit; vitale Nuggets werden von optional nützlichen unterschieden. Eine Frage nach einem Gründungsdatum kann das Jahr erfordern; der Name des Gründers ist nicht automatisch erforderlich. AutoNuggetizer erstellt und verfeinert Nuggets aus bewerteten Dokument-Pools und prüft anschließend deren Vorkommen in generierten Antworten. Der erste TREC-2024-Bericht umfasste 21 Topics und 45 Runs. Der TREC-2025-Überblick, veröffentlicht im März 2026, erweitert das Protokoll auf narrative Queries und evaluiert Retrieval Relevance, Answer Completeness und Attribution. Das sind öffentliche Evaluationsprotokolle und kein Beleg dafür, dass jedes Production-RAG-System dieselbe Nugget-Rubrik benötigt.
  • Refusal Behavior: Labele, ob die bereitgestellte Evidenz eine Antwort erlaubt, und miss anschließend korrekte Refusals unter allen Refusals sowie Refusals unter den Fällen, in denen eine Verweigerung erfolgen sollte. NoMIRACL (Findings of EMNLP 2024) testet Robustheit gegen relevante und nicht relevante bereitgestellte Passagen; es beweist nicht, dass der gesamte Corpus keine Antwort enthält. Trenne Retrieval Misses von tatsächlich Out-of-Scope-Queries in deiner eigenen Suite.

Verification nach der Generation

Die günstigsten Reliability Gains kommen oft aus deterministischen Post-Checks, nicht aus größeren Modellen.

  • Unseen-Entity-Flag: Erfasse Named-Entity-Strings in der Antwort, die in einem normalisierten Context-String fehlen (zum Beispiel spaCy’s ents plus Exact Matching). Dies ist ein günstiges, domänenspezifisches Flag für nicht im Context gesehene Entity-Strings, aber kein Grounding Check: Es kann Identität, Relation, Negation, Zeit oder Provenance nicht feststellen. Miss seine Precision und Recall auf lokalen Labels, bevor du es zur Freigabe eines Releases verwendest, und behalte Claim-Level-Entailment oder Human Review für die Verification bei.

    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: Extrahiere Claims, führe NLI gegen den Context aus und schlage fehl oder markiere alles unterhalb der Schwelle. NLI-as-Faithfulness-Modelle: cross-encoder/nli-deberta-v3-large, MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli. Erhöht die Latency. Für High-Stakes-Domänen den Aufwand wert.

  • Self-Consistency (Wang et al., ICLR 2023): Sample mehrere Generations bei Temperature > 0; berichte die Agreement Rate (z. B. den Anteil der Generations, die zur Modalantwort passen, oder paarweisen BERTScore). Wähle die Sample-Anzahl anhand der Stability–Cost-Kurve und markiere Antworten mit niedriger Agreement für Human Review.

  • Confidence Calibration: Sammle verbalisierte Confidence („Wie sicher bist du, 0–1?“) und vergleiche sie auf dem Eval-Set mit der tatsächlichen Correctness. Zeichne eine Calibration Curve und berichte 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)|, wobei BmB_m Confidence Bins sind. Implementierungen: netcal, torchmetrics.CalibrationError. Ein Modell, das 0,9 Confidence meldet, sollte in ungefähr 90 % vergleichbarer Fälle korrekt sein; miss die Abweichung, statt Calibration anzunehmen.


Teil 7: Ontologie-basierte RAG-Evaluation

Die Standardmetriken oben decken Open-Corpus-RAG ab. Wenn dein RAG gegen eine strukturierte Ontologie, Taxonomie oder einen Knowledge Graph retrievt, sind diese Metriken notwendig, aber nicht ausreichend. Beispiele sind Produkte in einem Katalog, Krankheiten in SNOMED, Komponenten in einer BOM und Security Techniques in MITRE ATT&CK. Du musst zusätzlich die Ontologie-Layer messen.

Genauigkeit des Entity Linking

Die erste Aufgabe besteht darin, eine Query-Mention einer Ontology Entity zuzuordnen („Aspirin“ → wikidata:Q18216, „the 737“ → aircraft:Boeing_737).

  • Precision/Recall/F1 auf Mention-Ebene: Standard gegen Gold Mention Spans (berechne mit seqeval oder einem Span-Set-Comparator).
  • Disambiguation Accuracy: Welcher Anteil der korrekt erkannten Mentions wird der richtigen Entity ID zugeordnet? Öffentliche Referenzen sind ReFinED, REL und GENRE; Benchmarks wie AIDA-CoNLL und BELB zeigen, dass die Ergebnisse je nach System und Domäne variieren.
  • NIL Handling: Precision/Recall für „Entity nicht in der Ontologie“. Miss Over-Linking auf ähnliche, aber falsche Entitäten separat von korrekter Abstention.

Hierarchie-bewusste Evaluation

Plain Accuracy behandelt „Sedan vorhergesagt, obwohl Hatchback korrekt wäre“ genauso wie „Sedan vorhergesagt, obwohl Submarine korrekt wäre“. Diese Fehler sind nicht gleichwertig.

  • Hierarchical Precision/Recall/F1 (Kosmopoulos et al., 2015): Bewerte gemeinsame Ancestors im Ontology-DAG. Mit P^q\hat{P}_q als vorhergesagtem Node plus allen seinen Ancestors und TqT_q als wahrem Node plus allen seinen 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}

    Implementiere dies mit networkx auf dem Ontology Graph: Erweitere jede Prediction und jedes Label um seine Ancestors und bilde anschließend die oben gezeigten Set-Überlappungen.

  • Wu-Palmer Similarity zwischen vorhergesagter und Gold Entity in der Taxonomie (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)}

    wobei LCA der Lowest Common Ancestor in der Taxonomie ist. Für WordNet ist dies in NLTK sofort verfügbar (from nltk.corpus import wordnet as wn; wn.synset("car.n.01").wup_similarity(wn.synset("truck.n.01"))); für benutzerdefinierte Taxonomien berechne LCA mit networkx.

  • Sibling-/Parent-Confusion-Rate: Verfolge Verwechslungen mit Siblings, Parents und Children separat – count_sibling / total_errors, count_parent / total_errors, count_descendant / total_errors. Verwende geprüfte Beispiele, um zu testen, ob Sibling-Fehler aus ambigen Mentions oder Parent-Fehler aus Over-Generalization entstehen.

False-Exclusion-Rate des Filters (erneut, jetzt kritisch)

In ontologiebasierten Systemen stammen harte Filter oft direkt aus der Ontologie („retrievt nur Dokumente, die mit Kategorie X getaggt sind“). Die in Teil 5 definierte Exclusion Rate wird zu einem primären Correctness-Signal. Eine falsche Kategorievorhersage kann den Recall auf null setzen; die Exclusion Rate ordnet diesen Verlust dem Filter zu.

Constrained-Generation-Konformität

Wenn dein Output einer Ontologie entsprechen muss (jeder Entity Name in der Antwort muss ein gültiges Ontology Member sein; jedes Predicate muss aus einem geschlossenen Vocabulary stammen), miss:

  • Schema Validity Rate: Prozentsatz der Outputs, die gegen das Ontology Schema geparst und validiert werden können. Validiere mit jsonschema oder pydantic. JSONSchemaBench ist der öffentliche Benchmark für allgemeine Structured Outputs; für ontologiespezifische Schemas erstellst du deinen eigenen Validator.
  • Vocabulary Conformance: Prozentsatz der Named Entities im Output, die gültige Ontology IDs sind – ein einfacher Set-Membership-Check gegen das geschlossene Vocabulary.
  • Semantic Conformance: Ein syntaktisch gültiger Output kann trotzdem die falsche, aber gültige Entity auswählen. Kombiniere Conformance mit Downstream-Answer-Correctness.

Constrained-Decoding-Frameworks (Outlines, XGrammar, Guidance, OpenAI Structured Outputs) wurden entwickelt, um Schema Validity zu erzwingen. JSONSchemaBench vergleicht Effizienz, Coverage und Quality verschiedener Implementierungen. Führe die zu deinen Schemas und deinem Serving Backend passenden Fälle erneut aus, da Coverage und Latency von beiden abhängen.

Auditierbarkeit

Für ontologiebasierte Systeme, deren Antworten geprüft werden:

  • Citation Completeness: Prozentsatz der faktischen Claims mit mindestens einem verifizierbaren Zitat.
  • Provenance Depth: Prozentsatz der Zitate, die bis zu einem Quelldokument mit stabiler ID aufgelöst werden können, nicht nur zu einem Chunk Hash.
  • Reproducibility Rate: Bei erneuter Ausführung derselben Query auf einem fixen Snapshot wird dieselbe Antwort zurückgegeben. Pinne Model-Version, Runtime, Decoding-Konfiguration und Seed und lege die erforderliche Repeat Rate anhand der Auditierbarkeitsanforderungen des Workflows fest. Temperature zero allein garantiert keine Deterministik. Ein Miss kann aus der Generation, der Serving Runtime oder jeder vorgelagerten Stage stammen.

Teil 8: Evaluation auf Systemebene

Ganzheitliche Answer-Quality

  • LLM-as-Judge (Zheng et al., NeurIPS 2023): Ein skalierbarer modellbasierter Evaluationsansatz. G-Eval (Liu et al., EMNLP 2023) erzeugt Evaluationsschritte aus Task und Kriterien und gewichtet anschließend Rating-Stufen anhand ihrer Token-Wahrscheinlichkeiten: score=ip(si)si\text{score} = \sum_i p(s_i)\,s_i. Dies sind Wahrscheinlichkeiten, keine Log-Wahrscheinlichkeiten. Die Übereinstimmung hängt von Judge, Task, Prompt und Calibration Set ab.
  • Pairwise Preference: Präsentiere dem Judge Antwort A vs. Antwort B und erfasse seine Präferenz. Dies ersetzt ein absolutes Rating durch eine vergleichende Entscheidung, benötigt aber weiterhin eine Kalibrierung gegen menschliche Präferenzen. MT-Bench berichtete unter seinen Benchmark-Bedingungen eine GPT-4-Judge-Übereinstimmung von über 80 % mit menschlichen Präferenzen und Human–Human-Agreement; übertrage diese Rate nicht ohne Kalibrierung auf eine andere Domäne.

LLM-as-Judge hat reale Biases:

  • Position Bias: Miss die Reihenfolge-Sensitivität auf von Menschen gelabelten Fällen für den ausgewählten Judge und Task. Randomisierung oder Aggregation über vertauschte Reihenfolgen kann bei manchen Modell-/Task-Paaren helfen; behalte sie aber nur bei, wenn sie die lokale Übereinstimmung mit Menschen verbessert. Die kontrollierte Studie von 2026 fand Positionswechsel bei ihren adversarialen Fällen schädlich.
  • Verbosity Bias: Judges können Länge mit Quality verwechseln. Die zitierte kontrollierte Studie von 2026, Version 2 fand heterogenes Verhalten bei Expansion-Paaren: Drei Judges bevorzugten längere Antworten, Claude bevorzugte prägnante Antworten, und GPT-4o war ungefähr neutral. Alle fünf schnitten bei Truncation Controls gut ab. Diese Ergebnisse sind an den Benchmark gebunden. Sage deinem Judge daher, wie er Completeness und Filler behandeln soll, und berichte die Length-Controlled Performance auf deiner eigenen Rubrik.
  • Risiko der Selbstpräferenz: Zheng et al. beobachteten in ihren Daten eine um 10 % höhere GPT-4-Self-Win-Rate und eine um 25 % höhere Claude-v1-Self-Win-Rate, kamen aber zu dem Schluss, dass begrenzte Daten und kleine Unterschiede keinen Self-Enhancement-Bias belegen konnten. Vergleiche Same- und Cross-Family-Judges mit lokalen Human Labels; wähle den besser kalibrierten Judge, statt eine der beiden Varianten als sicher anzunehmen.

Praktisches Rezept: Wähle einen Judge auf von Menschen gelabelten Calibration Data aus, maskiere die Model-Identitäten, miss Reihenfolge-Sensitivität und definiere die Length Policy in der Rubrik. Wiederhole Fälle nur, wenn die zusätzlichen Samples die Unsicherheit substanziell reduzieren. Für High-Stakes-Evaluationen solltest du Same- und Cross-Family-Judges vergleichen und Abweichungen anhand menschlicher Labels analysieren.

Schema-Guided Reasoning für Judges

Freiform-Output ist eine Quelle von Varianz in Judge-Runs. Zwei Runs gegen dieselbe Antwort können die Rubrik unterschiedlich strukturieren und unterschiedliche Scores erzeugen. Schema-Guided Reasoning (SGR) macht diese Rubrik explizit: Definiere die Evaluationsstufen als Pydantic-Schema und verwende anschließend Constrained Output über Outlines, XGrammar, vLLM Structured Outputs oder OpenAI response_format, um die unterstützten Schema Constraints zu erzwingen. Die Feldreihenfolge kann die Prüfung durch Menschen erleichtern, beweist aber nicht, dass das Modell die Stufen in dieser Reihenfolge durchlaufen hat.

Für RAG-Eval zerlegt das Schema das Judgment in explizite, auditierbare Felder, statt das Modell direkt zu einer Zahl springen zu lassen:

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

Dieser illustrative Contract berechnet den Score erst, nachdem die Verdict-Listen eine vollständige, disjunkte Partition der extrahierten Claims bilden. Ein No-Claim-Ergebnis hat keinen Faithfulness Score und muss aus dem Faithfulness-Aggregat herausgehalten werden; berichte seine Anzahl und bewerte Refusal Correctness separat, damit Abstentions den Durchschnitt nicht unbemerkt verbessern. Bewahre diese semantischen Checks in Production. Constrained Output garantiert die Form, aber kein unvoreingenommenes Verdict. Das Pydantic-Modell macht außerdem eine Rubrikänderung als Code-Diff sichtbar, während Human Calibration das Judgment selbst testet.

Das funktioniert für jeden rubrikenbasierten Judge, nicht nur für Faithfulness. Pairwise Preference, Citation Support und Refusal Correctness profitieren gleichermaßen davon.

Ein vereinfachter Rubrik-Judge plus Pairwise-, Position-Bias- und Cross-Family-Beispiele findet sich in Notebook 07; Modul: evaluation/llm_judge.py. In der geprüften Revision fordert die Funktion namens g_eval eine einzelne Integer-Bewertung an; sie erzeugt keine Evaluationsschritte und berechnet keine probabilitätsgewichteten Scores, reproduziert also nicht das G-Eval-Protokoll. Der Benchmark-Sweep (make benchmark im Repository) verbindet drei Modelle (gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash) in einem rotierenden Judge-Pairwise-A/B: Jeder Judge bewertet Antworten der beiden anderen Modellfamilien. Diese Topologie ermöglicht Cross-Family-Pairwise-Ergebnisse; zur Messung von Self-Preference werden zusätzlich Same-Family- und Cross-Family-Bedingungen benötigt, die gegen lokale Human Labels verglichen werden.

Latency und Kosten

  • p50, p95, p99 auf jeder Pipeline-Stage. Wähle SLO-Perzentil und Alert-Schwelle anhand User Journey, Traffic-Volumen und Error Budget.
  • Time-to-First-Token vs. gesamte Generation Time. Für Streaming-UX ist TTFT für Nutzer relevant.
  • Stage Breakdown: Retrieval, Reranking, Generation, Post-Processing. Verwende den Trace, um den Tail zu lokalisieren, statt die verursachende Stage anzunehmen; erfasse beim Vergleich von Runs Reranker Device und Batch Size.
  • Gesamte $/Query = Embedding + Retrieval + Rerank + Generation + Storage amortisiert. Verfolge p50 und p99; im Long Tail wird das Budget verbraucht.
  • Cache Hit Rates auf den Ebenen Embedding Cache, Retrieval Cache und KV Cache. Lege separate Ziele anhand beobachteter Wiederholungen, Invalidation Policy und vermiedener Kosten jeder Layer fest.

Per-Stage p50/p95/p99 mit Stage Breakdown sind in Notebook 08 und dem Runner unter evaluation/latency.py integriert; der Benchmark-Report kombiniert Latency und Faithfulness in einer einzelnen Matrix, die du mit make benchmark erneut ausführen kannst.

A/B-Testing

  • Unit of Randomization: Wähle die Einheit anhand Estimand, Carryover und Interference. Verwende Assignment pro User oder Session, wenn wiederholte Exposition das Verhalten verändern oder zu inkonsistenter UX führen kann. Assignment pro Query ist nur dann vertretbar, wenn diese Effekte vernachlässigbar sind und die Analyse wiederholte Beobachtungen modelliert.
  • Primary, Guardrail- und Exploratory-Metriken: Registriere sie vorab. Wähle die Primary Metric anhand des Product Outcomes; Satisfaction-Proxies umfassen Thumbs, Regenerations und Dwell. Behandle Latency und Kosten als Guardrails, wenn sie die Experience begrenzen.
  • Sample Size: Führe vor dem Launch eine Power Analysis durch – anhand des kleinsten erkennbaren Effekts, der Baseline-Varianz, der Assignment Unit und der Stopping Rule.

Teil 9: Erstellung des Test-Sets

Eine Metrik ist nur so gut wie das Test-Set, auf dem sie läuft. Wenn dein Golden Set drei Intents abdeckt, der Production-Traffic aber zwölf umfasst, misst Recall@10 nur diese drei Intents. Noch problematischer: Ein Test-Set, das auf einfache Fragen überfitten („Wie lautet die Rückerstattungsrichtlinie des Unternehmens?“), kann ein System freigeben, das bei schwierigen Fragen scheitert („Rückerstattungsberechtigung für eine teilweise Stornierung nach dem EU Digital Services Act von 2023, Abrechnung in EUR, Ursprung Irland?“). Der aggregierte Score steigt, während das System weiterhin einen wichtigen Teil des Production-Traffics verfehlt.

Unvollständige Relevance Labels können Recall in beide Richtungen verzerren. Wenn das tatsächlich relevante Set {a, b} ist, die Labels aber nur {a} enthalten, erreicht das Retrieval von {a} einen Score von 1,0 statt 0,5; das Retrieval von {b} erreicht null statt 0,5. Versioniere die Judgments und prüfe neu abgerufene, noch nicht bewertete Evidenz, bevor du ein Delta interpretierst.

Baue das Test-Set zuerst um die tatsächliche Query-Verteilung und Schwierigkeit herum. Wähle danach Metriken, die auf die gewünschten Failure Modes reagieren, und tune das System gegen diese Metriken.

Synthetische Query-Generation

Verwende ein LLM, um Fragen aus deinem Corpus zu erzeugen:

  • Pro Chunk: „Erzeuge drei Fragen, die ein Nutzer stellen könnte und die dieser Chunk beantwortet.“
  • Multi-Hop: Sample zwei Chunks und erzeuge eine Frage, die beide benötigt.
  • Adversarial: Erzeuge Fragen mit Distractor-Entitäten, Near-Duplicate-Phrasings und ambigen Mentions.

Ragas Test Generation verwendet graphbasierte Szenarien mit Single-Hop- und Multi-Hop-Queries sowie spezifischen oder abstrakten Information Needs. DataMorgana erzeugt konfigurierbare synthetische Benchmarks über User- und Fragekategorien hinweg. Synthetische Daten sind für Cold Starts und Coverage-Tests nützlich. Sie können echte User Queries nicht ersetzen.

Erstellung des Golden Dataset

Von Menschen kuratierte Daten verankern das Golden Set.

  1. Sample echte User Queries (oder simulierte Queries vor dem Launch), stratifiziert nach Intent.
  2. Lass SMEs jede Frage beantworten und die Dokumente identifizieren, die die Antwort enthalten.
  3. Dimensioniere das Set anhand der Coverage Matrix und des für Release-Entscheidungen erforderlichen Konfidenzintervalls; Coverage ist wichtiger als eine übernommene Query-Anzahl.
  4. Kuratiere erneut, wenn Release Cadence, Drift-Signale, Domain Risk und Annotation Capacity dies rechtfertigen.

Halte Development Queries, Judge Calibration, zurückgehaltene Release-Messung und Monitoring-Samples getrennt. Gruppiere gemeinsame Quelldokumente und Sessions vor dem Split. Sobald eine Query oder ein Label zum Tuning beiträgt, sind die Daten Development Data. Halte unberührte Fälle zurück, um die gewählte Konfiguration und den Judge zu validieren.

Erfasse Corpus-, Query-, Relevance-Label- und Policy-Versionen, die Einheit von k, das Supplied-Context-Budget, Revisionen von Scorern und Judges sowie Aggregationsregeln. Vergleiche gepaarte Per-Query-Deltas auf derselben Population, inklusive Unsicherheit und Slice Counts. Schließe jede erwartete Query ein: Ein Timeout, ein fehlendes Ergebnis oder eine nicht bewertbare Judge-Response muss in Completion-/Error-Accounting sichtbar bleiben. Lass einen Release-Score nicht durch stilles Entfernen von Fehlern steigen. ARES bietet einen Forschungsansatz mit automatisierten Judges, Human Validation und Prediction-Powered Inference für Systemschätzungen bei knapper Annotation; eine geprüfte lokale Suite kann einfacher beginnen.

Adversariale Test-Sets

  • Counterfactuals: Tausche zentrale Entitäten in der Query aus. Ruft das System die richtigen Chunks für die veränderte Query ab?
  • Distractors: Queries, bei denen der Corpus eine plausible, aber falsche Antwort enthält, die nicht abgerufen werden sollte. Das ist der Fokus von RGB (Chen et al., AAAI 2024): Noise Robustness, Negative Rejection, Information Integration und Counterfactual Robustness.
  • Negation und Quantifiers: Queries mit „nicht“, „außer“ und „nur“. Dense Retrievers haben damit häufig Schwierigkeiten.
  • Out-of-Scope: Queries ohne Antwort im Corpus. Das System sollte „Ich weiß es nicht“ sagen und nicht halluzinieren. NoMIRACL stellt Passage-Level-Relevance-/Answerability-Tests bereit; ergänze separate Corpus-Level-Out-of-Scope-Labels. Evaluiere Abstention explizit für deine Production-Query-Typen.

Coverage und kontinuierliche Evaluation

  • Erstelle eine Coverage Matrix: Query Intent × Document Type × Ontology Branch. Eine Query pro Zelle ist ein erster Coverage-Inventar, aber keine ausreichende statistische Power für eine Release-Entscheidung. Leere Zellen zeigen fehlende Coverage; dimensioniere gefüllte Slices anhand der tolerierbaren Unsicherheit.
  • Führe bei jedem PR ein begrenztes, schnelles Regression-Subset und nach einem langsameren Zeitplan die vollständige Suite aus.
  • Plane die vollständige Golden-Set-Eval anhand von Release Cadence und Evaluationskosten; führe sie auf Release Candidates aus.
  • Plane Drift-Evaluation anhand von Traffic-Volumen, erwarteter Änderung und Risiko. Verwende ein rollierendes Production-Sample und stratifiziere nach Feedback, statt die Zielverteilung stillschweigend zu ändern.

Teil 10: Production-Monitoring

Die ausgelieferte Eval-Suite beschreibt das System zum Launch. Der Production-Traffic verändert sich danach.

Implizites und explizites Feedback

  • Behandle implizite Events als potenzielle Signale und nicht als positive oder negative Quality-KPIs, bis sie auf einem lokalen Sample mit Blind Review oder explizitem Feedback korrelieren.
  • Click-Through / Open Rate auf zitierten Quellen, sofern deine UI dies anbietet.
  • Dwell Time auf der Antwort.
  • Regeneration Rate: Prozentsatz der Antworten, die der Nutzer erneut anfordert oder vom System wiederholen lässt. Behandle sie als ein Unzufriedenheitssignal und kalibriere sie gegen geprüfte Conversations.
  • Copy-/Share-/Export-Raten: potenzielle implizite Signale, die Nutzen, Verifikation, Übergabe oder Unzufriedenheit darstellen können. Miss ihren Zusammenhang und das Konfidenzintervall, bevor du eine Richtung festlegst.
  • Follow-up-Muster: Verwende „Bist du sicher?“ oder „Aber was ist mit X?“ als Review-Strata und labele ihren Zusammenhang mit Misstrauen oder ungelöstem Bedarf.
  • Thumbs Up/Down mit optionalen Begründungskategorien (falsch, unvollständig, Off-Topic, schädlich, langsam). Inline-Edits können mehr diagnostischen Context bewahren; bewerte diesen Nutzen auf geprüften Samples.

Drift Detection

  • Query Drift: Vergleiche Query Embeddings mit einem Referenzfenster anhand von MMD oder einem validierten Reference-vs.-Current-Classifier. KL benötigt einen definierten Probability Estimator, etwa festgelegte Histograms; rohe Embedding-Koordinaten sind keine Wahrscheinlichkeiten. Kalibriere Alarme anhand bekannter Shifts und untersuche anschließend betroffene Slices.
  • Embedding Drift: Pinne eine Repräsentation und ein Probe Set und miss anschließend Neighbor Stability und Retrieval Quality. Unterschiedliche Modellversionen müssen weder dieselben Dimensionen noch dieselbe Coordinate Basis haben; Cross-Version Cosine kann daher bedeutungslos sein. Migriere Query- und Document-Encoder gemeinsam, evaluiere den neuen Index und bewahre versionierte Snapshots für Rollback auf.
  • Performance Drift: Verfolge Production-äquivalente Metriken (Regeneration Rate pro Intent) über die Zeit. Plötzliche und graduelle Shifts legen unterschiedliche Hypothesen nahe, beweisen deren Ursache aber nicht; untersuche Data, Traffic, Provider, Policy und Deployment-Änderungen.

Shadow Evaluation und Human-in-the-Loop

Führe das Kandidatensystem parallel zu Production aus, vergleiche die Outputs offline und liefere sie nicht an Nutzer aus. So lassen sich Regressionen vor dem Launch erkennen. Shadow Inference verbraucht weiterhin Capacity und kann Tools aufrufen: Isoliere Ressourcen, unterdrücke Writes und prüfe, dass der Vergleich die Production Latency nicht verschlechtert.

Für Human-in-the-Loop-(HITL-)Review:

  • Sample Low-Confidence-Outputs in eine Review Queue.
  • Füge ein Random Sample des Production-Traffics für Blind Review hinzu; lege die Rate anhand von Traffic-Volumen, Risiko und Reviewer-Kapazität fest.
  • Oversample Thumbs-Down-Outputs für das Review zusammen mit dem Random Sample.
  • Verwende geprüfte Outputs, um das Golden Set zu erweitern.

Das minimale Guardrail-Set

Wähle Alert-Prioritäten und Schwellenwerte anhand von User Harm, SLOs und validierter Detector Performance. Mögliche Signale sind:

  1. Faithfulness-/HHEM-Score unterhalb der Schwelle auf einem rollierenden Production-Sample.
  2. p95 Latency oberhalb des SLO.
  3. False-Exclusion-Rate des Filters oberhalb der Schwelle (sample-basiert).
  4. Regeneration Rate außerhalb eines lokal kalibrierten Control Bands, das Window Size, Traffic, Saisonalität und False-Alert-Budget berücksichtigt.
  5. Cost/Query oberhalb des Budgets.

Verwende den Zeitpunkt des Releases als Hinweis für die Diagnose und verifiziere ihn anschließend anhand von Traces und betroffenen Slices. Ein Deployment kann mit Traffic Drift zusammenfallen, und eine Änderung bei Provider oder Daten kann ohne Application Release auftreten. Alerts sind Untersuchungsindizien; ihre Vorlaufzeit gegenüber User Reports solltest du messen.


Caveats

  • Ziele sind lokal, nicht universell. Jede in diesem Leitfaden als illustrativ bezeichnete Zahl ist eine Beispielkonfiguration oder ein ausgearbeitetes Ergebnis, kein Release-Schwellenwert. Kalibriere Schwellenwerte auf deine Domäne, die Stakes, die Unsicherheit des Eval-Sets und die Erwartungen der Nutzer.
  • Die Framework-Landschaft bewegt sich schnell. HHEM-Versionen, RAGAS-Metric-Namen, Model Cards und Leaderboard-Reihenfolgen können sich nach der Veröffentlichung ändern. Prüfe die verlinkte Quelle erneut und benchmarke neu, bevor du dich festlegst.
  • LLM-as-Judge-Übereinstimmungszahlen haben Fußnoten. Die 80-%-Zahl für GPT-4 gegenüber Menschen stammt aus den MT-Bench-/Chatbot-Arena-Bedingungen. Sie belegt keine Übereinstimmung in einer Nischendomäne oder einem adversarialen Slice. Verwende Judges als Force Multiplier, nicht als Ersatz für Stichprobenprüfungen.
  • Vendor-Benchmark-Uplifts sind oft nicht unabhängig reproduzierbar. Reproduziere sie auf deinen eigenen Daten, bevor du einer Zahl vertraust – insbesondere bei neueren Rerankern und OCR-Systemen.
  • Keine Metrik ersetzt das Betrachten von Outputs. Plane Blind Review eines zufälligen Production-Samples anhand von Traffic, Risiko und Reviewer-Kapazität. Die Metriken skalieren diese Gewohnheit; sie ersetzen sie nicht.

Als Nächstes in dieser Serie

Dies war der Index. Die folgenden Beiträge sind geplant:

  • Soft Boosts vs. Hard Filters: Ein Deep Dive zur False-Exclusion-Rate des Filters, mit Code, echten Production-Beispielen und einem Decision Framework.
  • Chunking Is the Hidden Variable: Ein kontrolliertes Experiment zu Recursive, Semantic, Late und Structural Chunking auf drei Corpora.
  • Reranker Selection in 2026: BGE vs. Cohere vs. ZeRank vs. aktuelle Cross-Encoder-Modelle, im direkten Vergleich nach Kosten, Latency und Uplift.
  • Ontology-Grounded RAG: An End-to-End Walkthrough: Aufbau des vollständigen Evaluations-Harness für ein Entity-Grounded-Retrieval-System.
  • LLM-as-Judge Without the Self-Preference Trap: Praktische Rezepte für unvoreingenommene automatisierte Evaluation.
  • Online Evaluation in Production: Instrumentation Patterns, Alerting Policies und Dashboards, die echte Regressionen erkennen.

Referenzen

Frameworks und Benchmarks

Retrieval und Ranking

Generation, Faithfulness und Judges

Drift und Production

Companion-Code

  • slavadubrov/rag-evals-demo – ausführbares Harness für ausgewählte Metriken dieses Artikels auf dem SciFact-Corpus sowie ein Benchmark-Sweep über Chunking × Embedding × LLM. Notebooks 00–09, Unit Tests, die die ausgearbeiteten Beispiele oben pinnen, und ein eingebetteter Qdrant-Index, sodass das Ganze ohne Docker läuft.