AI Engineering Playbook
RAG

The RAG Pipeline

The full end-to-end picture — offline ingest, online query, grounded generation.

Prerequisites

The intuition

An LLM sitting alone is a closed-book exam. It memorized an enormous amount during training, it cannot look anything up, and — crucially — it will never say "I didn't study that." It will write a fluent, well-structured, confidently wrong paragraph instead.

RAG turns it into an open-book exam. Before the model answers, you go to the library, find the three pages that matter, staple them to the question, and say: answer from these pages, and cite which one you used.

That's the whole idea. Everything else in this section is engineering in service of one question: how do you reliably find the right three pages?

Key insight

Notice where the difficulty sits. The "generation" half of Retrieval-Augmented Generation is nearly free — modern models are good at answering from provided text. The "retrieval" half is the entire engineering problem. Engineers who spend effort only on prompt wording are optimizing the easy half.

Why it exists

Three hard constraints make RAG necessary, and it's worth being able to name them individually because each one independently forces the architecture:

  1. The weights are frozen. A model's parametric knowledge stops at its training cutoff. Your Q3 policy change, yesterday's incident, this morning's ticket — none of it is in there.
  2. Your data was never in the training set, and shouldn't be. Internal wikis, contracts, customer records. You can't train on them cheaply, and for most regulated data you can't ship them to a third party at all.
  3. The model can't distinguish knowing from guessing. Next-token prediction always produces a next token. Without grounding, an unsupported answer looks exactly like a supported one — same fluency, same confidence, no signal.

The alternatives lose on all three. Fine-tuning bakes knowledge in statically: re-running it for every document change is hours and dollars, it produces no citations, and it can't express "user A may see this document, user B may not." Stuffing everything into a long context window doesn't scale past a corpus that fits, costs tokens linearly on every single query, and degrades in the middle of long inputs.

RAG sidesteps all of it by moving knowledge out of the weights and into a queryable store you can update in seconds, filter per user, and cite.

The core idea

RAG (Retrieval-Augmented Generation) grounds an LLM's answer in documents retrieved at query time instead of relying on what the model memorized during training. The model's weights are frozen at a training cutoff and contain none of your private data; RAG fixes both problems without touching the weights: fetch the relevant text, put it in the prompt, instruct the model to answer only from it.

Four reasons teams choose RAG over the alternatives:

  1. Knowledge cutoff — retrieval serves current data; the model's parametric knowledge is months-to-years stale.
  2. Private data — your wiki, tickets, and contracts were never in the training set and shouldn't be shipped to a fine-tune.
  3. Hallucination reduction — grounding plus "answer only from the context" cuts fabrication and, critically, enables citations, so users can verify claims.
  4. Cost and freshness vs fine-tuning — updating an index is minutes and cents; fine-tuning is hours and dollars, must be re-run for every data change, and is bad at adding facts anyway (it shifts behavior/style more reliably than knowledge).

The pipeline in one breath — two phases:

OFFLINE (ingest):  documents → parse → chunk → embed → index (vector DB + keyword index)
ONLINE  (query):   query → [rewrite] → embed → retrieve top-k → rerank → augment prompt → generate → cite

Offline, documents are parsed to text, split into chunks (~a few hundred tokens each), embedded into vectors, and stored in a vector index alongside a keyword (BM25) index. Online, the query is embedded, nearest-neighbor search returns the top-k candidate chunks, a reranker reorders them by true relevance, the winners are packed into the prompt with the question, and the LLM generates a grounded, cited answer.

The load-bearing fact: almost every RAG failure is a retrieval failure. If the right chunk isn't in the prompt, no model can answer correctly — so the engineering effort goes into retrieval quality and its evaluation, not prompt wording.

How it actually works

The two phases run on completely different clocks — one is a batch job measured in hours, the other is a request path measured in milliseconds. Keeping them separate is what makes the design legible:

What each stage contributes, and what breaks if you skip it:

