Neural Mastery

Retrieval-Augmented Generation (RAG)

An LLM's knowledge is frozen at training time and limited to what fit in its weights. RAG fixes both problems by retrieving relevant information at query time and feeding it into the prompt as grounding context.

Picture an open-book exam versus a closed-book one. A model with no RAG is closed-book: it can only answer from what it memorized during training, which might be stale, incomplete, or just never covered your specific documents. RAG turns it into an open-book exam — before answering, the model (or a system around it) goes and looks up the most relevant pages from your actual documents, then writes its answer using what it just read, not just what it memorized. The two hard parts are exactly like taking an open-book exam well: finding the right pages quickly out of a huge book (retrieval), and actually using what you found instead of ignoring it or misreading it (generation, grounded).

Everything below is how each piece of that "find the right pages, then use them well" pipeline actually works.

The RAG Pipeline, End to End

Nine steps, in the order they actually run. Everything else on this page is one of these steps covered in real depth — worth seeing the whole shape once before diving into any single piece:

  1. Ingestion: pull in your raw source documents — PDFs, wikis, tickets, whatever your knowledge base actually is — and get them into clean, parseable text.
  2. Chunking: split that text into retrievable units. The size and strategy chosen here shapes the quality of every step downstream.
  3. Embedding: turn each chunk into a vector using an embedding model (see Foundation Model Internals).
  4. Indexing: store those vectors in a structure built for fast similarity search at scale (see RAG Optimization for how that index is actually tuned in production, and Databases for the storage layer itself).
  5. Query Processing: transform the user's raw question before searching with it — decompose it, rephrase it, or generate a hypothetical answer to search with instead.
  6. Retrieval: search the index — dense, sparse, or hybrid — using the (possibly transformed) query, and pull back the chunks most likely to be relevant.
  7. Reranking: re-score the retrieved candidates with a more precise, more expensive method before committing to a final set.
  8. Generation: feed the reranked chunks into the LLM's context alongside the query (see Prompt Engineering for how that context actually gets structured into a prompt), and let it generate an answer grounded in what it just read.
  9. Evaluation & Monitoring: measure whether the whole pipeline actually produces faithful, relevant answers — both before shipping and continuously once it's live.

Click any node below for what it does, then type a real question and watch retrieval actually run — real chunking, real cosine-similarity ranking, a real (if simplified) reranking pass:

Interactive
RAG Pipeline Simulator
Chunk size
Reranker
Document
Chunking
Embeddings
Vector Store
Query
Retrieval
Reranker (on)
LLM
Answer
24 chunks indexed · showing top 3
#139.7% match
Gradient descent updates parameters by stepping in the direction opposite the gradient of the loss function.
#233.3% match
The learning rate controls step size: too large causes divergence or oscillation, too small makes training
#313.4% match
by setting the gradient of the loss to zero.
Real chunking, real bag-of-words cosine-similarity retrieval, and a real (if simplified) reranking pass -- type any question about the topics covered in the demo corpus and watch retrieval actually respond.

Chunking Strategies

Pipeline step 2. Once documents are ingested, this is the first real decision point — every later step operates on whatever units get produced here.

  • Fixed-size chunking: split every N tokens — simple, but can cut sentences/ideas in half.
  • Semantic chunking: split at natural boundaries (paragraphs, topic shifts) so each chunk is a coherent unit of meaning.
  • Recursive chunking: try splitting on large boundaries first (sections), fall back to smaller ones (sentences) only if a chunk is still too large.
  • Parent-child chunking: retrieve using small, precise child chunks, but pass the larger parent chunk (with more surrounding context) to the LLM — balances retrieval precision with generation context.
  • Late chunking: embed the entire document first with a long-context embedding model, then pool the resulting token embeddings into chunks only afterward — the reverse order of every strategy above. Because each chunk's embedding is derived from token representations that already attended to the full document, it carries whole-document context even for a chunk that, read in isolation, wouldn't obviously connect to it (a pronoun resolved only by an earlier paragraph, for instance). This solves a different problem than parent-child chunking: parent-child pairs a small retrieval unit with a larger separate context chunk passed to the LLM; late chunking bakes document-wide context directly into the retrieval embedding itself, before chunking ever happens.
