AI EngineeringZero to ProductionHome·About·What’s new·Contact
Retrieval (RAG) · Part 3.1 · Beginner

Foundations

Retrieval-Augmented Generation (RAG) is how you make an LLM answer from your documents instead of only its frozen training data. This beginner chapter builds the mental model from the ground up: why RAG exists, the retrieve → augment → generate loop, what an embedding really is, how chunking decides what the model can find, and how to assemble a minimal but correct pipeline in plain Python. You will finish able to explain and hand-build the simplest RAG that actually works.

⏱️ ~75 min🧲 Retrieval🎯 Beginner🐍 Runnable Python
🌱 What runs here, and what needs a keyThe retrieval math in this chapter — cosine similarity, chunking, ranking — is plain Python and runs right here in your browser (no key, no install). Only the final generation step calls a hosted LLM; that code is complete and correct, but to execute it you supply your own API key. We use a tiny fake embedder so the whole loop runs offline for learning — real embeddings come from a model, and we say exactly where to swap it in. Any numbers shown are illustrative.

Learning objectives

  • Explain why RAG exists — the three problems it solves that a bare LLM cannot.
  • Trace the retrieve → augment → generate loop and name what each stage owns.
  • Describe what an embedding is and why cosine similarity measures meaning.
  • Chunk a document sensibly and explain how chunk size changes what can be found.
  • Hand-build a minimal RAG pipeline in Python and reason about where it breaks.
  • Tell the difference between RAG, fine-tuning, and a bigger context window — and when to reach for each.

1 · Why RAG exists

Picture asking a brilliant new hire about your company on their first morning. They're genuinely smart — they'll answer anything confidently — but they've never seen your wiki, your last incident, or the contract signed yesterday. So when you ask "how do we restart the checkout service?", they don't say "I don't know." They guess, fluently, and their guess sounds exactly as authoritative as a correct answer would. That is a bare LLM. It's not lying — it simply has no way to tell the difference between what it knows and what it's confabulating, and no access to anything that happened after its training was frozen.

Here's the reframe that makes RAG click, and it's worth sitting with: the model's real skill isn't knowing facts — it's reading and reasoning over text you give it. Those are two very different jobs, and we've been leaning on the wrong one. Asking a model to recall your internal runbook from memory is like asking that new hire to recite a document they've never opened. But hand them the document and ask them to read it and answer — that they're excellent at. RAG is simply the plumbing that fetches the right document and puts it on their desk before they answer.

Why does this deserve to be the first idea in the whole track? Because it inverts where you put your effort. The tempting instinct is to make the model "know more" — bigger model, more training. RAG says: leave the model's knowledge alone and get better at finding and delivering the relevant material at question time. That single shift is what lets a small, cheap model answer accurately about a million private documents it never saw in training.

The mistake almost everyone makes first is trusting a confident answer because it reads well. Fluency is not accuracy. A model with no grounding produces beautifully-worded wrong answers, and there's no wobble in its tone to warn you — the sentence about a runbook step that doesn't exist looks identical to the sentence about one that does. Grounding the answer in retrieved text, with citations, is what turns "trust me" into "here's the passage I used, check it yourself." Keep that distinction — fluent versus correct — in mind as you read the three concrete failures below; every row is a different face of the same root problem.

A large language model knows only what it saw during training. That creates three concrete problems the moment you point it at real work:

bare LLM — no grounding question LLM guesses from weights stale · no private data · confident fluent — but maybe wrong RAG — grounded in your docs question docs LLM reads retrieved passage current · private · quotable grounded + cited answer Same question, two very different answers. The bare LLM (left, red) guesses from frozen weights; RAG (right, green) fetches your current document first, so the model answers from real text it can cite.
🗺️ How to read this diagram

Read the two columns top-to-bottom and compare them: the divider down the middle separates the naive path (left) from the RAG path (right).

  • Both start from the same question box at the top. On the left, nothing is fetched — the dashed grey arrows drop straight into the model.
  • The left red boxes show the failure modes from the table below: the model guesses from frozen weights (stale, no private data) and produces a fluent but maybe wrong answer.
  • On the right, the blue arrow first pulls in your docs (the amber box) before the model reads. The green boxes are the payoff: the model reads the retrieved passage and returns a grounded + cited answer.

