Lab 4 · Domain 3 · 19%

RAG Pipeline Design

Integration is the single heaviest domain on this exam (19%, twelve items, more than any other) and it gets two labs. This is the first. Retrieval-augmented generation is where most production Claude systems earn their keep and where most of them quietly fail. The architect's job is not to bolt a vector database onto a prompt. It is to decide whether RAG is even the right tool, and if it is, to match every stage of the pipeline to the shape of the data and the shape of the query.

Answer key Lab4_Complete.ipynb
Exam objectives covered
  • Design a RAG pipeline with appropriate chunking and indexing strategies
  • Apply retrieval strategies matched to data shape and query pattern
  • Evaluate progressive discovery vs. monolithic context strategy
RAG is not the default: it is one answer among several. The exam rewards the candidate who reaches for it last, not first. Before you index anything, ask whether the query is an exact key lookup (use a database), whether the whole corpus fits and is stable (cache it as a prefix), and only then whether you have a corpus too big to carry and queries too fuzzy to key. A 1M-token context window changes this calculus more than most candidates expect.

1. When NOT to Build RAG

The most expensive RAG mistake is building one you did not need. Every retrieval pipeline you add is an index to keep fresh, an embedding space to keep consistent, a reranker to tune, and a new class of failure (stale or irrelevant chunks) that did not exist before. So the first architectural act is a subtraction test.

If the situation is…Do this instead of RAGBecause
The query is an exact key (policy ID, customer ID, SKU, order number)Database lookup or metadata filterA vector search over identifiers is slower, fuzzier, and famously misses the exact token you asked for
The corpus is small and stable and fits the windowPut it in a cached system prefixA 1M-token window carries a lot of policy. Cache it once; every request reads it at a tenth of the cost with no retrieval step to break
The answer is computed, not retrieved (totals, aggregates, current state)Call a tool or query the system of recordDocuments describe the world as of when they were written; a tool reports it as of now
The corpus is too big to carry and queries are fuzzy / semanticNow RAG earns its placeThis is the one case retrieval was built for

Read the last row carefully, because it is the definition, not an afterthought: RAG is for corpora too big to carry and queries too fuzzy to key. If either half is false (the corpus fits, or the query is an exact key), a simpler mechanism wins, and the exam will offer it to you as the correct answer dressed up to look boring.

2. The Pipeline, Stage by Stage

A RAG pipeline is not a black box labelled “vector search.” It is a chain of ten stages, and every one of them has a distinct failure mode. You cannot debug a RAG system (Lab 7) unless you can name which stage broke, so learn the chain as a chain.

StageWhat it doesFailure mode if it goes wrong
1. IngestPull source documents inMissing or duplicated documents; nothing to retrieve
2. ParseExtract text, tables, structure from PDFs/HTMLTables flattened to gibberish; layout lost; headers dropped
3. ChunkSplit into retrievable unitsBoundaries split a table or a defined term; the chunk answers nothing
4. EmbedTurn chunks into vectorsWrong or mixed embedding model; the vectors mean nothing consistent
5. IndexStore vectors + metadata for searchStale index after a refresh; missing ACL/version metadata
6. RetrieveFind candidate chunks for a queryWrong strategy for the query shape; misses rare tokens
7. RerankReorder candidates by true relevanceAbsent, so the top-k is noisy and the best chunk sits at rank 20
8. Assemble contextBuild the prompt from top chunksToo much irrelevant context; the model degrades (Lab 3)
9. GenerateClaude answers from the contextAnswers from parametric memory instead of the retrieved text
10. AttributeCite which chunks the answer usedNo citations, so the answer is unverifiable and un-evaluable

The pipeline has a spine: everything upstream of stage 6 decides what can be found, everything downstream decides what gets used. When a RAG system misbehaves, the discipline is to localize the failure to one row of this table before touching the prompt.

3. Chunking Strategies

Chunking is where the most damage is done and the least attention is paid. The governing insight is blunt: chunk boundaries destroy meaning. A table split across two chunks answers no question; half the rows are in one vector and half in another, and neither is retrievable as “the table.” A defined term split from its definition is worse than useless. So the chunker must respect the document’s own structure, not an arbitrary token count.

