31

RAG: read before answering

How do you give a model your own knowledge without retraining it?

13 minExecutive path

RAG: read before answering

With voice. You can pause and jump to any step.

Lesson complete

Subtitles

A customer asks an electric bike shop’s chatbot: “How long is the battery warranty?” The answer is in the shop’s manual.

Voice

No voice: the lesson advances on its own with subtitles.

The voice for this lesson is being recorded: for now it advances on its own with subtitles.

0:00 / 0:00

What you will achieve

  • Explain what a RAG system does: prepare the library once (chunk and index) and, with every question, retrieve, build the prompt with sources, and answer with citations.
  • Predict which chunks a question retrieves and whether the right one reaches the prompt, given the chunk size and the value of k.
  • Recognize the three typical failures (not finding, contradictions, hidden instructions) and measure retrieval with your own questions.

Key ideas

  1. RAG does not retrain the model: with every question it looks up the chunks of your documents that are most similar and pastes them into the prompt, along with the question.
  2. It is prepared once (chunk, turn into vectors, index) and, with every question, you retrieve, build the prompt with numbered sources, and answer with citations.
  3. Chunk size and the number k of chunks pasted are decisions with a cost: you choose them by testing, not by hunch. A reranker can improve the order, but not always.
  4. Word search fails on paraphrases; embeddings compare meanings and rescue many of them, not all.
  5. Typical failures: not finding the right chunk, documents that contradict each other, and instructions hidden in the text. Quality depends mostly on retrieval: measure it with your own questions.

What many people believe

Common belief RAG “trains” the model on your documents: after reading them, the model knows them.

What is true The model learns nothing: its weights do not change. With every question the chunks are looked up and pasted into the prompt; if the search does not bring them, the model never sees them.

Check yourself

Four questions. No grade, no account: they just show whether the idea stuck.

ConceptWhat does a RAG system do before the model answers?
ConceptIf the chunks are too small, what usually happens?
CalculationOn ten test questions, the position of the right chunk was: 1, 1, 2, 1, 4, 1, did not appear, 3, 1, and 6. With k = 3, in how many does the right chunk reach the prompt?
ApplicationAn insurer wants an assistant that answers from the terms of its policies, which change every quarter, and that shows where each fact came from. What should it try first?

Under the hoodFor those who want the math and the code. You do not need it to follow the course.

TF-IDF and cosine similarity

This is how the scene’s word search is computed. First, very common words are removed (“the,” “of,” “and”…) and each word is cut down to its stem in a simple way (“batteries” and “battery” count the same). With that, the lesson’s manual has 228 distinct terms: each chunk is a vector of 228 numbers.

Given NN chunks, ct,dc_{t,d} is how many times term tt appears in chunk dd, and df(t)\mathrm{df}(t) is how many chunks it appears in. With the variants used in the lesson:

tf(t,d)=1+ln⁡ct,didf(t)=ln⁡N+1df(t)+1+1wt,d=tf(t,d)⋅idf(t)\mathrm{tf}(t,d)=1+\ln c_{t,d}\qquad \mathrm{idf}(t)=\ln\frac{N+1}{\mathrm{df}(t)+1}+1\qquad w_{t,d}=\mathrm{tf}(t,d)\cdot\mathrm{idf}(t)

With N=25N=25: “cycles” appears in 1 chunk (idf≈3.56\mathrm{idf}\approx 3.56) and “warranty” in 6 (idf≈2.31\mathrm{idf}\approx 2.31); that is why “cycles” weighs more. Each vector is normalized, d^=d/∥d∥\hat{\mathbf d}=\mathbf d/\lVert\mathbf d\rVert, and the question gets the same treatment with the index’s idf\mathrm{idf} values. Cosine similarity becomes a dot product:

cos⁡(q,d)=q^⋅d^∈[0,1]\cos(\mathbf q,\mathbf d)=\hat{\mathbf q}\cdot\hat{\mathbf d}\in[0,1]

Weighting by rarity goes back to Karen Spärck Jones (1972): a term that appears in few documents tells them apart better.

BM25, the other word-based score

BM25 (Robertson and Zaragoza, 2009) scores each chunk DD with a saturating term frequency and a length correction:

score(D,Q)=∑t∈Qidf(t) f(t,D) (k1+1)f(t,D)+k1 (1−b+b ∣D∣/avgdl)\mathrm{score}(D,Q)=\sum_{t\in Q}\mathrm{idf}(t)\,\frac{f(t,D)\,(k_1+1)}{f(t,D)+k_1\,(1-b+b\,|D|/\mathrm{avgdl})}

with 1.2<k1<21.2<k_1<2 and 0.5<b<0.80.5<b<0.8 as values that, according to its authors, work reasonably well in many cases. It is a hard baseline to beat with words alone, and many systems combine it with embeddings (hybrid search).

Embeddings and approximate search

With embeddings (lesson 14) the vector has a few hundred dense numbers and the words do not need to match: cosine similarity compares meanings. Comparing the question with all NN vectors costs O(N⋅D)O(N\cdot D); with millions of chunks, approximate indexes such as HNSW (Malkov and Yashunin) trade a little exactness for a lot of speed. In the paper that named RAG (Lewis et al., 2020), Wikipedia was cut into 100-word pieces, 21 million in all, indexed with FAISS and HNSW; during training, k=5k=5 or k=10k=10 were retrieved per question.

