Skip to content
advanced Phase 9 · RAG Architecture End-to-End

Diagnosing RAG Failure Modes

Attribute bad answers to retrieval, context, or generation and fix the right stage.

1h
0 problems
Topic Progress 0%

Look at the Passages First

An incident arrives as: "The bot told me parental leave is 20 weeks; the handbook says 14." The instinct — shared by almost every new team — is to open the prompt and start rewording it. That instinct is wrong often enough to be expensive, because a generation-side edit cannot fix evidence that never reached the model.

The first question is not "what did the model say?" but "what did the model see?" Log the three ids together and the argument becomes mechanical:

answer supported by the prompt?
  |
  +-- gold passage in the prompt? --+-- no --> in the retrieved set?
  |                                  |           |-- no  --> RETRIEVAL
  |                                  |           |        (chunking, embeddings,
  |                                  |           |         query rewriting)
  |                                  |           +-- yes --> ASSEMBLY
  |                                  |                    (evicted by dedupe or budget)
  |                                  |
  |                                  +-- yes --> GENERATION
  |                                               (instruction, model, decoding)
  |
  +-- yes --> SOURCE or LABEL
              (gold annotation wrong, document stale)

Read it as a series of eliminations. Retrieval is guilty when the evidence never entered the top-k. Assembly is guilty when retrieval found it and the token budget threw it away — a distinct bug with a distinct fix, and invisible unless you log prompt_pids separately from retrieved. Generation is guilty only once the right passage is provably sitting in the prompt.

The traversal itself is code, and it is the first thing to run on any report:

import re
from collections import Counter

REFUSAL = "Not covered by the provided documents."

def attribute(log: dict) -> str:
    gold = set(log.get("gold_pids", []))
    retrieved = {p["pid"] for p in log["retrieved"]}
    prompt = set(log["prompt_pids"])
    answer = log["answer"].strip()

    if not gold:
        return "eval-gap: no reference passage logged"
    if not (gold & retrieved):
        return "retrieval: gold passage outside top-k"
    if not (gold & prompt):
        return "assembly: retrieved then evicted before generation"
    if answer == REFUSAL:
        return "generation: refused despite evidence in the prompt"

    cited = {int(m) for m in re.findall(r"\[(\d+)\]", answer)}
    if cited - set(range(1, len(prompt) + 1)):
        return "generation: citation id not in the prompt"
    if re.search(r"\d", answer) and not cited:
        return "generation: numeric claim with no marker"
    return "source-or-label: prompt supported the answer"

Input: a log whose gold_pids = ["handbook:9a17:2"], retrieved contains it, prompt_pids does not. Output:

'assembly: retrieved then evicted before generation'

Every branch names a different owner. Fixing an assembly eviction by rewriting the prompt produces a second incident a week later, and the log would have said so.

The Failure Catalogue

These are the entries that recur across RAG systems. Severity is not how likely the failure is — it is what happens to the user when it fires:

  • critical — a confidently wrong answer is served, or a citation is fabricated
  • high — a common path returns wrong or missing information
  • medium — quality degrades but the answer is still usable
Failure Stage Severity Detection signal
Stale index after a document edit ingestion high source hash ≠ stored doc_hash, freshness metric drops
Parser flattened a table or heading ingestion medium extraction coverage; sampled chunks read as word soup
Chunk boundary split a row from its header ingestion medium citation quotes a number with no column
Vocabulary mismatch (query "severance", doc "exit package") retrieval high recall@k low on a slice; keyword-only hits
Duplicate chunks starve top-k retrieval medium duplicate rate inside retrieved sets
Gold passage evicted by the token budget assembly high retrieved ∌ prompt_pids diff
Dilution — many weak passages, one good one assembly medium context-relevance score; answer drifts toward filler
Model answers from memory over correct context generation critical faithfulness low while recall@k is healthy
Fabricated citation id generation critical cited − valid non-empty
Refusal on a question it had evidence for generation medium exact refusal string on cases with gold in prompt_pids
Wrong or missing gold label eval medium incidents where attribute returns source-or-label

Two patterns are worth memorizing because they invert the obvious fix:

High recall, low faithfulness means retrieval is fine and generation is drifting — a prompt, model, or temperature problem. Re-chunking makes it worse by changing the one half that already works.

Low recall on one slice only — the benefits questions, the German pages — usually means vocabulary mismatch or a parse defect on that document type, not a global embedding problem. The slice tells you where to look before you change anything global.

Diagnosis that stops at "it's the model" is a coin flip. Diagnosis that names a stage and a signal is an actionable ticket, and the difference is which log fields you captured.

Replay the Incident Offline

A report you cannot replay is a report you can only fix once. Store the retrieved set with the answer at request time, and every incident becomes a reproducible input to attribute() long after the request finished.

def replay(logs: list[dict]) -> dict:
    counts = Counter(attribute(log) for log in logs)
    return dict(sorted(counts.items(), key=lambda kv: -kv[1]))

Input: four stored logs — one with gold outside top-k, one retrieved-then-evicted, one refusal despite evidence, one clean. Output:

{'retrieval: gold passage outside top-k': 1,
 'assembly: retrieved then evicted before generation': 1,
 'generation: refused despite evidence in the prompt': 1,
 'source-or-label: prompt supported the answer': 1}