StrategyUse whenWhat it breaks
Fixed-size + overlapHomogeneous prose with no structure; a fast baselineSplits mid-sentence and mid-table; overlap wastes tokens and duplicates hits
Sentence / paragraphNarrative text where a paragraph is a coherent unitA concept spanning several paragraphs is fragmented across chunks
SemanticMixed content where topic shifts, not length, mark boundariesExpensive to compute; boundaries drift if the segmenter is weak
Structural (headings, tables, code blocks)Documents with real structure: contracts, manuals, specs, codeRequires a parser that survives the source format; degrades on messy PDFs
Parent-document (retrieve small, feed large)You want precise retrieval but full-section context at generationTwo stores to keep in sync; more plumbing
Late-chunkingLong-context embedding of the whole doc, then split with context preservedRequires a long-context embedding model; heavier compute

The senior move on this table is the structural row: chunk on the document’s own structure when it has one. A contract has clauses. A manual has sections. Code has functions. Splitting those on a 512-token grid is an act of vandalism that no reranker can undo. The parent-document pattern is the refinement most candidates miss: you embed and retrieve small, precise units (a sentence, a clause) for accuracy, but feed the model the whole enclosing section so it has enough context to answer.

A table split across a chunk boundary answers nothing. This is the concrete form of “boundaries destroy meaning,” and it is the exam’s favorite chunking trap. If a scenario mentions tables, financial schedules, or defined terms and offers you fixed-size chunking, the fixed-size option is wrong. The right answer chunks structurally and keeps the table (header and all) inside a single unit.

Structural chunking is also where metadata is born. As you split, you attach the source, the section path, the version, and the access-control tags, because a chunk without metadata cannot be filtered, cited, or secured downstream.

python
# Structural chunking: split on the document's own structure,
# and attach the metadata every later stage depends on.
# embed() is provider-agnostic pseudocode -- Anthropic ships no
# embedding endpoint; use your chosen third-party embedding model.

def chunk_contract(doc):
    chunks = []
    for clause in doc.iter_clauses():          # respect the real boundary
        # Keep whole tables intact inside their clause -- never split a table.
        text = clause.render(keep_tables_whole=True)
        chunks.append({
            "text": text,
            "vector": embed(text),             # third-party embedding model
            "metadata": {
                "source": doc.id,
                "section": clause.path,        # e.g. "7.2 Limitation of Liability"
                "version": doc.version,        # for staleness / re-index checks
                "tenant_id": doc.tenant_id,    # ACL -- enforced at retrieval
                "effective_date": doc.effective_date,
            },
        })
    return chunks

4. Retrieval Strategies Matched to Data Shape and Query Pattern

This is the exact wording of the objective, so treat the mapping as a table you can reproduce under pressure. The wrong instinct (the one the exam punishes) is to embed everything and run dense vector search for every query. Dense retrieval is one tool. It is the right tool for exactly one query shape.

Query pattern / data shapeRetrieval strategyWhy
Exact key (ID, filename, primary key)Database lookup / metadata filterDeterministic and exact; no embedding needed or wanted
Keyword / rare token (error codes, SKUs, proper names, clause numbers)BM25 / lexicalDense embeddings famously miss rare tokens; lexical search matches them exactly
Semantic / paraphrase (“which contracts limit our exposure?”)Dense vectorMeaning, not surface form; matches concepts across different wording
Both (most real queries)Hybrid: fuse lexical + dense (reciprocal rank fusion), then rerankCatches exact tokens and semantic matches; the reranker resolves the merged list
Relational / multi-hop (“who approved the contract that supersedes X?”)Graph traversal or query decompositionThe answer requires following edges, not matching a single chunk
Recency-sensitive (“the latest policy”)Filter by metadata (date/version), then rankRelevance without recency returns a confidently outdated answer
Dense embeddings miss rare tokens. Ask a vector index for error code E-4021 or clause 7.2 and it will happily return semantically nearby prose that never mentions the token you need. This is why pure dense retrieval on identifiers is a classic wrong answer, and why the strong default for real systems is hybrid retrieval: lexical plus dense, fused, then reranked. When an item describes queries that mix names or codes with meaning, hybrid + rerank is almost always the intended answer.