Chunk size is a real tradeoff, not a "bigger is safer" default: too small loses context a chunk needs to make sense on its own; too large dilutes the embedding's specificity and wastes context window on irrelevant text.

Recursive chunking in code, via LangChain's RecursiveCharacterTextSplitter — the standard implementation of the "try large boundaries first, fall back to smaller ones" strategy described above:

from langchain_text_splitters import RecursiveCharacterTextSplitter

# Tries splitting on paragraph breaks first, then sentences, then words --
# only falls back to a smaller separator when a chunk is still too large.
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(document_text)

chunk_overlap repeats a small amount of text between consecutive chunks, so a sentence or idea split across a chunk boundary still appears in full in at least one of the two chunks.

Chunking strategy
RAG retrievesrelevant chunksat query time.It embedsthe queryand searchesa vector DB.Retrieved textgrounds themodel's answer.
3 chunks from the same 10-unit document.
Splits at natural sentence/paragraph boundaries -- each chunk is a coherent unit of meaning.

Choosing an Embedding Model

Pipeline step 3. Whatever chunks step 2 produced now need to become vectors. Different embedding models trade off quality, speed, cost, and domain fit (a model tuned on general web text may underperform on legal or medical documents). Dimensionality matters too — higher-dimensional embeddings usually capture more nuance but cost more to store and search at scale.

In code, via the standard sentence-transformers library (see Retrieval & Reranking Architectures for the bi-encoder architecture this implements):

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

# Every chunk from the chunking step, embedded once and indexed --
# this is the vector data that step 4 (indexing) stores.
chunk_embeddings = model.encode(chunks)

What retrieval above is actually doing under the hood — comparing vectors by cosine similarity in a learned space where meaning clusters geometrically — is exactly what the Embedding Space Explorer makes clickable:

Interactive
Embedding Space Explorer
kingqueenmanwomanboygirlfathermotherdukeduchessdogcatwolflioneagledolphincomputeralgorithmnetworkrobotsoftwareinternetpizzabreadapplecoffeecheesericerainsnowsunshinestormwindfogjoyangerfearsadnesshopecalmcartrainbicycleairplaneshipbus
royalty
people
animals
tech
food
weather
emotion
transport
Nearest to "king"
queen0.599
man0.188
father0.188
duke0.184
boy0.183
Analogy: A − B + C ≈ ?
"king" − "man" + "woman" ≈ ...
queen (0.962)
duchess (0.198)
mother (0.188)
Hand-authored, structured demo vectors (no trained model here) -- but cosine similarity, nearest-neighbor lookup, the A - B + C word-vector analogy, and the 2D projection below are all real math, computed from scratch, live.

Query Transformation

Pipeline step 5. Before searching at all, the raw user question can itself be transformed into something more retrievable:

  • HyDE (Hypothetical Document Embeddings): have the LLM first generate a hypothetical answer to the query, then embed and search using that instead of the raw query — often more similar to real relevant documents than the terse original question.
  • Query decomposition: break a complex multi-part question into sub-questions, retrieve for each separately.
  • Step-back prompting: ask a more general question first to retrieve broader context, before answering the specific one.
  • Multi-query retrieval: have the LLM generate several rephrasings of the same query (different word choices, different framings), retrieve for each independently, and union the results — hedges against any single phrasing missing a relevant chunk purely due to vocabulary mismatch.
  • RAG-Fusion: multi-query retrieval's natural extension — generate multiple query variations, retrieve for each, then combine the ranked result lists via Reciprocal Rank Fusion (the same technique the Retrieval section below uses for hybrid search) rather than a simple union, so chunks ranked highly across multiple query variations surface to the top.
