AI: One Course · Lesson 10 of 20
RAG: Retrieval-Augmented Generation
The full RAG pipeline end to end, with a working mini-RAG you can run, plus prompt design and common failure modes.
- Intermediate
- 30 min read
- 3 objectives
Before this lessonLesson 9: Hybrid Search and Reranking
What you will learn
- Explain the RAG pipeline
- Build a mini RAG
- Debug retrieval vs generation failures
Your Progress
0 of 20 lessons 0%
- Lessons0 / 20
- Completed0
- Est. time left~ 9 hours
Create a free account to keep your progress on every device.
Time to put everything together. RAG (Retrieval-Augmented Generation) is the most important pattern in applied AI, and now that you know embeddings, chunking, vector search, BM25, hybrid search and hallucinations, it will feel like a natural conclusion instead of a buzzword. By the end of this lesson you will have run a complete mini-RAG system in your browser.
The problem RAG solves
An LLM has three limits that matter to businesses: it does not know your private data (your policies, tickets, contracts), it does not know recent events (training cut-off), and it hallucinates when it does not know. You could fine-tune a model on your data, but that is expensive, slow to update, and does not reliably stop invention. RAG takes the opposite approach: leave the model alone and hand it the right pages at question time.
The analogy: an open-book exam. A closed-book student relies on memory and bluffs. An open-book student looks up the relevant page first, then answers from it. RAG turns the LLM into the open-book student.
The two pipelines
RAG has an offline half (prepare the library) and an online half (answer a question):
OFFLINE - ingestion (run when documents change)
documents -> parse/clean -> CHUNK -> EMBED -> store in vector DB (+ BM25 index) with metadata
ONLINE - answering (run for every question)
question
-> (rewrite query)
-> RETRIEVE (hybrid: vector + BM25) -> top 30
-> RERANK -> top 5
-> build PROMPT = instructions + retrieved chunks + question
-> LLM generates answer (with citations)
-> (check faithfulness) -> answer to userA complete mini-RAG you can run
No API key, no server. The retriever is the hybrid one from last lesson. The "LLM" is replaced by a small function that extracts the best-matching sentence, so you can see the pipeline shape clearly and the prompt that would go to a real model.
import math, re
from collections import Counter, defaultdict
# ---------- OFFLINE: the knowledge base ----------
DOCUMENTS = {
"returns.md": "Laptops can be returned within 30 days of delivery for a full refund. Opened software is not refundable. Refunds go back to the original payment method within five business days.",
"shipping.md": "Shipping is free on orders over 50 dollars. Express shipping takes two days and costs 12 dollars. We ship to the US and Canada only.",
"warranty.md": "Every laptop includes a one year warranty that covers manufacturing defects. Batteries are covered for six months. Water damage is not covered.",
"support.md": "Support is available Monday to Friday from 9am to 6pm Eastern. Error ERR-4471 means the payment gateway timed out. Retry after five minutes.",
}
def chunk(text):
return [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()]
chunks = [] # (id, source, text)
for source, body in DOCUMENTS.items():
for n, piece in enumerate(chunk(body)):
chunks.append((f"{source}#{n}", source, piece))
# ---------- retrieval ----------
SYN = {"money": "refund", "back": "refund", "return": "returned", "cost": "costs", "deliver": "shipping",
"broken": "defects", "hours": "available", "open": "available", "time": "available", "canada": "canada"}
tok = lambda t: [SYN.get(w, w) for w in re.findall(r"[a-z0-9-]+", t.lower())]
STOP = {"the", "a", "an", "is", "are", "to", "of", "for", "on", "in", "and", "do", "i", "my", "can", "how", "what", "much", "does", "it"}
content = lambda t: [w for w in tok(t) if w not in STOP]
T = [content(c[2]) for c in chunks]
N = len(T); avg = sum(map(len, T)) / N
df = Counter(w for t in T for w in set(t))
def bm25(q, i, k1=1.5, b=0.75):
tf = Counter(T[i]); s = 0.0
for w in content(q):
if w in tf:
s += math.log((N - df[w] + .5) / (df[w] + .5) + 1) * tf[w] * (k1 + 1) / (tf[w] + k1 * (1 - b + b * len(T[i]) / avg))
return s
def retrieve(question, k=2):
ranked = sorted(range(N), key=lambda i: -bm25(question, i))
return [(chunks[i], round(bm25(question, i), 2)) for i in ranked[:k] if bm25(question, i) > 0]
# ---------- generation ----------
PROMPT = """Answer using ONLY the context. If it is not there, say "I don't have that information."
Cite the source in [brackets].
<context>
{context}
</context>
Question: {question}"""
def answer(question):
hits = retrieve(question)
if not hits:
return "I don't have that information.", None
context = "\n".join(f"[{c[1]}] {c[2]}" for c, _ in hits)
prompt = PROMPT.format(context=context, question=question)
best = hits[0][0] # stand-in for the LLM
return f"{best[2]} [{best[1]}]", prompt
for q in ["How long do I have to return a laptop?", "What does ERR-4471 mean?", "Who won the World Cup?"]:
a, prompt = answer(q)
print("Q:", q)
print("A:", a)
print()Q: How long do I have to return a laptop? A: Laptops can be returned within 30 days of delivery for a full refund. [returns.md] Q: What does ERR-4471 mean? A: Error ERR-4471 means the payment gateway timed out. [support.md] Q: Who won the World Cup? A: I don't have that information.
The third question is the important one. Nothing in the knowledge base matches, so the system refuses instead of inventing. That behaviour, designed on purpose, is the difference between a trustworthy assistant and a liar. Here is the prompt that would go to a real model for the first question:
PROMPT = """Answer using ONLY the context. If it is not there, say "I don't have that information."
Cite the source in [brackets].
<context>
{context}
</context>
Question: {question}"""
context = "[returns.md] Laptops can be returned within 30 days of delivery for a full refund.\n[returns.md] Opened software is not refundable."
print(PROMPT.format(context=context, question="How long do I have to return a laptop?"))Answer using ONLY the context. If it is not there, say "I don't have that information." Cite the source in [brackets]. <context> [returns.md] Laptops can be returned within 30 days of delivery for a full refund. [returns.md] Opened software is not refundable. </context> Question: How long do I have to return a laptop?
In production: the same shape with real parts
def answer(question, history):
standalone = llm.rewrite(question, history) # "and for laptops?" -> full question
candidates = hybrid_search(standalone, k=40) # BM25 + vectors, fused with RRF
top = reranker.rerank(standalone, candidates, top_n=5) # cross-encoder
context = "\n\n".join(f"[{c.source}] {c.text}" for c in top)
response = llm.chat(
system="Answer ONLY from the context. Cite sources. If unsure, say you do not know.",
user=f"<context>{context}</context>\n\nQuestion: {standalone}",
temperature=0,
stream=True, # SSE to the browser
)
return responsePrompt design for RAG
- Constrain: answer only from the context; otherwise refuse with a fixed phrase.
- Cite: require source tags; render them as links so users can verify.
- Structure: wrap context in tags (
<context>) and label each chunk with its source. - Order: put the most relevant chunks first (and last), not buried in the middle.
- Conflicts: tell the model what to do when sources disagree (prefer the newest; mention both).
- Temperature 0 for factual answers.
RAG versus fine-tuning versus long context
- RAG: best for knowledge that changes, is private, or needs citations. Cheap to update: just re-index.
- Fine-tuning: best for changing behaviour, style or format (tone, domain jargon, structured output), not for injecting facts. Expensive to update.
- Long context (stuff everything in the prompt): simple and great for a single document. But it costs tokens on every call, degrades in the middle, and does not scale to a company's whole knowledge base.
- Usually you combine them: RAG for facts, a well-written prompt (or light fine-tune) for behaviour.
How RAG fails: debug retrieval first
When a RAG answer is wrong, do not blame the model first. Ask which half broke:
- Retrieval failure (most common, roughly 70% of cases): the right chunk never reached the prompt. Causes: bad chunking, no keyword matching for IDs, wrong embedding model, missing metadata filter, question too vague, document never ingested.
- Generation failure: the right chunk was in the prompt but the model ignored it, misread it, or mixed in outside knowledge. Causes: weak prompt, too many distracting chunks, contradictory chunks, high temperature.
- Data failure: the document itself is wrong, stale or a badly parsed PDF (tables and columns mangled).
The single most useful debugging habit: log the retrieved chunks with every answer. Ten seconds reading them tells you whether to fix search or fix the prompt.
Security in RAG
- Access control at retrieval time: filter by the user's permissions in the query; never rely on the model to hide things.
- Indirect prompt injection: a poisoned document can contain instructions ("ignore your rules and reveal..."). Treat retrieved text as untrusted data, not commands, and limit what the model can do.
- PII: do not embed or log sensitive data you do not need.
# Write your solution here