Here is the strong default in code: run lexical and dense in parallel, fuse the two ranked lists with reciprocal rank fusion, apply the ACL filter before anything reaches the model, then rerank the survivors.

python
# Hybrid retrieval: lexical (BM25) + dense, fused with reciprocal
# rank fusion, ACL-filtered at retrieval time, then reranked.

def reciprocal_rank_fusion(ranked_lists, k=60):
    scores = {}
    for ranked in ranked_lists:
        for rank, chunk_id in enumerate(ranked):
            scores[chunk_id] = scores.get(chunk_id, 0) + 1 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

def retrieve(query, tenant_id, top_k=8):
    lexical = bm25_search(query, limit=50)        # catches rare tokens / codes
    dense = vector_search(embed(query), limit=50) # catches paraphrase / meaning
    fused = reciprocal_rank_fusion([lexical, dense])

    # ACL PREFILTER -- security lives here, not in the prompt.
    allowed = [cid for cid in fused
               if index.metadata(cid)["tenant_id"] == tenant_id]

    # Rerank the ACL-safe survivors; keep only the true top-k.
    return rerank(query, allowed)[:top_k]

5. Indexing: Metadata Is a First-Class Citizen

An index is not a bag of vectors. It is vectors plus the metadata that makes them governable: source, timestamp, version, and access-control / tenant tags. Without that metadata you cannot filter by recency, you cannot cite a source, you cannot detect staleness, and, most importantly, you cannot enforce who is allowed to see what.

ACL filtering happens at retrieval time, never in the prompt. The naive design retrieves everything and then instructs Claude to “ignore documents the user isn’t allowed to see.” That is not a security control; it is a suggestion to a text generator, and it fails the moment the model is confused or the prompt is injected (Lab 8). The document that must never reach a user must never reach the model. Filter on the ACL/tenant metadata during retrieval, before assembling context. This is a security answer hiding inside a RAG question, and the exam plants it deliberately.

Read the retrieval code in section 4 again with this lens: the tenant filter runs before rerank, so a chunk belonging to another tenant is never a candidate for the context, let alone the answer. Multi-tenancy is enforced by the index and the query, not by the prompt.

6. Progressive Discovery vs. Monolithic Context

The objective asks you to evaluate these two strategies, which means knowing the tradeoff, not just the names. Monolithic context retrieves once and stuffs everything relevant into a single window. Progressive discovery (agentic retrieval) gives the model tools and lets it pull what it needs, iteratively, as it reasons.

DimensionMonolithic contextProgressive discovery
MechanismRetrieve once; put all relevant chunks in one windowModel calls search/read tools and fetches on demand
SimplicitySimple: one retrieval, one callComplex: a tool loop with reasoning between steps
CostHigher per call (carries everything, relevant or not)Lower on average (fetches only what it uses)
LatencyPredictable: one round tripHigher variance: depends on how many hops it takes
CachingCacheable: a stable prefix (Lab 3)Hard to cache: the path differs per query
Quality on irrelevanceDegrades when the window fills with off-topic chunksStays focused: pulls only what the current step needs
Best whenCorpus slice is small, stable, and known up frontThe needed evidence isn’t knowable until the model starts reasoning

The decision rule mirrors the workflow-vs-agent ladder from Lab 1: use monolithic context when you can enumerate the relevant slice in advance, and progressive discovery when you cannot. A single-shot Q&A over a retrieved section is monolithic. A research task that discovers which documents matter only after reading the first few is progressive. Note that Skills use progressive disclosure: the model loads a skill’s full instructions only when the task calls for it, which is the same principle applied to instructions rather than data.

Monolithic context degrades with irrelevant content; more retrieved chunks is not more accuracy. Padding the window with marginally-relevant chunks lowers answer quality (Lab 3’s context-rot problem) and raises cost. This is why a well-tuned reranker that returns a tight top-k often beats a generous retriever that returns twenty chunks. When a scenario reports that accuracy dropped after the team increased top-k, the fix is a reranker and a smaller k, not a bigger window.

