Skip to content
advanced Phase 18 · Production Capstone and Interview Prep

Capstone: Production RAG Assistant

Build a full document assistant with ingestion, retrieval, evals, and monitoring.

2h
0 problems
Topic Progress 0%

Why a Demo RAG Breaks in Production

A notebook RAG prototype answers questions from three PDFs. It dies the moment real users arrive: ingestion silently drops tables, retrieval returns overlapping chunks, answers cite nothing, and nobody can tell when quality degrades. The capstone exists to close those gaps — not to add features, but to make each stage observable, replaceable, and safe.

The system has two paths. An offline path runs on a schedule: parse documents, chunk them, embed the chunks, and upsert them into a vector store with metadata. An online path serves each user request: retrieve candidate passages, rerank them, assemble a token-budgeted context, and generate an answer that cites its sources. A third component — the eval harness — scores the whole system against a golden question set so regressions surface in CI, not in production.

┌──────────────────────────────────────────────────────────────┐
│                  CAPSTONE RAG ASSISTANT                      │
├──────────────────────────────────────────────────────────────┤
│  OFFLINE (scheduled)          ONLINE (per request)           │
│                                                              │
│  1. INGESTION                 4. RETRIEVAL                    │
│     parse PDF / HTML             embed query → top-k         │
│     chunk + overlap              hybrid: vector + BM25       │
│     embed → upsert                          │                │
│     metadata: source,                       ▼                │
│     page, section             5. RERANK (cross-encoder)      │
│         │                        top-k → top-5               │
│         ▼                                   │                │
│  2. VECTOR STORE                          ▼                │
│     chunks + metadata         6. CONTEXT ASSEMBLY           │
│                                dedupe, budget, order         │
│  3. EVAL HARNESS                          │                │
│     golden Q&A set             7. GROUNDED GENERATION        │
│     recall@k, faithfulness       answer + [citations]        │
│                                            │                │
│                                            ▼                │
│                              8. GUARDRAILS + REVIEW          │
│                                 PII redaction, injection     │
│                                 filter, human review queue   │
└──────────────────────────────────────────────────────────────┘

Design rules that keep this maintainable: each stage is a replaceable module, so you can swap the embedding model without touching generation; ingestion is idempotent, so re-runs update changed documents instead of duplicating them; every stage emits metrics, so a quality regression is attributable to a stage, not a mystery. For the stage-by-stage map, see RAG Pipeline Overview; for ingestion depth, see Building the Ingestion Pipeline.

Ingestion: From Mixed Documents to a Queryable Index

Naive text extraction loses structure — headings, tables, and section boundaries vanish — and re-ingesting the same corpus creates duplicate chunks that double-count every answer. The ingestion pipeline fixes both: parse for structure, chunk with overlap, and derive stable IDs so re-runs upsert instead of duplicate.

import hashlib

def chunk_text(text: str, chunk_size: int = 512, overlap: int = 64) -> list[dict]:
    # Split text into overlapping chunks with stable IDs for idempotent upserts.
    chunks = []
    step = chunk_size - overlap
    for start in range(0, len(text), step):
        body = text[start : start + chunk_size].strip()
        if not body:
            continue
        chunk_id = hashlib.sha1(f"{start}:{body[:80]}".encode()).hexdigest()[:16]
        chunks.append({"id": chunk_id, "text": body, "start": start})
    return chunks

Two lines carry the design. step = chunk_size - overlap keeps 64 tokens of context across a boundary, so a sentence split between chunks survives in at least one of them. The ID is derived from position and content, not from a counter — re-running ingestion on an unchanged document produces the same IDs, so the vector store upserts rather than appends.

Embedding and upserting attach the metadata that citations depend on:

def upsert_chunks(chunks: list[dict], source: str, embed_fn, vector_store) -> None:
    for c in chunks:
        vector_store.upsert(
            id=c["id"],
            vector=embed_fn(c["text"]),
            metadata={"source": source, "start": c["start"]},
        )

Input: a 40-page PDF and 12 web pages. Output: 312 chunks, each carrying its source URL, page number, and section title. When a page changes, its content hash changes, only that page is re-embedded, and the rest of the index is untouched. For chunking strategy tradeoffs, see Fixed-Size Chunking and Semantic and Structural Chunking.

Retrieval, Reranking, and Answers That Cite

Top-k vector search alone returns overlapping chunks that waste the context budget, and when retrieval is thin the model quietly answers from memory — confidently, and without citations. The online path fixes this with a two-stage retrieve-then-rerank design, a hard token budget, and citation IDs the model must use.

def answer_question(question: str, embed_fn, vector_store, reranker, llm) -> dict:
    query_vec = embed_fn(question)
    candidates = vector_store.search(query_vec, k=20)                 # stage 1: recall
    top = reranker.rerank(question, [c.text for c in candidates], top_n=5)  # stage 2: precision

    context_parts, used, budget = [], [], 3000
    for c in top:
        if budget - c.tokens < 0:
            break                    # hard cap: lowest-ranked passages are evicted first
        context_parts.append(f"[{c.id}] {c.text}")
        used.append(c.id)
        budget -= c.tokens

    prompt = (
        "Answer using only the passages below. Cite each fact as [id]. "
        "If the passages do not say, reply 'Not covered'.\n\n"
        + "\n\n".join(context_parts)
        + f"\n\nQuestion: {question}"
    )
    return {"answer": llm.complete(prompt), "citations": used}