StageMechanismWhat it contributes / cost of skipping
ParsePDF/HTML/DOCX → clean text + structureGarbage-in dominates: broken tables and headers poison every later stage
ChunkSplit into retrieval units, typically 200–800 tokens, often with overlapWhole docs embed poorly (one vector can't summarize 50 pages); too-small chunks lose context
EmbedEncoder model → dense vector (~768–3072 dims) capturing semanticsEnables meaning-based match ("vacation policy" ↔ "PTO rules"); keyword search alone misses paraphrase
IndexANN structure (typically HNSW) over vectors + BM25 inverted indexSub-linear search: exact scan over millions of vectors is too slow at query time
Retrievek-NN over embeddings, ideally hybrid with BM25, fused via RRFRecall stage: cast a wide net (k=20–100). Dense catches paraphrase; BM25 catches exact IDs, SKUs, names
RerankCross-encoder scores each (query, chunk) pair jointlyPrecision stage: bi-encoder retrieval compresses query and doc separately and loses interaction; the cross-encoder reads them together and reorders 50 candidates → keep top 5–10
AugmentPack chunks + instructions + question into the promptOrdering matters — models attend most to the start and end of context ("lost in the middle")
GenerateLLM answers from the provided context, with citations and an "I don't know" escape hatchWithout the escape hatch the model answers from parametric memory when retrieval fails — the worst failure mode, because it looks grounded

Two details worth understanding one level down:

  • Why retrieve-then-rerank? Embedding retrieval is fast but lossy (independent encodings, single dot product). Cross-encoders are accurate but O(candidates) LLM-ish inference — far too slow to run over the whole corpus. So the cheap stage maximizes recall, the expensive stage buys precision on a shortlist. Same funnel design as web search. (Mechanics: rerankers.md.)
  • Why hybrid search? Dense vectors blur exact tokens: "error TS2345" or a part number embeds near its neighbors but BM25 nails it exactly. Reciprocal Rank Fusion (RRF) merges the two ranked lists without needing score calibration. Anthropic's contextual-retrieval benchmarks show each added stage compounds: contextual embeddings cut retrieval failures ~35%, +BM25 ~49%, +reranking ~67%. (Mechanics: hybrid-search.md.)

Key insight: recall first, then precision

The retrieve→rerank shape is one idea applied twice, and it's worth internalizing because it recurs everywhere in search and recommendation systems: a cheap, wide, high-recall stage followed by an expensive, narrow, high-precision stage. You can afford to be sloppy early because a later stage cleans up; you can afford to be expensive late because the candidate set is tiny. Get the order wrong and you either lose the answer permanently (narrow first) or blow your latency budget (expensive first).

RAG vs long context: stuffing a 1M-token window doesn't replace RAG at corpus scale (10M docs don't fit), costs linearly per query in tokens, is slower, and models still retrieve unreliably from mid-context. In practice long context relaxes chunking pressure (bigger chunks, more of them) rather than eliminating retrieval.

The flows

Two clocks, two paths — plus the refusal branch when retrieval has nothing useful to give.

FlowSequenceWhen it appliesWhat breaks it
Offline ingestdocuments → parse → chunk → embed → write to vector index + BM25New docs, updates, deletes, full re-index after chunking/embedding changesBad parse (garbage text), wrong chunk boundaries, missing metadata/ACLs, orphaned stale chunks after update
Online queryquery → rewrite → embed → hybrid retrieve (dense + BM25, RRF) → rerank → augment prompt → generate + citeEvery user questionUnderspecified query (no rewrite), dense-only miss on exact tokens, no rerank (noisy context), prompt overflow, model ignoring context
Honest miss / refuseretrieve → score below threshold or empty after filters → skip freestyle generation → "I don't know" / escalateQuestion outside the corpus, ACL-filtered empty set, garbage retrievalMissing threshold → model invents from parametric memory; looks grounded, is wrong

The operational model: offline builds the cache; online reads it under a latency budget; refusal is a first-class path, not an error.

A worked example

Trace one query end to end through the governed enterprise LLM platform. An employee asks:

"Does the new expense policy still cover international travel?"

Offline, this already happened. The policy document was parsed to text, split into 14 chunks of ~400 tokens with 50-token overlap, embedded with a 1024-dimension model, and written to the vector index along with metadata — {doc_id, version, effective_date, visible_to: ["all-staff"]} — plus a BM25 entry per chunk.

Online, per request:

StepWhat happensConcretely
1. RewriteChat history condensed into a standalone query"new expense policy" becomes expense policy 2026 international travel coverage — "new" on its own would have matched nothing
2. EmbedOne encoder call1024 floats, ~20 ms
3. RetrieveDense k-NN and BM25, each top-50, fused by RRF, filtered by entitlementDense finds "Travel outside the country of employment…" — zero shared keywords with the query, pure semantic match. BM25 finds the chunk literally headed "International Travel". RRF ranks them 1 and 2. ~35 ms
4. RerankCross-encoder scores 50 (query, chunk) pairs, keeps 5Drops a chunk about domestic mileage reimbursement that scored well on the token "travel" but answers a different question. ~120 ms
5. Augment5 chunks (~2 000 tokens) + instructions + questionInstructions: answer only from the context; cite the chunk number for each claim; if the context doesn't answer it, say so
6. GenerateModel answers from the packed context"Yes — international travel remains covered, with pre-approval required above the stated threshold [3]. Per-diem rates changed effective 1 April [1]." ~2.4 s, streamed

Total ≈ 2.6 s, of which retrieval is ~175 ms — under 8%.

Now break it deliberately, one stage at a time — each omission has a distinctive production fingerprint:

What each omission looks like in production

  • No rewrite → the query stays "does it still cover international travel?", the pronoun resolves to nothing, retrieval returns generic travel chunks. Wrong answer, confident tone.
  • No BM25 → fine here, but ask about "form T-490" and dense retrieval returns semantically-adjacent forms while BM25 would have matched the exact token.
  • No reranker → the domestic-mileage chunk survives into the top 5, the model sees two conflicting per-diem numbers, and the answer either hedges or picks the wrong one.
  • No entitlement filter → an unreleased draft visible only to HR gets retrieved and quoted. That is not a quality bug, it's a data-leak incident. See filtering-and-metadata.md.
  • No "I don't know" instruction → ask about a policy that doesn't exist, retrieval returns five loosely-related chunks, and the model synthesizes a plausible one from parametric memory. The worst failure mode, because the output is indistinguishable from a good answer.