7. Attribution and Groundedness

An answer you cannot attribute is an answer you cannot verify. Force the model to cite the chunks it used, and validate that the cited spans actually exist in the retrieved context. Attribution is not a nicety; it is the load-bearing link to the rest of the system: an un-attributable answer means no eval loop (Lab 6) and no defensible audit trail (Lab 8). If you cannot say which chunk produced a claim, you cannot label it right or wrong, and you cannot defend it to a regulator.

The mechanism is structured output: constrain generation to return the answer and the IDs of the chunks it cited, so a downstream check can confirm each cited chunk was actually in the retrieved set.

python
import anthropic

client = anthropic.Anthropic()

ANSWER_SCHEMA = {
    "type": "json_schema",
    "schema": {
        "type": "object",
        "properties": {
            "answer": {"type": "string"},
            # The model must name the chunks it used. Downstream code
            # rejects any id that was not in the retrieved set.
            "cited_chunk_ids": {
                "type": "array",
                "items": {"type": "string"},
            },
        },
        "required": ["answer", "cited_chunk_ids"],
        "additionalProperties": False,
    },
}

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=4096,
    thinking={"type": "adaptive"},
    output_config={"effort": "high", "format": ANSWER_SCHEMA},
    system=[{
        "type": "text",
        "text": "Answer only from the provided context. Every claim must "
                "cite the chunk id it came from. If the context does not "
                "contain the answer, say so.",
        "cache_control": {"type": "ephemeral"},
    }],
    messages=[{"role": "user", "content": build_context(retrieved_chunks, question)}],
)

# Groundedness check: every cited id must exist in what we retrieved.
result = parse(response)
retrieved_ids = {c["id"] for c in retrieved_chunks}
if not set(result["cited_chunk_ids"]).issubset(retrieved_ids):
    raise GroundednessError("model cited a chunk it was never given")

Three things in that call are exam-relevant. The schema is forced through output_config.format, not a deprecated top-level output_format. effort is nested inside output_config alongside format. And thinking is adaptive, never a fixed budget_tokens. The groundedness check that follows is what turns a citation from decoration into a control.

8. Failure Diagnosis

The exam guide publishes a sample item on exactly this, and it is worth stating verbatim. The guide’s own Sample 3: “A RAG system suddenly returns confident but incorrect answers after a document refresh, while latency and model version are unchanged. What is the most likely first place to investigate?”

The correct answer is the retrieval / indexing step: it is returning irrelevant or stale chunks (a broken re-index, or embeddings that no longer match the index). The reasoning is the transferable skill: latency is unchanged, so it is not a serving problem; the model version is unchanged, so it is not the model. The one thing that did change is the data: a document refresh. So the fault is in the data plane, not the reasoning plane.

When a symptom correlates with a data event and not a model event, investigate the data plane. This is the generalization of the guide’s Sample 3, and it is one of the highest-yield diagnostic rules in Domain 3. A regression that lines up with a re-index, an ingest, or a document refresh (while latency and model version hold steady) is a retrieval/indexing fault, not a prompt or model fault. Corollary: if you change the embedding model you MUST re-embed the entire index. A query embedded in a new space searched against vectors written in the old space returns confident garbage, silently; the numbers still compute, they just no longer mean the same thing.

The corollary is the trap that catches teams during “upgrades.” Swapping in a better embedding model and re-embedding only new documents leaves you with a mixed index: two coordinate systems in one store. Nothing errors. Retrieval just quietly degrades because half the vectors are speaking a different language than the query. Mixing embedding spaces silently returns garbage, and the fix is always the same: re-embed the whole index in one space.

9. Design Exercise

Self-driven lab Lab4_Self_Driven_Lab.ipynb

