Skip to content
MINH VO A working notebook
by an engineer in Vietnam
Foundation7 min read

Build a RAG pipeline with inspectable evidence

Trace a policy question through retrieval, context assembly, citations, and abstention to build a RAG pipeline whose evidence can be inspected.

Open notebook with connected abstract shapes and olive thread linking paper swatches

For a developer comfortable with request handlers and document identifiers, retrieval-augmented generation, usually shortened to RAG, gives a language model external information while it prepares an answer. In an application, that usually means searching a document collection, selecting passages, and placing those passages in the model’s input. Each stage can fail independently, so a useful implementation preserves enough evidence to identify where an answer went wrong.

Consider a hypothetical employee question: “How many days do I have to submit a travel receipt?” The policy collection contains an old rule allowing 30 days and a current rule allowing 14 days. A fluent answer with the wrong number is a failure even if the retrieval engine found something related to receipts.

Keep document identity through ingestion

At ingestion, store a stable document identifier, revision identifier, publication or effective date when available, and access metadata. A chunk should retain the identity of the document it came from. If a passage contains “within 14 days” but loses the heading that says “travel receipts,” it may be hard to interpret correctly later.

For this example, imagine two records:

ChunkRevisionText excerptStatus
expense-r3-12Revision 3Submit travel receipts within 30 daysSuperseded
expense-r4-09Revision 4Submit travel receipts within 14 daysCurrent

These records are synthetic. The identifiers make an important property visible: the retrieval result is more than a string of text. The answer generator and reviewer need to know which revision supports the number.

Indexing can use lexical search, embeddings, or a combination. The original RAG paper combines a retriever with a generator for knowledge-intensive tasks. A product does not need to reproduce that paper’s training architecture to use external retrieval, but it should be precise about its own design when discussing results.

RAG flow selects the current fourteen-day receipt rule, assembles labeled evidence, and verifies answer citationsView full-size image ↗

The figure shows a current-policy answer path. The authorization and revision filters belong before context assembly. Searching broadly and asking the model to disregard inaccessible documents would still expose those documents to the model and potentially to logs.

Retrieve candidates, then assemble evidence

A query might retrieve ten candidate passages. That list is not automatically the final prompt context. Remove duplicates, preserve useful neighboring material, and ensure that each selected passage includes the conditions needed to interpret it. A passage about “14 days” can be misleading if a nearby exception extends the deadline for a particular travel category.

For this policy question, the context could contain the current receipt rule and one exception paragraph. Give them stable labels such as P1 and P2. Ask the model to cite those labels beside the claims they support. The application should reject invented labels and map accepted labels back to actual source records.

This mapping matters because a plausible citation string is easy to generate. A link that looks official is not enough. The generator should choose from the evidence identifiers supplied by the application. Rendering a URL from that mapping also avoids trusting an arbitrary generated link.

The model input can state that the current rule is the controlling source and that superseded documents must not define the answer. Ideally, revision handling already removed the old rule from the candidate set. The instruction provides clarity; it should not be the only defense against stale records.

Make the answer auditable

A minimal answer might say, “The current travel policy requires submission within 14 days, unless the exception in the supplied policy applies [P1, P2].” Whether that wording is correct depends on what P2 actually says. If the exception applies only to international travel, the answer should preserve that condition.

Record the query, selected chunk identifiers, revision identifiers, prompt version, model identifier, and resulting answer under an appropriate privacy policy. Full document text may be unnecessary if the source snapshot is reproducible and access controlled. The goal is to reconstruct the evidence path without duplicating every sensitive document into an unrestricted log.

A reviewer can then ask separate questions. Did retrieval find the current rule? Did context assembly keep its conditions? Did the answer state only supported claims? Was the citation attached to the right claim? A single overall “good answer” label would conceal these distinctions.

Reproduce a lost exception

