Showing posts with label LLMs. Show all posts
Showing posts with label LLMs. Show all posts

Monday, April 20, 2026

AI Reasoning Models in 2026: How o3, DeepSeek R1, and Extended Thinking Actually Work

Hero image: abstract visualization of a neural network chain of thought, glowing nodes connected by reasoning paths

The first time I handed a tricky competitive programming problem to GPT-4, it confidently produced a solution that failed on the second test case. I tweaked the prompt, added "think step by step," and got the same broken logic presented with more elaborate justification. It wasn't that the model was dumb — it was that standard next-token prediction has a hard ceiling on reasoning depth.

Then I tried the same problem on o3 in early 2026. It spent 47 seconds "thinking" before outputting anything. The solution was correct. What happened in those 47 seconds is the story of reasoning models.


The Ceiling Standard LLMs Hit

Before diving into reasoning models, it helps to understand exactly where vanilla LLMs fall short.

A standard transformer generates one token at a time, left to right, with no ability to revise earlier decisions. That architecture is remarkably powerful for pattern matching, code completion, and summarisation. But multi-step logical deduction — the kind that requires holding intermediate conclusions, checking consistency, and backtracking — doesn't map cleanly onto a single forward pass.

Chain-of-thought prompting ("think step by step") improves results because it forces the model to externalise intermediate reasoning into the context window. Each step can condition the next. But the model is still constrained: it can't revise a step it already emitted, and it has no mechanism for exploring alternative reasoning branches.

The result is a model that looks like it's reasoning but is really completing a pattern of reasoning-shaped text. For easy problems, the distinction doesn't matter. For hard ones — complex math, multi-constraint planning, adversarial code review — it does.


What Reasoning Models Do Differently

Architecture diagram: standard LLM forward pass vs reasoning model with internal scratchpad and verification loop

Reasoning models like OpenAI's o1/o3, DeepSeek R1, and Claude's extended thinking mode all share a common idea: give the model compute budget at inference time to generate and evaluate intermediate reasoning steps before producing a final answer.

The implementation details differ, but the pattern is consistent:

  1. The model generates a "scratchpad" — internal reasoning tokens that are not directly shown in the final answer
  2. It uses those tokens to explore multiple approaches, check work, and catch contradictions
  3. The final answer is conditioned on the full reasoning trace

This is sometimes called inference-time compute scaling — spending more compute during inference rather than purely during training.

OpenAI o3

o3 was the most significant reasoning-model release of early 2026. OpenAI haven't published full technical details, but from benchmarks and the o1 paper, we know:

  • It was trained with reinforcement learning on verifiable outcomes (math proofs, code tests, logic puzzles) rather than supervised next-token prediction
  • It uses a "think" budget that can be set low (fast, cheaper) or high (slower, more thorough)
  • On ARC-AGI 2, o3 (high compute) achieved 87.5% — up from GPT-4o's 5% on the same benchmark

The practical implication: on a hard coding problem, o3 with high budget will outperform o3 with low budget. Reasoning ability is partially a function of how many tokens the model gets to think with. That's a fundamentally new tradeoff in LLM deployment.

DeepSeek R1

DeepSeek R1, released in January 2025, was the open-source reasoning model that forced the industry to take inference-time compute seriously. Critically, DeepSeek published their training recipe.

They trained R1 using GRPO (Group Relative Policy Optimisation), a variant of PPO that evaluates a group of completions against each other rather than a fixed reward model. The reward signals were:
- Format reward: does the output follow <think>...</think><answer>...</answer> structure?
- Accuracy reward: is the final answer correct (verifiable for math/code)?

No human feedback. No human-written chain-of-thought examples in the initial training. The model learned to reason by trial and error against verifiable outcomes.

The result: R1-Zero (the base RL-trained model) spontaneously developed behaviours like self-correction — pausing mid-reasoning with phrases like "Wait, I made an error..." and revising its approach. The researchers didn't program this in. It emerged from the RL process.

Claude's Extended Thinking

Anthropic's Claude 3.7 Sonnet (February 2026) and later Claude 4 introduced extended thinking: a configurable mode where the model generates a visible chain-of-thought scratchpad before its final response.

Unlike o3's opaque thinking process, Claude's extended thinking is shown to the user by default (with an option to hide it). This is both a design choice and a transparency statement — you can audit the reasoning, not just trust the answer.

Extended thinking is enabled via the API by setting thinking: {type: "enabled", budget_tokens: N}. Claude will spend up to N tokens on its scratchpad before outputting the final answer.

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={
        "type": "enabled",
        "budget_tokens": 10000  # up to 10k tokens for internal reasoning
    },
    messages=[{
        "role": "user",
        "content": "A 10×10 grid has all cells initially white. You flip all cells in row 3, then all cells in column 7, then all cells in any row that has an odd number of black cells. How many black cells remain?"
    }]
)