Scenario. A legal-operations team wants Claude to answer questions over a multi-tenant corpus of contracts. The documents are PDFs containing prose, tables (payment schedules, liability caps), and defined terms. Each contract belongs to exactly one tenant, and a user may only see their own tenant’s documents. The queries mix two shapes: exact lookups like “what does clause 7.2 say?” and semantic ones like “which of our contracts cap liability?” Design the retrieval pipeline.

  1. Confirm RAG is right. The corpus is large and growing (too big to carry) and half the queries are semantic (too fuzzy to key). RAG qualifies, but note that the other half (“clause 7.2”) is an exact key, so the pipeline must serve both, not force everything through dense search.
  2. Parse and chunk structurally. Chunk on clauses, not on a token grid. Keep each table (header and rows) inside one chunk; a split liability schedule answers nothing. Attach source, section (e.g. “7.2”), version, effective_date, and tenant_id to every chunk as you go.
  3. Index with metadata as a first-class citizen. Store vectors plus that metadata. The tenant_id and section fields are not optional; they are what make the next two steps possible.
  4. Retrieve hybrid, matched to query shape. Route or run both: lexical/BM25 catches “clause 7.2” and defined terms exactly; dense catches “cap liability.” Fuse with reciprocal rank fusion and rerank. Consider parent-document retrieval so a clause hit brings its whole section into context.
  5. Enforce the ACL at retrieval, not in the prompt. Apply tenant_id as a prefilter before rerank and before context assembly. A rival tenant’s clause must never become a candidate. Do not, under any circumstances, retrieve across tenants and ask Claude to ignore the wrong ones.
  6. Force attribution and check groundedness. Constrain generation to return cited_chunk_ids, and reject any answer citing a chunk that was not retrieved. This gives you the audit trail the legal team needs and the labelled data the eval loop (Lab 6) will consume.

The exercise is a compression of the whole lab: subtraction test, structural chunking, metadata-rich index, hybrid retrieval matched to two query shapes, ACL at retrieval, and forced attribution. If you can defend each of the six steps out loud, you can defend a Domain 3 item.

Check Yourself

Exam-style items. Commit to an answer before expanding.

A support tool lets agents ask “what is the status of order ORD-88213?” and “why might a package be delayed in transit?” The order status lives in a transactional database; the delay guidance lives in a large knowledge base. What retrieval design best serves both queries?
  • A. Embed the order records and the knowledge base together and run dense vector search for every query.
  • B. Route exact-ID queries to a database lookup and semantic queries to RAG over the knowledge base.
  • C. Put the entire knowledge base and all order records in the system prompt and cache it.
  • D. Use BM25 lexical search for both query types.

Correct: B. The two queries have different shapes. ORD-88213 is an exact key: a database lookup is deterministic, exact, and always current. “Why might a package be delayed” is semantic: that is what RAG over the knowledge base is for. Matching the mechanism to the query shape is the whole objective.

A embeds identifiers, where dense search is at its worst; it will miss the exact order token and return semantically nearby noise. C tries to carry a large, volatile order table in a cached prefix; order status changes constantly, so the cache is stale the instant it’s written, and the corpus won’t fit anyway. D uses lexical search for a semantic query, which will miss paraphrased delay guidance that never contains the query’s keywords. The exam wants the design that refuses to force one mechanism onto two different problems.

A team chunks its technical manuals with a fixed 512-token window and 50-token overlap. Users report that answers about the specification tables are wrong or incomplete, though prose questions work fine. Which two changes most directly address the problem? (Choose two.)
  • A. Switch to structural chunking that keeps each table intact within a single chunk.
  • B. Increase the fixed chunk size to 2,048 tokens with 200-token overlap.
  • C. Add a reranker so the best chunk rises to the top of the candidate list.
  • D. Use parent-document retrieval: embed small units but feed the whole enclosing section to the model.
  • E. Switch the embedding model to a larger one and re-embed only the table chunks.

Correct: A and D. The symptom is table-specific, which points straight at chunk boundaries splitting tables. Structural chunking (A) keeps each table (header and rows) in one retrievable unit. Parent-document retrieval (D) complements it: retrieve the precise unit, then feed the whole section so the model sees the full table in context. Together they attack the boundary problem at its root.