Make the exception concrete: revision 4 allows 21 days for international travel; other travel keeps the 14-day limit. Assign the exception chunk expense-r4-10. For a question specifically about international travel, annotate both current chunks as required. These are human-written expectations for this fixture, not labels inferred from a similarity score.

Suppose the retriever returns both chunks, but a context-length limit drops the exception. The answer’s citation to the 14-day rule can be a real, resolvable identifier while the answer is still incomplete. Checking only retrieved results would miss the loss. Preserve the context after the final trimming step, because an earlier assembly preview cannot prove what the generator actually received.

The following Python helper compares stage identifiers. Inputs are a nonempty set of required chunk IDs, a sequence of retrieved IDs, a mapping from final context labels to chunk IDs, and the answer’s citation labels. Assume the caller already validated these types and captured the actual request. Parent passages added during expansion must appear in the recorded retrieval/expansion inventory too.

def diagnose(required, retrieved, context, citations):
    if not required:
        raise ValueError("required evidence must be annotated")
    found = set(retrieved)
    supplied = set(context.values())
    checks = {
        "retrieval_miss": required - found,
        "context_loss": (required & found) - supplied,
        "unrecorded_context": supplied - found,
        "unknown_citation": set(citations) - set(context),
    }
    return {name: sorted(ids) for name, ids in checks.items() if ids}

required = {"expense-r4-09", "expense-r4-10"}
retrieved = ["expense-r4-09", "expense-r4-10"]
context = {"P1": "expense-r4-09"}
print(diagnose(required, retrieved, context, ["P1"]))

This run reports context_loss for expense-r4-10. Add P2 mapped to that chunk and the result becomes an empty dictionary. That means these identifier checks passed. It does not mean the answer mentioned international travel, stated 21 days, attached the correct citation, or refused an unauthorized approval request. Those judgments require the actual answer and source text.

A trace matrix distinguishes a missing retrieval result, a lost context passage, an invented citation, and identifiers ready for claim reviewView full-size image ↗

The trace makes a small regression exercise reproducible. Remove the exception from both retrieval and context: expect retrieval_miss. Keep retrieval complete but omit its context entry: expect context_loss. Supply both passages but cite P9: expect unknown_citation. Finally, use valid labels with a deliberately wrong 30-day answer. The helper reports no identifier problem, so a separate claim audit must reject that answer. If a test treats the empty dictionary as factual approval, the test itself is incomplete.

Eugene Yan’s July 30, 2023 discussion of LLM system patterns treats retrieval as an external-knowledge component and evaluation as a separate system concern. That distinction informs this trace. The article also distinguishes ANN recall against exact neighbors from other quality and operating measures. Our policy fixture measures none of its cited systems’ performance; finding the nearest vectors is not equivalent to finding all evidence required for this question.

Design the no-answer path

Suppose the user asks, “Will finance approve my late receipt?” The documents may describe a deadline without granting the assistant authority to approve exceptions. The pipeline should explain the documented rule and identify the missing approval decision. It should not infer a manager’s decision from a nearby policy paragraph.

A retrieval score alone cannot settle this. High similarity can indicate that a passage concerns late receipts while offering no answer about approval. An answerability check needs to consider the requested claim and the available evidence. The assistant can ask for a relevant policy section or direct the user to the responsible reviewer without pretending to know an outcome.

The evaluation best-practices guide, retrieved on September 20, 2026, recommends task-specific evaluation sets. For this pipeline, include a current-rule question, a superseded-rule trap, an exception case, and an approval request that the documents cannot decide. Keep the expected evidence identifiers beside the answer requirements.

A small test set cannot establish quality across every policy or language. It can expose a specific broken stage. If the current passage never reaches the prompt, change retrieval or indexing. If it reaches the prompt and the answer still says 30 days, inspect the generation contract and conflicting context. Those are different repairs, and the stored evidence lets you choose between them.

Sources & further reading

  1. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks
  2. OpenAI evaluation best practices
  3. Eugene Yan: Patterns for Building LLM-based Systems & Products
← Back to the journal
All notes

Illustration

100%