Three lines do the real work. k=20 then top_n=5 exists because cross-encoders are far more accurate than embeddings but too slow to score a full corpus — the cheap first stage narrows the field, the expensive second stage orders it. The budget loop evicts lowest-ranked passages first, so the most relevant signal survives when the context window fills. And [c.id] markers let post-processing verify that every citation in the answer exists in the retrieved set — a cheap check that catches fabricated sources. For the reranker itself, see Reranking with Cross-Encoders; for context ordering, see Assembling Context for the Model.

Evals, Dashboards, and Guardrails

A confident wrong answer is worse than a refusal, and you cannot improve what you cannot measure. The eval harness scores every change against a frozen golden set before it reaches production, and guardrails wrap the generation stage so a hostile input or a PII leak is caught at the boundary.

def run_eval(questions: list[dict], answer_fn, judge) -> dict:
    results = []
    for q in questions:
        out = answer_fn(q["question"])
        results.append({
            "id": q["id"],
            "recall": q["relevant_chunk_id"] in out["citations"],
            "faithfulness": judge.score(q["question"], out["answer"], q["gold_answer"]),
        })
    return {
        "recall@k": sum(r["recall"] for r in results) / len(results),
        "faithfulness": sum(r["faithfulness"] for r in results) / len(results),
    }

Input: 200 golden questions, each with a relevant chunk ID and a gold answer. Output: recall@k 0.87, faithfulness 0.91. The first number says the right passage is retrieved 87% of the time; the second says the answer stays faithful to it 91% of the time. When those numbers move, the stage that moved is obvious. For harness design, see Offline Evaluation Harness; for retrieval scoring, see Evaluating Embedding Quality.

Guardrails sit at both edges of the request. On input, a PII redaction pass strips emails and phone numbers before the prompt is built, and an injection filter screens for instruction-like patterns. On output, flagged conversations — injection attempts, low faithfulness, user reports — land in a human review queue rather than disappearing into logs. See Input and Output Guardrails and Human Review Workflows. With evals, dashboards, and guardrails wired in, the capstone is a system you can operate, not a demo you can only hope works.

Quiz

1. Why retrieve top-20 and rerank to 5 instead of retrieving top-5 directly?

Question 1 options

2. What is the purpose of deriving chunk IDs from content position in the ingestion pipeline?

Question 2 options

3. In the context assembly loop, why evict lowest-ranked passages when the token budget runs out?

Question 3 options

4. What does a faithfulness score of 0.91 on the eval set tell you?

Question 4 options

Flashcards

Question

What are the two stages of retrieval in the capstone, and why two?

Answer

Vector search retrieves a wide top-k (recall), then a cross-encoder reranker re-scores a small subset (precision) — cross-encoders are too slow for the full corpus.

Question

Why does the ingestion pipeline derive chunk IDs from content position?

Answer

So re-running ingestion upserts existing chunks instead of creating duplicates — idempotent ingestion.

Question

What does the context assembly loop do when the token budget runs out?

Answer

It stops adding passages, keeping the highest-ranked ones and evicting the lowest-ranked first.

Question

What two metrics does the offline eval harness report?

Answer

recall@k (did the right chunk get retrieved) and faithfulness (is the answer supported by the passages).

Question

Which guardrails wrap the generation stage?

Answer

PII redaction on input, an injection filter, and a human review queue for flagged conversations.

Revision Notes

Key Takeaways

  • 1. Each stage is a replaceable module — swap embeddings without touching generation
  • 2. Idempotent ingestion with content-derived IDs prevents duplicate chunks
  • 3. Two-stage retrieval balances recall (vector) and precision (cross-encoder)
  • 4. Evals gate releases; guardrails limit blast radius at the boundary

Interview Tips

  • • Walk the data flow out loud: ingest, retrieve, rerank, assemble, generate — name each stage
  • • Give concrete numbers: top-20 retrieved, top-5 reranked, 3000-token budget, recall@k 0.87
  • • Explain why two retrieval stages exist, not just that you used them
  • • Close with the eval and guardrail story — it shows you operate systems, not just build them

Cheat Sheet

Capstone RAG Assistant — Cheat Sheet

Architecture: offline path (ingest → chunk → embed → upsert) + online path (retrieve → rerank → assemble → generate) + eval harness + guardrails.

Ingestion:

  • step = chunk_size - overlap keeps context across boundaries
  • chunk ID = hash of position + content → idempotent upserts
  • store source, page, section with every chunk for citations

Retrieval:

  • stage 1: vector search top-k=20 (recall, cheap)
  • stage 2: cross-encoder rerank to top-5 (precision, expensive)
  • hard token budget; evict lowest-ranked first
  • require [id] citations; verify every cited ID exists in the retrieved set

Evals: recall@k (retrieval) + faithfulness (generation) on a frozen golden set

Guardrails: PII redaction in, injection filter, human review queue for flagged conversations

Continue Learning

Before You Start

Related Reading