Query transformation technique
original: "How did GDP growth affect inflation in tech-heavy economies post-2020?"
↓ Multi-query
GDP growth impact on inflation in tech economies since 2020
Post-pandemic inflation drivers in technology-driven economies
Relationship between economic growth and price inflation, tech sector
3 queries actually embedded and searched, not the raw question above.
Generate several rephrasings, retrieve for each, union the results -- hedges against one phrasing missing a relevant chunk.

Whichever version of the query comes out of this step — rephrased, decomposed, or hypothetical — is what actually gets searched next.

Dense vs. Sparse Retrieval, and BM25

Pipeline step 6. Two structurally different ways to find relevant chunks — worth distinguishing precisely rather than lumping together as "search," because they fail in genuinely different, complementary ways.

Dense Retrieval

What is it? Finding chunks by meaning, using the same embedding-space idea the rest of this page already relies on for chunk vectors.

How does it work? Embed both the query and every chunk into the same continuous vector space (see Foundation Model Internals), and retrieve by vector similarity — the same cosine-similarity comparison the Embedding step above computes.

Why is it useful? It captures semantic similarity: a query about "canines" can retrieve a chunk about "dogs" even with zero shared words, because the embedding space clusters them near each other regardless of exact wording.

Limitation: Because it's comparing meaning, not exact text, dense retrieval can miss exact matches that matter — a specific product code, an exact legal term, a precise error message — where the literal string is the point, not the general topic.

Scoring mode
query: "canines that bark"
"Dogs often bark at strangers."
0.86
"Canines that bark loudly."
0.94
"A quiet afternoon in the park."
0.12
Toggle modes -- the "dogs" document's score flips from near-top to zero.
Dense retrieval scores by meaning in embedding space -- the "dogs" document scores high despite sharing zero words with the query.

Dense retrieval's blind spot — missing exact term matches — is precisely what sparse retrieval, the classical information-retrieval approach, was built to guarantee.

Sparse Retrieval and BM25

What is it? Finding chunks by exact term overlap with the query, the pre-embeddings approach to search that's still highly effective today.

How does it work? Represent text as a high-dimensional, mostly-zero vector over the vocabulary — which exact terms appear, and how often — then score query-document overlap with BM25, the standard modern scoring function. BM25 refines raw term-frequency matching with two corrections: it saturates the benefit of a term appearing many times in a document (the 5th occurrence of a word matters far less than the 1st), and it downweights matches on terms that are common across the whole corpus (matching "the" tells you nothing; matching a rare technical term tells you a lot) while upweighting matches on documents that are otherwise concise (a short document containing your query terms is more likely to be specifically about them than a long document that happens to mention them once).

Why is it useful? It guarantees exact term matches aren't missed — the opposite failure mode from dense retrieval — which is exactly why it still matters even in an embeddings-heavy pipeline.

Limitation: Sparse retrieval has no concept of meaning — a query about "canines" simply won't match a chunk that only ever says "dogs," no matter how relevant that chunk actually is, because there's zero literal term overlap for BM25 to score.

In code, via rank_bm25, the standard Python implementation — it deliberately does no text preprocessing itself, so tokenizing (and lowercasing, stemming, etc. if wanted) is the caller's job:

from rank_bm25 import BM25Okapi

tokenized_corpus = [chunk.lower().split() for chunk in chunks]
bm25 = BM25Okapi(tokenized_corpus)

tokenized_query = query.lower().split()
top_chunks = bm25.get_top_n(tokenized_query, chunks, n=5)
score contributiontf →
(k1+1)·tf / (k1+tf) = 1.92 — approaching the ceiling of k1+1 = 2.5
score(t,d)=IDF(t)(k1+1)tfk1+tf\text{score}(t,d) = \text{IDF}(t) \cdot \frac{(k_1+1)\, \text{tf}}{k_1 + \text{tf}}
Raw term frequency (dashed) keeps climbing linearly. BM25's saturated term (solid) flattens fast toward k1+1 -- exactly the correction that stops a document from scoring 10x higher just for repeating a word 10x.

Dense misses exact matches; sparse misses meaning — two failures with no overlap, which is exactly what makes running both together, rather than picking one, worth the extra cost.