# The response contains thinking blocks and text blocks
for block in response.content:
    if block.type == "thinking":
        print(f"[Thinking: {len(block.thinking)} chars]")
    elif block.type == "text":
        print(f"Answer: {block.text}")

Running this on the grid puzzle (a problem designed to require systematic tracking of state), the thinking block shows Claude explicitly constructing a 10×10 grid, applying each operation step by step, and verifying row parity before the final operation. The answer is correct. The same prompt without extended thinking produces a confidently wrong answer in roughly 1/10th the tokens.


The Training Mechanism: RLHF vs RL on Verifiable Rewards

Standard LLMs are typically trained with:
1. Supervised fine-tuning on human-written text
2. RLHF — human raters score outputs, those scores train a reward model, PPO updates the policy

Reasoning models shift step 2. Instead of human preference feedback (which is expensive and subjective), they use RL on verifiable signals:
- Code: does it pass the test suite?
- Math: does it match the ground-truth answer?
- Logic: does the conclusion follow from the premises under formal verification?

This is only possible for domains with objectively checkable answers. DeepSeek's bet was that math and code are rich enough to develop general reasoning capabilities, which transfer to other domains. The results suggest they were right — R1 generalises beyond math to multi-step planning and argument analysis.

The implication for developers: if you're building a domain where outputs are verifiable, reasoning models (or custom RL training on your verification signal) may be the right architectural path. If your domain is inherently subjective (creative writing, brand voice), standard RLHF or preference tuning remains dominant.

flowchart LR A[Problem] --> B[Standard LLM\nSingle forward pass] B --> C[Answer — may be wrong] A --> D[Reasoning Model] D --> E[Internal scratchpad\nExplore approach 1] E --> F[Verify / spot error] F --> G[Explore approach 2] G --> H[Synthesise answer] H --> I[Final Answer — higher accuracy] style E fill:#f9f,stroke:#333 style F fill:#f9f,stroke:#333 style G fill:#f9f,stroke:#333

When to Use Reasoning Models (and When Not To)

flowchart TD Start([New task]) --> Q1{Requires multi-step\nlogical deduction?} Q1 -->|No| Q2{Latency-sensitive\nor cost-sensitive?} Q1 -->|Yes| Q3{Verifiable\noutcome?} Q2 -->|Yes| STD[Standard LLM\nFast, cheap] Q2 -->|No| Q3 Q3 -->|Yes| RM[Reasoning Model\nHigh budget] Q3 -->|No| Q4{Extended thinking\nwith medium budget?} Q4 -->|Acceptable| MED[Reasoning Model\nMedium budget] Q4 -->|Too slow/costly| STD style RM fill:#4CAF50,color:#fff style MED fill:#8BC34A,color:#fff style STD fill:#2196F3,color:#fff

Use reasoning models for:

Complex code generation or debugging. When the problem requires holding multiple constraints simultaneously — correctness, performance, security, API contract — reasoning models outperform standard models by a measurable margin. Aider's benchmark data shows o3 achieving 71.6% on SWE-bench Verified vs GPT-4o's 49.2% (Aider leaderboard, March 2026).

Multi-step planning. Tasks like "design a database schema that satisfies these 8 business constraints" benefit enormously from a model that can check constraint satisfaction before committing to an answer.

Mathematical and algorithmic reasoning. This is the canonical use case. AIME 2024 pass rates: o3 (high compute) scored 96.7%; GPT-4o scored 9.3%.

Don't use reasoning models for:

Latency-critical applications. A 47-second thinking time is fine for a batch job. It's a dealbreaker for a live chat interface.

Simple retrieval or classification. Using o3 to extract structured fields from a form is like hiring a neurosurgeon to change a lightbulb — technically capable, economically absurd.

Cost-sensitive high-volume workloads. o3 at high compute is approximately 25× the price of GPT-4o per output token (OpenAI pricing page, April 2026). For 10,000 requests/day, that difference is material.


A Gotcha I Hit in Production

I was building a compliance checker — a system that takes a contract clause and verifies it against 12 specific regulatory requirements. My first instinct was to use a reasoning model with high budget. The accuracy was excellent.

The problem: latency. The p99 was 68 seconds. Legal review workflows can tolerate that. But I'd also wired the results into a real-time UI that highlighted clauses as the user typed. 68-second lag is unusable.

