Showing posts with label Prompt Engineering. Show all posts
Showing posts with label Prompt Engineering. Show all posts

Sunday, May 31, 2026

Context Engineering as Infrastructure: The 2026 Field Guide

A build pipeline assembling context blocks into a model's input window

Introduction

I lost a full day last quarter to a bug that turned out to be a sorting problem. Our support agent had started giving subtly stale answers, quoting a refund policy we had retired months earlier. The retrieval was fine. The policy doc in the vector store was current. The model was the same one that had worked the week before. The bug was that our context assembler appended retrieved chunks in similarity order, and a high-similarity but outdated changelog snippet kept landing in the last few hundred tokens before the question, right where the model pays the most attention. The model was not wrong. It was answering the context we actually gave it, which was not the context I thought we were giving it.

That day reframed how I think about this work. I had spent weeks treating the prompt as the thing to tune, when the real artifact was the pipeline that decided what went into the prompt. That pipeline is what the field now calls context engineering, and in 2026 it has become the defining discipline of building with LLMs, the practice of architecting the entire information environment for a model rather than wordsmithing a single instruction (Sombra, AI Context Engineering 2026). Context quality, not context volume, is the limiting factor now (The New Stack, 2026).

This is a field guide to treating context as infrastructure: a pipeline you build, test, and monitor, with the same rigor you give any other production system.

The Problem: The Prompt Was Never the Artifact

Prompt engineering treated the model's input as a string to be crafted. That worked when the input was small and static. It stops working the moment the input is assembled at runtime from many sources: retrieved documents, conversation history, tool outputs, user profile, system rules. At that point the interesting decisions are no longer about wording. They are about selection, ordering, compression, and provenance.

Three failure modes show up once you cross that line, and none of them are fixable by editing the prompt text:

  1. Position blindness. Models attend unevenly across their window. Critical facts buried in the middle of a long context get underweighted, a pattern robust enough that retrieval order materially changes answers. My stale-refund bug was exactly this.

  2. Context dilution. Stuffing more into the window feels safer but is not. Every irrelevant token competes with the relevant ones for attention and pushes up cost and latency. Beyond a point, more context makes answers worse, not better.

  3. Untraceable answers. When something goes wrong, you need to know which tokens produced the answer. If your assembly step keeps no record of what it put in the window and why, every incident becomes an archaeology dig instead of a log query.

Architecture diagram of a context assembly pipeline: sources feeding a curate-rank-compress-assemble stage into the model

The shift is from asking what I should say to the model toward asking what information environment I should construct for it, and how I know I constructed the right one. That second question is an engineering question, and it has engineering answers.

How It Works: The Assembly Pipeline

Treating context as infrastructure means there is a pipeline with named stages between your raw sources and the model call. Here is the shape of it.

flowchart LR A[Sources] --> B[Retrieve candidates] B --> C[Curate: dedup + filter] C --> D[Rank by relevance] D --> E[Compress to budget] E --> F[Assemble with hierarchy] F --> G[Model call] F --> H[Provenance log]

The stage that earns its keep first is curation, because it is where you remove the noise that would otherwise dilute everything downstream. Deduplication and filtering before ranking mean the ranker is choosing among genuinely distinct, plausibly-relevant candidates rather than near-duplicate chunks that crowd each other out. Smart summarization that keeps the critical content while pruning redundancy is what separates a system that stays usable over long sessions from one that degrades (Digital Applied, Agent Reliability Playbook 2026).

The second load-bearing stage is assembly with hierarchy. Headers segment context into addressable units, and a model working through clearly-sectioned context navigates to what is relevant for the task (Packmind, Context Engineering Best Practices 2026). Order matters too: put the most decision-relevant material where the model attends most, which in practice means near the question, not buried in the middle.

Implementation Guide: Building the Pipeline

Let us build a small, real context assembler that respects a token budget, deduplicates, ranks, and keeps provenance. Start with the budget, because every other decision is a negotiation against it.

from dataclasses import dataclass, field

@dataclass
class Chunk:
    source: str
    text: str
    score: float          # relevance, 0..1
    tokens: int

@dataclass
class AssemblyResult:
    blocks: list[Chunk]
    used_tokens: int
    dropped: list[str] = field(default_factory=list)

