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.
- 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
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 RAG | Because |
|---|---|---|
| The query is an exact key (policy ID, customer ID, SKU, order number) | Database lookup or metadata filter | A 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 window | Put it in a cached system prefix | A 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 record | Documents 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 / semantic | Now RAG earns its place | This 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.
| Stage | What it does | Failure mode if it goes wrong |
|---|---|---|
| 1. Ingest | Pull source documents in | Missing or duplicated documents; nothing to retrieve |
| 2. Parse | Extract text, tables, structure from PDFs/HTML | Tables flattened to gibberish; layout lost; headers dropped |
| 3. Chunk | Split into retrievable units | Boundaries split a table or a defined term; the chunk answers nothing |
| 4. Embed | Turn chunks into vectors | Wrong or mixed embedding model; the vectors mean nothing consistent |
| 5. Index | Store vectors + metadata for search | Stale index after a refresh; missing ACL/version metadata |
| 6. Retrieve | Find candidate chunks for a query | Wrong strategy for the query shape; misses rare tokens |
| 7. Rerank | Reorder candidates by true relevance | Absent, so the top-k is noisy and the best chunk sits at rank 20 |
| 8. Assemble context | Build the prompt from top chunks | Too much irrelevant context; the model degrades (Lab 3) |
| 9. Generate | Claude answers from the context | Answers from parametric memory instead of the retrieved text |
| 10. Attribute | Cite which chunks the answer used | No 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.
| Strategy | Use when | What it breaks |
|---|---|---|
| Fixed-size + overlap | Homogeneous prose with no structure; a fast baseline | Splits mid-sentence and mid-table; overlap wastes tokens and duplicates hits |
| Sentence / paragraph | Narrative text where a paragraph is a coherent unit | A concept spanning several paragraphs is fragmented across chunks |
| Semantic | Mixed content where topic shifts, not length, mark boundaries | Expensive to compute; boundaries drift if the segmenter is weak |
| Structural (headings, tables, code blocks) | Documents with real structure: contracts, manuals, specs, code | Requires 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 generation | Two stores to keep in sync; more plumbing |
| Late-chunking | Long-context embedding of the whole doc, then split with context preserved | Requires 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.
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.
# 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 shape | Retrieval strategy | Why |
|---|---|---|
| Exact key (ID, filename, primary key) | Database lookup / metadata filter | Deterministic and exact; no embedding needed or wanted |
| Keyword / rare token (error codes, SKUs, proper names, clause numbers) | BM25 / lexical | Dense embeddings famously miss rare tokens; lexical search matches them exactly |
| Semantic / paraphrase (“which contracts limit our exposure?”) | Dense vector | Meaning, not surface form; matches concepts across different wording |
| Both (most real queries) | Hybrid: fuse lexical + dense (reciprocal rank fusion), then rerank | Catches 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 decomposition | The answer requires following edges, not matching a single chunk |
| Recency-sensitive (“the latest policy”) | Filter by metadata (date/version), then rank | Relevance without recency returns a confidently outdated answer |
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.
# 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.
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.
| Dimension | Monolithic context | Progressive discovery |
|---|---|---|
| Mechanism | Retrieve once; put all relevant chunks in one window | Model calls search/read tools and fetches on demand |
| Simplicity | Simple: one retrieval, one call | Complex: a tool loop with reasoning between steps |
| Cost | Higher per call (carries everything, relevant or not) | Lower on average (fetches only what it uses) |
| Latency | Predictable: one round trip | Higher variance: depends on how many hops it takes |
| Caching | Cacheable: a stable prefix (Lab 3) | Hard to cache: the path differs per query |
| Quality on irrelevance | Degrades when the window fills with off-topic chunks | Stays focused: pulls only what the current step needs |
| Best when | Corpus slice is small, stable, and known up front | The 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.
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.
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.
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.ipynbScenario. 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.
- 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.
- 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, andtenant_idto every chunk as you go. - Index with metadata as a first-class citizen. Store vectors plus that metadata. The
tenant_idandsectionfields are not optional; they are what make the next two steps possible. - 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.
- Enforce the ACL at retrieval, not in the prompt. Apply
tenant_idas 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. - 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?
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.)
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?
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?
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.