Production concerns

  • Retrieval quality is the KPI. Track retrieval metrics (recall@k, MRR) separately from answer metrics — a bad answer with good retrieval is a prompt/model problem; with bad retrieval it's an index problem. (Full treatment: rag-evaluation.md.)
  • Latency budget: embed query ~10–50 ms, ANN search ~10–100 ms, rerank 50 candidates ~50–300 ms, generation dominates (seconds). Retrieval overhead is usually <20% of end-to-end; don't micro-optimize ANN while streaming tokens is the bottleneck. (Full treatment: latency.md.)
  • Cost: per-query = 1 embedding call + vector DB read + rerank + LLM tokens. Context stuffing is the silent cost driver — 10 chunks × 500 tokens = 5k prompt tokens per query before the question. Reranking pays for itself by letting you send fewer, better chunks. (Full treatment: cost.md.)
  • Freshness: the index is a cache of your corpus and goes stale; you need an ingestion pipeline with updates/deletes, not a one-shot script. (Full treatment: production-rag.md.)
  • Failure modes to name: empty/garbage retrieval (need thresholds + graceful "not found"), stale chunks after doc updates, prompt overflow when k is too high, and the model ignoring context in favor of parametric memory.

Common drill-downs

Explain RAG to me like I'm a backend engineer. It's a read-through pattern: the LLM is a stateless compute layer, the vector DB is the queryable state. At request time you query the state for relevant records, join them into the prompt, and the model computes the answer over them. Updating knowledge = writing to the DB, not retraining the compute layer.

RAG vs fine-tuning — when each? RAG for knowledge: facts, freshness, private data, citations, per-user access control. Fine-tuning for behavior: format, tone, domain jargon, following a house style. They compose — fine-tune the style, retrieve the facts. Fine-tuning is a poor knowledge store: expensive to refresh, no provenance, can't do per-user permissions. (Deeper: fine-tuning.md.)

Why chunk documents at all? Why not embed the whole document? An embedding is a fixed-size vector; a 50-page doc collapses into one point and its specifics average away, so specific queries can't find specific passages. Chunks make the retrieval unit match the granularity of questions. Also embedding models have context limits, and you want to fill the prompt with only the relevant slice.

Why do you need a reranker if the vector search already ranks by similarity? Bi-encoders encode query and document independently — no term-level interaction, one dot product. A cross-encoder attends across both texts jointly and is far more accurate, but too expensive to run corpus-wide. Retrieval optimizes recall over millions; reranking buys precision over ~50. Skipping it typically costs the most quality per unit of effort saved.

With 1M-token context windows, is RAG dead? No — corpus size (millions of docs), per-query token cost, latency, and mid-context recall degradation ("lost in the middle") all remain. What changes: chunking can get coarser and k larger. Long context and RAG are complements — retrieval selects, context carries.

Your RAG system hallucinates. Walk me through debugging. Isolate the stage: (1) inspect retrieved chunks for a failing query — if irrelevant, it's retrieval (chunking, embeddings, missing hybrid search); (2) if relevant, it's generation — tighten grounding instructions, add "say I don't know", cite-or-refuse; (3) check the chunk itself is complete — mid-sentence splits make the model interpolate; (4) build a small eval set so the fix is measured, not vibes.

Where does the latency go in a RAG request, and what would you optimize first? Generation dominates (seconds); retrieval+rerank is typically tens to a few hundred ms. Optimize perceived latency first: stream tokens, cut prompt size (fewer, better chunks — reranking helps), then parallelize embed+BM25, then consider a smaller generation model.

What's the single highest-leverage improvement to a naive RAG pipeline? Usually hybrid search + a cross-encoder reranker: no re-ingestion, immediate recall and precision gains (Anthropic measured ~67% retrieval-failure reduction stacking contextual embeddings, BM25, and reranking). Second: fix chunking to respect document structure.

Test yourself

Why is 'almost every RAG failure is a retrieval failure' true rather than just catchy?

You have a 3-second latency budget. Retrieval takes 175 ms. Your team wants to spend two weeks tuning HNSW parameters. What do you say?

Dense retrieval and BM25 both return the same chunk, at different ranks. How does RRF combine them, and why not just average the two scores?

A user asks about a policy your company has never published. Describe the ideal behaviour, stage by stage.

Why does the offline/online split matter when designing a RAG system?

Go deeper

Where this connects

  • Ingestion & Chunking — the offline half in detail; decisions made there constrain everything downstream and are the most expensive to reverse.
  • Retrieval Patterns — the rewrite step above, expanded: HyDE, multi-query, parent-child, contextual retrieval.
  • RAG Evaluation — how you know retrieval is working, rather than believing it.
  • Production RAG — freshness, ACLs, degradation: what the diagram above needs before real users touch it.
  • Advanced RAG — what to reach for when this fixed pipeline isn't enough.
Retrieval Patterns

On this page