What is it? Running dense and sparse retrieval independently, then merging their two ranked result sets into one — the practical default in production RAG, not a niche option.

How does it work? Merge the two ranked lists via Reciprocal Rank Fusion, which combines rankings based on each result's position in each list rather than trying to normalize and compare two differently-scaled similarity scores directly.

Why is it useful? It gets both failure-avoidance properties at once — a chunk can surface either because it's semantically close (dense) or because it shares exact terms with the query (sparse), covering each method's blind spot with the other.

Dense and sparse retrieval fail in different, complementary ways — dense misses exact matches, sparse misses meaning. Hybrid search isn't a compromise between the two; it's a way to get both failure-avoidance properties at once.
Dense ranking
#1 Doc B
#2 Doc A
#3 Doc D
#4 Doc C
Sparse ranking
#1 Doc C
#2 Doc B
#3 Doc A
#4 Doc D
Fused (RRF)
#1 Doc B
#2 Doc C
#3 Doc A
#4 Doc D
RRF score = 1/(k+rank_dense) + 1/(k+rank_sparse), k=60 (standard default)
Hover a document -- its dense rank and sparse rank each contribute 1/(k+rank) to its fused score. A doc ranked highly by BOTH lists rises to the top even if neither single ranking put it #1.

Re-ranking with Cross-Encoders

Pipeline step 7. The retrieval step above (dense, sparse, or hybrid) has to be fast enough to search potentially millions of chunks — it uses a bi-encoder architecture, embedding the query and each document independently so document embeddings can be precomputed and indexed once (see Recommender Systems — Two-Tower Architecture for the identical architectural pattern). A cross-encoder re-ranker instead feeds the query and a candidate document into the model together, letting it directly attend across both — far more accurate at judging true relevance, but far too slow to run against the entire corpus, since nothing about it can be precomputed per-document. The standard pattern is exactly this two-stage shape: cheap bi-encoder retrieval narrows millions of chunks to a few dozen candidates, then an expensive cross-encoder re-ranks just those few dozen precisely — the same retrieval-then-rank funnel as Recommender Systems and Learning-to-Rank.

Bi-EncoderQueryDocumentbge-large-en-v1.5 (BERT-large)Token + PositionEmbeddingsSelf-Attention (16 heads)Feed-Forward (→ 4096 →)× 24[CLS]poolsame weights both passes, never cross-attending to each other1024-dim, CLS-pooled[CLS] vec[CLS] veccos()Cosine SimilaritySimilarity ScoreCross-EncoderQuery + Passageone sequenceMiniLM-L6× 6Token + PosEmbeddingsJoint Self-Attn (12 heads)FFN (→ 1536 →)[CLS]every token attends to every other token, across the query/passage boundary384-dim[CLS] vecLinear384 → 1Relevance Score
Real internal architecture, not just labels: embeddings → attention + feed-forward (repeated N times, same shared weights) → pooling. One pipeline drawn once and run twice, converging/diverging (bi-encoder) vs. one joint sequence through it once (cross-encoder).
Bi-encoder (BAAI/bge-large-en-v1.5): BERT-large, 24 transformer layers, hidden size 1024, 16 attention heads, FFN size 4096. The SAME shared weights run as two independent forward passes (query, then each document), never attending to each other -- both converge into and diverge out of the identical internal pipeline shown once. Each pass pools to a single 1024-dim vector via CLS-token pooling (not mean pooling), precomputable and stored ahead of time. Cross-encoder (cross-encoder/ms-marco-MiniLM-L6-v2): 6 transformer layers, hidden size 384, 12 attention heads, FFN size 1536. Query and passage are concatenated into ONE sequence and go through the model together in a SINGLE pass, with real self-attention across the query/passage boundary -- the final [CLS] hidden state feeds a 384-to-1 classification head. Nothing here is precomputable, which is exactly why it only reranks a shortlist rather than searching a full corpus.