The fix was a two-tier system:
1. Fast path (GPT-4o, 1-2 seconds): check whether the clause is likely compliant using a simpler prompt. Shows a preliminary green/yellow/red indicator.
2. Slow path (o3 medium budget, 8-12 seconds): runs in the background, confirms or overrides the fast-path indicator, surface detailed reasoning to the user in a collapsible "audit trail" panel.

This cut the perceived latency to ~1.5 seconds while keeping accuracy at the reasoning model level. The key insight: you don't have to choose one or the other. Use fast models for preliminary signals, slow models for verification.


Benchmarks Worth Trusting (and Some to Ignore)

Not all reasoning benchmarks are created equal.

Trust these:
- SWE-bench Verified: real GitHub issues, real test suites, no data contamination risk. As of April 2026: o3 71.6%, Claude Sonnet 4.6 49.0%, GPT-4o 38.2% (SWE-bench leaderboard).
- ARC-AGI: abstract reasoning tasks humans solve easily but LLMs typically fail. o3 high compute: 87.5%. GPT-4o: 5.3% (ARC Prize 2025 results).
- AIME 2024: AMC/AIME competition math, hard to contaminate due to limited public solutions.

Be skeptical of:
- MMLU scores for reasoning models: MMLU is multiple-choice trivia-style. Standard LLMs have near-saturated it. A 2-point MMLU improvement tells you almost nothing about reasoning capability.
- HumanEval: widely contaminated in training data. Use SWE-bench or LiveCodeBench instead.
- Self-reported benchmarks: always check whether evals use the publicly released checkpoint or a separate "eval model" that's been specifically tuned for benchmark performance.

%%{init: {'theme': 'base'}}%% xychart-beta title "SWE-bench Verified Scores (April 2026)" x-axis ["GPT-4o", "Claude Sonnet 4.6", "o3 (low)", "o3 (high)"] y-axis "% Resolved" 0 --> 80 bar [38.2, 49.0, 58.4, 71.6]

Production Considerations

Token budget tuning matters. Don't set the thinking budget to max and call it done. Run evals at 2k, 5k, 10k, and 20k thinking tokens. For most tasks, 5k-8k tokens captures 90% of the accuracy gain at 40% of the cost of 20k. Plot accuracy vs. budget and find your knee in the curve.

Thinking tokens aren't free but they're cheaper than output tokens. On the Anthropic API, extended thinking tokens are billed at the input token rate ($3/MTok for Sonnet 4.6), not the output rate ($15/MTok). This makes generous thinking budgets more economical than they first appear.

Cache the reasoning, not just the answer. If you're running the same reasoning task repeatedly (e.g., evaluating 1,000 contracts against the same 12 rules), the system prompt and rule list can be prompt-cached, reducing costs by ~90% for the static portion. The dynamic portion (the contract clause) still incurs full cost.

# Example: prompt caching with extended thinking
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=8000,
    thinking={"type": "enabled", "budget_tokens": 6000},
    system=[{
        "type": "text",
        "text": "You are a compliance checker. Check clauses against these 12 rules:\n[rules here]",
        "cache_control": {"type": "ephemeral"}  # Cache the static rules
    }],
    messages=[{
        "role": "user",
        "content": clause_text  # Only this varies per request
    }]
)

Stream thinking blocks separately. The Anthropic API and OpenAI streaming API both support streaming thinking content. Stream the thinking to the client to provide progress feedback during long reasoning sessions — users can see "still thinking..." with intermediate reasoning rather than a frozen spinner.


Conclusion

Reasoning models don't replace standard LLMs — they extend the capability ceiling for tasks that require genuine multi-step deduction. The right mental model is "when does the problem require the model to check its own work?"

For routine tasks — drafting, classification, simple code completion — standard LLMs are faster and cheaper. For complex planning, algorithmic reasoning, and constraint-heavy generation, reasoning models provide accuracy gains that are hard to achieve through prompt engineering alone.

The economics will continue to shift. Inference-time compute is improving on the same curve as training compute — meaning today's "expensive reasoning" will be next year's baseline. Building your system to route intelligently between fast and slow models now means you're positioned to upgrade automatically as the cost curves drop.


Sources

  1. SWE-bench Leaderboard — verified benchmark for code agents (accessed April 2026)
  2. ARC Prize 2025 Results — ARC-AGI 2 benchmark results including o3 high-compute score
  3. DeepSeek R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning — DeepSeek AI, January 2025
  4. Anthropic Extended Thinking Docs — Claude extended thinking API reference
  5. OpenAI o3 System Card — OpenAI, December 2024
  6. Aider LLM Leaderboard — independent coding benchmark (accessed April 2026)

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-20 · 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...