RAG Basics
Retrieval-augmented generation: find the passages that match a question, put them in the prompt, and get answers grounded in your own documents.
On this page
Two students sit the same exam. One studied the textbook a year ago and answers from memory. The other is allowed to bring three pages of notes, but only three, and must pick them right before the exam starts. On questions about the topics in those three pages, the second student wins easily, and can point to exactly where each answer came from.
A model answering from training is the first student. A shop's help pages, a company handbook or last week's meeting notes were never in that memory. You could paste them all into every request, but a real document collection is far bigger than any context window. The fix is to be the second student: look up the few passages that matter, then answer from them.
Retrieve, augment, generate
That is all RAG (retrieval-augmented generation) means:
- Retrieve. Search your documents for the passages most related to the question.
- Augment. Put those passages into the prompt, with instructions to answer from them.
- Generate. The model writes the answer, ideally quoting or citing the passages.
The model is unchanged. RAG is a context engineering technique: an automatic way to select the right material for each question.
The search usually works on meaning rather than exact words, using embeddings. That matters because people rarely use the same words as your documents. Someone asks "when do I get my money back?" and the answer sits under the heading "Refund processing times".
Indexing is the slow part, so it happens ahead of time. Each document is cut into passages called chunks (how to cut them well is the whole of the next lesson), each chunk is turned into a vector, and the vectors are saved. At question time you only embed the question and compare.
Retrieval by hand
A bookshop's help centre has five chunks. To trace it by hand, pretend embeddings have just three numbers, meaning roughly "about money", "about shipping" and "about accounts". Real embeddings have hundreds or thousands of dimensions, but the arithmetic is the same.
The question "When will I get my money back?" embeds to [5, 1, 0]: mostly money, a little shipping. Its length is √26 ≈ 5.10. Score each chunk with cosine similarity, from the Embeddings lesson, and keep the top 2.
q = embed(question)for each chunk c: score[c] = cosine(q, vector[c])sort chunks by score, highest firstreturn the first k- chunk0refundc1returns2delivery3password4gift cardscore011234
- q
- [5, 1, 0]
- dot
- 21
1Refund times,
[4, 1, 0]: dot = 20 + 1 + 0 = 21, length √17 ≈ 4.12. Cosine = 21 / (5.10 × 4.12) ≈ 1.00. Nearly the same direction, with no shared words. - chunk0refund1returnsc2delivery3password4gift cardscore0110.92234
- q
- [5, 1, 0]
- dot
- 17
2Returns rules,
[3, 2, 0]: dot = 15 + 2 = 17, length √13 ≈ 3.61. Cosine ≈ 0.92. About money and about sending things back. - chunk0refund1returns2deliveryc3password4gift cardscore0110.9220.1934
- q
- [5, 1, 0]
- dot
- 5
3Delivery times,
[0, 5, 1]: dot = 0 + 5 + 0 = 5, length √26 ≈ 5.10. Cosine ≈ 0.19. It is mostly about shipping, so it points a different way from a money question, even though both ask how long something takes. - chunk0refund1returns2delivery3passwordc4gift cardscore0110.9220.19304
- q
- [5, 1, 0]
- dot
- 0
4Password reset,
[0, 0, 5]: dot = 0. Cosine = 0. Completely unrelated. - chunk0refund1returns2delivery3password4gift cardcscore0110.9220.193040.88
- q
- [5, 1, 0]
- dot
- 10
5Gift cards are non-refundable,
[2, 0, 1]: dot = 10, length √5 ≈ 2.24. Cosine ≈ 0.88. Close behind, because it is also about money. - chunk0refund1returns2gift card3delivery4passwordscore0110.9220.8830.1940
- k
- 2
- comparisons
- 5
6Sort and keep k = 2: refund times and returns rules go into the prompt. Five comparisons, one per chunk. The gift-card chunk came a close third, a reminder that k is a judgement call.
The code
Here is the retrieval step and the prompt it builds. The embed function is whatever embedding model you use; the vectors below are the toy ones from the trace.
const dot = (a, b) => a.reduce((sum, x, i) => sum + x * b[i], 0);
const cosine = (a, b) => dot(a, b) / Math.sqrt(dot(a, a) * dot(b, b));
function topK(queryVec, chunks, k) {
return chunks
.map((c) => ({ ...c, score: cosine(queryVec, c.vec) }))
.sort((a, b) => b.score - a.score)
.slice(0, k);
}
function buildPrompt(question, passages) {
const sources = passages.map((p, i) => `[${i + 1}] ${p.text}`).join("\n");
return `Answer using only the sources below. Cite them like [1].
If they do not contain the answer, say "I don't know."
${sources}
Question: ${question}`;
}
const chunks = [
{ text: "Refunds reach your card 5 to 7 days after we receive the return.", vec: [4, 1, 0] },
{ text: "You can return items within 30 days of delivery.", vec: [3, 2, 0] },
{ text: "Standard delivery takes 2 to 4 working days.", vec: [0, 5, 1] },
{ text: "Reset your password from the sign-in page.", vec: [0, 0, 5] },
{ text: "Gift cards cannot be refunded.", vec: [2, 0, 1] },
];
const best = topK([5, 1, 0], chunks, 2);
console.log(buildPrompt("When will I get my money back?", best));import math
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def cosine(a, b):
return dot(a, b) / math.sqrt(dot(a, a) * dot(b, b))
def top_k(query_vec, chunks, k):
scored = [{**c, "score": cosine(query_vec, c["vec"])} for c in chunks]
scored.sort(key=lambda c: c["score"], reverse=True)
return scored[:k]
def build_prompt(question, passages):
sources = "\n".join(f"[{i + 1}] {p['text']}" for i, p in enumerate(passages))
return (
"Answer using only the sources below. Cite them like [1].\n"
'If they do not contain the answer, say "I don\'t know."\n\n'
f"{sources}\n\nQuestion: {question}"
)
chunks = [
{"text": "Refunds reach your card 5 to 7 days after we receive the return.", "vec": [4, 1, 0]},
{"text": "You can return items within 30 days of delivery.", "vec": [3, 2, 0]},
{"text": "Standard delivery takes 2 to 4 working days.", "vec": [0, 5, 1]},
{"text": "Reset your password from the sign-in page.", "vec": [0, 0, 5]},
{"text": "Gift cards cannot be refunded.", "vec": [2, 0, 1]},
]
best = top_k([5, 1, 0], chunks, 2)
print(build_prompt("When will I get my money back?", best))The printed prompt, sent to a model, looks like this:
The instruction "say I don't know" is not decoration. Without it, when retrieval finds nothing useful, the model falls back on its training and produces a confident, generic answer that looks just as grounded as a real one.
| Approach | Time | Space | Why |
|---|---|---|---|
| Compare with every chunk | O(n·d + n log n) | O(n) | n chunks with d numbers each, then a sort. Fine for thousands of chunks. |
| Approximate index | much less than O(n·d) | O(n·d) | Vector databases use indexes that check only promising regions, trading a little accuracy for big speed-ups at millions of chunks. |
When RAG is the right tool
- Use it when the material is too big to send whole, changes often, or is private. Updating the documents updates the answers, with no retraining.
- Skip it when the material is small. If the whole handbook fits comfortably, sending all of it avoids retrieval mistakes entirely.
- Do not use fine-tuning instead for facts that change. Fine-tuning is good for teaching a style or format; it is a poor way to store facts you will need to correct next month.
Common mistakes
Try it yourself
A staff assistant was asked "How many days of parental leave do we get?" It answered "Most companies offer 12 weeks." The two retrieved passages were: [1] "Annual leave: 25 days per year, plus public holidays." [2] "Sick leave must be reported to your manager before 10am." Where did the system fail, and what would you change? There are two separate problems.
Show a sample answer
Problem 1: retrieval. Neither passage is about parental leave. Either the parental leave policy is not in the index, or it was chunked or embedded badly so a generic "leave" match beat it. Check whether the policy exists in the store, then search for it directly to see where it ranks.
Problem 2: generation. Given passages that did not answer the question, the model should have said so. Instead it answered from general knowledge. Add (or strengthen) an instruction such as: "If the sources do not answer the question, reply: I couldn't find this in the staff handbook." Asking for citations also helps, because an answer with no source to cite stands out.
Fixing only the prompt would make it honest but still unhelpful. Fixing only retrieval would leave it free to invent answers next time something is missing. Fix both.
Check yourself
Why does RAG usually search with embeddings rather than exact keywords?
Your company's policies change every month. Which approach keeps a staff assistant's answers current most easily?
In the trace, the gift-card chunk scored 0.88 but was left out. What would happen with k = 3?
What to practise next
Retrieval quality starts with how documents are cut up. Chunking and Retrieval covers chunk size, overlap, and the tricks that fix the most common misses.