A third architecture sits between these two: late interaction (ColBERT) keeps a separate embedding per token instead of pooling to one vector per document, and scores query-document pairs via MaxSim — for each query token, the single best-matching document token, summed. Document token embeddings stay precomputable and indexable, like a bi-encoder's, but nothing gets collapsed away, so it recovers much of a cross-encoder's precision without the joint forward pass. See Retrieval & Reranking Architectures for the real MaxSim formula, a fully worked numeric example, and a three-way comparison of when production systems reach for each of the three.

Advanced RAG Architectures

Beyond the single retrieve-then-generate pass above, several architectures address specific failure modes by adding a control loop or changing what's actually retrieved:

  • Agentic RAG: instead of one fixed retrieve→generate pass, an agent (see Agent Architectures) decides whether to retrieve, what to retrieve, and how many times — it can issue multiple retrieval calls, reformulate its own queries based on what came back, or decide the retrieved context is insufficient and search again, rather than being locked into exactly one retrieval step per query.
  • Corrective RAG (CRAG): adds an explicit evaluation step after retrieval — a lightweight grader assesses whether the retrieved chunks are actually relevant/sufficient, and if not, triggers a fallback (query rewriting, a broader search, or falling back to general web search) before generation proceeds, rather than generating from possibly-irrelevant context regardless.
  • Self-RAG: trains the LLM itself to emit special reflection tokens during generation — deciding when retrieval is needed, critiquing whether retrieved passages are actually relevant, and assessing whether its own generated output is properly supported by them — folding the "should I retrieve, was this useful, is my answer grounded" judgment directly into the generating model's own token stream instead of a separate pipeline stage.
  • Multimodal RAG: retrieves and grounds generation in non-text content — image embeddings (via CLIP-style models, see Computer Vision — Modern Vision & Multimodal) alongside or instead of text chunks, for questions that need to reference charts, diagrams, or photos in the source material.
  • SQL RAG: instead of retrieving unstructured text chunks, translates a natural-language question into a SQL query against a structured database (see Databases — Relational), executes it, and feeds the query result to the LLM as grounding context — the right approach when the answer genuinely lives in structured tables (aggregates, exact counts) rather than prose a chunk-based retriever would need to happen to contain.
  • Code RAG: retrieval over a codebase specifically — chunking strategies built around code structure (functions, classes) rather than fixed token windows, and embeddings from code-aware models, used for AI coding assistants that need to ground suggestions in a specific repository's actual code rather than the model's general programming knowledge.
Architecture
QueryRetrieveGenerateAnsweragent reformulates & re-retrieves
Same retrieve→generate skeleton (gray) — each architecture adds a different decision point (highlighted).
An agent decides whether to retrieve, what to retrieve, and how many times -- it can loop back to Retrieve with a reformulated query before ever reaching Generate.

GraphRAG

Standard RAG retrieves isolated chunks, which struggles with questions that require connecting facts across multiple documents (multi-hop reasoning). GraphRAG instead builds a knowledge graph (see Databases — Graph) from the source documents, and retrieval can traverse relationships between entities directly, rather than relying purely on chunk-level similarity.

Acme CorpCEO Jane LeeAcquired 2023Beta IncFounded by Jane Lee
A knowledge graph built from source documents -- entities as nodes, relationships as edges.
Click a node -- its directly-connected facts light up. "Who founded the company Acme acquired?" resolves in one traversal: Acme → Acquired 2023 → Beta Inc → Founded by Jane Lee, chaining across what were originally separate document chunks.

RAG Optimization

Everything above this section establishes correctness — this section is about making the same pipeline fast and cheap enough to actually run at real production query volume, without rebuilding any of it.

Index optimization. Production vector search almost universally runs on HNSW (Hierarchical Navigable Small World) graphs rather than exact nearest-neighbor search, which stops scaling past a small corpus. Two parameters do most of the work:

  • ef_construction: how thoroughly the index explores candidate connections while it's built — higher values produce a higher-quality graph (better recall later) at the cost of slower, more expensive indexing, paid once.
  • ef_search: the equivalent knob at query time — how many candidates the search explores before returning results. This is the parameter actually tuned in production, live: push it up for better recall at higher per-query latency, pull it down for faster, cheaper queries at some recall cost. It's the single biggest recall/latency lever in a deployed vector index.

