The Course
Eight lessons. Each one is: a claim → a command that tests it → the number you should see → what it means → what to do about it.
Rules:
- Predict before you run. Write the number down. Being wrong on record is the entire mechanism by which this becomes skill. Reading the answer teaches nothing.
- Change one thing. The harness is built so the context strategy is the only variable. Change two, learn zero.
- Lessons 1–6 cost no API calls. That's deliberate — the bottleneck is loop speed, not model access.
Time: ~4 hours to work through. Lesson 5 is the one that matters most.
Lesson 0 — Orientation (5 min)
cd lab
python3 llm.py gemini # smoke test
python3 corpus_hard.py # what the task is144 docs, ~7.7k tokens if you dump all of it. 24 questions of two kinds:
- trap — "What is the current deploy region for Project Peregrine?" A superseded
2023 doc exists that is lexically better than the current one: it repeats the project name, says "current", and has no filler diluting its term frequency.
- multihop — "What is the budget code charged for Project Peregrine?" The answer
lives in a doc that never mentions Peregrine. You must learn the portfolio name first, then look up the portfolio.
Everything downstream is about those two failure shapes.
Lesson 1 — Accuracy is a compound number. Decompose it. (15 min)
Claim: accuracy = (fact entered the window) × (model used it). Only the first factor is yours. Measure it separately or you'll spend days tuning prompts against a ceiling you can't see.
Predict: at budget 800, what fraction of questions have the gold string present in S1_topk's assembled context?
python3 recall.py --budget 800 strategy recall multihop trap
S0_dump_all 46% 0% 92%
S1_topk 71% 42% 100%
S0b_dump_full 100% 100% 100%What it means: S1's accuracy can never exceed 71%. It measured 75% end-to-end earlier — which is above its own recall, meaning the model guessed right a couple of times. That's not a win, it's noise you'd mistake for signal.
The habit: measure assembly recall before you touch a prompt. Zero API calls, one second. If recall is 40%, prompt engineering is a waste of your afternoon.
Lesson 2 — Truncation is the #1 silent killer (20 min)
Predict: S0 dumps docs in corpus order up to the budget. At 800 tokens its multihop recall is 0%. What is it at 1600?
python3 recall.py
python3 inspect_ctx.py --kind multihop --i 1 --rank 5Q (multihop): What is the budget code charged for Project Vireo?
GOLD: BC-2781
retrieval ranking:
2023-04-11 Vireo-region-old <- stale doc ranks FIRST
2026-02-19 Vireo-ownership
2026-02-19 portfolio-Everglade-finance
...
strategy gold in ctx ctx_tok
S0_dump_all NO 780
S1_topk YES 790What it means: S0 scored 8% end-to-end. Not because the model is weak — because the answer was never in the window. It correctly said UNKNOWN.
The habit: the first question on any context bug is "was the fact even there?" inspect_ctx.py answers it in one second with zero API calls. In production this is the majority of "the model is dumb" tickets.
Lesson 3 — A broken component can hide behind a good score (30 min)
Predict: on trap questions, how often does the stale doc rank above the current one in S1_topk? Trap accuracy was 100%, so presumably rarely.
python3 recall.py --ranktrap questions where BOTH docs retrieved : 12/12
...and the STALE one ranks HIGHER : 12/12Every single time. Trap accuracy is 100% by luck: both docs fit in an 800-token budget, and the model resolves the conflict using the dates. The ranker is completely wrong and the metric says everything is fine.
Now shrink the budget so only one doc fits:
python3 recall.py --budget 300Multihop recall collapses to 0% for every retrieval strategy.
What it means: end-to-end accuracy is a lagging indicator. It hides broken components whenever slack elsewhere in the system compensates. Remove the slack — tighten the budget, add distractors — and the latent bug surfaces.
This is why you need component metrics, not just end-to-end scores.
→ Do exercise E1 in exercises.py. Target: 0/12 stale-above-current at budget 300. Verify with python3 check.py E1.
Lesson 4 — Your obvious improvements probably do nothing (20 min)
S1 → S2 (recency sort) → S3 (explicit "prefer later dates" framing). Both changes are things every RAG guide tells you to do.
Predict: how many accuracy points does S3 buy over S1?
S1_topk 75% 50% 100%
S2_topk_recency 75% 50% 100%
S3_framed 75% 50% 100%Zero. All three. The framing targets stale-vs-current confusion, and traps were already at 100% — there was no headroom in the failure mode it addresses.
What it means: an intervention only helps where the corresponding failure mode is actually costing you. Without the per-kind breakdown, the aggregate would have looked plausible and you'd have shipped three no-ops and believed in them.
The habit: before building a fix, find the failure it targets in your breakdown and confirm it's non-zero. If you can't point at the number it will move, don't build it.
Lesson 5 — The wall that a bigger window cannot fix (45 min) ★
This is the most important lesson in the lab.
python3 inspect_ctx.py S1_topk --kind multihop --i 1 --showThe context contains: "Project Vireo is owned by portfolio Pinecrest." It does not contain the Pinecrest finance doc — because the word "Pinecrest" does not appear in the question. BM25 cannot retrieve on a term that doesn't exist yet.
Nor can embeddings. Nor can a 10M-token window at real corpus scale. The missing information is not relevance, it's ordering: hop B's query is a function of hop A's result.
The only fix is another turn:
S1_topk (1 call, 785 ctx tok) multihop 50%
S5_iterative (3 calls, 345 tok) multihop 67%What it means: this is the actual boundary between "RAG" and "agent". Not a buzzword — a structural property of the task. If the next query term is unknowable until you read the current result, you need a loop. If it isn't, a loop is wasted money.
Diagnostic you can apply at work: can I write down every search I'll need before seeing any results? Yes → one-shot retrieval. No → you need turns.
→ Do exercise E2. Beat S1's 42% multihop recall with zero LLM calls — extract the entity with a regex and issue a second retrieval. Target ≥90% at ≤800 tokens. python3 check.py E2
The lesson inside the lesson: S5 spends 2 extra LLM calls on a planner. E2 gets the same structural win for free. Reach for a loop when you need one; don't reach for a model when a regex does it.
Lesson 6 — Compression is the highest-leverage move, and the easiest to botch (30 min)
S4_compressed ctx 173 tok multihop 33% (S1: 785 tok, 42%)S4 is 4.5× cheaper and worse — it compressed away the very sentence carrying the second hop.
What it means: compression that drops recall isn't compression, it's deletion. The correct order is recall first, then squeeze. Lossy compression is where agents die silently, because the loss shows up three steps later as an inexplicable wrong answer.
→ Do exercise E3. Take your E2 solution to ≤200 context tokens while keeping ≥90% multihop recall. Reference solution: 100% at 109 tokens — 7× smaller than S1 and 2.4× more accurate. python3 check.py E3
Lesson 7 — Your grader is a measuring instrument, and it's wrong (30 min)
The original grader used gold in pred. It scored this correct:
gold: 'Luca Moretti'
pred: 'Yuki Tanaka, Sana Iqbal, Nils Eriksen, Mira Castellanos, Luca Moretti, Lene Brandt, Emre Demir'A grader that rewards listing every candidate rewards exactly the behaviour context engineering exists to prevent. Fixed grader, replayed from disk:
python3 regrade.py --diffS4_compressed 67% → 58% multihop 33% → 17%No re-run, no new API calls. That's what JSONL-logging every trial buys you: when you discover the instrument was miscalibrated — and you will — you replay history instead of re-arguing it from memory.
What it means: every metric you write will be gameable, usually by your own optimization, silently. Attacking your own metric is not paranoia; it is eval design.
→ Do exercise E4. Write a strategy that scores well while doing something you'd never ship. Then fix evaluate_hard.grade() so it fails. Repeat until you can't cheat it.
Lesson 8 — Which of your conclusions are real? (30 min, uses API)
python3 evaluate_hard.py --provider groq --model openai/gpt-oss-120b --n 12Compare per-kind numbers against the Gemini run in README.md.
Conclusions that survive a model swap were about context engineering. Ones that don't were about one model's quirks, and you should stop believing them. Structural findings (Lessons 2, 5, 6) will transfer. Margin-level findings (a 3-point difference between S2 and S3) will not.
The habit: before you generalize a result, change something you believe is irrelevant and check the result survives.
The eight rules, compressed
Context engineering
- Assembly recall is your accuracy ceiling. Measure it first, separately, for free.
- When something looks broken, ask "was the fact even in the window?" before anything else.
- Compress only after recall is high. Deletion masquerading as compression is the classic silent failure.
- An intervention only pays where its failure mode is currently costing you.
- If the next query term depends on the current result, you need a turn, not a bigger window.
- Budget is a policy about what deserves the window, not a limit on what fits.
Evals
- Never report one aggregate. Break down by failure kind; grade into mechanisms (
correct/stale/hedged/unknown/wrong/error), never binary. - Log every trial to disk. Verify the eval can fail before trusting that it passed. A saturated benchmark measures nothing.
Doing this on your own system
The transferable artifact is the shape, not the corpus:
| Lab file | Your equivalent |
|---|---|
corpus_hard.py | 30–50 real queries, hand-labelled, chosen to include your known failure modes |
recall.py | does the gold doc/fact make it into the assembled prompt? |
inspect_ctx.py | dump the exact final prompt for one input |
evaluate_hard.py | run N strategies × M queries, break down by kind, log JSONL |
regrade.py | replay logs under a new grader |
check.py | targets, so "better" is defined before you start |
Start with 30 labelled examples. That's enough to catch the failures in Lessons 2, 3, and 5 — which is most of what actually breaks in production.
Context Engineering + Evals — a working lab
▶ START HERE: LESSONS.md — 8 lessons, ~4 hours, mostly zero API calls.
Prefer a browser? python3 build_site.py && xdg-open course.html — one self-contained HTML file with prediction boxes, spoiler-blurred results, and saved progress.
The course is the learning material. This README is the reference / results appendix.
Workflow:
read LESSONS.md lesson N
-> predict the number
-> run the command
-> be wrong, understand why
-> do the exercise in exercises.py
-> python3 check.py (pass/fail against verified targets)Everything runs against free API keys already on this box (GEMINI_API_KEY, GROQ_API_KEY).
lab/
LESSONS.md THE COURSE. 8 lessons, each: claim -> command -> number -> meaning
exercises.py YOUR WORKBENCH. 4 stubs you fill in. This is the file you edit.
check.py auto-grader for the exercises. 0 API calls. Targets pre-verified.
recall.py assembly recall = your accuracy ceiling. 0 API calls. Run this first.
build_site.py renders LESSONS.md + README.md -> course.html (no deps, no server)
llm.py minimal OpenAI-compatible client (no deps), retries, token accounting
corpus.py easy synthetic corpus (v1 — saturated, kept as a lesson)
corpus_hard.py hard corpus: multi-hop + lexical traps + distractor twins
strategies.py one-shot context assembly strategies (S0–S4)
agentic.py multi-turn solvers that manage their own context (S5–S6)
evaluate.py harness v1 (easy corpus)
evaluate_hard.py harness v2 — per-kind breakdown + cost accounting
inspect_ctx.py see what entered the window. 0 API calls. Your main debugger.
regrade.py replay past runs under a new grader. 0 API calls.
runs/ runs_hard/ every trial, one JSON per line, diffableRun it:
cd lab
python3 llm.py gemini # smoke test
python3 corpus_hard.py # inspect the task
python3 evaluate_hard.py --provider gemini --n 12 --workers 3The measured result
gemini-3.1-flash-lite, 144 docs (~7.7k tokens whole), 12 questions × 8 strategies, budget 800 tokens, temperature 0.
strategy acc multihop trap ctx_tok in_tok calls
S0_dump_all 8% 0% 17% 780 918 1.0
S0b_dump_full 92% 83% 100% 8912 9301 1.0
S1_topk 75% 50% 100% 785 1011 1.0
S2_topk_recency 75% 50% 100% 785 1011 1.0
S3_framed 75% 50% 100% 785 989 1.0
S4_compressed 67% 33% 100% 173 336 1.0
S5_iterative 83% 67% 100% 345 1379 3.0
S6_iter_compressed 75% 50% 100% 65 382 2.4Efficiency — what you actually optimize in production:
strategy acc in_tok/q correct per 1k tokens
S0b_dump_full 92% 9301 0.10
S1_topk 75% 1011 0.74
S5_iterative 83% 1379 0.60
S4_compressed 67% 336 1.99
S6_iter_compressed 75% 382 1.96What each row teaches
S0 vs S0b — truncation is the silent killer. Same strategy, same order, one capped at 800 tokens. S0 scores 8%: the relevant doc simply fell off the end and the model answered UNKNOWN honestly. Most "the model is dumb" bugs are this. Always verify the gold fact survived assembly before blaming the model.
S0b — "just use a big window" works, and costs 9x. 92% for 9.3k tokens/question. It is the accuracy ceiling and the efficiency floor (0.10 correct/1k tok). At 144 docs it fits. At 144k it does not, and you are back to engineering.
S1 → S2 → S3 — three "obvious" improvements that did nothing. Recency sorting and explicit conflict framing changed accuracy by exactly 0 points, because the trap questions were already at 100% — there was no headroom in the failure mode they target. This is the most valuable negative result in the table. Without the per-kind breakdown you would have shipped all three and called it progress.
S4 — 20x compression costs 8 points. 173 vs 785 context tokens, 67% vs 75%. It is the best strategy per token (1.99 correct/1k) and the wrong one if accuracy is the constraint. There is no "better" without a cost axis.
S5 — the loop beats the window. 83% at 345 context tokens vs S1's 75% at 785. Multi-hop went 50% → 67%. It buys this with 3 LLM calls instead of 1. A one-shot retriever structurally cannot solve hop B: the search term (Ironwood) does not exist until hop A is read. No embedding model and no larger window fixes that — only another turn does.
S6 — compression inside the loop. 65 context tokens (120x less than S0b) for the same 75% as full-context top-k. Near-tied with S4 on efficiency, better on accuracy.
The two failures that were the real lesson
1. The first benchmark saturated. v1 (corpus.py) scored every strategy at 100%. A benchmark where everything ties measures nothing. The task was one-hop, single-doc, exact-string — too easy to separate a good strategy from a bad one. Fix: build the task around the failure mode you want to detect. That is corpus_hard.py.
2. The first "hard" corpus wasn't. With 5 portfolios, top-k accidentally retrieved all of them, so hop B came free and multi-hop quietly degraded back into one-hop. Scaling to 30 portfolios made retrieval selective and the failure appear. Corollary: verify your eval can fail before trusting that it passed.
Both failures cost ~20 minutes and are worth more than the passing runs. Rate limits also silently produced 28 error grades in an early run — errors must be a distinct grade, never folded into "wrong", or infra flakiness reads as a quality regression.
The transferable rules
Context engineering
- Budget is a policy, not a limit. Decide what deserves the window, not what fits.
- Verify assembly independently of generation —
gold in ctxis a one-line test that explains most failures. - Compression is the highest-leverage move (20x tokens for 8 points here). Measure the trade; never assume it's free.
- Ordering and framing only help where there is headroom in that specific failure mode.
- When the next query term is unknown until you read the current result, you need a turn, not a bigger window. That is the boundary between RAG and an agent.
Evals
- Deterministic where possible: fixed seed, temperature 0, fixed question set.
- Never report a single aggregate. Break down by failure kind — the aggregate hid a 0% multi-hop score behind a respectable 75%.
- Grade into mechanisms (
correct/stale/unknown/wrong/error), not binary. Each name points at a different fix. - Score cost alongside quality.
correct per 1k tokensreversed the ranking here. - Log every trial to disk. Regressions get diffed, not re-argued.
- A saturated or unfailable eval is a broken instrument. Fix the eval first.
How to actually use this to get good
Work through LESSONS.md. Eight lessons, each structured as: a claim → the command that tests it → the number you should see → what it means → the exercise that makes you do it yourself.
The single rule that makes it work: predict the number before you run the command. Being wrong on record is the mechanism. Reading results teaches nothing.
The loop
python3 recall.py --budget 800 # 0 calls: what's my ceiling?
python3 inspect_ctx.py --kind multihop --i 1 --rank 5 # 0 calls: did gold survive?
# edit exercises.py
python3 check.py E2 # 0 calls: hit the target?
python3 evaluate_hard.py --only S1_topk,E2_two_stage # end-to-end, costs calls
python3 regrade.py --diff # 0 calls: replay historyLessons 1–7 cost zero API calls. That is deliberate — the bottleneck in learning this skill is loop speed, not model access.
Why the two zero-call tools matter most
inspect_ctx.py separates assembly bugs from model bugs. Run it and you see:
strategy gold in ctx ctx_tok
S0_dump_all NO 780
S1_topk YES 790
S4_compressed YES 191Any NO means the model never had a chance. In this lab that single check explains S0's 8% score outright. In production it explains most "the model is dumb" tickets. This is the habit worth building: verify assembly before you touch the prompt.
regrade.py exists because the grader was wrong and the data caught it. The obvious gold in pred substring check scored this as CORRECT:
gold: 'Luca Moretti'
pred: 'Yuki Tanaka, Sana Iqbal, Nils Eriksen, Mira Castellanos, Luca Moretti, ...'A grader that rewards listing every candidate rewards exactly the behaviour context engineering exists to prevent. Under a strict grader (new hedged category), S4_compressed drops 67% → 58%, and multi-hop 33% → 17% — replayed from disk, no re-run. Your eval is a measuring instrument, and instruments need calibration.
Drills, in order
- Predict before you run. Write down the expected number, then run. Calibration comes from being surprised, and only from being surprised on record.
- Break S1 on purpose. Set
--budget 300. Watch where it collapses and which kind goes first. Truncation behaviour is the most useful thing to have felt. - Write S7. Retrieve the entity from hop A, then issue a templated hop-B query without an LLM planner call. Beat S5's 83% at fewer than 3 calls.
- Make S3 matter. Framing scored 0 here because traps were saturated. Change
corpus_hard.pyso superseded docs outrank current ones badly enough that traps drop below 100%, then re-run S2/S3. Learn to build headroom before claiming a win. - Break your own grader. Write a strategy that games the strict grader. Then fix the grader. Repeat until you cannot cheat it. This is what eval design is.
- Change one thing at a time. The whole harness is built so strategy is the only variable. Every time you change two, you learn nothing.
What transfers to real work
The corpus is fake; the failure modes are not. Truncation dropping the answer, top-k retrieving stale-but-lexically-better docs, distractor twins, and the hard wall where the next query term is unknowable until you read the current result — those are the four things that break real RAG and real agents. You will have already debugged each of them here, with ground truth available, which you never get in production.
Where to take it next
- Add an LLM-as-judge grader for open-ended answers, then measure the judge against
your own labels — an unvalidated judge is just a second unverified model.
- Add position sensitivity: same docs, gold at the start vs. middle vs. end.
- Add adversarial context: a doc that confidently states the wrong answer.
- Add a subagent boundary: give a fresh window per hop, return only the fact.
Measure what crossing the boundary costs.
- Swap
--provider groq --model openai/gpt-oss-120band see which conclusions survive
a model change. The ones that don't were never about context engineering.