In short: The only structural difference between a hallucinating model and a trustworthy one is that extra arrow to your documents — that arrow is RAG.

ProblemWhat it looks likeHow RAG fixes it
Stale knowledgeThe model has no idea about last week's incident, your internal runbook, or a document written after its training cutoff.You retrieve the current document at question time and put it in front of the model.
No private dataIt was never trained on your company's wiki, tickets, or contracts.You index those sources yourself and feed the relevant pieces in.
Confident hallucinationAsked something it doesn't know, it invents a plausible-sounding answer.Grounding the answer in retrieved text (with citations) gives it real facts to quote instead of guessing.

The insight behind RAG is simple: don't ask the model to remember — give it the material and ask it to read. Instead of baking knowledge into the weights, you keep knowledge in a searchable store and fetch just the relevant slice for each question.

The one-sentence definitionRAG = search your documents for the pieces relevant to a question, paste those pieces into the prompt, and ask the model to answer using them. Everything else in this track is making each of those three verbs better.

2 · The retrieve → augment → generate loop

Every RAG system you will ever build or debug is the same three-step recipe — and once it's burned into your head, a broken RAG stops being a mystery and becomes a question of "which of the three steps failed?" That's the whole payoff of this section: not just what the loop is, but a diagnostic map you'll reach for every time an answer comes back wrong.

Think of it like a research assistant answering a question for you. First they go to the shelf and pull the handful of books that look relevant (retrieve). Then they lay the open books on the desk next to your question, with a sticky note saying "answer using only these" (augment). Only then do they read those pages and write you an answer, footnoting which book each claim came from (generate). Notice the strict order: the assistant can only write from books they actually pulled. If they grabbed the wrong shelf, no amount of careful reading saves the answer — they'll write a confident report from irrelevant material.

That ordering is why the loop matters so much for reasoning about failures. Each stage can only work with what the previous stage handed it, so errors don't cancel out — they cascade. A missed chunk in retrieve is invisible to augment and generate; they never learn the fact existed. This is the opposite of how beginners debug: they stare at the prompt wording or blame the model, tuning the last two stages, when the damage was almost always done in the first one. The single most useful habit in all of RAG is to print the retrieved chunks and look at them before touching anything else.

One more distinction that trips people up: this loop is orthogonal to how fancy your system is. A 30-line script and a platform serving millions run the exact same three verbs — the platform just makes each verb more sophisticated (better search in retrieve, smarter prompt assembly in augment, a stronger model in generate). Learn the skeleton now and every advanced technique later slots cleanly into one of these three boxes. Trace it once in the diagram below, then read the table — the "gets wrong when…" column is the debugging checklist you'll use for years.

Question from user Retrieve find relevant chunks Augment build the prompt Generate LLM reads + writes Answer grounded + cited
🗺️ How to read this diagram

Read it strictly left-to-right along the arrows: each box hands its output to the next, and a box can only work with what the box before it passed on.

  • Start at the blue Question box ("from user") on the far left — the user's raw query is the only input the whole chain gets.
  • Follow the arrows through the three working stages: the amber Retrieve box ("find relevant chunks"), then the blue Augment box ("build the prompt"), then the green Generate box ("LLM reads + writes") — each arrow is a strict one-way handoff, so a chunk missed at Retrieve is invisible to everything downstream.
  • The payoff is the final blue Answer box ("grounded + cited") — it earns those two words only because the earlier boxes fetched real text and put it in front of the model.

In short: Every RAG system is this same left-to-right chain — retrieve, augment, generate — so a wrong answer is always "which of these three boxes failed?"

Every RAG system, from a 30-line script to a platform serving millions, is this same loop:

StageOwnsGets wrong when…
RetrieveTurn the question into a search and pull the top-k most relevant chunks from the store.the right chunk isn't found (bad chunking, weak search) — nothing downstream can recover it.
AugmentAssemble a prompt that puts those chunks in front of the model with clear instructions.too much/irrelevant text is stuffed in, burying the useful part or blowing the context budget.
GenerateThe LLM reads the chunks and writes an answer, ideally citing which chunk it used.the model ignores the context and answers from memory, or invents a citation.
Retrieval is the hard partBeginners obsess over the prompt and the model. But if retrieve hands up the wrong chunks, a perfect prompt and the best model will still be wrong — they can only work with what they're given. Most of this track is therefore about retrieval quality. Remember the phrase: garbage in, garbage out.

3 · Embeddings — turning meaning into numbers

Here's the problem the whole retrieve step has to solve. A user types "my car won't start" and the answer they need is filed under "vehicle fails to turn over." Keyword search is useless here — those two phrases share zero words. Yet a human instantly sees they mean the same thing. So how do you get a computer to match on meaning rather than on the literal characters? That question is what embeddings exist to answer, and it's the hinge the entire chapter turns on.

The trick is to give every piece of text an address in "meaning-space." Imagine a giant map where texts about similar things sit near each other: all the car-trouble sentences cluster in one neighborhood, all the baking recipes cluster in another, far away. An embedding is just the coordinates of a piece of text on that map — a list of numbers (a vector) that pins it to a spot. "Car won't start" and "vehicle fails to turn over" get placed as next-door neighbors even though they spell nothing alike, because a model trained on mountains of text learned they're used the same way. Real maps have 2 coordinates; these have hundreds or thousands, but the intuition survives: close on the map = close in meaning.

Once meaning is coordinates, "how similar are these two texts?" becomes "how close are these two points?" — a geometry question a computer answers instantly. The specific measure we use is the angle between the two vectors rather than the straight-line distance, and that choice matters: it means we care about the direction a text points in meaning-space, not how long its vector happens to be. A one-word note and a full paragraph on the same topic point the same way, so they count as similar even though one is "bigger." That's exactly what you want.

The mistake to inoculate yourself against right now: the tiny hand-written vectors in the code below are a teaching prop, not how it really works. Nobody types embeddings by hand, and the fake bag-of-words embedder you'll meet in section 5 can't actually match "car" to "vehicle" — it only knows word overlap. Real embeddings come from a trained model that has read enough text to place synonyms near each other. We use fakes so the whole pipeline runs offline in your browser; just hold the thought that the numbers are illustrative and the real magic lives in the model that produces them. With that caveat set, here's the core idea in runnable form.

To “search by meaning,” we need a way to measure how similar two pieces of text are. An embedding is a list of numbers (a vector) that represents the meaning of a piece of text. Texts with similar meaning get vectors that point in similar directions — even if they share no words. “car won't start” and “vehicle fails to turn over” land near each other; “banana bread recipe” lands far away.

We compare two vectors with cosine similarity: the cosine of the angle between them, from -1 (opposite) through 0 (unrelated) to 1 (identical direction). Here is the whole idea in runnable Python — no library needed:

meaning-space — direction, not distance, carries meaning origin small angle "car won't start" "vehicle fails to crank" "banana bread recipe" wide angle → unrelated Similar meaning = similar direction. The two car sentences (green) leave the origin at nearly the same angle, so cosine ≈ 1; the recipe (red) points off on its own, so cosine ≈ 0 — even though none of them share words.
🗺️ How to read this diagram

Start at the origin dot bottom-left and follow each arrow outward — each arrow is one text's embedding vector, and what matters is the direction it points, not how far it reaches.

  • The two green arrows ("car won't start" and "vehicle fails to crank") leave the origin at almost the same angle — they land in the same neighborhood of meaning-space.
  • The amber wedge between them is the angle cosine similarity measures. A small angle means cosine near 1 (very similar); the two car sentences sit here despite sharing zero words.
  • The red arrow ("banana bread recipe") shoots off in a wholly different direction — a wide angle, so cosine near 0. Direction, not length, is why a one-word note and a full paragraph on the same topic still count as similar.

In short: Embedding turns each text into an arrow in meaning-space; cosine reads the angle between arrows, so "close in meaning" becomes a number a computer can rank on.

python · cosine similarity (runnable — click ▶ Open in terminal)
cosine.py# Cosine similarity from scratch — the heart of semantic search.
import math

def dot(a, b):
    return sum(x * y for x, y in zip(a, b))

def norm(a):
    return math.sqrt(sum(x * x for x in a))

def cosine(a, b):
    return dot(a, b) / (norm(a) * norm(b) + 1e-9)   # +tiny to avoid /0

# Pretend these are 4-number embeddings of three sentences.
car_wont_start   = [0.9, 0.1, 0.0, 0.2]
vehicle_no_crank = [0.8, 0.2, 0.1, 0.1]   # same meaning, different words
banana_bread     = [0.0, 0.1, 0.9, 0.8]   # unrelated topic

print('car vs vehicle :', round(cosine(car_wont_start, vehicle_no_crank), 3))
print('car vs banana  :', round(cosine(car_wont_start, banana_bread), 3))
car vs vehicle : 0.965
car vs banana  : 0.229

The two car sentences score high (~0.97) despite sharing no words; the unrelated one scores low. That is semantic search in one function. In a real system you don't hand-write these vectors — an embedding model produces them. The rule to remember: embed the documents once, embed each question at query time, and rank documents by cosine similarity to the question.

Where real embeddings come fromIn production you call an embedding model (e.g. via a hosted API or a local sentence-transformer) that turns text into a vector of typically 384–3072 numbers. The code shape is identical to above — only the source of the numbers changes. We use a deterministic fake embedder below so the full pipeline runs offline; swap in a real embed() and everything else stays the same.

4 · Chunking — deciding what can be found

Chunking is the least glamorous decision in RAG and quietly the one that sinks the most projects. Teams pour weeks into picking an embedding model and tuning prompts, then wonder why the right answer never surfaces — when the real culprit was how they sliced the documents in the very first step. If a fact isn't sitting inside a chunk that scores well for the question, it does not exist as far as your system is concerned. No model, no clever prompt, no re-ranker can retrieve a fact that got chopped in half. That's why this earns a full section right after embeddings.

Think of it like scanning a textbook to answer an exam question. If you photocopy one sentence per page, each page is razor-sharp about a single idea — but half of them read "it must be restarted immediately," and you've lost what "it" refers to; the subject was on the previous sentence. If instead you photocopy a whole chapter per page, every page holds the full story, but it also covers ten unrelated topics, so no single page is strongly about your one question — it's a blurry average of everything. The sweet spot is a photocopy of about one coherent paragraph: enough context to stand on its own, focused enough to match one idea sharply. Chunk size is that photocopy setting, and it trades precision against context.

The subtle killer is the boundary fact — a fact that straddles where you happened to cut. "To recover, drain the queue" ends one chunk and "and scale the consumer group" begins the next, so neither chunk holds the complete instruction and both match the question weakly. The fix is overlap: let each chunk repeat the last sentence or two of the one before it, like overlapping roof shingles, so any fact near a seam lands whole in at least one chunk. It costs a little duplication and buys you a lot of recall — a trade almost always worth making.

The mistake to avoid: hunting for a magic universal chunk size. There isn't one — dense legal contracts, chatty support threads, and code all want different settings, and the only honest way to find yours is to try a size, look at what comes back, and adjust. Treat the numbers below (~100–300 words, small overlap) as a sturdy starting point, not a law. With that framing, here's how chunk size shapes what your system can and can't find.

You don't embed a whole 40-page document as one vector — that would blur every topic together. You split it into chunks (say, a few sentences or a paragraph each), embed each chunk, and retrieve chunks. Chunking is quietly one of the most important decisions in RAG, because a fact can only be retrieved if it sits inside a chunk that scores well for the question.

too small "it must restart" sharp but "it" = ? loses context just right (~a paragraph) one coherent idea precise AND standalone too large 10 topics averaged blurry match the dashed box = overlap: the seam sentence repeats into the next chunk Chunk size trades precision against context. Too small (red) is sharp but orphans references like "it"; too large (amber) averages ten topics into a blurry match; a paragraph-sized chunk (green) is both focused and self-standing — and overlap repeats the boundary sentence so a fact at a seam still lands whole.
🗺️ How to read this diagram

Scan the three columns left-to-right — each shows the same document sliced at a different chunk size, from tiny (left) to huge (right).

  • The left red stack is chunks that are too small: each is razor-sharp about one fragment, but a chunk saying "it must restart" has lost what "it" refers to — the context sat in the previous chunk.
  • The right amber block is one oversized chunk: it keeps all the context but averages ten topics together, so it only ever matches any single question weakly.
  • The middle green chunks are the sweet spot — about a paragraph, one coherent idea that still stands on its own. The dashed blue box is overlap: the last sentence of one chunk is repeated into the next, so a fact straddling the seam lands whole in at least one chunk.

In short: Aim for paragraph-sized chunks with a little overlap — small enough to match one idea sharply, large enough to carry its own context.

Chunk sizeUpsideDownside
Too small (a sentence)Very precise; the vector is about one idea.Loses context — a chunk saying “it must be restarted” is useless if “it” (the service) is in the previous sentence.
Too large (a whole page)Keeps context together.The vector averages many topics, so it matches weakly for any single question; you also waste prompt space.
Just right (a paragraph, ~100–300 words, small overlap)One coherent idea with enough surrounding context to stand alone.Requires tuning per corpus — there is no universal number.

A common, sturdy default: split on paragraphs, then pack ~200 words per chunk with a small overlap (repeat the last sentence or two into the next chunk) so a fact spanning a boundary still lands whole in at least one chunk. Here's a minimal chunker:

python · chunk
chunk.pydef chunk_words(text, size=60, overlap=15):
    """Split text into ~size-word chunks that overlap by `overlap` words.
    Overlap keeps a fact whole even if it straddles a boundary."""
    words = text.split()
    chunks, i = [], 0
    step = size - overlap
    while i < len(words):
        chunks.append(' '.join(words[i:i + size]))
        i += step
    return chunks

doc = ('The checkout service reads orders from the queue. '
       'If the queue backs up, latency climbs and the pod restarts. '
       'To recover, drain the queue and scale the consumer group. '
       'Never restart the database to fix a queue problem.') * 1

for i, c in enumerate(chunk_words(doc, size=12, overlap=3)):
    print(f'chunk {i}: {c}')
chunk 0: The checkout service reads orders from the queue. If the queue backs up,
chunk 1: queue backs up, latency climbs and the pod restarts. To recover, drain
chunk 2: recover, drain the queue and scale the consumer group. Never restart the
chunk 3: restart the database to fix a queue problem.

Notice how the overlap repeats a few words between chunks — so “drain the queue” appears in two chunks and can be found from either side of the boundary.

5 · Build a minimal RAG pipeline

This is the section where RAG stops being a diagram and becomes something you can run. Everything so far — the three-verb loop, embeddings, chunking — has been building toward one small, honest program that ties it all together. And it's genuinely small: a working RAG is closer to forty lines than four hundred. The reason production systems balloon isn't the core idea; it's the scaling, the monitoring, and the edge cases layered on top. Strip those away and what's left is exactly what you're about to read.

The one deliberate cheat is the fake embedder, and it's worth understanding why we cheat here. A real embedding model needs a download or an API key, which would stop this from running in your browser — so we substitute a deterministic bag-of-words stand-in that turns text into a vector by counting words. It has a real limitation you should feel rather than just be told: it matches only on shared words, so it will happily fail the "car" vs "vehicle" test from section 3. That's not a bug to fix here — it's the whole point. Seeing the fake embedder's blind spot with your own eyes is what makes you appreciate what a trained model buys you, and it's Exercise 5 in the ladder below.

As you read the code, watch for the shape rather than the specific tricks, because the shape is what survives contact with production. You index once (embed every document and keep the vectors), then per question you embed the query, score it against the stored vectors, take the top-k, and paste those into a grounded prompt. That skeleton — index → embed query → score → top-k → prompt → generate — is identical whether your store holds three sentences or three billion. When you later swap in a real embedder and a real vector database, you are replacing the insides of these functions, not their arrangement. Read it top to bottom; each numbered comment marks one stage of the loop you already know.

Now assemble the loop end to end. We use a fake but deterministic embedder (a bag-of-words hash) so this runs offline and you can see every moving part; then we show exactly where a real embedder and a real LLM plug in.

done ONCE (index) index docs embed each · keep vectors stored vectors reused every query done PER question embed query the question score cosine vs index top-k best chunks prompt grounded generate LLM the stored vectors feed the score step Index once, then repeat the right-hand chain per question. The amber "index" step runs a single time; every query embeds itself, scores against those stored vectors, takes the top-k, builds a grounded prompt, and generates. Swapping in a real embedder or vector DB changes the boxes' insides, never their order.
🗺️ How to read this diagram

Read the top-left box first, then follow the arrows left-to-right across the top row — the split label separates the one-time setup from the per-question loop.

  • The amber "index docs" box on the left happens once: every document is embedded and its vector stored. The dashed line down to the row below is a reminder that those stored vectors are reused on every query.
  • The right-hand chain runs per question: embed the query → score it by cosine against the index → keep the top-k best chunks → build a grounded prompt → generate. The diagonal arrow shows the stored vectors feeding the score step.
  • The green "generate" box is the only step that calls an LLM; everything before it is plain arithmetic. This exact order — index → embed → score → top-k → prompt → generate — is what survives when you later swap in a real embedder and vector database.

In short: Do the expensive indexing once, then run the cheap five-step chain for each question — that shape is identical from three sentences to three billion.

python · mini rag
mini_rag.pyimport math, re
from collections import Counter

# ---- 1. a FAKE embedder (deterministic bag-of-words). Swap for a real model later. ----
VOCAB = {}
def embed(text):
    counts = Counter(re.findall(r'[a-z]+', text.lower()))
    for w in counts:
        VOCAB.setdefault(w, len(VOCAB))
    vec = [0.0] * len(VOCAB)
    for w, c in counts.items():
        vec[VOCAB[w]] = float(c)
    return vec

def cosine(a, b):
    n = max(len(a), len(b)); a = a + [0.0]*(n-len(a)); b = b + [0.0]*(n-len(b))
    dot = sum(x*y for x, y in zip(a, b))
    na = math.sqrt(sum(x*x for x in a)); nb = math.sqrt(sum(x*x for x in b))
    return dot / (na*nb + 1e-9)

# ---- 2. index the documents (embed once, keep the vectors) ----
docs = [
    'To restart the checkout service, drain the queue then scale the consumer group.',
    'The billing service stores invoices in Postgres and retries failed charges.',
    'Never restart the database to fix a queue backlog; it makes recovery slower.',
]
index = [(d, embed(d)) for d in docs]

# ---- 3. retrieve: top-k chunks by cosine similarity to the question ----
def retrieve(question, k=2):
    q = embed(question)
    scored = sorted(index, key=lambda di: cosine(q, di[1]), reverse=True)
    return [d for d, _ in scored[:k]]

# ---- 4. augment: build a grounded prompt ----
def build_prompt(question, chunks):
    context = '\n'.join(f'[{i+1}] {c}' for i, c in enumerate(chunks))
    return (f'Answer ONLY from the context. Cite sources like [1].\n\n'
            f'Context:\n{context}\n\nQuestion: {question}\nAnswer:')

q = 'how do I restart checkout?'
chunks = retrieve(q)
print('RETRIEVED:')
for c in chunks: print('  -', c)
print('\nPROMPT SENT TO THE LLM:\n' + build_prompt(q, chunks))
RETRIEVED:
  - To restart the checkout service, drain the queue then scale the consumer group.
  - Never restart the database to fix a queue backlog; it makes recovery slower.

PROMPT SENT TO THE LLM:
Answer ONLY from the context. Cite sources like [1].

Context:
[1] To restart the checkout service, drain the queue then scale the consumer group.
[2] Never restart the database to fix a queue backlog; it makes recovery slower.

Question: how do I restart checkout?
Answer:

That is a complete RAG retrieve+augment. The right two chunks came back and the prompt grounds the model in them. The last step — generate — sends that prompt to an LLM:

python · generate

The generation step — in Claude or OpenAI

The retrieve + augment code above is vendor-neutral — only this final generate call changes. In Claude you call client.messages.create(messages=[…]) and read msg.content[0].text; in OpenAI it's client.responses.create(input=…) and resp.output_text. Toggle the tabs below.

generate.py# The generation step. Complete and correct; needs YOUR API key to actually run.
# pip install anthropic ; export ANTHROPIC_API_KEY=sk-...
from anthropic import Anthropic          # runs in your own environment, not the sandbox

client = Anthropic()
def generate(prompt):
    msg = client.messages.create(
        model='claude-opus-4-8',           # any current model id
        max_tokens=300,
        messages=[{'role': 'user', 'content': prompt}],
    )
    return msg.content[0].text

# answer = generate(build_prompt(q, retrieve(q)))
# print(answer)  ->  'Drain the queue, then scale the consumer group. [1]'
You just built RAGRetrieve (cosine top-k) → augment (grounded prompt) → generate (LLM). Everything in the rest of this track makes one of these three verbs better: better retrieval (3.2), better ranking (3.3), scale (3.4), and a real production system (3.5). The skeleton never changes.

6 · RAG vs fine-tuning vs a bigger context window

This is the question that separates people who use these tools from people who just heard of them. Pick the wrong approach and you'll spend weeks and a fortune solving a problem the right tool handles in an afternoon — retraining a model every time a document changes, or paying to paste a 300-page manual into every single API call. Getting this choice right is one of the highest-leverage judgments in the whole field, which is why we close the chapter on it.

The cleanest way to hold the three apart is an analogy: imagine getting an employee ready for a task. RAG is handing them a reference binder to consult while they work — the binder can be updated any morning, and they cite the page they used. Fine-tuning is sending them on a training course that reshapes how they habitually work — it changes their style and skills, but you can't send them back to school just to teach them one new fact. A bigger context window is stapling the whole document to the task sheet every single time — fine for a one-page memo, absurd for an encyclopedia. Notice these aren't rivals so much as answers to different questions: what should they know, how should they behave, and how much can they hold at once.

That framing dissolves the beginner confusion neatly. The core rule is RAG for knowledge, fine-tuning for behavior. Facts that change, are private, or are large belong in a retrieval store you can update without touching the model — that's RAG. A consistent voice, an output format, a specialized skill the model should apply every time — that's fine-tuning. And a single small document you always need in full? Just paste it in; don't build a pipeline for a problem you can solve with copy-paste.

The most common mistake here is treating this as a fork in the road. It isn't — the three compose, and mature systems routinely use two or all three together: fine-tune a model to always answer in your JSON schema (behavior), feed it retrieved facts from RAG (knowledge), and paste in a short static policy header every call (context). The skill isn't picking the "winner"; it's matching each kind of requirement to the tool built for it. The table lays out where each shines and, just as importantly, where each quietly falls down.

Newcomers often ask “why not just fine-tune the model, or paste the whole document in?” Each has a place:

RAG reference binder consult + cite; update daily for KNOWLEDGE changing · private · large Fine-tuning training course reshapes habits + style for BEHAVIOR voice · format · skill Bigger context stapled document whole doc, every call for ONE small doc static · always needed not rivals — they compose: know (RAG) + behave (fine-tune) + hold (context) Three different questions, three different tools. RAG changes what the model can know, fine-tuning changes how it behaves, a bigger context changes how much it can hold at once — and mature systems use all three together.
🗺️ How to read this diagram

Read the three panels left-to-right as three ways to get an employee ready for a task — each answers a different question, shown in the coloured line under it.

  • The green "reference binder" is RAG: the model consults and cites material you can update any morning. It answers what should it know? — the place for facts that change, are private, or are large.
  • The blue "training course" is fine-tuning: it reshapes the model's habits and style. It answers how should it behave? — voice, output format, a specialized skill applied every time.
  • The amber "stapled document" is a bigger context window: you attach the whole document on every call. It answers how much can it hold at once? — fine for one small static doc, absurd for a corpus.

In short: Match each kind of requirement to its tool — know / behave / hold — and remember they compose rather than compete.

ApproachBest forWeakness
RAGKnowledge that changes, is large, or is private; answers that must cite a source.Adds a retrieval step to build and tune; quality is capped by retrieval quality.
Fine-tuningTeaching a style, format, or skill (e.g. always answer as JSON in our schema).Poor for facts — retraining to add one document is slow and costly; can still hallucinate.
Bigger context (paste it all)One small, static document you can include every time.Doesn't scale to a big corpus; costs tokens on every call; the model can lose facts in the middle of very long context.
The practical ruleUse RAG for what the model should know (facts, documents), and fine-tuning for how it should behave (style, format). They compose — many production systems do both. There's a whole chapter on this trade-off in Specialized Topics (T4).
✓ Knowledge check

If your RAG answer is wrong, which stage should you suspect first, and why?

Show answer
Retrieve. If the right chunk never came back, the augment and generate stages had no chance — they can only use what retrieval provided. Always inspect the retrieved chunks before blaming the prompt or the model.
✓ Knowledge check

Why do we embed documents once but embed the question every time?

Show answer
Document vectors don't change, so you compute them once and store them (the index). Each new question is different text, so it must be embedded at query time and then compared against the stored document vectors.

🪜 Practice — from a working toy to real intuition beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Change the queryBeginner

Change the question in mini_rag.py to “where are invoices stored?” and confirm the billing chunk is now retrieved first.

Show solution Set q = 'where are invoices stored?' and re-run. The billing/Postgres document should now rank first because its words overlap the question.
Exercise 2 · Add a documentIntermediate

Add a fourth document about a topic of your choice to docs, re-run, and check it's only retrieved for relevant questions.

Show solution Append a new string to docs; because the index is rebuilt from docs, it's embedded automatically. Ask an unrelated question and confirm it does not surface.
Exercise 3 · Tune kIntermediate

Raise k from 2 to 3 and observe how the prompt grows. Note the cost/benefit of retrieving more chunks.

Show solution More chunks = more chance the answer is present, but more tokens and more noise the model must ignore. Beyond a point, extra chunks hurt (covered in 3.3).
Exercise 4 · Chunk-level retrievalAdvanced

Use chunk_words() from section 4 to split a longer paragraph, embed each chunk, and retrieve at the chunk level instead of the document level.

Show solution Replace docs with the list returned by chunk_words(long_text). Retrieval now returns fine-grained chunks, which is what real systems index.
Exercise 5 · Show the bag-of-words failureExpert

The bag-of-words embedder fails when question and answer share no words (“car won't start” vs “vehicle won't crank”). Demonstrate it, then explain why a real embedding model wouldn't have this problem.

Show solution Index “vehicle won't crank” and query “car won't start”: cosine is ~0 because no words overlap. A real embedding model maps both to nearby vectors because it encodes meaning, not word identity.
Exercise 6 · Refuse when unsureProfessional

Add a similarity threshold: if the top chunk scores below it, return “I don't know” instead of a chunk. Explain why refusing is sometimes the correct RAG behavior.

Show solution Guard retrieve(): if the best cosine < threshold, return [] and have the prompt instruct the model to say it lacks the information. Grounded refusal beats a confident wrong answer.

Context: You're handing this beginner pipeline to a teammate who will make it real.

Your task: Write a short note (5–8 sentences) explaining what to change to move from the fake embedder to a production one, and which single stage you'd tell them to measure first.

Requirements:

  • Name the exact function that must be swapped (embed()) and what it should return.
  • State that the rest of the loop is unchanged by that swap.
  • Recommend measuring retrieval quality first (did the right chunk come back?), and suggest a simple way to eyeball it.
  • Mention chunking as the other high-leverage knob.

💡 Hint: You don't need code — this is about communicating the mental model. The next chapter (3.2) builds the real retrieval layer.

✓ Checkpoint — you can move on when you can…

  • RAG solves stale knowledge, private data, and hallucination by retrieving facts at question time.
  • The loop is always retrieve → augment → generate; retrieval quality caps the whole thing.
  • An embedding turns meaning into a vector; cosine similarity ranks by meaning, not words.
  • Chunking decides what can be found — a fact must live in a retrievable chunk; overlap protects boundaries.
  • You can hand-build the entire loop in plain Python; real systems only swap the embedder and the LLM.
  • Use RAG for what the model knows, fine-tuning for how it behaves.