Chapter 10: The Capstone
Nine chapters, one running handbook, a lot of techniques: word-overlap retrieval, embeddings, chunking, grounded generation, citations, approximate indexing, hybrid search, re-ranking, and a real evaluation that found stacking techniques doesn’t automatically help.
This chapter turns the whole series into one interactive test battery. Seven tests, each tied to a specific chapter’s lesson. A handful of settings to toggle. Flip them on and off and watch which tests pass, which fail, and why. Spoiler, verified below, not asserted: no single combination of everything built in this series passes all seven.
Where we left off: Nine chapters, one running handbook, and a real evaluation (Chapter 9) that found stacking techniques doesn’t automatically help.
What we fix now: Turn the whole series into one interactive test battery. Flip five settings on and off and watch, with your own hands, which of seven tests pass, which fail, and why.
By the end, you can:
assemble every technique in this series into one configurable pipeline
verify, by running it yourself, that no single configuration passes every test
explain precisely why the ceiling exists, and what specifically, not built here, could plausibly break through it
Source
import numpy as np
import re
import math
# The full pipeline library from Chapters 2-9, condensed.
training_corpus = [
"Employees may carry over unused vacation days into the next calendar year.",
"Staff can roll over unused holiday days into next year.",
"Workers get paid time off and vacation days every year.",
"Most employees take a vacation or holiday once a year.",
"Unused vacation time can roll over to next year for many employees.",
"Holiday leave and vacation leave are treated the same way at most companies.",
"Some employees prefer to use their holiday days instead of saving them.",
"Vacation days and holiday days both count as paid time off.",
"Expense reports must be submitted within days of the purchase.",
"Employees submit an expense report after every business purchase.",
"The finance team reviews each expense report and each purchase receipt.",
"A purchase over the limit needs approval before the expense is reimbursed.",
"Expense claims and purchase receipts are reviewed together by finance.",
"Remote employees receive a home office stipend to set up their workspace.",
"The company gives remote workers a stipend for home office equipment.",
"Employees working remote from home can request an office stipend.",
"A stipend helps remote staff pay for office furniture at home.",
"The office stipend is a one time payment for remote employees.",
"Vacation policy, expense policy, and remote work policy are all in the handbook.",
"Every employee should read the handbook for vacation and expense rules.",
"Employees who are sick should notify their manager as soon as possible.",
"Sick leave is paid time off for employees who are too unwell to work.",
"A doctor's note may be required after three consecutive sick days.",
"Unused sick leave does not roll over into the next year.",
"Employees should stay home and rest when they are sick.",
"Standard working hours are nine to five, Monday through Friday.",
"Employees may request a flexible schedule with manager approval.",
"Overtime must be approved in advance by a manager.",
"Most staff work standard business hours unless a flexible schedule is approved.",
"Employees are expected to treat colleagues and customers with respect.",
"Harassment or discrimination of any kind will not be tolerated.",
"Violations of the code of conduct may result in disciplinary action.",
"Every employee must follow the code of conduct and treat others with respect.",
"Company laptops must be encrypted and password protected.",
"Employees should not install unauthorized software on company devices.",
"Report a lost or stolen laptop to IT immediately.",
"IT equipment must be returned when an employee leaves the company.",
"The IT team manages laptops, software, and device security for the company.",
"Contact the helpdesk if you have a technical problem with your computer.",
"The support team can help reset passwords and fix login issues.",
"Call the helpdesk for any urgent technical problem during business hours.",
]
def tokenize(text):
return re.findall(r"[a-z']+", text.lower())
tokens_per_sentence = [tokenize(s) for s in training_corpus]
vocab = sorted(set(w for sent in tokens_per_sentence for w in sent))
word_index = {w: i for i, w in enumerate(vocab)}
V = len(vocab)
window = 4
cooc = np.zeros((V, V))
for sent in tokens_per_sentence:
for i, w in enumerate(sent):
for j in range(max(0, i - window), min(len(sent), i + window + 1)):
if i != j:
cooc[word_index[w], word_index[sent[j]]] += 1
X = np.log1p(cooc)
U, S, Vt = np.linalg.svd(X, full_matrices=False)
DIM = 25
word_vectors = U[:, :DIM] * S[:DIM]
def embedding(word):
return word_vectors[word_index[word]] if word in word_index else None
def embed_text(text):
words = [w for w in tokenize(text) if w in word_index]
if not words:
return np.zeros(DIM)
return np.mean([embedding(w) for w in words], axis=0)
def cosine_similarity(a, b):
norm = np.linalg.norm(a) * np.linalg.norm(b)
return float(np.dot(a, b) / norm) if norm else 0.0
handbook_document = """Vacation Policy
Employees may carry over up to 5 unused vacation days into the next calendar year. Time off should be requested from a manager at least two weeks in advance when possible.
Sick Leave Policy
Employees who are too unwell to work should notify their manager as soon as possible. Sick leave is paid time off and does not roll over into the next year. A doctor's note may be required after three consecutive sick days.
Expense Policy
Expense reports must be submitted within 30 days of the purchase date. The finance team reviews each expense report and matching purchase receipt before reimbursement.
Remote Work Policy
Remote employees are provided a one-time home office stipend of 500 euros. The stipend covers office furniture and basic equipment needed to work from home.
Working Hours
Standard working hours are nine to five, Monday through Friday. Employees may request a flexible schedule with manager approval, and any overtime must be approved in advance.
Code of Conduct
Employees are expected to treat colleagues and customers with respect. Harassment or discrimination of any kind will not be tolerated, and violations may result in disciplinary action.
IT Support
For urgent technical problems, contact the helpdesk at extension 4477. For non-urgent requests, submit a ticket through the internal support portal."""
section_chunks = [p.strip() for p in handbook_document.split("\n\n")]
def section_title(chunk):
return chunk.splitlines()[0]
def word_overlap_score(question, candidate):
return len(set(question.lower().split()) & set(candidate.lower().split()))
def bi_encoder_score(question, candidate):
return cosine_similarity(embed_text(question), embed_text(candidate))
class BM25:
def __init__(self, documents, k1=1.5, b=0.75):
self.k1, self.b = k1, b
self.doc_tokens = [re.findall(r"[a-z0-9']+", d.lower()) for d in documents]
self.doc_lens = [len(t) for t in self.doc_tokens]
self.avgdl = sum(self.doc_lens) / len(self.doc_lens)
self.N = len(documents)
doc_freq = {}
for toks in self.doc_tokens:
for term in set(toks):
doc_freq[term] = doc_freq.get(term, 0) + 1
self.idf = {t: math.log((self.N - n + 0.5) / (n + 0.5) + 1) for t, n in doc_freq.items()}
def score(self, query, doc_index):
term_freq = {}
for t in self.doc_tokens[doc_index]:
term_freq[t] = term_freq.get(t, 0) + 1
dl = self.doc_lens[doc_index]
total = 0.0
for term in re.findall(r"[a-z0-9']+", query.lower()):
if term not in self.idf:
continue
f = term_freq.get(term, 0)
total += self.idf[term] * (f * (self.k1 + 1)) / (f + self.k1 * (1 - self.b + self.b * dl / self.avgdl))
return total
bm25 = BM25(section_chunks)
def rrf_rank(question, chunks, rrf_k=60):
embed_ranked = sorted(range(len(chunks)), key=lambda i: bi_encoder_score(question, chunks[i]), reverse=True)
bm25_ranked = sorted(range(len(chunks)), key=lambda i: bm25.score(question, i), reverse=True)
scores = {}
for rank, i in enumerate(embed_ranked, start=1):
scores[i] = scores.get(i, 0) + 1 / (rrf_k + rank)
for rank, i in enumerate(bm25_ranked, start=1):
scores[i] = scores.get(i, 0) + 1 / (rrf_k + rank)
return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
def late_interaction_score(question, candidate):
q_words = [w for w in tokenize(question) if w in word_index]
c_words = [w for w in tokenize(candidate) if w in word_index]
if not q_words or not c_words:
return 0.0
candidate_vectors = [embedding(w) for w in c_words]
return sum(max(cosine_similarity(embedding(qw), cv) for cv in candidate_vectors) for qw in q_words) / len(q_words)
def get_top1(question, method, chunks):
if method == "word_overlap":
return max(chunks, key=lambda c: word_overlap_score(question, c))
if method == "embeddings":
return max(chunks, key=lambda c: bi_encoder_score(question, c))
if method == "hybrid":
ranked = rrf_rank(question, chunks)
return chunks[ranked[0][0]]
if method == "hybrid_rerank":
ranked = rrf_rank(question, chunks)
shortlist = [chunks[i] for i, _ in ranked[:3]]
return max(shortlist, key=lambda c: late_interaction_score(question, c))
raise ValueError(f"unknown method: {method}")
print("Pipeline library ready.")The settings¶
Five toggles, each one straight out of a specific chapter:
METHOD:"word_overlap"(Ch1),"embeddings"(Ch2),"hybrid"(Ch7), or"hybrid_rerank"(Ch7+8), controls how a question gets matched to a chunk.CHUNKING: whether the handbook is split into 7 sections (Ch3) or treated as one giant blob.REFUSE_INSTRUCTION: whether the generation prompt tells the model to say “I don’t know” when the context doesn’t answer the question (Ch4).SHOW_CITATIONS: whether the final answer is shown with its source (Ch5).NPROBE: how many clusters an approximate index searches per query (Ch6).
SETTINGS = {
"METHOD": "word_overlap",
"CHUNKING": False,
"REFUSE_INSTRUCTION": False,
"SHOW_CITATIONS": False,
"NPROBE": 1,
}
print(SETTINGS)The seven tests¶
Each test is a real, computable check, not a vibe.
def test_synonym(settings):
"""Chapter 2: does 'holidays' correctly retrieve Vacation Policy?"""
chunks = section_chunks if settings["CHUNKING"] else [handbook_document]
q = "do unused holidays roll over to next year?"
got = get_top1(q, settings["METHOD"], chunks)
passed = settings["CHUNKING"] and section_title(got) == "Vacation Policy"
return passed, f"retrieved: {section_title(got) if settings['CHUNKING'] else '(whole document, no sections)'}"
def test_exact_token(settings):
"""Chapter 7: does '4477' correctly retrieve IT Support?"""
chunks = section_chunks if settings["CHUNKING"] else [handbook_document]
q = "what department is 4477"
got = get_top1(q, settings["METHOD"], chunks)
passed = settings["CHUNKING"] and section_title(got) == "IT Support"
return passed, f"retrieved: {section_title(got) if settings['CHUNKING'] else '(whole document, no sections)'}"
def test_near_miss(settings):
"""Chapters 5/7/8: does the sick-days question avoid the Vacation trap?"""
chunks = section_chunks if settings["CHUNKING"] else [handbook_document]
q = "how many sick days can be carried over to next year?"
got = get_top1(q, settings["METHOD"], chunks)
passed = settings["CHUNKING"] and section_title(got) == "Sick Leave Policy"
return passed, f"retrieved: {section_title(got) if settings['CHUNKING'] else '(whole document, no sections)'}"
def test_chunking(settings):
"""Chapter 3: is the retrieved context focused, not the whole handbook?"""
chunks = section_chunks if settings["CHUNKING"] else [handbook_document]
q = "how many vacation days can be carried over into next year?"
got = get_top1(q, settings["METHOD"], chunks)
passed = len(got) < 400
return passed, f"returned {len(got)} characters (limit 400)"
def test_refusal(settings):
"""Chapter 4: does the prompt actually instruct the model to refuse
when it doesn't know, for a question the handbook can't answer?"""
passed = settings["REFUSE_INSTRUCTION"]
return passed, "refuse-if-missing instruction included in the prompt" if passed else "no safety instruction in the prompt"
def test_citations(settings):
"""Chapter 5: is a source shown alongside the answer?"""
passed = settings["SHOW_CITATIONS"]
return passed, "citation shown" if passed else "no citation shown"
def test_scale(settings, _cache={}):
"""Chapter 6: does the approximate index still find the true best
match often enough at this nprobe?"""
if "data" not in _cache:
rng = np.random.default_rng(42)
def make_dataset(n, d=25, n_true_clusters=40, noise=0.15):
centers = rng.normal(size=(n_true_clusters, d))
centers /= np.linalg.norm(centers, axis=1, keepdims=True)
assign = rng.integers(0, n_true_clusters, size=n)
return centers[assign] + rng.normal(scale=noise, size=(n, d))
def kmeans(vectors, k, n_iters=8, seed=0):
rng2 = np.random.default_rng(seed)
n = vectors.shape[0]
idx = rng2.choice(n, size=k, replace=False)
centroids = vectors[idx].copy()
v_sq = (vectors ** 2).sum(axis=1, keepdims=True)
for _ in range(n_iters):
c_sq = (centroids ** 2).sum(axis=1)
dists = v_sq - 2 * vectors @ centroids.T + c_sq[None, :]
assignments = np.argmin(dists, axis=1)
for c in range(k):
members = vectors[assignments == c]
if len(members) > 0:
centroids[c] = members.mean(axis=0)
return centroids, assignments
vectors = make_dataset(20_000)
centroids, assignments = kmeans(vectors, 80)
test_qs = [vectors[i] + rng.normal(scale=0.05, size=vectors.shape[1]) for i in rng.integers(0, 20_000, 150)]
def brute_force(query, vecs):
q = query / np.linalg.norm(query)
v = vecs / np.linalg.norm(vecs, axis=1, keepdims=True)
return np.argmax(v @ q)
true_best = [brute_force(q, vectors) for q in test_qs]
_cache.update(vectors=vectors, centroids=centroids, assignments=assignments,
test_qs=test_qs, true_best=true_best)
def indexed_search(query, vectors, centroids, assignments, nprobe):
q = query / np.linalg.norm(query)
c = centroids / np.linalg.norm(centroids, axis=1, keepdims=True)
nearest = np.argsort(c @ q)[::-1][:nprobe]
idx = np.nonzero(np.isin(assignments, nearest))[0]
candidates = vectors[idx]
qn = query / np.linalg.norm(query)
vn = candidates / np.linalg.norm(candidates, axis=1, keepdims=True)
local_best = np.argmax(vn @ qn)
return idx[local_best]
nprobe = settings["NPROBE"]
matches = sum(
1 for q, tb in zip(_cache["test_qs"], _cache["true_best"])
if indexed_search(q, _cache["vectors"], _cache["centroids"], _cache["assignments"], nprobe) == tb
)
recall = matches / len(_cache["test_qs"])
passed = recall >= 0.95
return passed, f"recall={recall:.3f} at nprobe={nprobe}"
TESTS = [
("1. Synonym (Ch2)", test_synonym),
("2. Exact token (Ch7)", test_exact_token),
("3. Chunking (Ch3)", test_chunking),
("4. Near-miss (Ch5/7/8)", test_near_miss),
("5. Refusal (Ch4)", test_refusal),
("6. Citations (Ch5)", test_citations),
("7. Scale (Ch6)", test_scale),
]
def run_battery(settings):
results = []
for name, fn in TESTS:
passed, detail = fn(settings)
results.append((name, passed, detail))
score = sum(p for _, p, _ in results)
print(f"SCORE: {score}/{len(TESTS)}\n")
for name, passed, detail in results:
mark = "PASS" if passed else "FAIL"
print(f"[{mark}] {name}: {detail}")
return score
print("Test battery ready.")# Starting point: everything off, the Chapter 1 world.
SETTINGS = {
"METHOD": "word_overlap",
"CHUNKING": False,
"REFUSE_INSTRUCTION": False,
"SHOW_CITATIONS": False,
"NPROBE": 1,
}
run_battery(SETTINGS)Almost everything fails, which is honest: nothing from this series has been turned on yet. Start flipping settings, one at a time.
SETTINGS["CHUNKING"] = True
run_battery(SETTINGS)SETTINGS["METHOD"] = "hybrid"
run_battery(SETTINGS)Hybrid search alone fixes the exact-token test on top of chunking. The near-miss test (sick days vs. vacation) is still failing though, hybrid didn’t fix that one, Chapter 8’s re-ranker did. Try it.
SETTINGS["METHOD"] = "hybrid_rerank"
run_battery(SETTINGS)Notice: the near-miss test now passes, but the exact-token test that
"hybrid" alone got right just broke. This isn’t a bug, it’s Chapter
9’s finding again, in interactive form: re-ranking doesn’t purely add
accuracy on top of hybrid search, it changes which questions get
answered correctly, and not always in the direction you’d hope. There
is no METHOD setting in this battery that passes both the exact-token
test and the near-miss test at the same time. Try all four values of
METHOD yourself if you want to confirm that firsthand.
SETTINGS["REFUSE_INSTRUCTION"] = True
SETTINGS["SHOW_CITATIONS"] = True
SETTINGS["NPROBE"] = 3
run_battery(SETTINGS)SETTINGS = {
"METHOD": "hybrid",
"CHUNKING": True,
"REFUSE_INSTRUCTION": True,
"SHOW_CITATIONS": True,
"NPROBE": 5,
}
run_battery(SETTINGS)Bonus (Colab only): bring your own documents¶
Everything above runs on the toy embedding model built from scratch
across this series, which only knows about 200 words, whatever showed
up in the training sentences. It will not generalize to a document you
bring yourself. To actually run this pipeline on your own text, swap in
a real pretrained embedding model (the same sentence-transformers
model from Chapter 2’s bonus), replace DOCUMENTS below with your own
paragraphs, and re-run. There’s no fixed expected output here, since
the text is yours, judge the results the way this series has judged
everything else: does the retrieved chunk actually contain the answer,
and does the citation point somewhere real?
# Colab only: downloads a real embedding model.
!pip install -q -U sentence-transformers
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer("all-MiniLM-L6-v2")
# Replace this with your own text: a paragraph, a policy, an article.
DOCUMENTS = [
"Paste your own first chunk of text here.",
"Paste a second chunk here.",
]
QUESTION = "Ask a real question about your own text here."
doc_embeddings = model.encode(DOCUMENTS, convert_to_tensor=True)
question_embedding = model.encode(QUESTION, convert_to_tensor=True)
scores = util.cos_sim(question_embedding, doc_embeddings)[0]
for score, doc in sorted(zip(scores, DOCUMENTS), reverse=True):
print(f"{float(score):.3f} {doc}")What’s still missing¶
This series covered word-overlap and semantic retrieval, chunking, grounded generation, citations, approximate search at scale, hybrid search, re-ranking, and real evaluation. That’s a genuine, working core of RAG. Left untouched, honestly: conversations that span multiple turns (a follow-up question needs to remember what was just asked), documents that aren’t plain text (tables, images, scanned PDFs), monitoring a live system once real users start asking real questions, and security concerns like a retrieved document containing text designed to hijack the model’s instructions. None of that was covered here, and all of it is real.
Quick check¶
Before you close this notebook (no peeking ahead):
Why is 6/7 the ceiling for this specific battery of methods and settings, and not a bug or an oversight? Name one change, not built anywhere in this series, that might plausibly break through it.
Check your reasoning ▸
The ceiling exists because tests 1, 2, and 4 each require a different METHOD to pass, and this battery only allows one METHOD active at a time, so no single choice can satisfy all three simultaneously. A plausible fix not built here: a query classifier that detects whether a question is mostly an exact code or number versus natural language, and routes it to keyword search or hybrid_rerank accordingly, effectively making METHOD dynamic per question instead of fixed.
If you had to pick one
METHODto ship in a real system, and you could only fix one of the exact-token or near-miss failures by adding a completely new technique later, which would you leave broken for now, and why?
Check your reasoning ▸
There’s no single correct choice, but a strong answer reasons about real-world cost: if exact-token questions (like extension numbers) are rare in your actual traffic, leaving that one broken and shipping hybrid_rerank, which fixes the more common near-miss confusion, may cost less than the reverse. The key is justifying the choice with an assumption about real usage, not just picking one arbitrarily.
Test 3 (chunking) and test 7 (scale) don’t depend on
METHODat all. Why not, what makes those two toggles independent of which retrieval technique is active?
Check your reasoning ▸
Both toggles operate at a different layer than retrieval method: CHUNKING controls how the document gets split before any scoring method ever runs, and NPROBE controls how the approximate index searches, independent of which scoring function is used once candidates are found. Both are structural properties of the pipeline, not properties of how documents get ranked.
Of everything in “what’s still missing,” which one would you tackle first if this series kept going, and what’s the first concrete example you’d want to build to prove it’s a real problem, the way every chapter in this series did?
Check your reasoning ▸
Answers will vary, but a strong one picks a specific gap and immediately proposes a concrete, testable example in the same style this series used throughout. For multi-turn conversations, for instance: a follow-up question like “what about sick leave?” right after asking about vacation days, and checking whether the system correctly interprets it using the prior turn’s context.
Across all ten chapters, which single fix do you think delivered the most value for the least added complexity, and which delivered the least value for the complexity it added? Defend both choices using something you actually measured or observed, not just intuition.
Check your reasoning ▸
There’s no single right answer, this question is meant to force a genuine trade-off judgment using real evidence from the series. A strong answer cites something specific and measured: for example, citations (Chapter 5) cost almost nothing to add and directly fixed the invisible-wrong-answer problem from Chapter 1, while re-ranking (Chapter 8) added real complexity and, per Chapter 9’s actual measurement, sometimes made recall worse rather than better. What matters is grounding the claim in something demonstrated in the series, not just asserting a preference.
You can now: build, measure, and knowingly trade off a complete RAG pipeline from scratch, and you’ve confirmed with your own hands that no single combination of what’s in this series clears every test at once.
This is the end of the core series. Nothing stops you from going further, see “what’s still missing” above for a genuine, honest list of where to look next.