Blogs / Eine RAG-Pipeline evaluieren: Recall@k, MRR, Faithfulness

Eine RAG-Pipeline evaluieren: Recall@k, MRR, Faithfulness

Veröffentlicht
26. September 2026
Autor
Faizan Nadeem
Tags
RAG AI Engineering Backend Development LLM
Laptop screen displaying performance analytics graphs and data visualizations
Photo by Luke Chesser on Unsplash

Die meisten Teams „evaluieren" ihre RAG-Pipeline, indem sie ihr eine Handvoll Fragen stellen, die Antworten lesen und entscheiden, ob es sich richtig anfühlt. Das funktioniert genau so lange, bis es nicht mehr funktioniert — bis eine Chunking-Änderung, ein Embedding-Modell-Tausch oder eine Prompt-Anpassung das Retrieval still schlechter machen, während die zwei oder drei Fragen, die immer getestet werden, zufällig weiterhin gut zurückkommen. Ohne ein echtes Eval-Set ist jede Änderung an der Pipeline eine Wette, die man eigentlich nicht messen kann — Regressionen werden still ausgeliefert und Verbesserungen können nicht bewiesen, sondern nur behauptet werden.

Das tiefere Problem beim Durchlesen von Antworten ist, dass zwei Fehlermodi, die völlig unterschiedliche Korrekturen brauchen, vermischt werden. Eine RAG-Pipeline kann eine falsche Antwort produzieren, weil das Retrieval versagt hat (der richtige Chunk wurde nie gefunden) oder weil die Generierung versagt hat (der richtige Chunk wurde gefunden, aber das Modell hat ihn nicht korrekt verwendet, oder es hat ihn verwendet und trotzdem halluziniert). Nur die finale Antwort zu lesen, kann nicht sagen, welches von beidem passiert ist — man braucht Metriken, die jede Stufe isolieren. Dieser Beitrag behandelt den Aufbau eines Eval-Sets aus dem eigenen Korpus und das separate Messen beider Hälften: Recall@k und MRR für Retrieval, Faithfulness und Answer Relevance für die Generierung.

Du wirst lernen:

  • Warum das Lesen finaler Antworten nicht sagen kann, ob ein RAG-Fehler ein Retrieval- oder ein Generierungsproblem ist
  • Wie man ein echtes Eval-Set aus dem eigenen Korpus aufbaut, anstatt auf generische Benchmark-Fragen zu setzen
  • Wie man Recall@k und MRR für die Retrieval-Stufe berechnet – mit ausführbarem Python
  • Wie man Faithfulness misst – ob die generierte Antwort tatsächlich durch den abgerufenen Kontext gestützt wird
  • Wo RAGAS und ähnliche Frameworks passen, und was sie automatisieren versus was noch menschliches Urteil erfordert
  • Wie man diese Metriken in ein Regressionsgate umwandelt, das tatsächlich bei jeder Pipeline-Änderung ausgeführt wird

Inhaltsverzeichnis

  1. Warum „Es sieht richtig aus" keine Evaluationsstrategie ist
  2. Retrieval-Fehler von Generierungs-Fehlern trennen
  3. Ein Eval-Set aus dem eigenen Korpus aufbauen
  4. Retrieval-Metriken: Recall@k und MRR
  5. Context Precision: Rauschen im Retrieval bestrafen
  6. Ein konkretes Beispiel: Fünf Abfragen durch das Harness
  7. Generierungs-Metriken: Faithfulness und Answer Relevance
  8. Wo RAGAS passt und was noch ein Mensch braucht
  9. Metriken in ein Regressionsgate umwandeln
  10. Häufige Fallstricke
  11. Fazit

Warum „Es sieht richtig aus" keine Evaluationsstrategie ist