Reranking

A dense retriever (bi-encoder), such as DPR (Karpukhin et al., 2020), the one RAG uses, encodes the question and the chunk separately, so chunks are turned into vectors only once. A reranker (cross-encoder) reads the question and the chunk together in one model pass and returns a score: it is finer, but it costs one pass per candidate, so it is only applied to the top of the list. Nogueira and Cho (2019) used BERT to rerank the top 1,000 passages returned by BM25.

In the lesson, the embedding and reranker figures were measured offline (see build-dense.py) with two open models: intfloat/multilingual-e5-small (Wang et al., 2024) and cross-encoder/mmarco-mMiniLMv2-L12-H384-v1, trained on mMARCO (Bonifacio et al., 2021). The reranker is applied to the top 5 candidates.

How retrieval is measured

For each test question you record the right chunk and its position rir_i in the ranked list (ri=0r_i=0 if it does not appear). With QQ questions:

hit@k=1Q∑i1[1≤ri≤k]MRR=1Q∑i{1/riri>00ri=0\mathrm{hit@}k=\frac1Q\sum_i \mathbf 1[1\le r_i\le k]\qquad \mathrm{MRR}=\frac1Q\sum_i\begin{cases}1/r_i & r_i>0\\ 0 & r_i=0\end{cases}

On the scene’s 10 questions, with k=3k=3: word search, 7 of 10; embeddings, 9 of 10; embeddings plus reranker, 8 of 10 (in both languages). Ten questions are far too few for general conclusions: the point is the method.

This measures only the first half. The second, whether the answer relies on what was retrieved, is evaluated with test cases and with human review or a model as judge (lesson 34). Ragas (Es et al., 2023) splits it into three measures: faithfulness (what the answer says follows from the context), answer relevance, and context relevance.

Updating without retraining

The RAG paper tested swapping only the index: for 82 world leaders who changed between 2016 and 2018, the model was right 70% of the time with the 2016 Wikipedia index for 2016 leaders, and 68% with the 2018 index for 2018 leaders; with the mismatched index, only 12% and 4%. To update what it knows, you change documents, not weights.

In code

A complete TF-IDF search, with no libraries. It is the same recipe as the lesson, without stemming:

import math, re
from collections import Counter

STOP = {"the", "has", "an", "or", "you", "can", "your", "of", "on", "is", "at", "how", "long"}

def terms(text):
    return [w for w in re.findall(r"[a-z0-9]+", text.lower()) if w not in STOP]

chunks = [
    "the battery has an 18 month warranty or 500 charge cycles",
    "you can cancel your purchase within 30 days of delivery",
    "shipping is free on purchases over 1500",
    "charge the battery at room temperature",
]

N = len(chunks)
bags = [Counter(terms(c)) for c in chunks]
df = Counter(t for b in bags for t in b)
idf = {t: math.log((N + 1) / (df[t] + 1)) + 1 for t in df}

def vector(bag):
    v = {t: (1 + math.log(c)) * idf[t] for t, c in bag.items() if t in idf}
    norm = math.sqrt(sum(x * x for x in v.values())) or 1.0
    return {t: x / norm for t, x in v.items()}

index = [vector(b) for b in bags]

def search(question, k=2):
    q = vector(Counter(terms(question)))
    scores = [sum(w * d.get(t, 0.0) for t, w in q.items()) for d in index]
    return sorted(enumerate(scores), key=lambda p: -p[1])[:k]

for i, s in search("how long is the battery warranty"):
    print(f"{s:.2f}  {chunks[i]}")
# 0.51  the battery has an 18 month warranty or 500 charge cycles
# 0.27  charge the battery at room temperature

What comes next

Here the model only reads. In lesson 32 it can also use tools: search, calculate, write. Prompt injection comes back in lesson 35, and full system evaluation in lesson 34.

Decision lens

What this idea means if your job is to decide, not to code.

  • RAG gives answers grounded in your data, with sources that can be checked, without retraining: to update what it knows, you change documents, not the model. In the original paper, swapping the 2016 Wikipedia index for the 2018 one was enough to answer about the new world leaders. Ask how the index is updated and how often.
  • Quality is decided at retrieval: if the right chunk never reaches the prompt, no model can cite it. Ask for the retrieval hit rate measured on your own questions, not on a demo.
  • Govern the documents: version, date, owner, and permissions. Whatever the system can retrieve, it can say: old versions that contradict new ones, and data that not everyone should see.
  • All retrieved text deserves limited trust: a review, a web page, or an email can carry hidden instructions. Limit what the system can do with what it reads, and log which sources it used for each answer.

Terms in this lesson

Full transcript
CommentsThis lesson’s public conversation: questions, facts to check, ideas.

Comments live in GitHub Discussions, in the course’s public repository; writing one requires a GitHub account. They load from giscus.app when you open this section. The course reviewers read them, and if a change is accepted, it is applied in both languages. Open the thread on GitHub

Sources