Week 2
Build this week's exercise, then run the gate.
make check WEEK=2
| Build | Metrics, the heading chunker, the version filter, hybrid search and search_docs. Then the context experiment and six configurations, on a real model. |
| Files | app/retrieval/metrics.py, app/retrieval/chunkers.py, app/retrieval/versions.py, app/retrieval/hybrid.py, app/tools/search.py |
| Measure | make evidence WEEK=2 on a real model, into evidence/week-2.json |
| Record | decisions/week-2.md from the evidence, ai-use/week-2.yml, reflections/week-2.md |
| Guides | Worked example A: retrieval · Where the vectors live · Glossary |
checks fail 17 of 34 checks pass
Red is the normal state of a week you have not built yet. Each name is a test in the file beside it.
Written last: make evidence WEEK=2, then decisions/week-2.md and ai-use/week-2.yml.
| mode | hits | mean tokens in | total $ | mean ms |
|---|---|---|---|---|
| top3 | 11/30 | 559 | 0.02203 | 0 |
| stuffed | 19/30 | 191228 | 5.74570 | 49 |
| repaired | 11/30 | 208 | 0.00936 | 0 |
The exercise is in your repository, where you can run it:
code weeks/2/README.md make next
A model behind an interface: it survives the provider misbehaving, never pays twice, records what it cost and swaps provider from a setting. Output against a schema, and the agent's first two tools, get_customer and get_ticket.
Hybrid search, keyword and meaning together, restricted to the customer's version and called by the agent as a tool, with every choice in it made by a number.
Week 3 grounds answers in retrieved text. Grounding a bad retrieval just makes the wrong answer better written.
The same model gives a different answer when you give it different context. Choosing the context is the job, and retrieval is how you choose it.
Read this once (about 20 minutes). Write the sentence in your own words in reflections/week-2.md,
Q1, before you touch code.
Context engineering is deciding what the model sees on each call. Everything you put in the window competes for attention with everything else. More context is not more knowledge; past a point it is more noise, more latency and more money for the same answer, and sometimes a worse one. The named failure modes:
| Failure | What it looks like |
|---|---|
| Lost in the middle | A fact placed mid-context is missed more often than one at the start or end |
| Poisoning | A wrong or hostile passage in the context steers the answer |
| Distraction | Plausible but irrelevant passages pull the answer off the question |
| Clash | Two passages disagree and the model picks one silently |
The disciplines that follow: select (only what this call needs), compress (summarise what persists), isolate (keep tasks in separate contexts), cache (reuse a stable prefix so you do not pay to re-read it), and be explicit about memory: what persists between turns and sessions, and what must not.
flowchart LR Q[question] --> S["select
retrieve only what this call needs"] S --> W["context window
an attention budget, not a bucket"] W --> M[model] subgraph failures [what goes wrong when the window is treated as storage] L[lost in the middle] ~~~ P[poisoning] ~~~ D[distraction] ~~~ X[clash] end W -.- failures
Retrieval is how you select. Cut the corpus into chunks, embed them, find the nearest to the question, hand those to the model. Every choice in that sentence is a measurable decision:
hash) so you can see the mechanics, and a real one (fastembed) so you can
see the difference.E1104 and KESTREL_CONNECT_TIMEOUT, which carry little meaning and must
match exactly, and a support desk is full of them. Keyword search is the opposite. Reciprocal
rank fusion merges the two lists by rank, because their scores are on different scales. You
build both halves this week and measure what fusing them buys.The same clause, cut four ways. Only one strategy keeps "3. Fees." with its fee.
flowchart TB
subgraph fixed [fixed, 500 chars]
f1["…sixty (60) days written no"] --- f2["tice. 3. Fees. Client will pay Provider 6,500 GBP…"]
end
subgraph sentence [sentence, ~400 chars]
s1["…written notice. 3. Fees."] --- s2["Client will pay Provider 6,500 GBP per month…"]
end
subgraph paragraph [paragraph, ~800 chars]
p1["3. Fees. Client will pay Provider 6,500 GBP per month, invoiced monthly in advance, payable within 14 days of invoice."]
end
subgraph heading [heading-aware, per section]
h1["## Fees
Client will pay Provider 6,500 GBP…"]
end
Where the measurement sits: before the model, so a wrong answer can be traced to the step that produced it.
flowchart LR
Q[question] --> E1[embed]
D[("chunks + vectors
one index per strategy")] --> C
E1 --> C[cosine over every chunk]
C --> K[top k hits]
K --> MEAS{{"measure here
P@k · R@k · MRR · hit rate"}}
K --> M[model, Week 3]
The cleanest way to see this week's sentence is to hold everything still except the context.
scripts/context_experiment.py asks the same twenty questions of the same model with the same
prompt, five times each: with no passages, with the wrong version's page, with the right page,
with the right page and forty irrelevant ones, and with the right page buried in the middle of
eighty. On the right page the answers are mostly right. On the wrong version's page they are
confidently wrong, and they cite it. With too much, they cost ten times as much, and on smaller
models they start to slip. The model did not change. The answers did. Your decision on how
much context to send is read off that table.
From this week retrieval is not only an endpoint: it is search_docs, the agent's second tool,
called with the version get_customer returned. The tool's schema makes the version required,
so the model cannot leave it out and fall back into the version trap. That is the pattern every
later week repeats: the boundary lives in the schema and the code, not in the prompt.
| Idea | Where it lives | What you do this week |
|---|---|---|
| Chunking | app/retrieval/chunkers.py |
Build the heading-aware one; measure it against paragraph and section-aware |
| Hybrid search | app/retrieval/hybrid.py, SEARCH_MODE |
Build BM25 and reciprocal rank fusion; measure dense against hybrid |
| Embedding | app/retrieval/embed.py |
Switch EMBED_PROVIDER and see what changes |
| Store and search | app/retrieval/store.py, /index, /search |
Read it; it is the interface a vector database would sit behind |
| Metrics | app/retrieval/metrics.py |
Build precision@k, recall@k, MRR, hit rate |
| Attention budget | explore/w2_02_lost_in_the_middle.py |
Bury one fact, watch cost and accuracy move |
| The context itself | app/retrieval/context.py, scripts/context_experiment.py |
Same model, five contexts: measure what the context alone does |
| Retrieval as a tool | app/tools/search.py |
Design search_docs, version required |
Checked September 2026.
explore/w2_02_lost_in_the_middle.py with --filler 200./search plus Week 3's /ask reproduce in miniature.hybrid.py, with what each of k1 and b does.chunkers.py and a few this repo leaves to you.EMBED_PROVIDER=fastembed: what it was trained on, and what its similarity scores mean.Answers are kept in this browser only, under hub.quiz in local storage. They are never sent anywhere: not to the repository, not to the progress export, not to your mentor, and they have no effect on your route. Clearing your browser data clears them. The explanations are the point, not the score.