Das Kernproblem mit manuellem Spot-Checking ist nicht, dass es langsam ist — es ist nicht-repräsentativ und nicht-wiederholbar. Eine Handvoll Fragen, die jemand während einer Demo eintippt, tendieren dazu, die einfachen Fälle zu sein: gut formuliert, nah am Wortlaut des Dokuments, über den offensichtlichsten Inhalt im Korpus. Produktionstraffic sieht überhaupt nicht so aus — er ist seltsam formuliert, fragt nach Edge-Case-Inhalten und stellt Dinge, die niemand gedacht hat, manuell zu testen. Eine Pipeline, die bei fünf Demo-Fragen großartig aussieht, kann echte Recall-Lücken bei den Abfragen haben, die echte Nutzer senden.

Das zweite Problem ist, dass manuelle Prüfung über Änderungen hinweg nicht zusammensetzbar ist. Wenn jemand in diesem Monat die Chunking-Strategie anpasst, im nächsten den Retrieval-Prompt und im übernächsten das Embedding-Modell tauscht, gibt es keine Möglichkeit zu wissen, welche Änderung geholfen, welche geschadet und welche eine andere aufgehoben hat — es sei denn, dasselbe feste Eval-Set wurde vor und nach jeder Änderung mit Zahlen zum Vergleich ausgeführt, nicht mit Eindrücken.

Retrieval-Fehler von Generierungs-Fehlern trennen

Die finale Antwort einer RAG-Pipeline ist das Produkt zweier Stufen, die unabhängig voneinander versagen, und die Korrektur für jede ist völlig unterschiedlich:

  • Retrieval-Versagen: Der Chunk, der die Antwort enthält, wurde dem LLM überhaupt nie gezeigt. Kein Prompt-Engineering oder besseres Modell behebt das — die Antwort ist buchstäblich nicht im Kontextfenster. Das ist ein Chunking-, Indexierungs- oder Embedding-Problem, das in warum RAG die falschen Chunks abruft ausführlich behandelt wird.
  • Generierungs-Versagen: Der richtige Chunk wurde abgerufen und war im Kontext vorhanden, aber das Modell hat trotzdem eine falsche oder nicht gestützte Antwort produziert — es hat den Kontext falsch gelesen, über das, was der Kontext tatsächlich sagte, hinaus verallgemeinert oder den abgerufenen Inhalt zugunsten seines eigenen Vorwissens ignoriert.

Der einzige Weg, diese systematisch auseinanderzuhalten, ist zu prüfen — für jede Eval-Abfrage — ob der Gold-Antwort-Inhalt tatsächlich in dem vorhanden war, was abgerufen wurde, bevor beurteilt wird, ob die finale Antwort korrekt ist. Eine falsche Antwort mit dem richtigen abgerufenen Chunk ist ein Generierungsfehler; eine falsche Antwort mit dem fehlenden richtigen Chunk ist ein Retrieval-Fehler — und diese sollten nie in einer einzigen „Pipeline-Genauigkeits"-Zahl zusammengefasst werden, weil die Behebung eines Problemtyps nichts für den anderen tut.

Ein Eval-Set aus dem eigenen Korpus aufbauen

Generische Benchmark-Datensätze (Natural Questions, HotpotQA und Ähnliches) messen etwas Reales, aber nicht das, was man wissen muss: wie die eigene Pipeline auf den eigenen Dokumenten in der Formulierung der eigenen Domain gegen die tatsächlichen Fragemuster der eigenen Nutzer abschneidet. Ein Eval-Set aus dem eigenen Korpus ist das einzige, das die Frage beantwortet, auf die es eigentlich ankommt.

Fragen beschaffen. Die zwei zuverlässigsten Quellen sind echte Nutzerabfragen aus Logs (wenn man bereits Produktionstraffic hat) und handgeschriebene Fragen, die von jemandem erstellt wurden, der den Korpus kennt, und bewusst eine Bandbreite von Formulierungsstilen abdecken — einige nah am Wortlaut des Dokuments, einige paraphrasiert, einige mit Synonymen, die das Dokument nie verwendet.

Die Gold-Antwort aufzeichnen. Für jede Frage zwei Dinge aufzeichnen: die spezifische Passage oder Chunk-ID, die die Antwort enthält (für Retrieval-Scoring), und eine Referenzantwort oder die Schlüsselfakten, die sie enthalten muss (für Generierungs-Scoring).