Quantization. Storing every embedding at full float32 precision gets expensive at real corpus scale. Scalar quantization (e.g. float32 → int8) shrinks storage with modest accuracy loss; Product Quantization (PQ) goes further — splitting each vector into sub-vectors and replacing each sub-vector with the nearest entry in a small learned codebook, trading some retrieval accuracy for a much larger compression ratio than scalar quantization alone. The right choice is a direct function of how much accuracy loss the application can tolerate versus how much the index costs to store and search at the corpus's real size.

Semantic caching. Real production query traffic repeats — not usually word-for-word, but semantically: many users ask close variants of the same handful of questions. Semantic caching embeds each incoming query and checks it against previously served queries by similarity (not exact string match); a close-enough hit reuses the cached retrieval (or even the cached final answer) instead of re-running the full pipeline. This is a genuine, increasingly standard production technique (tools like GPTCache implement it directly) — the tradeoff is the similarity threshold itself: too loose and semantically different queries get served a wrong cached answer; too tight and the cache rarely hits at all.

Embedding fine-tuning on domain data. A general-purpose embedding model is trained on broad web text, not your specific domain's vocabulary and relevance judgments — fine-tuning it on domain query-document pairs (contrastive/triplet-loss training, pulling truly relevant pairs closer and irrelevant ones apart) can meaningfully close the gap between "generically similar" and "actually relevant for this specific corpus," at the cost of needing labeled or weakly-labeled domain relevance data to train on.

None of the optimizations above are free wins — every one is a real, explicit tradeoff against some other production metric: ef_search trades recall for query latency, quantization trades accuracy for storage and search cost, semantic caching trades a small risk of a stale answer for skipping the pipeline on repeat traffic, and reranker choice trades accuracy for cost the same way. Tune each one against the system's actual latency and cost budget, not a generic default.

Evaluating RAG

Pipeline step 9, and the last one — none of the steps above matter if there's no way to tell whether the whole pipeline is actually working:

  • Faithfulness / groundedness: does the generated answer actually match what the retrieved context says (or does it hallucinate beyond it) — is every claim in the answer traceable back to the retrieved chunks?
  • Answer relevance: does the generated answer actually address the user's question (a faithful-to-context but off-topic answer still fails the user).
  • Context precision/recall: of the chunks retrieved, how many were relevant (precision); of all relevant chunks that exist, how many were retrieved (recall) — see Model Evaluation & Metrics for the general precision/recall definitions this specializes.
  • MRR and NDCG: the same ranking-quality metrics from Learning-to-Rank apply directly to retrieval quality — did the most relevant chunk rank first, not just appear somewhere in the top-k.
Faithfulness
Answer relevance
Context precision
Context recall
12345678
● retrieved only● relevant only● both
precision = 2 relevant / 4 retrieved = 0.50
Of the chunks retrieved, how many were actually relevant?

See LLM Evaluation & RAGOps for the full evaluation tooling (Ragas, DeepEval) and production RAG monitoring.

Common Failure Modes

  • Hallucination despite correct context → the model ignores or misreads retrieved content; mitigated with stronger grounding instructions and citation requirements
  • "Lost in the middle" on long retrieved context → reorder chunks to put the most relevant near the start/end of the prompt
  • Multi-hop questions failing → GraphRAG, or explicit query decomposition
  • Stale knowledge base → versioned, incremental re-indexing pipelines rather than full rebuilds
  • PDF/table parsing issues → dedicated document-layout parsers instead of naive text extraction
  • Per-user access control → metadata filtering at retrieval time, scoped to what each user is authorized to see

Next: Evaluation & Serving — measuring whether any of this actually works, and running it in production.

Last updated Sep 5, 2026Edit this pageReport an issue
← Previous
Prompt Engineering
Next →
Retrieval & Reranking Architectures: Bi-Encoder, Cross-Encoder, Late Interaction