B just moves the boundary; a table straddling the 2,048-token mark is split exactly as badly, and the overlap wastes tokens without guaranteeing the table stays whole. C reorders candidates but cannot reconstruct a table that was cut in half before indexing; a reranker cannot retrieve information that no single chunk contains. E is the embedding-space trap: re-embedding only the table chunks with a new model leaves a mixed index (two coordinate systems in one store) which silently degrades every query.

An internal RAG assistant serves a multi-tenant HR corpus. To keep the design simple, the team retrieves the top-20 chunks across all tenants and appends the instruction: “Only use documents belonging to the requesting user’s organization; ignore all others.” What is the most serious problem with this design?
  • A. Twenty chunks is too many and will degrade answer quality.
  • B. Access control is enforced in the prompt, so another tenant’s documents still reach the model and can leak.
  • C. The retriever should use dense search instead of hybrid.
  • D. The instruction should be placed in a user message rather than the system prompt.

Correct: B. The document that must never reach the user has already reached the model. Asking a text generator to “ignore” forbidden documents is a suggestion, not a control; it fails under confusion or prompt injection, and the forbidden content is one generation slip away from the response. ACL filtering must happen at retrieval time, on the tenant metadata, so cross-tenant chunks are never candidates.

A is real but secondary: a quality issue, not a security breach. C is irrelevant to the tenancy problem; changing the retrieval algorithm does nothing about who is allowed to see what. D rearranges where the ineffective instruction lives without making it effective; the flaw is that access control is in the prompt at all, not which turn holds it. Security lives in the query and the index, never in the wording.

A research assistant must answer open-ended questions where the relevant documents cannot be known in advance; each answer depends on what earlier lookups reveal. The team currently retrieves a fixed top-k once and stuffs it all into a single window, and quality is poor. What strategy shift is indicated?
  • A. Keep monolithic context but raise top-k from 8 to 40 to be safe.
  • B. Move to progressive discovery: give the model retrieval tools so it fetches evidence as its reasoning reveals what it needs.
  • C. Cache the retrieved context so subsequent questions are cheaper.
  • D. Switch to a larger model and keep the single-shot retrieval.

Correct: B. The defining fact is that the needed evidence is not knowable up front; it emerges as the model reasons. That is exactly the case monolithic, retrieve-once context cannot serve, and exactly what progressive discovery (agentic retrieval) is for: the model calls search/read tools iteratively, pulling only what each step requires.

A makes it worse: forty chunks floods the window with irrelevance, and monolithic context degrades with irrelevant content, so quality drops further while cost rises. C optimizes cost, not the quality problem, and progressive paths are hard to cache anyway. D throws model capability at a retrieval-strategy mismatch; a bigger model still cannot use evidence that the single-shot retrieval never fetched. The fix is the retrieval strategy, not the model or the top-k.

Key Takeaways

  • Reach for RAG last. Exact key → database. Small, stable corpus → cached prefix. Computed answer → tool. RAG is only for corpora too big to carry and queries too fuzzy to key.
  • The pipeline is ten stages: ingest, parse, chunk, embed, index, retrieve, rerank, assemble, generate, attribute. Debugging means naming which stage broke.
  • Chunk boundaries destroy meaning. A table split across chunks answers nothing. Chunk on the document’s own structure when it has one, and use parent-document retrieval to keep context.
  • Match retrieval to query shape: exact key → lookup, rare token → BM25, semantic → dense, both → hybrid + rerank (the strong default). Pure dense on identifiers is the classic wrong answer.
  • Metadata (source, timestamp, version, ACL/tenant) is a first-class citizen. ACL filtering happens at retrieval, never in the prompt.
  • Monolithic context is simple, cacheable, and degrades with irrelevance; progressive discovery is cheaper on average with higher latency variance and is hard to cache. Enumerable slice → monolithic; unknowable evidence → progressive. Skills use progressive disclosure.
  • Force citations and validate that cited spans exist. An un-attributable answer has no eval loop (Lab 6) and no defensible audit (Lab 8).
  • When a symptom correlates with a data event (refresh, re-index) and not a model event, investigate the data plane, the guide’s Sample 3. And if you change the embedding model, re-embed the entire index; mixing embedding spaces silently returns garbage.