eval_set = [
    {
        "query": "How long does a webhook retry before giving up?",
        "gold_chunk_ids": ["doc_112#chunk_4"],
        "gold_answer": "Webhooks retry with exponential backoff for 24 hours before being marked failed.",
    },
    # ...
]

Fehleranfällige Kategorien bewusst abdecken, nicht nur einfache Fälle. Fragen einbeziehen, bei denen die Antwort zwei Chunks überspannt, Fragen, die mit Synonymen statt den exakten Begriffen des Dokuments formuliert sind, und Fragen über Inhalte in der Nähe von Abschnittsgrenzen — das sind die Kategorien, die am ehesten echte Chunking- und Retrieval-Schwächen aufdecken, und genau die Kategorien, die ein schneller manueller Spot-Check tendenziell überspringt.

Ein Eval-Set im Bereich von 50–150 Fragen, einmal aufgebaut und im Laufe der Zeit erweitert, wenn neue Fehlerkategorien in der Produktion auftauchen, reicht aus, um die meisten Regressionen zu erkennen — es braucht keine Tausende von Beispielen, um nützlich zu sein. Es muss repräsentativ sein und konsistent ausgeführt werden.

Retrieval-Metriken: Recall@k und MRR

Recall@k beantwortet: War unter den top k abgerufenen Chunks der Gold-Chunk überhaupt vorhanden?

def recall_at_k(retrieved_ids: list[str], gold_ids: set[str], k: int = 5) -> int:
    return int(bool(set(retrieved_ids[:k]) & gold_ids))

def mean_recall_at_k(results: list[dict], k: int = 5) -> float:
    hits = [recall_at_k(r["retrieved_ids"], set(r["gold_chunk_ids"]), k) for r in results]
    return sum(hits) / len(hits)

Mean Reciprocal Rank (MRR) beantwortet eine schärfere Frage: nicht nur ob der Gold-Chunk abgerufen wurde, sondern wie hoch er gerankt wurde. Es ist der Durchschnitt über alle Abfragen von 1 / Rang des ersten korrekten Chunks (0, wenn er nie erscheint):

def reciprocal_rank(retrieved_ids: list[str], gold_ids: set[str]) -> float:
    for rank, doc_id in enumerate(retrieved_ids, start=1):
        if doc_id in gold_ids:
            return 1.0 / rank
    return 0.0

def mean_reciprocal_rank(results: list[dict]) -> float:
    scores = [reciprocal_rank(r["retrieved_ids"], set(r["gold_chunk_ids"])) for r in results]
    return sum(scores) / len(scores)

Die beiden Metriken erkennen unterschiedliche Regressionen. Recall@k kann gleich bleiben, während MRR still sinkt — der richtige Chunk wird immer noch abgerufen, rutscht aber von Rang 1 auf Rang 4, was nichts ausmacht, wenn man immer die Top-5 an das LLM übergibt, aber problematisch wird, sobald k enger gestellt oder ein Reranker mit einem kleineren top-n-Cutoff hinzugefügt wird. Beide zu verfolgen ist der Weg, eine Chunking-Strategieänderung zu bemerken, die den Recall stabil hält, aber die Ranking-Qualität verschlechtert — eine Änderung, die ein Recall-only-Dashboard als nicht-Ereignis melden würde.

Context Precision: Rauschen im Retrieval bestrafen

Recall@k und MRR fragen beide nur, ob der Gold-Chunk aufgetaucht ist und wie hoch er gerankt wurde — keine von beiden bestraft ein abgerufenes Set, das technisch korrekt, aber mit irrelevanten Chunks aufgefüllt ist. Context Precision schließt diese Lücke: Von den k abgerufenen Chunks, welcher Bruchteil ist tatsächlich relevant für die Abfrage?

def context_precision_at_k(retrieved_ids: list[str], relevant_ids: set[str], k: int = 5) -> float:
    top_k = retrieved_ids[:k]
    hits = sum(1 for doc_id in top_k if doc_id in relevant_ids)
    return hits / k