def estimate_tokens(text: str) -> int:
    # Rough heuristic: ~4 chars per token. Swap for a real tokenizer in prod.
    return max(1, len(text) // 4)

Next, deduplicate near-identical chunks before ranking. The cheap, effective approach is shingled Jaccard similarity: if two chunks share most of their word-shingles, keep the higher-scored one.

def shingles(text: str, n: int = 5) -> set[str]:
    words = text.lower().split()
    return {" ".join(words[i:i + n]) for i in range(len(words) - n + 1)}

def dedupe(chunks: list[Chunk], threshold: float = 0.8) -> list[Chunk]:
    kept: list[Chunk] = []
    for c in sorted(chunks, key=lambda x: x.score, reverse=True):
        c_sh = shingles(c.text)
        dup = False
        for k in kept:
            k_sh = shingles(k.text)
            if c_sh and k_sh:
                jac = len(c_sh & k_sh) / len(c_sh | k_sh)
                if jac >= threshold:
                    dup = True
                    break
        if not dup:
            kept.append(c)
    return kept

Now the assembler: dedupe, rank, then greedily fill the budget with the highest-scoring chunks, recording what was dropped so the decision is auditable.

def assemble(chunks: list[Chunk], budget_tokens: int) -> AssemblyResult:
    deduped = dedupe(chunks)
    ranked = sorted(deduped, key=lambda c: c.score, reverse=True)

    blocks: list[Chunk] = []
    used = 0
    dropped: list[str] = []
    for c in ranked:
        if used + c.tokens <= budget_tokens:
            blocks.append(c)
            used += c.tokens
        else:
            dropped.append(f"{c.source} (score={c.score:.2f}, {c.tokens} tok)")

    # Position the highest-scoring block LAST, nearest the question.
    blocks.sort(key=lambda c: c.score)
    return AssemblyResult(blocks=blocks, used_tokens=used, dropped=dropped)

Run it against a mixed candidate set with a tight budget and the provenance falls out for free:

$ python assemble.py --budget 800
[assemble] 11 candidates -> 7 after dedupe -> 5 fit in 800 tokens
  kept:
    policy/refunds-v3.md      score=0.94  120 tok   (placed nearest question)
    faq/refund-window.md      score=0.88  140 tok
    policy/shipping.md        score=0.71  160 tok
    kb/returns-process.md     score=0.66  180 tok
    chat/turn-14.md           score=0.61  190 tok
  dropped (over budget):
    changelog/2025-q3.md      score=0.83  220 tok   <-- the stale snippet, correctly dropped
    faq/refund-window.md      (duplicate of kept chunk)
used 790/800 tokens

That changelog/2025-q3.md line is the bug from my introduction, now visible and handled. Because dedupe and the budget log every decision, the stale snippet either gets dropped or, if it does sneak in, shows up in a log I can grep instead of a mystery I have to reproduce.

Decision Flow: What Goes in the Window

Not every available token should be spent. The assembler needs a policy for what is worth including, and that policy is itself a guardrail against dilution.

flowchart TD A[Candidate chunk] --> B{Score above floor?} B -->|no| X[Drop: not relevant enough] B -->|yes| C{Duplicate of a kept chunk?} C -->|yes| X2[Drop: redundant] C -->|no| D{Fits in remaining budget?} D -->|yes| E[Include + log provenance] D -->|no| F{Higher score than a kept chunk?} F -->|yes| G[Evict lower-scored, include this] F -->|no| X3[Drop: budget full]

The rule that does the most work is the relevance floor. A chunk that scores below the floor never enters the window even if there is budget to spare, because empty budget is cheaper than diluted budget. This is the counterintuitive heart of context engineering: leaving the window partly empty is often the right call. More tokens are not more help.

A Gotcha: When Compression Ate the Answer

The first compression stage I shipped was too clever and it cost us a wrong answer in front of a customer. To fit more into the budget, I summarized each retrieved chunk with a small model before assembly, on the theory that a 50-token summary of a 200-token doc let me fit four times as much. It worked in testing and then failed on a precise question.

The customer asked whether refunds applied to digital goods specifically. The relevant doc spelled out that refunds apply to all physical goods within the standard return window, and that digital goods are non-refundable. My summarizer compressed that down to a generic line about refunds applying within the return window, which is true in spirit and catastrophically wrong for this question. The summary dropped the exact qualifier the question hinged on.

$ python debug_answer.py --q "are digital goods refundable?"
retrieved: policy/refunds-v3.md (full): physical goods within return window;
           digital goods are non-refundable.
assembled: policy/refunds-v3.md (summary): refunds apply within return window.
model answer: Yes, you can request a refund.   <-- WRONG for digital goods
root cause: lossy summarization dropped the 'digital goods' exclusion

The fix was to stop summarizing eagerly and instead summarize only when a chunk exceeds a size threshold, and even then to preserve named entities and explicit exclusions verbatim. Better still, for high-stakes factual chunks, I now pass them through whole and spend the budget I save by dropping low-score chunks entirely. The lesson: compression is a tradeoff against fidelity, and the tokens you save mean nothing if you compress away the one clause the answer depended on. Test your compressor against precise, qualifier-heavy questions, not just broad ones.

Scoring Beyond Similarity

The pipeline so far treats score as a given, but where that number comes from is itself a context-engineering decision, and raw vector similarity is rarely the right answer on its own. Cosine similarity tells you a chunk is semantically near the query. It does not tell you the chunk is fresh, authoritative, or the kind of source this question needs. A high-similarity but stale changelog, the exact villain of my refund bug, scores well on similarity and badly on everything that actually matters.

A more honest score blends similarity with signals you already have. Recency, source authority, and a light penalty for length all push the ranker toward chunks that are not just topically close but actually trustworthy for the task.

import math

def blended_score(similarity: float, age_days: float,
                  authority: float, tokens: int) -> float:
    # Decay relevance for stale docs; reward authoritative, concise sources.
    recency = math.exp(-age_days / 180.0)        # half-life ~6 months
    length_penalty = 1.0 / (1.0 + tokens / 500)  # gently disfavor bloat
    return 0.6 * similarity + 0.25 * recency + 0.15 * authority * length_penalty

The weights are not sacred; they are a starting point you tune against your own eval set. What matters is that the score the assembler ranks on encodes more than topical nearness. Re-running the earlier example with blended scoring, the stale changelog falls below the relevance floor on its own, before the budget stage ever has to drop it.

$ python rank.py --query "are digital goods refundable?" --blended
  policy/refunds-v3.md   sim=0.91 age=12d  auth=1.0  -> 0.93  keep
  faq/refund-window.md   sim=0.88 age=40d  auth=0.8  -> 0.85  keep
  changelog/2025-q3.md   sim=0.83 age=240d auth=0.4  -> 0.61  below floor (0.65), dropped
floor=0.65: 1 stale chunk dropped before budget stage

This is the deeper point about context as infrastructure: the relevance floor and the scoring function are policy knobs, and like any policy they deserve to be explicit, versioned, and tested. A team that hardcodes top-k cosine similarity has made a scoring decision by accident. A team that writes blended_score has made one on purpose, and can change it deliberately when the data shifts. The difference shows up months later, when a stale source starts creeping into answers and one team can adjust a weight while the other is reverse-engineering why retrieval "suddenly got worse."

The same discipline extends to negative signals. If a source has been flagged as deprecated, the cleanest fix is not to delete it from the store but to give it an authority of zero so it can never outrank a live document, while still being available if a user explicitly asks about historical policy. Encoding that as a score is far more robust than hoping it never gets retrieved.

Comparison and Tradeoffs

How do the common context strategies compare in practice? Here is my scoring after a year of running this pipeline.

Strategy Controls dilution Handles position Traceable Latency cost Verdict
Stuff everything in the window No No No High Feels safe, degrades quality
Tune the prompt wording only No No No None Necessary, not the real lever
Top-k retrieval, raw order Weak No Weak Medium The common default, leaves wins on the table
Dedupe + rank + budget Yes Partial Yes Low The baseline worth building
Eager summarize-everything Partial No Weak Medium Risks dropping the key clause
Curate + rank + position + provenance Yes Yes Yes Low The pipeline you actually want
flowchart LR subgraph Prompt["Prompt-engineering era"] P1[Craft the string] --> P2[Hope retrieval helps] --> P3[Debug by re-reading] end subgraph Context["Context-engineering era"] C1[Build the pipeline] --> C2[Curate + rank + budget] --> C3[Debug by grepping provenance] end Prompt -.the input grew dynamic.-> Context
Comparison visual: prompt-engineering era versus context-engineering era

The core tradeoff is fidelity versus density. Every compression and every dropped chunk buys you room and risks losing something. The discipline is to make those tradeoffs explicit and logged rather than implicit and invisible. A pipeline that records what it dropped and why turns a class of silent quality bugs into visible, debuggable events, which is the whole reason to treat context as infrastructure in the first place.

Production Considerations

A few things that matter once the pipeline is live.

Log provenance on every call. Record which chunks went into each window, their scores, and what was dropped. This is your single most useful artifact when an answer goes wrong, and it is nearly free to produce. Treat the context window like any other request you would trace.

Monitor budget utilization and drop rates. If you are constantly dropping high-score chunks, your budget is too small or your retrieval is too noisy. If your window is half empty on hard questions, your relevance floor may be too high. Both are dashboards, not guesses.

Version your assembly logic. Changing the ranker or the compressor changes every answer the system gives. Treat assembly changes like schema migrations: version them, and be able to replay old questions against a new pipeline to catch regressions before users do.

Test against qualifier-heavy questions. The questions that break context pipelines are the precise ones, where a single dropped clause flips the answer. Keep a suite of these and run it on every pipeline change.

Exploit the cache by ordering for stability. Most providers cache a common prefix of the input, so the layout of your window has a direct cost consequence. Put the stable material first, the system rules and long-lived reference docs that rarely change between requests, and the volatile material last, the retrieved chunks and the user turn. A pipeline that reshuffles its whole window on every request throws away the cache and pays full price each time; one that keeps a stable prefix can see large reductions in cost and latency on repeat traffic. This is a place where the context-as-infrastructure framing pays off directly: the same provenance log that tells you what went into the window also tells you how much of it was cacheable, which turns a vague sense that the LLM bill is high into a specific diagnosis: prefix stability is low, and here is the chunk that keeps invalidating it.

Conclusion

The prompt was never the real artifact. The pipeline that assembles what the model sees is, and in 2026 building that pipeline well is the skill that separates reliable LLM systems from flaky ones. Context engineering is infrastructure work: selection, ordering, compression, and provenance, each a stage you can build, test, and monitor.

Start with a budget and a provenance log, because together they make every assembly decision explicit and auditable. Add deduplication and a relevance floor to fight dilution. Position your strongest material where the model attends most. Compress carefully, and never compress away the clause the answer depends on. Do that, and the next time an answer goes stale you will find the cause in a log line instead of losing a day to it, which is exactly the trade I wish I had made before that refund bug.

Working code for the full assembler, the deduper, and a provenance-logging harness lives in the companion repo: github.com/amtocbot-droid/amtocbot-examples/tree/main/262-context-engineering.


Get the next one

I send a weekly engineering note with one production failure, the debug trail, and the code or checklist that came out of it. No spam, unsubscribe anytime.

👉 Subscribe (free)

Reader challenge: inspect one LLM request path in your own system and write down which chunks entered the context window, which chunks were dropped, and why. Reply to the email or comment with the failure mode you found.


Revision History

Date Summary Old Version
2026-06-07 Added the newsletter signup and reader-challenge block so this recent context-engineering post feeds the owned audience funnel. View previous version

Sources

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-06-03 · Updated: 2026-06-07 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Tuesday, April 14, 2026

Context Engineering: The Skill That Replaced Prompt Engineering

Hero Image

Prompt engineering taught you what to ask — context engineering teaches you what the model needs to know before it answers, and that difference is what separates toy demos from production AI systems.

For the past few years, the AI community obsessed over prompt engineering. Craft the perfect instruction. Use the magic words. Add "think step by step." Chain your prompts. There were courses, certifications, and job titles built around the art of asking AI the right question. And it worked — to a point.

But production AI systems kept failing in ways that better prompts couldn't fix. Chatbots would forget important details mid-conversation. RAG pipelines would retrieve the wrong chunks and confidently hallucinate. Agents would lose track of their task state. The model wasn't stupid; it was blind. It was answering the question you asked while missing the information it needed.

That gap between what the model knows and what it needs to know has a name now: context engineering. And in 2026, it has become the core competency separating developers who build AI systems that actually work from those who are still fighting with their system prompts.

This post is part of the AI Agent Engineering: Complete 2026 Guide. Context engineering is one of the core layers of a production agent stack — see the guide for how it fits into the full picture.

The Problem: Why 70% of LLM Failures Aren't the Model's Fault

According to a 2026 analysis by The New Stack, over 70% of production LLM application failures trace back to context problems — not model limitations. The model was capable of answering correctly. It simply didn't have what it needed in its context window to do so.

This finding reshapes how we should think about debugging AI systems. When your chatbot gives a bad answer, the instinct is to improve the prompt. Rewrite the instruction. Add more examples. Make it more explicit. But if the real problem is that the model is missing relevant background information, has too much irrelevant noise competing for attention, or is working from stale data — no amount of prompt refinement will help.

Consider a customer support bot. A user asks: "Why was my last order delayed?" The model has a beautiful system prompt explaining its role, its tone, its capabilities. But the user's order history isn't in the context. The shipping status isn't there. The warehouse disruption from last Tuesday isn't there. The model hallucinates a generic answer about carrier delays. The prompt was fine. The context was empty.

There are four recurring failure modes that context engineering addresses:

Context overflow — The context window fills up with accumulated conversation history, old tool outputs, or verbose retrieved documents. Important information gets pushed out. Attention dilutes. The model starts "forgetting" things that were mentioned earlier.

Stale context — The model is working from information that was accurate when injected but is no longer current. Cached user profiles, outdated product descriptions, old document versions. The model answers confidently based on facts that have changed.

Irrelevant context flooding — RAG retrieval returns chunks that are topically adjacent but not actually useful for the current question. The model's attention gets pulled toward noise. Answer quality degrades not because information is missing but because too much irrelevant information is present.

Missing context — The simplest failure. The model simply doesn't have information it needs. No retrieval was triggered, the conversation history was truncated, or the relevant data was never surfaced into the window in the first place.

All four of these are engineering problems. They require systems thinking, not prompt tweaking.

Architecture Diagram

Technical Breakdown: What Context Engineering Actually Is

Context engineering is the practice of strategically managing what goes into an LLM's context window — what information is included, how it's structured, in what order, at what level of compression, and when it's refreshed.

The context window is not a dump. It's a carefully curated working memory. Every token you put in the window costs attention and inference compute. Every irrelevant token competes with relevant ones. The model's ability to reason over a context degrades as that context becomes noisier, longer, or more redundant.

Context engineering operates across five primary dimensions:

1. Retrieval Strategy

Not all retrieval is equal. Naive RAG concatenates the top-K chunks by cosine similarity and calls it done. Engineered context retrieval asks: is similarity the right signal here? Maybe recency matters more. Maybe the user's stated intent should weight the retrieval differently than the literal query terms. Maybe you need to retrieve context from multiple knowledge bases and interleave them intelligently.

Hybrid retrieval (dense + sparse), re-ranking with a cross-encoder, query expansion, and HyDE (Hypothetical Document Embeddings) are all tools in the context engineer's toolkit. Each controls what information makes it into the window.

2. Context Compression

More tokens in the window does not mean more useful information. Long documents, verbose conversation history, and redundant retrieved chunks all need to be compressed before injection. Summarization, key-point extraction, and chunk deduplication reduce token consumption while preserving semantic density.

The goal is maximum information per token, not maximum tokens.

3. Ordering and Recency Weighting

Where you put information in the context window matters. LLMs exhibit a "lost in the middle" phenomenon — information at the beginning and end of long contexts is better attended to than information buried in the middle. Critical context should be placed close to the query. Background information can go earlier. Ordering is not cosmetic.

Recency weighting applies to conversation history specifically. The last few turns are almost always more relevant than what was said ten turns ago. Selectively compressing old history while preserving recent turns maintains coherent conversation without blowing the token budget.

4. Hierarchical Context Architecture

Production systems benefit from thinking about context in layers:

  • Global context: System-level information that applies to every interaction — persona, capabilities, hard constraints, domain knowledge. Typically 200-500 tokens.
  • Session context: User-specific information that applies to this conversation — user profile, preferences, prior session summaries, account state. Injected at session start.
  • Turn context: Information relevant to this specific query — retrieved documents, tool outputs, recent conversation history. Dynamically assembled per turn.

Each layer has different freshness requirements, different token budgets, and different strategies for compression and retrieval.

5. Relevance Filtering

Before injecting any retrieved content, filter it for actual relevance. A relevance score of 0.72 from your vector database doesn't tell you whether that chunk actually helps answer the current question. Post-retrieval filtering using LLM-as-judge, semantic similarity to the query intent (not just the query terms), or rule-based filters (e.g., date ranges, entity matching) can dramatically reduce context noise.

flowchart TD Q[User Query] --> QE[Query Expansion\n+ Intent Detection] QE --> RET[Hybrid Retrieval\nDense + Sparse] RET --> RANK[Cross-Encoder\nRe-Ranking] RANK --> FILT[Relevance Filter\nScore Threshold] FILT --> COMP[Chunk Compression\n+ Deduplication] SYS[System Prompt\nGlobal Context] --> ASSEMBLE SESS[Session Context\nUser Profile + History Summary] --> ASSEMBLE COMP --> ASSEMBLE[Context Assembly\nOrdering + Token Budget] HIST[Recent Turn History\nLast N Turns] --> ASSEMBLE ASSEMBLE --> WIN[Context Window\nFinal Payload] WIN --> LLM[LLM Inference] LLM --> ANS[Response] style WIN fill:#2d6a4f,color:#fff style LLM fill:#1b4332,color:#fff style ANS fill:#40916c,color:#fff

How Context Engineering Differs from Prompt Engineering

The confusion between the two is understandable — both deal with what you send to the model. But the distinction is fundamental.

Prompt engineering is about the instruction: the task description, the output format request, the few-shot examples, the chain-of-thought nudge. It assumes the model has what it needs and focuses on directing how the model should process and respond. Prompt engineering asks: "How do I phrase this?"

Context engineering is about the information: what background knowledge, retrieved documents, conversation history, user state, and domain data the model has available when it processes the prompt. It assumes the instruction is clear and focuses on ensuring the model has the right inputs. Context engineering asks: "What does the model need to know?"

In a well-architected system, both matter. But they have different leverage points. A mediocre prompt with excellent context often outperforms an excellent prompt with mediocre context. The model is fundamentally a reasoning engine — it reasons over what's in its window. The quality of the window determines the ceiling on answer quality.

Comparison
Dimension Prompt Engineering Context Engineering
Focus How the model is instructed What information the model has
Scope System prompt + task framing Retrieved docs, history, user state, dynamic injections
When it matters most Simple tasks, instruction-following benchmarks Multi-turn systems, RAG, agents, personalization
Primary failure mode Unclear instructions, wrong format Missing context, context overflow, stale data
Core skill Writing clear instructions, few-shot examples Retrieval design, compression, token budgeting
Tooling Prompt templates, prompt versioning Vector DBs, chunking pipelines, context managers
Iteration speed Fast (text edits) Slower (pipeline changes, eval frameworks)
Ceiling Limited by information available Limited by retrieval quality and token budget
2026 relevance Necessary but insufficient The differentiating skill in production AI
flowchart TD START([New User Query]) --> HIST_CHECK{History\nAvailable?} HIST_CHECK -->|Yes| HIST_LEN{History\nLength?} HIST_CHECK -->|No| RETRIEVAL HIST_LEN -->|Short < 5 turns| KEEP_FULL[Keep Full History] HIST_LEN -->|Medium 5-15 turns| COMPRESS_RECENT[Compress Older Turns\nKeep Last 5 Verbatim] HIST_LEN -->|Long > 15 turns| SUMMARIZE[Summarize + Rolling Window\nKeep Last 3 Verbatim] KEEP_FULL --> RETRIEVAL COMPRESS_RECENT --> RETRIEVAL SUMMARIZE --> RETRIEVAL RETRIEVAL{Query Needs\nExternal Context?} RETRIEVAL -->|No - chitchat/general| ASSEMBLE_SIMPLE[Assemble Simple Context\nSystem + History + Query] RETRIEVAL -->|Yes - factual/domain| HYBRID_SEARCH[Hybrid Search\nDense + BM25] HYBRID_SEARCH --> RERANK[Re-rank Top 20\nCross-encoder] RERANK --> TOKEN_CHECK{Chunks Fit\nToken Budget?} TOKEN_CHECK -->|Yes| INJECT_ALL[Inject All Chunks] TOKEN_CHECK -->|No| COMPRESS_CHUNKS[Compress + Deduplicate\nChunks to Budget] INJECT_ALL --> ASSEMBLE_FULL[Assemble Full Context\nSystem + Session + Chunks + History + Query] COMPRESS_CHUNKS --> ASSEMBLE_FULL ASSEMBLE_SIMPLE --> LLM_CALL[Send to LLM] ASSEMBLE_FULL --> LLM_CALL style LLM_CALL fill:#1b4332,color:#fff style ASSEMBLE_FULL fill:#2d6a4f,color:#fff style ASSEMBLE_SIMPLE fill:#2d6a4f,color:#fff

Implementation Guide

Building a Production ContextManager

The following Python class implements a complete context management system. It handles adding messages, compressing history, retrieving relevant documents, and assembling the final context window within a token budget.

import tiktoken
from dataclasses import dataclass, field
from typing import Optional
from openai import OpenAI

# Token estimation using tiktoken (works for GPT-4o, Claude approximation)
enc = tiktoken.get_encoding("cl100k_base")

def count_tokens(text: str) -> int:
    """Estimate token count for a string."""
    return len(enc.encode(text))


@dataclass
class Message:
    """Represents a single turn in conversation history."""
    role: str          # "system", "user", or "assistant"
    content: str
    tokens: int = field(init=False)

    def __post_init__(self):
        self.tokens = count_tokens(self.content)


@dataclass
class ContextConfig:
    """Configuration for context window management."""
    max_tokens: int = 8000          # Hard limit for assembled context
    system_budget: int = 500        # Tokens reserved for system prompt
    session_budget: int = 400       # Tokens reserved for session context (user profile etc.)
    history_budget: int = 2000      # Tokens for conversation history
    retrieval_budget: int = 4000    # Tokens for retrieved documents
    recency_turns: int = 4          # Number of recent turns to always keep verbatim


class ContextManager:
    """
    Manages LLM context window assembly for production systems.

    Handles:
    - Sliding window conversation history with compression
    - Relevance-filtered document injection
    - Token budget enforcement across context layers
    - Global → Session → Turn context hierarchy
    """

    def __init__(
        self,
        system_prompt: str,
        config: Optional[ContextConfig] = None,
        llm_client: Optional[OpenAI] = None,
        session_context: Optional[str] = None,
    ):
        self.system_prompt = system_prompt
        self.config = config or ContextConfig()
        self.client = llm_client  # Used for summarization compression
        self.session_context = session_context or ""

        self.history: list[Message] = []
        self.compressed_summary: str = ""  # Rolling summary of old history
        self.retrieved_docs: list[str] = []

    def add(self, role: str, content: str) -> None:
        """
        Add a new message to conversation history.
        Automatically triggers compression if history budget is exceeded.
        """
        msg = Message(role=role, content=content)
        self.history.append(msg)

        # Check if we've exceeded the history budget
        total_history_tokens = sum(m.tokens for m in self.history)
        if total_history_tokens > self.config.history_budget:
            self._compress_history()

    def _compress_history(self) -> None:
        """
        Compress older history turns into a rolling summary.
        Always preserves the most recent `recency_turns` verbatim.
        The rest gets summarized via LLM call and stored as compressed_summary.
        """
        # Split: keep recent turns verbatim, compress the rest
        recent = self.history[-self.config.recency_turns:]
        to_compress = self.history[:-self.config.recency_turns]

        if not to_compress:
            return

        # Build a text block from older turns for summarization
        history_text = "\n".join(
            f"{m.role.upper()}: {m.content}" for m in to_compress
        )

        if self.compressed_summary:
            # Append to existing summary rather than replacing it
            summary_input = (
                f"Previous summary:\n{self.compressed_summary}\n\n"
                f"New turns to incorporate:\n{history_text}"
            )
        else:
            summary_input = history_text

        if self.client:
            # Use LLM to generate a high-quality summary
            response = self.client.chat.completions.create(
                model="gpt-4o-mini",  # Use a cheap model for compression
                messages=[
                    {
                        "role": "system",
                        "content": (
                            "Summarize this conversation history concisely. "
                            "Preserve: key decisions, important facts stated by the user, "
                            "unresolved questions, and any commitments made. "
                            "Output 2-4 sentences maximum."
                        ),
                    },
                    {"role": "user", "content": summary_input},
                ],
                max_tokens=200,
            )
            self.compressed_summary = response.choices[0].message.content
        else:
            # Fallback: simple truncation (use in testing / no LLM available)
            self.compressed_summary = f"[Earlier conversation compressed. Key context: {history_text[:300]}...]"

        # Replace history with just the recent turns
        self.history = recent

    def retrieve(self, docs: list[str], max_tokens: Optional[int] = None) -> None:
        """
        Inject retrieved documents into the context.
        Enforces token budget — drops lowest-priority docs if over budget.

        Args:
            docs: List of document strings, ordered by relevance (most relevant first).
            max_tokens: Override the configured retrieval budget if provided.
        """
        budget = max_tokens or self.config.retrieval_budget
        self.retrieved_docs = []
        tokens_used = 0

        for doc in docs:
            doc_tokens = count_tokens(doc)
            if tokens_used + doc_tokens <= budget:
                self.retrieved_docs.append(doc)
                tokens_used += doc_tokens
            else:
                # Once we're over budget, skip remaining docs
                # (they're lower relevance anyway since docs are ranked)
                break

    def get_window(self) -> list[dict]:
        """
        Assemble the final context window as a list of messages ready for the LLM API.

        Returns messages in this order:
        1. System prompt (global context)
        2. Session context (user profile, preferences) — injected as system message
        3. Compressed history summary (if any)
        4. Retrieved documents (if any)
        5. Recent verbatim history
        (The caller appends the current user query as the final message.)
        """
        messages = []

        # Layer 1: Global system context
        system_content = self.system_prompt
        if self.session_context:
            # Append session-specific context to system message
            system_content += f"\n\n## User Context\n{self.session_context}"

        messages.append({"role": "system", "content": system_content})

        # Layer 2: Compressed history summary (if exists)
        if self.compressed_summary:
            messages.append({
                "role": "system",
                "content": f"## Earlier Conversation Summary\n{self.compressed_summary}",
            })

        # Layer 3: Retrieved documents
        if self.retrieved_docs:
            docs_block = "\n\n---\n\n".join(self.retrieved_docs)
            messages.append({
                "role": "system",
                "content": f"## Relevant Context\n{docs_block}",
            })

        # Layer 4: Recent verbatim history
        for msg in self.history:
            messages.append({"role": msg.role, "content": msg.content})

        return messages

    def token_usage(self) -> dict:
        """
        Return a breakdown of current token usage across context layers.
        Useful for monitoring and debugging context budget allocation.
        """
        return {
            "system": count_tokens(self.system_prompt + self.session_context),
            "compressed_summary": count_tokens(self.compressed_summary),
            "retrieved_docs": sum(count_tokens(d) for d in self.retrieved_docs),
            "history": sum(m.tokens for m in self.history),
            "total": (
                count_tokens(self.system_prompt + self.session_context)
                + count_tokens(self.compressed_summary)
                + sum(count_tokens(d) for d in self.retrieved_docs)
                + sum(m.tokens for m in self.history)
            ),
        }

This class encapsulates the four core operations of context engineering: add() for managing history with automatic compression, _compress_history() for rolling summarization, retrieve() for budget-aware document injection, and get_window() for assembling the final layered context payload.

The key insight in this implementation is the separation of concerns. History compression is triggered automatically — the caller doesn't need to think about it. Retrieved documents are prioritized by order (most relevant first) and cut at the budget boundary. The final assembly follows a strict hierarchy that puts global context first, session context second, and turn-specific content last.

Engineered RAG Context Assembly vs Naive Concatenation

The second critical pattern is the difference between how naive RAG systems and engineered context systems assemble retrieved content. This example shows both approaches side by side, then demonstrates the quality gap.

import numpy as np
from sentence_transformers import SentenceTransformer, CrossEncoder
from typing import NamedTuple

# Models for embedding and re-ranking
embedder = SentenceTransformer("all-MiniLM-L6-v2")
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")


class Chunk(NamedTuple):
    text: str
    source: str
    date: str        # ISO date string for recency weighting
    score: float     # Embedding similarity score


def naive_rag_context(query: str, chunks: list[Chunk], top_k: int = 5) -> str:
    """
    NAIVE APPROACH: Embed query, take top-K by cosine similarity, concatenate.
    Problems:
    - No re-ranking for actual query relevance
    - No deduplication of similar chunks
    - No token budget enforcement
    - No ordering strategy (important context may land in the "lost in the middle" zone)
    - All chunks treated equally regardless of recency
    """
    query_emb = embedder.encode(query)
    chunk_embs = embedder.encode([c.text for c in chunks])

    # Cosine similarity
    similarities = np.dot(chunk_embs, query_emb) / (
        np.linalg.norm(chunk_embs, axis=1) * np.linalg.norm(query_emb)
    )

    # Take top-K and concatenate — that's it
    top_indices = np.argsort(similarities)[-top_k:][::-1]
    top_chunks = [chunks[i] for i in top_indices]

    # Naive: just dump them all in order of similarity
    return "\n\n".join(c.text for c in top_chunks)


def engineered_rag_context(
    query: str,
    chunks: list[Chunk],
    top_k_retrieval: int = 20,    # Retrieve more, then filter down
    top_k_final: int = 5,
    token_budget: int = 3000,
    recency_boost_days: int = 30,
) -> tuple[str, dict]:
    """
    ENGINEERED APPROACH: Multi-stage retrieval with re-ranking, deduplication,
    recency weighting, and budget-aware ordering.

    Returns the assembled context string plus metadata for monitoring.
    """
    from datetime import datetime, timedelta

    # Stage 1: Broad retrieval — get more candidates than we need
    query_emb = embedder.encode(query)
    chunk_embs = embedder.encode([c.text for c in chunks])
    similarities = np.dot(chunk_embs, query_emb) / (
        np.linalg.norm(chunk_embs, axis=1) * np.linalg.norm(query_emb)
    )

    # Get top_k_retrieval candidates
    top_indices = np.argsort(similarities)[-top_k_retrieval:][::-1]
    candidates = [(chunks[i], float(similarities[i])) for i in top_indices]

    # Stage 2: Cross-encoder re-ranking for actual relevance
    # Cross-encoders are slower but measure true query-document relevance
    pairs = [[query, c.text] for c, _ in candidates]
    rerank_scores = cross_encoder.predict(pairs)

    # Combine embedding similarity (0.3) with cross-encoder score (0.7)
    combined = []
    for (chunk, embed_score), rerank_score in zip(candidates, rerank_scores):
        combined_score = 0.3 * embed_score + 0.7 * (rerank_score / 10.0)  # Normalize
        combined.append((chunk, combined_score))

    # Stage 3: Recency boost — reward recent documents
    cutoff = datetime.now() - timedelta(days=recency_boost_days)
    boosted = []
    for chunk, score in combined:
        try:
            chunk_date = datetime.fromisoformat(chunk.date)
            if chunk_date > cutoff:
                # Apply a 15% boost for recent content
                score = score * 1.15
        except (ValueError, AttributeError):
            pass
        boosted.append((chunk, score))

    # Stage 4: Sort by final score
    boosted.sort(key=lambda x: x[1], reverse=True)

    # Stage 5: Deduplicate similar chunks (cosine sim > 0.92 = near-duplicate)
    selected: list[Chunk] = []
    selected_embs = []
    for chunk, score in boosted:
        chunk_emb = embedder.encode(chunk.text)
        if selected_embs:
            sims = np.dot(selected_embs, chunk_emb) / (
                np.linalg.norm(selected_embs, axis=1) * np.linalg.norm(chunk_emb)
            )
            if np.max(sims) > 0.92:
                # Near-duplicate of an already-selected chunk — skip
                continue
        selected.append(chunk)
        selected_embs.append(chunk_emb)
        if len(selected) >= top_k_final:
            break

    # Stage 6: Token budget enforcement + ordering strategy
    # Most relevant at the END (recency bias in LLM attention — recent = bottom)
    # Background/supporting context at the START
    token_count = 0
    final_chunks: list[Chunk] = []
    for chunk in reversed(selected):  # Less relevant first (will appear earlier)
        chunk_tokens = len(chunk.text.split()) * 1.3  # Rough token estimate
        if token_count + chunk_tokens > token_budget:
            break
        final_chunks.append(chunk)
        token_count += chunk_tokens

    final_chunks.reverse()  # Restore: background first, most relevant last

    # Assemble with source attribution (helps model weight information)
    assembled_parts = []
    for chunk in final_chunks:
        assembled_parts.append(
            f"[Source: {chunk.source} | Date: {chunk.date}]\n{chunk.text}"
        )

    assembled_context = "\n\n---\n\n".join(assembled_parts)

    # Return context + metadata for monitoring
    metadata = {
        "chunks_retrieved": top_k_retrieval,
        "chunks_after_rerank": len(candidates),
        "chunks_after_dedup": len(selected),
        "chunks_final": len(final_chunks),
        "estimated_tokens": int(token_count),
        "sources": [c.source for c in final_chunks],
    }

    return assembled_context, metadata

The difference in output quality between these two functions is significant in practice. The naive approach frequently returns redundant chunks (three slightly different paragraphs from the same document saying the same thing) and misses the actual most-relevant content because embedding similarity and true relevance diverge for complex queries. The engineered approach routes through re-ranking, removes near-duplicates, rewards fresh content, and places the highest-relevance material where LLM attention is strongest.

Production Considerations

Token Budget Management

Every production context engineering system needs a token budget framework. Define hard limits per layer, build monitoring to track actual token consumption per request, and set up alerts when budgets are consistently exceeded or when retrieval is returning too few results within budget.

Practical budget allocation for a general-purpose assistant on a 16K context model:
- System prompt: 400-600 tokens
- Session context (user profile, preferences): 300-500 tokens
- Compressed history summary: 200-400 tokens
- Retrieved documents: 5,000-8,000 tokens (the largest budget)
- Recent verbatim history (last 4-6 turns): 1,500-2,500 tokens
- Current query: 50-500 tokens
- Output buffer: 1,000-2,000 tokens

The output buffer is easy to forget. Your context window is shared between input and output — if you fill 15,900 tokens of a 16K context with input, the model has 100 tokens to respond. Build in headroom.

Context Cache Warming

For latency-sensitive applications, context cache warming is a significant optimization. Many LLM providers (Anthropic's prompt caching, OpenAI's cached tokens) allow you to cache a prefix of the context and pay reduced rates for cache hits. Design your context assembly so that the static portions (system prompt, global knowledge base content that rarely changes) appear early and consistently — they'll get cached across requests. Dynamic, per-query content (retrieved documents, recent history) goes after the cache boundary.

This can reduce per-request latency by 30-60% and cost by 50-80% for cache hits on the static prefix.

Monitoring Context Quality

You can't improve what you don't measure. Build these metrics into your context engineering pipeline:

Retrieval precision — Of the chunks injected into context, what percentage were actually cited or used in the model's response? Low precision indicates over-retrieval (too much irrelevant noise going in).

Context utilization rate — What fraction of your token budget is being used? Consistently at 95%+ suggests compression is insufficient. Consistently at 30-40% suggests over-conservative retrieval that's leaving relevant information on the table.

Answer grounding rate — For RAG systems, what percentage of factual claims in the response can be traced back to a specific injected chunk? Low grounding rates indicate hallucination despite good retrieval — often caused by poor ordering or context flooding.

Compression ratio — How much does your history compression reduce tokens while preserving the information the model needs to answer subsequent questions? Evaluate by testing how often the model answers questions that require information from compressed history correctly.

Context freshness lag — For systems with dynamic data, how old is the newest piece of context on average? A high lag indicates your retrieval system isn't surfacing recent enough data.

graph LR subgraph "Naive Context" N1["Query: 100 tokens"] --> NW["Context Window"] N2["Retrieved Docs: 4,000 tokens\n(20% relevant, 80% noise)"] --> NW N3["Full History: 3,000 tokens\n(15 turns, no compression)"] --> NW NW --> NR["Result:\n7,100 tokens used\nHigh noise ratio\nOld history dilutes attention"] end subgraph "Engineered Context" E1["Query: 100 tokens"] --> EW["Context Window"] E2["System + Session: 800 tokens\n(global + user context)"] --> EW E3["Compressed Summary: 300 tokens\n(15 turns → summary)"] --> EW E4["Retrieved Docs: 2,800 tokens\n(re-ranked, deduplicated, filtered)"] --> EW E5["Recent History: 800 tokens\n(last 4 turns verbatim)"] --> EW EW --> ER["Result:\n4,800 tokens used\nLow noise ratio\nRecent history preserved\n32% fewer tokens, better answers"] end style NR fill:#9b2335,color:#fff style ER fill:#2d6a4f,color:#fff

The Context Refresh Problem

Static context goes stale. A user profile injected at the start of a long session may be outdated by turn 30 if the user has updated their preferences mid-session. Product documentation injected at session start may reference prices or features that have changed. Build explicit context refresh triggers: after N turns, re-fetch session context; before answering questions about pricing or availability, always retrieve fresh data rather than relying on session-start injection.

The architectural principle is: treat context like a cache with TTLs. Every piece of injected context has an implicit freshness guarantee. When that guarantee expires, refresh it.

Conclusion

The shift from prompt engineering to context engineering reflects a maturation in how the industry builds AI systems. Prompt engineering was the right first skill — you had to learn to communicate with these models at all, and that took work. But it's a necessary precondition, not a sufficient one.

Context engineering is where the real leverage lives in 2026. The models are capable. GPT-4o, Claude 3.5 Sonnet, Gemini 2.0 — these are genuinely powerful reasoning systems. The bottleneck in almost every production failure isn't the model's intelligence; it's the quality of the information it's reasoning over.

If you're building AI systems today, audit your context before you audit your prompts. Ask: is the model seeing everything it needs? Is it being flooded with irrelevant noise? Is critical information being pushed out by conversation history overflow? Is the retrieved content actually fresh and relevant, or is it a semantic similarity score that doesn't translate to real-world usefulness?

The answers to those questions will tell you where your system is failing — and the techniques in this post give you the tools to fix it. Sliding window compression, hierarchical context layers, multi-stage retrieval with re-ranking, token budget management, and context quality monitoring are no longer advanced topics. They are table stakes for any AI application that needs to work reliably in production.

Start with the ContextManager class. Instrument your retrieval pipeline with the metadata logging from the engineered_rag_context function. Add the five monitoring metrics to your dashboards. You'll have a clearer picture of your system's context health within a week, and actionable improvements to make within two.

The model is not the problem. What you feed it is.


Tools mentioned in this post

Disclosure: the links below are affiliate links. If you sign up via them, we earn a small commission at no extra cost to you. This helps fund the writing of more posts like this one.

  • Anthropic Claude API — production LLM access. Sign up
  • OpenAI Platform — GPT-4 and embedding APIs. Sign up
  • Hugging Face — Pro / Enterprise tier. Sign up

Sources

About the Author

Toc Am

Founder of AmtocSoft. Writing practical deep-dives on AI engineering, cloud architecture, and developer tooling. Previously built backend systems at scale. Reviews every post published under this byline.

LinkedIn X / Twitter

Published: 2026-04-21 · Written with AI assistance, reviewed by Toc Am.

Get These In Your Inbox

Weekly deep-dives on AI engineering, no fluff. Join the newsletter →

Subscribe (free)

Or grab the book ($39, ~100 pages) · Buy me a coffee

Buy Me a Coffee · 🔔 YouTube · 💼 LinkedIn · 🐦 X/Twitter

Bigger Is Not the Same as Better. The Job That Moved Is the Phone, Not the Lab.

Bigger is a plan. The phone is the receipt. The brief for this cycle is a question: does bigger always mean better in AI? The 2026 answer i...