Module 28: Prompting and context engineering 40 min12 min quick readLLMs
Context engineering
Decide what goes into the context window and in what order (instructions, tools, retrieved documents, history, memory) under a token budget, and keep per-call cost bounded as conversations grow.
Before this: Prompt engineering, The RAG pipeline
Quick read: the intuition, interactive visualization, summary, and interview questions. The math, implementation, engineering, and knowledge-check sections are hidden.
Mental model. The context window is a small desk, not a filing cabinet. Everything the model will use for this answer has to be on the desk at once, and a cluttered desk makes the important page harder to find. Context engineering is deciding what goes on the desk, how much space each thing gets, and where it sits.
You will learn to
- List the components of a model call's context (system prompt, tool schemas, retrieved documents, history, memory, the query, output reserve) and budget tokens across them.
- Explain why a long window does not make long contexts free: cost, latency, and lower accuracy on facts buried in the middle.
- Order retrieved material so the most relevant items sit at the edges.
- Compact conversation history and select tools per query, and compute the savings.
- Choose between short-term context, long-term memory, and retrieval for a given piece of information.
Why it matters. Most failures of LLM applications that look like "the model is dumb" are context failures: the right document wasn't retrieved, it was there but buried, the instructions were pushed out by a long history, or 40 similar tool descriptions confused the choice. Prompt wording (prompt engineering) is one slice of the context. This lesson is about the whole window, and it ties together RAG, agents, and inference cost.
1. Intuition
Everything the model knows at answer time is in the window. A model call sees one sequence of tokens. It typically holds:
| Component | Typical role | Changes per call? |
|---|---|---|
| System prompt | role, rules, output format | rarely |
| Tool schemas | names, descriptions, JSON argument schemas | sometimes |
| Few-shot examples | demonstrations of the format | sometimes |
| Long-term memory | stored facts about the user or project | per session |
| Retrieved documents | chunks from search or a vector index | every call |
| Conversation history | earlier turns, tool results | grows every turn |
| User query | the current request | every call |
| Output reserve | room for the answer | fixed |
The context window limit covers input and output, so the output reserve comes out of the same budget.
Big windows are not free. Windows of hundreds of thousands of tokens, and some of a million or more, are common in 2026. But every input token costs money, adds prefill time before the first output token, and occupies KV cache memory on the server. And more text is not more understanding: measured accuracy on long-context tests generally drops as irrelevant text grows, and models often use information at the start and end of a long input better than information in the middle. That last effect is called lost in the middle (Liu et al., 2023, "Lost in the Middle: How Language Models Use Long Contexts"). Its size varies by model and task and has shrunk in newer models, but the safe assumption is still: less, better-placed context beats more context.
Four moves cover most of context engineering.
- Select. Include only what this call needs: the top few relevant chunks, not the top twenty; the tools for this intent, not all of them.
- Order. Stable material first (system prompt, tool schemas), so prefix caching can reuse it. The most relevant retrieved items at the edges of the retrieved block, the least relevant in the middle. The question last, close to where generation starts.
- Compress. Replace old conversation turns with a summary; trim long tool outputs to the fields that matter; paginate large results.
- Remember outside the window. Facts that must survive across sessions go into a store (files, a database, a vector index) and are retrieved when relevant. Long-term memory is just retrieval over your own notes.
Key distinctionContext engineering versus prompt engineering. Prompt engineering is how you phrase the instructions. Context engineering is everything that ends up in the window and where it sits: retrieval, tool selection, history management, memory, and ordering. In agents and RAG systems, the second matters more.
2. Visualization

Synthetic component sizes for a coding assistant (token counts estimated at 1.3 tokens per word). Left: one call uses under a tenth of the window. Right: what happens across a conversation.
The left panel is why "we have a big window" is the wrong question. The window is mostly empty; the problem is not space, it's what you pay for and what the model attends to. The right panel shows the naive approach (all 30 tools, 8 documents, full history) growing by one turn's worth of tokens every call, forever. The engineered approach (10 relevant tools, 3 documents, the last 4 turns plus a 150-token summary) stops growing once compaction kicks in.
Try it (predict first, then edit context_engineering.py):
- Set
KEEP = 10in the engineered setup. Predict where the green line flattens and its height. - Double
TURNto 500 tokens. Predict which line changes slope and by how much. - In the ordering demo, add a second document with score 1.0. Predict where edges-first ordering puts it.
- Change the query in the tool-selection demo to mention "calendar" and "email". Predict the number of selected tools and tokens saved.
3. The math
Symbols
| Symbol | Meaning | Unit |
|---|---|---|
| context window size | tokens | |
| output reserve | tokens | |
| fixed input per call: system + tools + documents + query | tokens | |
| tokens added to history per turn | tokens | |
| number of recent turns kept verbatim | count | |
| size of the running summary | tokens | |
| input tokens for call | tokens | |
| number of calls in the conversation | count |
Budget
The input must fit in what's left after the reserve:
For the synthetic assistant: , , so the input budget is . Components: , which is of the window. Room left: .
Per-call input, naive
Keeping full history, call re-reads earlier turns:
This is the same arithmetic series as the agent loop: total billed input grows with .
Per-call input, with compaction
Keeping the last turns plus an -token summary:
Once , is constant, so the total grows linearly in .
Worked example: 20 calls
Naive: 30 tools × 150 = 4,500; 8 documents × 500 = 4,000; system 500; query 200. . .
- Call 1: . Call 20: .
- Total: .
Engineered: 10 tools × 150 = 1,500; 3 documents × 500 = 1,500; system 500; query 200. . , .
- Calls 1 to 5 (history 0, 250, 500, 750, 1,000): .
- Calls 6 to 20 (15 calls), each : .
- Total: , which is of the naive bill.
Selection (fewer tools and documents) cut the fixed part from 9,200 to 3,700. Compaction cut the growing part. Both changes also leave fewer distractors in the window.
Edges-first ordering
Sort items by relevance score, best first: . Put the odd ranks at the front and the even ranks, reversed, at the back:
With six documents scored , the order is : the two best at the two edges, the weakest in the middle. The relevance score here is simple word overlap, the fraction of query words in the document: for "refund window for damaged items" and "damaged items ship back free", 2 of 5 query words match, so .
4. Implementation
Build it from scratch
No model call is needed: the subject is what you would send. Start with a token counter. Real counts come from the model's tokenizer; 1.3 tokens per English word is a planning estimate.
def count_tokens(text):
"""Rough estimate. Use the provider's token-count endpoint or tokenizer in production."""
return int(len(text.split()) * 1.3)Predict: is this estimate likely to be high or low for source code full of symbols?
The budget object reserves output space up front and trims each component to the lesser of its cap and what is left. Components added first get space first, which is why you add the essentials first.
class ContextBudget:
def __init__(self, window, reserve_output):
self.available = window - reserve_output # output tokens share the window
self.used = {}
def add(self, name, text, cap=None):
limit = min(cap or 10**9, self.available - sum(self.used.values()))
words = text.split()[: int(limit / 1.3)] # keep the words that fit (1.3 tokens/word)
self.used[name] = count_tokens(" ".join(words))
return " ".join(words)Predict: with an 8,000 window, 1,000 reserve, a 400-word system prompt, and 3,000 words of documents capped at 2,000 tokens, how many tokens remain? (Script: 4,481.)
Ordering: score relevance, then place the best items at the edges.
def score_relevance(query, docs):
q = set(query.lower().split())
return [len(q & set(d.lower().split())) / len(q) for d in docs]
def edges_first(items, scores):
"""Best item first, second best last, weakest in the middle."""
ranked = [it for _, it in sorted(zip(scores, items), key=lambda p: -p[0])]
return ranked[::2] + ranked[1::2][::-1]Predict: with only two documents, does edges-first change anything?
History compaction keeps the last few turns verbatim and replaces the rest with a summary. Here the summary is a stub; in production it's a cheap model call ("summarize decisions, open questions, and user preferences").
def compact_history(turns, max_tokens, keep_recent=4, summarize=None):
summarize = summarize or (lambda ts: "Summary of earlier turns: " + "; ".join(t[:40] for t in ts))
total = sum(count_tokens(t) for t in turns)
if total <= max_tokens:
return turns
old, recent = turns[:-keep_recent], turns[-keep_recent:]
return [summarize(old)] + recent # a real system asks an LLM for the summaryPredict: if you re-summarize the summary every turn, what slowly happens to early details?
Tool selection keeps only the tools for the areas the query mentions. Real systems classify intent with a small model or embed tool descriptions and retrieve the closest ones.
def select_tools(query):
hits = {area for area in set(TOOLS.values()) if area in query.lower() or (area == "files" and "file" in query)}
return [t for t, area in TOOLS.items() if area in hits]Predict: the query "search the git log and read the file that changed" selects which areas, and how many of the 30 tools?
Use the library
In production, each piece maps to a real component:
- Token counts: the provider's token-counting endpoint or the model's tokenizer (for example
tiktokenfor OpenAI models, or the Anthropic API's token-count endpoint). - Retrieval and ordering: your RAG pipeline returns ranked chunks; apply a relevance threshold and a top-k, then order. A reranker gives better scores than word overlap.
- Compaction: a cheap model call that summarizes old turns, triggered by a token threshold. Some agent SDKs and APIs offer built-in context editing or compaction that clears old tool results automatically.
- Memory: a file the agent reads at session start (for example a project notes file), a key-value store of user preferences, or a vector index of past sessions queried like RAG.
- Tool selection: connect tools through MCP servers and expose only the relevant servers or tools per task.
The assembled context goes to the model as the system prompt, tool list, and messages of an ordinary API call.
Run it
cd guide
uv run code/28-prompting-and-context/context_engineering.pyKey output:
total input 12,200 (9.5% of the window)
edges-first order (what the prompt sees, top to bottom):
1.00 refund window is 30 days for damaged items
0.20 refund goes to the original card
...
0.40 damaged items ship back free
14 turns, 1,120 tokens -> 5 messages, 416 tokens
all tools 30 x 150 = 4,500 tokens; selected 10 -> 1,500 tokens (67% saved)
naive (30 tools, 8 docs, full history) call 1 9,200 call 20 13,950 total 231,500
engineered (10 tools, 3 docs, compacted) call 1 3,700 call 20 4,850 total 93,750Full script: code/28-prompting-and-context/context_engineering.py.
5. Engineering
Layout for caching. Put stable content first and in a fixed order: system prompt, tool schemas, long reference documents that rarely change. Append new turns at the end; don't edit or reorder earlier messages. Prefix caches only match byte-identical prefixes, so a timestamp at the top of the system prompt can disable caching for every call.
Retrieval quality beats quantity. Three relevant chunks usually beat ten mediocre ones: fewer tokens, fewer distractors, less for the model to reconcile. Tune top-k and a relevance threshold on a labelled set, the same way you tune chunk size in RAG.
Repeat the critical instruction. For long inputs, put the key instruction both before the material and right before the question ("Using only the documents above, answer…"). Placing the question after long documents generally works better than before them.
Compaction loses information. A summary drops details, and summaries of summaries drift. Keep task-critical facts (IDs, decisions, constraints) in a structured notes block that is updated, not re-summarized. Let the agent re-fetch old tool outputs by ID rather than keeping them all.
Memory is untrusted input. Anything written to long-term memory from user or web content can carry injected instructions into future sessions. Store memories with provenance, let users view and delete them, and never treat a memory as a permission (see Securing AI systems).
Measure it. Log tokens per component per call. Build a small evaluation set where the answer depends on one fact placed at different depths in a long context, and check accuracy for your model before trusting long inputs.
Common mistakes
- Treating a large window as permission to include everything. You pay for every token on every call, and accuracy can drop.
- Forgetting the output reserve, so long inputs cut the answer short.
- Putting changing content (dates, user names) at the top of the system prompt, which defeats prefix caching.
- Exposing dozens of overlapping tools; the model picks the wrong one more often.
- Summarizing history with no record of key decisions, so the agent repeats work or contradicts itself.
- Storing everything in memory and retrieving it all; long-term memory needs the same relevance filtering as RAG.
6. Knowledge check
Quiz: Context engineering
How does context engineering differ from prompt engineering?
Practice exercise. A support bot has a 1,000-token system prompt, 2,000 tokens of tool schemas, and 3,000 tokens of retrieved documents per call, plus a 300-token query. Each turn adds 400 tokens of history. Compute total input tokens over 10 calls with full history, then with compaction that keeps the last 3 turns plus a 200-token summary.
Solution
. .
Full history: .
Compaction, , : calls 1 to 4 have history , total . Calls 5 to 10 (6 calls) each have , total . Sum: .
Only a 9% saving: history is small next to the fixed 6,300. The bigger lever here is : retrieving 2 documents instead of 6 and selecting tools would cut far more.
Implementation challenge. Add a "needle" test to the script: build contexts of 20 documents where one contains a planted fact, place it at each position 1 to 20, and for each position record the fact's position as a fraction of the context. Then plug in a real model client and measure accuracy by position on 10 planted facts.
Solution sketch
Generate 19 filler documents (synthetic, on unrelated topics) and one with a planted fact ("The vault code is 4817"). For each position, build the context, append the question "What is the vault code?", call the model at temperature 0, and check whether the answer contains the code. Repeat with 10 different facts and fillers, and plot accuracy against position. If the curve dips in the middle for your model, use edges-first ordering and fewer documents; if it's flat at your context sizes, you have evidence that ordering matters less for this model.
Summary
- The context window holds everything the model uses: system prompt, tools, examples, memory, retrieved documents, history, the query, and the output reserve.
- Long windows are available but not free: every token costs money and latency, and accuracy can drop on facts buried in long inputs.
- Select (fewer, more relevant items), order (stable prefix first, best items at the edges, question last), compress (summaries, trimmed tool output), and remember outside the window (retrieval over stored notes).
- Full history makes total input grow with T²; compaction makes per-call input constant after a few turns.
- Measure tokens per component and test long-context accuracy for your model instead of assuming it.
Next: The RAG pipeline
Related: RAG pipeline · Prompt engineering · Few-shot and chain-of-thought · LLM inference · Reliable agents
Parts of this lesson are adapted from AI Engineering from Scratch by Rohit Ghumare (MIT license, notice): Context Engineering: Windows, Budgets, Memory, and Retrieval.
Interview angle
What is context engineering, and how is it different from prompt engineering?
Prompt engineering is how you word the instructions. Context engineering is deciding everything that goes into the window for each call and where it sits: which retrieved chunks, which tools, how much history, what long-term memory, which examples, and how much room to leave for the output. In a chatbot with one prompt, they're nearly the same thing. In RAG systems and agents, the context is assembled dynamically on every call and changes far more than the instructions do, so most quality and cost problems live there: the right document wasn't retrieved, it was buried among irrelevant ones, old turns crowded out instructions, or too many similar tools confused selection. The core moves are select, order, compress, and store outside the window.
The model has a one-million-token window. Why not put the whole knowledge base in every call?
Three reasons. Cost: you pay for every input token on every call, so a million-token context at a few dollars per million tokens is dollars per question instead of fractions of a cent. Latency: prefill time grows with input length, so the first token arrives much later, and the server holds a large KV cache per request, which cuts throughput. Quality: accuracy on long-context tests generally falls as irrelevant material grows, and facts in the middle of a long input are often used less reliably than facts near the edges. Long contexts are great for one-off analysis of a single large document. For repeated questions over a knowledge base, retrieval of a few relevant chunks is cheaper, faster, and usually more accurate.
An agent gets worse the longer a session runs. What do you look at?
I'd log tokens per component per call and look at what's growing. Usually it's history: raw tool outputs and old turns pile up until instructions and key facts are a small fraction of the context, or until the window overflows and something gets truncated silently. I'd check whether the system prompt or task statement is still intact, whether old tool results are being kept verbatim, and whether summaries are dropping decisions so the agent repeats work. Fixes: trim tool outputs to the needed fields, keep the last few turns verbatim and summarize the rest, maintain a structured notes block of decisions and constraints that is updated rather than re-summarized, and let the agent re-fetch old results by ID. Then I'd add a long-session case to the evaluation set.
Fixed context is 6,000 tokens and each turn adds 500. Roughly how many input tokens over 30 turns, and what would you change?
With full history, call t reads 6,000 plus 500 times t minus 1. The total is 30 times 6,000, which is 180,000, plus 500 times 30 times 29 over 2, which is 500 times 435, or 217,500. So about 397,500 input tokens, more than half from re-reading history. Keeping the last 5 turns plus a 300-token summary caps each call at 6,000 plus 2,500 plus 300, or 8,800, so the total becomes roughly 30 times 8,800 minus a little for the early turns, around 255,000. Then I'd attack the fixed 6,000: select tools per intent and retrieve fewer, better chunks. And I'd keep the stable prefix byte-identical so prefix caching discounts what remains.