Das ist wichtig, weil ein Retrieval-Set mit niedriger Präzision echte nachgelagerte Kosten hat, selbst wenn der Recall perfekt ist: Jeder irrelevante Chunk im Kontextfenster ist etwas, das das LLM korrekt ignorieren muss, was genau die Art von Rauschen ist, die die Wahrscheinlichkeit eines Generierungs-Fehlers erhöht, obwohl die Retrieval-Stufe den richtigen Chunk „technisch" gefunden hat. Eine Pipeline mit Recall@5 von 100%, aber Context Precision von 0,2 ruft den richtigen Chunk unter vier irrelevanten begraben ab — das ist es wert, separat zu kennzeichnen von einer Pipeline mit demselben Recall, aber Precision von 0,8, auch wenn ein nur-Recall-Dashboard sie als identisch zeigen würde.

Ein konkretes Beispiel: Fünf Abfragen durch das Harness

Um die Zahlen greifbar zu machen, hier ein kleiner illustrativer Trace — fünf Abfragen aus einem Toy-Eval-Set, durch die Recall@5-, MRR- und Context-Precision-Funktionen oben ausgeführt, auf einer hypothetischen Retrieval-Pipeline:

AbfrageGold-Chunk-RangRecall@5Reziproker RangContext Precision@5
„How long until a webhook retry gives up?"111,000,8
„What’s the rate limit for the search endpoint?"310,330,6
„Can I rotate an API key without downtime?"nicht in Top-500,000,4
„Default timeout for a webhook delivery"210,500,8
„How are refunds handled for annual plans?"111,001,0

Durchschnitt: recall@5 = 0,80, MRR = 0,57, mittlere context precision@5 = 0,72. Die Lücke zwischen recall@5 (0,80) und MRR (0,57) ist das Signal, das hier genau betrachtet werden sollte — drei der vier erfolgreichen Retrievals haben den Gold-Chunk nicht zuerst gereiht, was bedeutet, dass ein kleineres k (sagen wir, top-3 an die Generierung übergeben statt top-5) anfangen würde, Abfragen zu verlieren, die derzeit noch erfolgreich sind. Das ist ein konkreter, gemessener Grund, k bei 5 zu halten, anstatt es nur zur Einsparung von Kontextfenster-Tokens zu verknappen — und genau die Art von Kompromiss, der unsichtbar ist, wenn man nur den Recall betrachtet.

Generierungs-Metriken: Faithfulness und Answer Relevance

Retrieval-Metriken sagen einem, ob das richtige Material das Modell erreicht hat. Faithfulness sagt einem, ob das Modell es tatsächlich korrekt verwendet hat — ob jede Behauptung in der generierten Antwort tatsächlich durch den abgerufenen Kontext gestützt wird, im Gegensatz dazu, dass das Modell Lücken mit Außenwissen füllt oder direkt halluziniert.

Ein praktischer Weg, Faithfulness ohne ein spezialisiertes Framework zu berechnen, ist LLM-als-Richter: Die generierte Antwort in einzelne Behauptungen zerlegen, dann ein Richter-Modell fragen, ob jede Behauptung allein durch den abgerufenen Kontext gestützt wird.

FAITHFULNESS_PROMPT = """Context:
{context}

Claim: {claim}

Is this claim directly supported by the context above? Answer only "yes" or "no".
"""

def faithfulness_score(claims: list[str], context: str, judge_fn) -> float:
    supported = [judge_fn(FAITHFULNESS_PROMPT.format(context=context, claim=c)) for c in claims]
    return sum(1 for s in supported if s.strip().lower() == "yes") / len(claims)

Answer Relevance ist eine separate Prüfung in die andere Richtung: Beantwortet die generierte Antwort tatsächlich, was gefragt wurde, unabhängig davon, ob sie dem Kontext treu ist? Eine Antwort kann dem abgerufenen Kontext vollständig treu sein und trotzdem scheitern, die Frage tatsächlich zu beantworten — zum Beispiel wenn sie treu eine tangential verwandte Passage zusammenfasst statt die, die die spezifische Frage beantwortet. Eine einfache Version: Ein Richter-Modell bitten, eine plausible Frage zu generieren, auf die die Antwort reagieren würde, und dann ihre Embedding-Ähnlichkeit mit der ursprünglichen Abfrage messen — niedrige Ähnlichkeit deutet darauf hin, dass die Antwort von dem, was tatsächlich gefragt wurde, abgedriftet ist.

Faithfulness und Relevance erfassen unterschiedliche Generierungs-Bugs: Niedrige Faithfulness bei hoher Relevance bedeutet, dass das Modell die richtige Frage beantwortet, aber mit Informationen auffüllt, die der Kontext tatsächlich nicht stützte; hohe Faithfulness bei niedriger Relevance bedeutet, dass das Modell genau etwas aus dem Kontext berichtet, das eigentlich nicht das war, was gefragt wurde.

Wo RAGAS passt und was noch ein Mensch braucht

RAGAS (und ähnliche Evaluierungs-Frameworks) automatisieren das oben beschriebene LLM-als-Richter-Muster in fertige Metriken — Faithfulness, Answer Relevance, Context Precision, Context Recall — ohne die Richter-Prompts selbst handschreiben zu müssen. Das ist wirklich nützlich, um eine Metriken-Pipeline schnell zum Laufen zu bringen, und ein vernünftiger Ausgangspunkt statt etwas, das man von Tag eins an von Grund auf bauen muss.

Was es nicht beseitigt, ist die Notwendigkeit eines echten, handaufgebauten Eval-Sets spezifisch für den eigenen Korpus — RAGAS bewertet welche Abfragen und Gold-Daten auch immer man ihm zuführt; es generiert kein sinnvolles Eval-Set aus dem Nichts, und ein generisches oder synthetisches Eval-Set produziert Zahlen, die präzise, aber für die tatsächlichen Fehlermodi nicht bedeutsam sind. Es beseitigt auch nicht die Notwendigkeit einer regelmäßigen menschlichen Überprüfung des Richters selbst — LLM-als-Richter-Scoring hat eigene Fehlermodi (ein Richter-Modell kann nachsichtig sein oder subtile, nicht gestützte Behauptungen falsch beurteilen), sodass das Überprüfen einer Stichprobe der Faithfulness-Urteile des Richters gegen tatsächliches menschliches Urteil auf wiederkehrender Basis das ist, was die automatisierten Zahlen vertrauenswürdig hält, anstatt nur präzise aussehend.

Es lohnt sich auch, bewusst zu entscheiden, welches Richter-Modell man verwendet. Dasselbe Modell sowohl für Generierung als auch für das Richten zu verwenden, riskiert einen subtilen Self-Preference-Bias, bei dem der Richter gegenüber Ausgaben nachsichtiger ist, die seinen eigenen Formulierungsgewohnheiten entsprechen. Ein separates, unabhängig gewähltes Richter-Modell — oder zumindest Bewusstsein für diesen Bias bei der Interpretation von Grenzwert-Scores — hält die Faithfulness-Zahlen näher an einem ehrlichen Signal als einem selbstbeglückwünschenden.

Metriken in ein Regressionsgate umwandeln

Das Eval-Set zahlt sich nur aus, wenn es konsistent ausgeführt wird, nicht einmal. Ein minimales Setup, das Regressionen abfängt, bevor sie ausgeliefert werden:

def run_eval(pipeline, eval_set: list[dict]) -> dict:
    retrieval_results = []
    for item in eval_set:
        retrieved_ids = pipeline.retrieve(item["query"])
        retrieval_results.append({"retrieved_ids": retrieved_ids, "gold_chunk_ids": item["gold_chunk_ids"]})

    return {
        "recall_at_5": mean_recall_at_k(retrieval_results, k=5),
        "mrr": mean_reciprocal_rank(retrieval_results),
        # faithfulness/relevance computed similarly over generated answers
    }

