Skip to content

    Forward Deployed Engineer

    RAG Evaluation: Recall, Groundedness and Failure Modes

    Measure recall@3 and groundedness on a golden set, then sort every miss into a failure taxonomy using the capstone's real misses q01, q02, q07, q09 and q36.

    By Published 8 min read

    • fde
    • rag
    • evaluation
    • recall
    • groundedness
    • failure-modes
    Share

    This lesson is part of Forward Deployed Engineer: RAG, Search and Data. Enroll for free, then sign in, to read it in full.

    What you'll learn: how the capstone computes recall@3 and groundedness, how to read its results table without overclaiming, and how to sort every miss into a failure class with a decision tree, using the capstone's five real misses (q01, q02, q07, q09, q36).

    At a glance

    Figure 1. The evaluation loop

    flowchart LR accTitle: The evaluation loop accDescr: A golden set of 36 questions is run through a pipeline strategy. Each question produces a graded row. Rows are aggregated into metrics for retrieval, answers, safety and the refuse or ask gate. Misses are listed, classified into a failure class, and one change is made. The golden set is run again and the two result tables are compared. G["Golden set, 36 questions"] --> S["Run one strategy"] S --> R["One graded row per question"] R --> M["Metrics: recall, groundedness, leaks, stale, refusal"] R --> X["List the misses"] X --> C["Classify each miss"] C --> H["Change one thing"] H --> S

    How to read this: metrics tell you that something is wrong, classification tells you where, and the loop changes one thing at a time so the next table can be compared with this one.

    • Recall@3 asks whether the expected documents are among the first three the model was given.
    • Groundedness asks whether each recommendation is a real, cited product with numbers found in the evidence.
    • The capstone's five misses fall into four classes (retrieval, ordering twice, selection, gate): no single fix helps them all.
    • All numbers are in-sample regression numbers on 36 questions the author wrote, not an accuracy estimate.

    Scenario

    Composite scenario, not a real company. The Lumen sales director sees a results table with 94.2% document recall and 100% groundedness and asks whether the assistant is 94 percent accurate. A teammate wants to report it that way. Another wants to fix the one wrong refusal by raising a threshold. Neither has checked what the numbers measure or what the misses have in common. All figures are illustrative.

    What you will be able to do

    • Compute recall@3 for a question by hand and reproduce the table's 94.2% from per-question values.
    • State what groundedness measures and why it reads 100% for every strategy here.
    • Classify any miss with the decision tree, and name who owns the fix.
    • Explain the results table to a customer in three sentences without overclaiming.

    Prerequisites

    Metadata filters and tenant-aware access control for the leak count, and Context assembly and citations for the gate and context order. Metric theory (judges, nDCG, confidence intervals) is in Evaluation.

    Core ideas

    What the capstone measures

    Metric Question it answers
    Doc recall@3 Are the expected documents among the first 3 in the context?
    SKU recall@3 Are the expected products among the first 3 recommendations?
    Groundedness Is each recommendation real, cited, with numbers found in the evidence?
    Access leaks Did partner-only content reach the model or answer?
    Stale citations Did the answer cite a forbidden superseded document?
    Refusal accuracy Did it refuse exactly when it should?

    Recall is a share of what was expected, so it needs a labelled set.

    Verified: capstone/fde-product-discovery-copilot/eval/metrics.py

    def recall_at_k(expected: list[str], ranked: list[str], k: int = 3) -> float | None:
        """Share of expected items found in the first k ranked items. None when nothing was expected."""
        if not expected:
            return None
        return len(set(expected) & set(ranked[:k])) / len(expected)

    For q01 the expected documents are ds-vs-100, compat-opcua and faq-humid-environments; the first three in its context are ds-vs-100, ds-gw-200 and compat-opcua, so 2 / 3 = 66.7%. For q02 (expected ds-gw-200 and compat-opcua) the first three are ds-gw-200, ds-gw-300 and ds-gw-500: 1 / 2 = 50%.

    The table's 94.2% is the mean over the 23 questions with expected documents. Twenty score 100%; q01, q02 and q07 lose 1/3, 1/2 and 1/2: (23 - 1.333) / 23 = 94.2%. SKU recall: q09 scores 50%, so (23 - 0.5) / 23 = 97.8%.

    Reading the results table

    These rows are copied from eval/results/latest.md (strategy B is production):

    Strategy Doc recall@3 SKU recall@3 Groundedness Refusal accuracy Access leaks Stale citations
    baseline 94.2% 95.7% 100.0% 94.4% 5 2
    A 94.2% 97.8% 100.0% 97.2% 0 2
    B 94.2% 97.8% 100.0% 97.2% 0 0
    • Recall did not move. The strategies differ in safety and staleness, not in finding documents.
    • Groundedness is 100% everywhere. The mock composes only from its context, so the metric cannot tell strategies apart.
    • The safety counts moved. The in-query filter removes all 5 leaks; excluding superseded documents removes the 2 stale citations.
    • The set is small and in-sample. One question is 2.8 percentage points of refusal accuracy.

    With a real model every number would need measuring again.

    Spot the flaw. A status note reads: "Doc recall is 94.2% and groundedness is 100%, so the assistant is 94 percent accurate and safe to launch. After raising the refusal threshold to 0.72, refusal accuracy will be 100%."

    Answer

    Four flaws. Recall measures whether expected documents reached the context, not whether answers are correct. Groundedness is 100% because the mock cannot add facts. A 0.72 threshold fixes q36 but refuses q08 (coverage exactly 0.6), so refusal accuracy stays at 35 of 36. And 36 in-sample questions cannot support "safe to launch".

    A failure taxonomy as a decision tree

    Aggregate numbers say that something failed; the tree says where. The first "yes" decides the class.

    Figure 2. Which stage failed?

    flowchart TD accTitle: Which stage failed accDescr: A decision tree for one graded question. Hidden content reached the model gives the access class. A superseded document cited gives stale. An outcome different from expected gives the gate class. An expected document missing from the context gives retrieval. An expected document outside the first three gives ordering. An expected product not recommended gives selection. An ungrounded recommendation gives generation. Otherwise the question is ok. Q["Graded question"] --> A{"Hidden content reached model or answer?"} A -- "yes" --> CA["access"] A -- "no" --> B{"Superseded document cited?"} B -- "yes" --> CB["stale"] B -- "no" --> C{"Outcome as expected: answer, refuse, ask?"} C -- "no" --> CC["gate"] C -- "yes" --> D{"Every expected document in the context?"} D -- "no" --> CD["retrieval"] D -- "yes" --> E{"Expected documents within the first 3?"} E -- "no" --> CE["ordering"] E -- "yes" --> F{"Every expected product recommended?"} F -- "no" --> CF["selection"] F -- "yes" --> G{"Every recommendation grounded?"} G -- "no" --> CG["generation"] G -- "yes" --> OK["ok"]

    How to read this: each question is a stage of the pipeline, so the class name is also the place to look first and the owner of the fix.

    The lesson code implements the tree as diagnose, a chain of early returns in the figure's order.

    Verified: capstone/fde-product-discovery-copilot/lessons/M3-L6/m3_l6_failures.py

        if exposure:
            return Diagnosis(qid, "access", {"exposed": exposure[:3]})
        if set(item["forbidden_doc_ids"]) & metrics.cited_docs(response) & stale_docs:
            return Diagnosis(qid, "stale")
        outcome = metrics.outcome_of(response)
        if outcome != item["expected_outcome"]:
            return Diagnosis(qid, "gate", {"expected": item["expected_outcome"], "got": outcome})
        context_docs = metrics.distinct_docs(run.context_refs)
        missing = [d for d in item["expected_doc_ids"] if d not in context_docs]
        if missing:
            return Diagnosis(qid, "retrieval", {"missing": missing, "rank_in_plain_search": {d: _retrievable(item, svc, d) for d in missing}})
        late = [d for d in item["expected_doc_ids"] if d not in context_docs[:3]]
        if late:
            return Diagnosis(qid, "ordering", {"late": {d: context_docs.index(d) + 1 for d in late}})
        sku_recall = metrics.recall_at_k(item["expected_skus"], [r.sku for r in response.recommendations])
        if sku_recall is not None and sku_recall < 1:
            return Diagnosis(qid, "selection", {"sku_recall": sku_recall, "recommended": [r.sku for r in response.recommendations]})
        if not all(metrics.grounded_flags(response, run, svc.catalog, svc.store)):
            return Diagnosis(qid, "generation")
        return Diagnosis(qid, "ok")

    Under strategy B, 31 questions are ok and the five misses fall in four classes:

    Question Class Evidence from the run Where to look first
    q01 retrieval faq-humid-environments not in the context; a deep plain search finds it 11th evidence query, search depth
    q02 ordering compat-opcua is the 4th document context order
    q07 ordering faq-humid-environments is the 4th document context order, and k = 3
    q09 selection catalogue search returned 1 product (LUM-SW-600) the tool step
    q36 gate expected refuse, got clarify; coverage 5 / 7 = 0.714 the gate

    Under the baseline the tree reports 5 access questions (q08, q10, q12, q33, q34); under strategy A, 2 stale (q13, q22): the table's counts.

    • q02 and q07 are not retrieval failures: the evidence reached the model. The first-three cut makes them misses, so choose k before you look at results.
    • q09 is a miss retrieval cannot fix: both licence datasheets were in the context, but the catalogue search returned one product. The next module's tool lessons cover that class.

    Worked example

    Classify q01. Assumptions: strategy B, public user, mock provider, default settings.

    1. Hidden content, or a superseded document cited? No: partner_exposure is empty.
    2. Outcome as expected? Yes: answer and answer.
    3. Every expected document in the context? No: faq-humid-environments is missing. Class: retrieval.
    4. Why: a plain search with a depth of 30 ranks it 11th; the general search takes only the top 6 chunks, at most 3 per document. That is a hypothesis; the lesson does not test a fix. It is not the token budget: the context is identical without it.

    Recall for q01 is 2 / 3 = 66.7%.

    Build it

    Run the classifier and the tests from capstone/fde-product-discovery-copilot:

    PYTHONPATH=. ./.venv/Scripts/python lessons/M3-L6/m3_l6_failures.py
    ./.venv/Scripts/python -m pytest lessons/M3-L6 -v

    The first command prints (real output, mock provider):

    strategy B: ok 31 of 36
      q01 retrieval {'missing': ['faq-humid-environments'], 'rank_in_plain_search': {'faq-humid-environments': 11}}
      q02 ordering {'late': {'compat-opcua': 4}}
      q07 ordering {'late': {'faq-humid-environments': 4}}
      q09 selection {'sku_recall': 0.5, 'recommended': ['LUM-SW-600']}
      q36 gate {'expected': 'refuse', 'got': 'clarify'}

    The tests pin the five classes, the other strategies' counts, and the stored evaluation (doc_recall_at_3 == 0.942, zero leaks, zero stale citations).

    Exercise

    Explain it back. You are speaking to the Lumen sales director, who asks: "What does 94.2% document recall mean?" Answer in three sentences. A good explanation:

    • says it is the share of expected documents that reached the model, over 36 questions the team wrote itself;
    • says what it is not: accuracy, or a promise on new questions;
    • names one thing the table shows, such as zero leaks and zero stale citations here, and the next step.

    Done when you have the three sentences, and a one-line class and first place to look for each of q01, q02, q07, q09 and q36 without opening the table.

    Hint 1: the sentences sound like a report

    Start with what the number counts: "Out of the documents we expected the model to see, 94 in 100 reached it."

    Hint 2: you cannot remember the classes

    Walk the tree from the top; the first yes is the class.

    Common mistakes

    • Reading recall as accuracy. It measures a stage, not the answer.
    • Quoting groundedness as quality when the generator cannot add facts. Say which provider produced it.
    • Tuning until the 36 questions pass, then reporting the pass rate. Keep a held-out set.
    • Treating every miss as a retrieval problem. Classify first; q09 cannot be fixed in the retriever.

    In the interview: "How would you evaluate a RAG system?" A good answer separates retrieval, generation and safety; names a labelled set and its limits (size, author, in-sample); gives recall at k and groundedness as examples; classifies misses before changing anything; and says what comes next (a held-out set, a calibrated judge).

    Knowledge check

    1. Why is recall@3 for q02 equal to 50% even though SKU recall is 100%?

    compat-opcua is the fourth document in the context, behind three gateway datasheets. The evidence reached the model; the position cut-off makes it a miss.

    2. Why does groundedness read 100% for baseline, A and B?

    The mock composes only from its context and product records, so it cannot invent anything. The metric only fails when a generator does.

    3. A teammate raises the coverage threshold to 0.72 to fix q36. What do you check first?

    Run the gate over all 36 questions. At 0.72 it refuses q08 (coverage exactly 0.6), trading one error for another, and any gain is in-sample.

    4. What class is q09, and who would own the fix?

    Selection. Retrieval worked, but the catalogue search returned one product. The fix belongs to the tool step.

    5. Why does the tree check access and stale before the outcome?

    A leaked or stale source is a safety defect even when the answer looks right. Checking first stops a good outcome from hiding it.

    You can now

    • Compute recall@3 for a question and explain the 94.2% in the table.
    • Say why groundedness is 100% here.
    • Classify a miss with the decision tree and name the first place to look.
    • Explain the results to a customer without calling them accuracy.

    Further reading

    Last verified: 2026-11-02

    Next

    A bounded workflow or an agent?

    Enroll to keep reading

    Lesson 6 of 10 · Forward Deployed Engineer: RAG, Search and Data

    Measure recall@3 and groundedness on a golden set, then sort every miss into a failure taxonomy using the capstone's real misses q01, q02, q07, q09 and q36.

    In the rest of this lesson

    • Worked example
    • Build it
    • Exercise
    • Common mistakes
    • Knowledge check
    • You can now

    Enrolling is free. It unlocks every lesson in Forward Deployed Engineer: RAG, Search and Data, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments