What is RAG
RAG is a simple idea with big consequences: before the model answers, look up the relevant facts and hand them over. This lesson explains why that works, when RAG is the right tool (and when it is not), the components of every RAG system, and then builds a complete — if tiny — RAG pipeline in plain Python so you can see every moving part.
The problem RAG solves
| LLM limitation | Example | How RAG fixes it |
|---|---|---|
| No private knowledge | “What is our parental-leave policy?” | Retrieves the policy from your HR documents. |
| Knowledge cutoff | “What changed in last week's release?” | Retrieves the release notes, indexed today. |
| Hallucination | Invents a plausible but wrong refund window | Answers from retrieved text and cites it. |
| No provenance | Users cannot verify the answer | Shows the source passages and links. |
| Access control | Everyone should not see every document | Filters retrieval by the user's permissions. |
Anchoring a model's answer in specific, provided evidence rather than its internal memory. A grounded answer can be checked against its sources.
RAG vs fine-tuning vs long context
| RAG | Fine-tuning | Paste everything (long context) | |
|---|---|---|---|
| Adds new knowledge | Yes, instantly | Poorly — better at style and format | Yes |
| Updating data | Re-index changed files | Retrain | Edit the prompt |
| Citations | Natural | No | Possible |
| Cost per question | Low (only relevant chunks) | Low | High (every token, every time) |
| Scales to | Millions of documents | N/A | One context window |
| Best for | Q&A over changing knowledge | Consistent behaviour, tone, format | A few documents, one-off analysis |
They combine well: a fine-tuned model can be used inside a RAG system, and a long-context model lets you retrieve more chunks. But for “answer questions from our documents”, RAG is almost always the first thing to build.
The components
| Component | Job | Lesson |
|---|---|---|
| Loader | Read PDFs, HTML, Markdown, docs, tickets into text | Loading and chunking |
| Chunker | Split text into retrievable passages | Loading and chunking |
| Embedding model | Turn chunks and questions into vectors | Embeddings and similarity |
| Vector store | Store vectors and find nearest neighbours fast | Vector stores |
| Retriever | Choose which chunks to use (search, filters, reranking) | Retrieval patterns |
| Generator | Prompt the LLM with context and produce a cited answer | Generation with sources |
A complete RAG in 50 lines
Here is the whole pipeline with no libraries beyond NumPy. Read it top to bottom: it indexes four tiny documents, embeds a question, retrieves the best two chunks, and builds the prompt that would be sent to an LLM.
import hashlib
import re
import numpy as np
STOPWORDS = {"a", "an", "the", "is", "are", "be", "can", "do", "does", "i", "my", "to",
"of", "in", "on", "for", "and", "or", "what", "how", "it", "you", "your", "at"}
def words(text: str) -> list[str]:
"""Lowercase, drop stopwords, and strip a few suffixes (refunded -> refund)."""
out = []
for w in re.findall(r"[a-z0-9-]+", text.lower()):
if w in STOPWORDS:
continue
for suffix in ("ing", "ed", "s"):
if w.endswith(suffix) and len(w) > len(suffix) + 3:
w = w[: -len(suffix)]
break
out.append(w)
return out
def embed(text: str, dim: int = 256) -> np.ndarray:
"""Toy embedding: hashed bag of words, L2-normalised. Real models capture meaning."""
vec = np.zeros(dim, dtype=np.float32)
for word in words(text):
bucket = int(hashlib.md5(word.encode()).hexdigest(), 16) % dim
vec[bucket] += 1.0
norm = np.linalg.norm(vec)
return vec / norm if norm else vec
# 1. INDEX: documents -> chunks -> vectors
documents = {
"refunds.md": "Annual plans can be refunded within 30 days of purchase. Monthly plans are not refunded.",
"backups.md": "Backups run every night at 2 AM. You can restore any file from the last 90 days.",
"security.md": "All backups are encrypted with AES-256. Two-factor authentication is required for admins.",
"billing.md": "Invoices are emailed on the first day of each month. You can change your card in Settings.",
}
chunks = [{"source": name, "text": text} for name, text in documents.items()]
matrix = np.stack([embed(c["text"]) for c in chunks]) # shape (4, 256)
# 2. RETRIEVE: question -> vector -> most similar chunks
def retrieve(question: str, k: int = 2) -> list[dict]:
scores = matrix @ embed(question) # cosine similarity
best = np.argsort(-scores)[:k]
return [{**chunks[i], "score": round(float(scores[i]), 3)} for i in best]
# 3. AUGMENT: build a grounded prompt
def build_prompt(question: str, hits: list[dict]) -> str:
context = "\n".join(f"[{h['source']}] {h['text']}" for h in hits)
return (
"Answer using only the context. Cite sources in brackets. "
"If the answer is not in the context, say you don't know.\n\n"
f"Context:\n{context}\n\nQuestion: {question}"
)
question = "Can I get a refund on an annual plan?"
hits = retrieve(question)
for h in hits:
print(h["score"], h["source"])
print()
print(build_prompt(question, hits))
Step 4 — generate — is one API call with that prompt, exactly as in the LLM Fundamentals course. The model reads the refund policy and answers “Yes, within 30 days of purchase [refunds.md]”. Everything else in this course is about making each of these steps more robust.
Where RAG systems fail
A RAG answer can only be as good as the context it receives. When answers are wrong, diagnose which stage failed:
| Failure | Symptom | Stage to fix |
|---|---|---|
| Content missing | The answer is not in any document | Data: add or update documents |
| Bad chunking | The answer is split across chunks or drowned in noise | Chunking |
| Missed retrieval | The right chunk exists but is not in the top-k | Embeddings, hybrid search, query rewriting |
| Wrong ranking | The right chunk is retrieved but ranked low or cut off | Reranking, top-k |
| Unfaithful generation | Correct context, but the answer ignores or distorts it | Prompt, model, citations |
| Stale index | Answers reflect old versions of documents | Re-indexing pipeline |
Practitioners consistently find that most RAG quality problems are retrieval problems, not generation problems. When debugging, always print the retrieved chunks first.
- Run the mini RAG and ask “How long are backups kept?” — which document wins?
- Ask “What encryption do you use?” and check the retrieval scores.
- Ask something unrelated (“What is the CEO's name?”) and look at the scores. What threshold would you use to say “I don't know”?
- RAG retrieves relevant passages and gives them to the LLM as context for each question.
- It solves private knowledge, freshness, hallucination, provenance, and access control.
- Prefer RAG for knowledge, fine-tuning for behaviour, and long context for small one-off document sets.
- Every RAG system has the same components: loader, chunker, embedder, vector store, retriever, generator.
- Most failures are retrieval failures — always inspect what was retrieved.