baseline = run_eval(pipeline, eval_set)
# after a chunking, embedding, or prompt change:
candidate = run_eval(pipeline_v2, eval_set)

assert candidate["recall_at_5"] >= baseline["recall_at_5"] - 0.02, "recall regression"

Das Verdrahten in CI – oder zumindest das manuelle Ausführen vor dem Mergen einer retrieval-beeinflussenden Änderung – verwandelt „hilft diese Änderung?" von einer Vermutung in eine Zahl. Eine Zwei-Punkte-Recall-Toleranz (statt strenger Nicht-Regression zu fordern) berücksichtigt das Eval-Set-Rauschen bei einem bescheiden großen Query-Set — wähle einen Schwellenwert, der der Größe deines Eval-Sets entspricht, da ein 50-Fragen-Set mehr Messrauschen hat als ein 500-Fragen-Set.

Häufige Fallstricke

Das Eval-Set aus Dokumenten aufbauen, die die Pipeline in der Produktion nie sehen wird. Ein Eval-Set aus einem veralteten Export oder einem Sample-Korpus, der nicht dem tatsächlich indexierten entspricht, wird Zahlen melden, die gut aussehen und nichts bedeuten — baue Gold-Daten immer gegen den lebendigen, aktuellen Index auf.

Das Eval-Set veralten lassen, während der Korpus wächst. Neue Dokumentenkategorien und neue Abfragemuster tauchen in der Produktion lange nach dem ersten Schreiben des Eval-Sets auf. Behandle es als lebendiges Artefakt — füge jedes Mal, wenn ein echter Produktionsfehler eine Kategorie aufdeckt, die das Eval-Set nicht abgedeckt hat, eine Handvoll Fälle hinzu.

Einem einzigen Faithfulness-Score ohne Spot-Checking des Richters vertrauen. LLM-als-Richter-Scoring ist ein Proxy, keine Wahrheit. Stichprobenartig 10–20 beurteilte Fälle überprüfen und von einem Menschen nachprüfen lassen, ob das Urteil des Richters tatsächlich dem entspricht, was ein aufmerksamer Leser schlussfolgern würde — ein Richter, der still zu nachsichtig ist, wird echte Generierungs-Regressionen verdecken.

Recall und Context Precision in eine einzige „Retrieval-Score"-Zahl zusammenfassen. Eine Pipeline kann perfekten Recall und schreckliche Precision haben, oder umgekehrt, und sie in einer einzigen Zahl zu mitteln, verbirgt, welche eigentlich behoben werden muss. Immer separat berichten.

Nur messen, nachdem etwas kaputtgeht. Der gesamte Wert eines Eval-Harness ist es, eine Regression abzufangen, bevor sie ausgeliefert wird, nicht eine zu diagnostizieren, nachdem Nutzer sich beschweren. Mit demselben Review-Schritt wie die Code-Änderung verdrahten, die Retrieval oder Generierung berührt, nicht als Post-mortem-Tool.

Fazit

Ein Eval-Set verwandelt RAG-Pipeline-Änderungen von Wetten in Messungen. Die spezifischen Zahlen — Recall@5, MRR, Faithfulness — sind weniger wichtig als die Disziplin, Retrieval-Qualität von Generierungs-Qualität zu trennen und beides auf Fragen zu messen, die aus dem eigenen Korpus aufgebaut wurden, nicht auf einer Handvoll einfacher Demo-Abfragen oder dem Benchmark-Datensatz von jemand anderem. Sobald dieses Harness existiert, wird jeder andere Beitrag in dieser Reihe — Chunking, hybride Suche, Reranking — zu etwas, das man tatsächlich validieren kann, anstatt zu etwas, dem man vertrauen muss.

Faizan Nadeem
Autor
Faizan Nadeem

AI/GenAI engineer specialising in agentic RAG, LangGraph, and production LLM systems with Python and FastAPI. 2 years building AI agents, RAG pipelines, and the backends behind them.

Weitere Artikel