chapter 10 / rag · estimated study time 120 min

RAG and Vector Retrieval
Give the Model an Open-Book Exam

AUDIO // Chapter Audio Guide
Chapter Contents
  1. The Three Sins of Parametric Knowledge
  2. Embedding: Turning Semantics into Geometry
  3. Interactive Lab: Semantic Retrieval vs. Keyword Retrieval
  4. ANN Indexes: How HNSW Finds Neighbors in a Billion Vectors in Milliseconds
  5. Chunking: The Invisible Ceiling on RAG Quality
  6. Production-Grade Retrieval: Hybrid Recall and Reranking
  7. RAG Evaluation and a Failure-Mode Checklist
  8. Long Context vs. RAG, and Agentic RAG
  9. Hands-On Code: A 60-Line Minimal RAG
  10. Chapter Quiz

The Three Sins of Parametric Knowledge

Chapter 6 said it: a model's factual knowledge lives mainly in the FFN weights. This kind of parametric knowledge has three structural defects:

RAG (Retrieval-Augmented Generation) solves this by turning a closed-book exam into an open-book one: before answering, it first retrieves relevant material from an external knowledge base and stuffs it into the context, so the model "cites" rather than "recalls." The pipeline in one breath: document → chunk → embed → store; query → embed → nearest-neighbor retrieval → (rerank) → assemble prompt → generate. Every link has technical detail; we unpack them one by one.

Embedding: Turning Semantics into Geometry

The core abstraction was already laid down in Chapter 5: semantic similarity = closeness in vector space. An embedding model maps arbitrary text to a dense vector (768–3072 dimensions), trained with contrastive learning: pull related pairs together (question↔answer, title↔body) and push unrelated pairs apart (other samples in the same batch serve as negatives). The loss is InfoNCE (cross-entropy's twin sibling from Chapter 2—a "find the positive within the batch" classification):

$$L = -\log \frac{e^{\text{sim}(q, d^+)/\tau}}{\sum_{d \in \text{batch}} e^{\text{sim}(q, d)/\tau}}$$

Similarity uses cosine (equivalent to the dot product after normalization). Two engineering points: ① the two-tower (dual-encoder) structure—query and document are each encoded independently, so document vectors can be precomputed offline, and at query time you only compute the query embedding once plus a nearest-neighbor lookup (this is why RAG can be fast); ② its cost is that query and document have no interaction, so its accuracy ceiling is lower than a cross-encoder that concatenates the two and runs them through the model together—a trade-off we cash in during the reranking of §6.

Interactive Lab: Semantic Retrieval vs. Keyword Retrieval

18 toy documents are laid out on a 2D "semantic map" (a cartoon version of embedding space), with 4 preset queries. Must-do experiments: ① Query "can I get my money back"—keyword mode returns zero results (the corpus contains no exact words "money" or "back"), while semantic mode lands precisely on the refund cluster: this is the "lexical gap" that embeddings solve; ② Query "what perks do VIPs get"—"VIP" never appears in the corpus, yet semantic mode still drops right into the center of the membership cluster; ③ Query "how do I export my pixel artwork," then switch between the two modes—both hit: keyword retrieval is not dead, which is why production systems use hybrid retrieval.

retrieval.compare(semantic, keyword)

Cyan Q = query · green numbered dots = Top-k hits · gray dots = unmatched documents · note the recipe/weather distractor clusters in the center

ANN Indexes: How HNSW Finds Neighbors in a Billion Vectors in Milliseconds

Brute-force scanning a billion 1024-dimensional vectors to find the nearest neighbor: ~4TB of memory reads per query, infeasible. Production uses approximate nearest neighbors (ANN), and the mainstream choice is HNSW (Hierarchical Navigable Small World graph):

VIDEO 01
RAG Explained: Embedding, Sentence BERT, Vector Database (HNSW)
Umar Jamil 49:24
Viewing guide · a full-pipeline technical deep dive, matching §2–§4 of this chapter
  • 12:00 Sentence-BERT and contrastive learning—why you can't just use a vanilla BERT's CLS vector directly.
  • 28:00 Where the vector database sits in the RAG pipeline.
  • 35:00 A layer-by-layer illustration of HNSW—the full algorithmic version of "fly first, then bike," the on-screen version of this chapter's §4.

Chunking: The Invisible Ceiling on RAG Quality

How you cut up a document determines the semantic integrity of the retrieval unit—get chunking wrong, and everything downstream is wasted:

Production-Grade Retrieval: Hybrid Recall and Reranking

The engineered version of Lab experiment ③: semantic retrieval understands synonymous rewrites but is actually worse than BM25 keyword retrieval for exact strings (model numbers, error codes, person names). The production standard is two-path recall + fusion + reranking:

  1. Hybrid recall: BM25 takes the top-50 ∪ vectors take the top-50.
  2. RRF fusion: don't compare the two paths' heterogeneous scores—use only the ranks: $\text{RRF}(d) = \sum_r \frac{1}{60 + \text{rank}_r(d)}$, absurdly simple yet robust and effective.
  3. Cross-encoder reranking: concatenate the query with each candidate and run it through a small model to score it (the higher-accuracy-ceiling kind mentioned in §2), refining the fused top-20 down to the top-5. The two-stage funnel of dual-encoder recall (fast but coarse) + cross-encoder reranking (slow but precise) is exactly isomorphic to the recall/ranking stages of a recommender system.

RAG Evaluation and a Failure-Mode Checklist

RAG is a pipeline, so evaluation must attribute by segment (the RAG version of Chapter 8's evaluation-first methodology):

DimensionQuestionMetric
Retrieval qualityDid it find what it should have found?Recall@k / MRR (requires annotated query→relevant-chunk pairs)
FaithfulnessIs the answer based only on the retrieved content?LLM-as-judge, checking citations sentence by sentence (the RAGAS approach)
Answer relevanceDid it answer the actual question?LLM-as-judge / manual spot checks

A failure-mode checklist (diagnose by pipeline position, with the most common remedy attached):

Long Context vs. RAG, and Agentic RAG

"The context is already 1M tokens—can't I just stuff all the documents in?" Three reasons keep RAG alive: cost (Chapter 9 just did the math: you pay the prefill bill for the whole library's tokens on every request, and the KV cache eats VRAM; retrieval only pays for the top-5 chunks); accuracy (lost in the middle is worse at a million tokens—needle-in-a-haystack benchmarks look good, but multi-needle reasoning still drops the ball); permissions and freshness (the retrieval layer naturally supports per-user ACL filtering and second-level incremental updates, which stuffing the context cannot do). In practice it's a spectrum: for documents under 50K tokens, stuff them directly (it's even cheap with caching, Chapter 9); for large libraries, use RAG; and the two are often mixed.

The direction of evolution is Agentic RAG: retrieval is no longer a fixed "one query, one generation" pipeline, but rather making search a tool for the Agent—the model decides for itself whether to search, what to search, and how many rounds, rewriting the query and searching again if it's unhappy with the results, and reasoning across documents in multiple hops. This is precisely the topic of the next chapter: once an LLM is given tools and a loop, every pipeline becomes its own decision.

Hands-On Code: A 60-Line Minimal RAG

python · minimal_rag.py (sentence-transformers + numpy, no vector store needed)
import numpy as np
from sentence_transformers import SentenceTransformer, CrossEncoder

# ---- Offline: build the index (the document side of the two-tower, precomputed once) ----
docs = open('knowledge.md').read().split('\n## ')        # structure-aware chunking: split by level-2 headings
embedder = SentenceTransformer('BAAI/bge-m3')             # Chinese-English bilingual embedding
doc_vecs = embedder.encode(docs, normalize_embeddings=True)  # (n, 1024); after normalization, dot product = cosine

# ---- Online: retrieve + rerank + generate ----
def retrieve(query, k_recall=20, k_final=5):
    q = embedder.encode([query], normalize_embeddings=True)
    sims = doc_vecs @ q.T                                 # one matrix multiply = cosine similarity over the whole library
    cand = np.argsort(-sims[:, 0])[:k_recall]             # recall top-20 (brute-force scan at toy scale; swap in HNSW to scale up)
    reranker = CrossEncoder('BAAI/bge-reranker-v2-m3')    # cross-encoder fine-grained ranking
    scores = reranker.predict([(query, docs[i]) for i in cand])
    return [docs[cand[i]] for i in np.argsort(-scores)[:k_final]]

def answer(query):
    chunks = retrieve(query)
    context = '\n\n'.join(f'[Source {i+1}] {c}' for i, c in enumerate(chunks))
    prompt = (f"Answer based only on the materials below. For information not in the materials, "
              f"state clearly 'not mentioned in the materials', and cite the source numbers.\n\n{context}\n\nQuestion: {query}")
    return llm(prompt)   # any LLM API; note: putting context first lets you hit the prompt cache (Chapter 9)

print(answer("How long until a refund arrives?"))   # → Per [Source 2], 3-5 business days, refunded by the original route.
Apply Chapter 9's cost engineering to RAG: where can this prompt's structure still be optimized?
Put the fixed, unchanging instruction part at the very front (done correctly), but the context changes every time and sits ahead of the query—meaning the cache hit on every request stops at the end of the instructions. Two improvements: ① if the knowledge base is small and the query distribution is concentrated, popular chunk combinations will recur, so you can "concatenate common chunks in a fixed order" to raise prefix overlap; ② make the instructions + few-shot examples thick (it's all cached anyway) and make the variable part thin. A more aggressive plan: stuff the entire document library into the context to ride a long cache (suitable for libraries under 50K, the left end of Chapter 9's spectrum). The cost perspective changes how you design your prompt structure—this is wringing the two chapters' knowledge together.

Chapter Quiz