Replaying a batch rather than a single case changes the decision you make. One retrieval failure might be a fluke; four incidents bucketing into three different stages says your problems are not concentrated, and a single global knob change will not fix them.

The same replay is also where fixes get validated. After re-chunking or raising the budget, re-run attribute() over the stored logs and compare bucket counts — a fix that moves incidents between buckets rather than reducing them is a fix that renamed the problem. That comparison loop, with a frozen question set and recorded metric deltas per change, is what the RAG iteration workflow topic formalizes.

Minimum fields to log per request: query, retrieved (pid, score, section), prompt_pids, answer, model, prompt_version, gold_pids where an eval label exists, and a timestamp. Missing prompt_pids is the single most common gap — without it, assembly and retrieval failures are indistinguishable and every diagnosis collapses into "retrieval was bad."

Quiz

1. An answer contradicts the handbook. You can log three things: retrieved pids, prompt pids, and the answer. What is the first comparison?

Question 1 options

2. A gold passage is in the retrieved set but not in prompt_pids. Which stage failed, and what usually caused it?

Question 2 options

3. What does "high retrieval recall with low answer faithfulness" tell you?

Question 3 options

4. A batch replay shows incidents landing in four different attribute() buckets. What does that imply?

Question 4 options

Flashcards

Question

What is the first question when a RAG answer is wrong, and why?

Answer

"What did the model see?" — inspect retrieved and prompt ids before touching the prompt. A generation-side edit cannot repair evidence that never reached the model.

Question

The four attribution outcomes of the passage-first traversal

Answer

gold absent from top-k → retrieval; gold retrieved but not in the prompt → assembly; gold in prompt but answer departs → generation; prompt supports the answer → source or gold label.

Question

Why log prompt_pids separately from retrieved?

Answer

Because dedupe and the token budget can evict a passage retrieval found. Without both lists, assembly failures are indistinguishable from retrieval failures and every diagnosis collapses to "retrieval was bad."

Question

How is severity defined in the failure catalogue?

Answer

By user impact when the failure fires, not by likelihood: critical = confidently wrong answer or fabricated citation; high = common path returns wrong or missing information; medium = degraded but usable.

Question

What makes an incident report fixable rather than anecdotal?

Answer

The full request context stored with the answer — query, retrieved pids with scores, prompt pids, answer, model, prompt version, gold labels. The log is replayable offline, so the fix can be validated against the same inputs.

Revision Notes

Key Takeaways

  • 1. Start every bad-answer report with what the model saw, not what it said — stage attribution is set arithmetic on logged ids
  • 2. Log prompt ids separately from retrieved ids or assembly evictions become permanently misdiagnosed as retrieval failures
  • 3. Severity measures user impact, not likelihood; that is what makes the catalogue triageable
  • 4. High recall with low faithfulness indicts generation; low recall on a slice indicts vocabulary or parsing on those documents
  • 5. A replayed batch tells you whether problems concentrate (one fix) or spread (fix the largest bucket, then re-measure)

Interview Tips

  • • Walk an incident through the tree out loud, naming the log field that decides each branch — the traversal, not the vocabulary, is what gets evaluated
  • • Defend the order: passages before prompts. Be ready with the counter-example where rewording the prompt could not have helped
  • • Explain what you would log and why prompt ids are the field teams most often omit
  • • Connect replay to iteration: same stored incidents, re-run after a change, compare buckets — that sentence signals you have shipped this before

Cheat Sheet

Diagnosing RAG Failure Modes — Cheat Sheet

Triage: passages first, prompts last

gold in prompt? --+-- no  --> in retrieved? --+-- no  --> RETRIEVAL
                  |                           +-- yes --> ASSEMBLY (evicted)
                  +-- yes --> answer supported? --+-- no --> GENERATION
                                                   +-- yes --> SOURCE / LABEL
def attribute(log):
    gold, retrieved = set(log.get("gold_pids", [])), {p["pid"] for p in log["retrieved"]}
    prompt, answer = set(log["prompt_pids"]), log["answer"].strip()
    if not gold:                              return "eval-gap"
    if not (gold & retrieved):                return "retrieval"
    if not (gold & prompt):                   return "assembly"
    if answer == REFUSAL:                     return "generation: refusal despite evidence"
    cited = {int(m) for m in re.findall(r"\[(\d+)\]", answer)}
    if cited - set(range(1, len(prompt) + 1)):return "generation: bad citation id"
    if re.search(r"\d", answer) and not cited:return "generation: unattributed claim"
    return "source-or-label"

Catalogue shortcuts (stage / severity)

Signal Points at
recall@k low on one slice only retrieval — vocabulary mismatch or parse defect on that doc type
duplicates crowding top-k retrieval/assembly — dedupe threshold or doc fan-out
gold in retrieved, absent from prompt assembly — budget eviction
recall healthy, faithfulness low generation — instruction, model, decoding
cited − valid non-empty generation — fabricated marker, never serve it
refusal with gold in prompt generation — contract too aggressive
hash mismatch vs source ingestion — stale index

Log fields per request: query · retrieved (pid, score, section) · prompt_pids · answer · model · prompt_version · gold_pids · timestamp.

Replay rule: bucket counts before and after a fix. Incidents moving between buckets means the fix renamed the problem.

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading