← Back to DevBytes

Context Window Optimization with LangGraph: Complete Guide

Context Window Optimization with LangGraph: Complete Guide

Large language models have grown dramatically in capability, but their context windows remain a finite and expensive resource. Every token you pass into a model costs latency, money, and attention. When you build agentic workflows with LangGraph, unoptimized context windows quickly become the bottleneck that turns a snappy assistant into a sluggish, expensive, and unreliable system. This guide walks through what context window optimization means, why it matters inside LangGraph specifically, and how to implement it with practical, production-ready patterns.

What Is Context Window Optimization?

Context window optimization is the practice of managing the tokens that enter a model's prompt at each step of an agent's execution. It is not simply about staying under the model's maximum context length. It is about ensuring that, at every node in your graph, the model receives only the tokens that are relevant to the decision it needs to make next.

In a naive agent loop, the entire conversation history, every tool result, every intermediate thought, and every system instruction is passed forward at every step. As the agent runs longer, the prompt grows linearly or worse. Optimization techniques compress, summarize, filter, route, and structure this information so the model's input stays small, focused, and semantically dense.

Why It Matters in LangGraph

LangGraph models agents as stateful graphs. Each node receives a shared State object, performs work, and returns an update. Because state is passed between nodes by default, it is very easy to accumulate context bloat: tool outputs pile up, messages accumulate, and every downstream node pays the cost of carrying that history forward.

Understanding LangGraph State and Message Accumulation

Before optimizing, you need to understand where context lives. In LangGraph, the canonical pattern uses a MessagesState or a custom typed dictionary that holds a list of messages. Each node appends to that list, and the next node reads the full list when calling the model.

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI

class State(TypedDict):
    messages: Annotated[list, add_messages]

llm = ChatOpenAI(model="gpt-4o")

def call_model(state: State):
    response = llm.invoke(state["messages"])
    return {"messages": [response]}

graph = StateGraph(State)
graph.add_node("model", call_model)
graph.add_edge(START, "model")
graph.add_edge("model", END)
app = graph.compile()

This minimal graph works, but every invocation passes the entire messages list to the model. After dozens of turns or large tool outputs, this list becomes the dominant cost in your system.

Strategy 1: Message Trimming with trim_messages

The simplest optimization is to cap the number of messages passed to the model. LangChain provides trim_messages, which lets you enforce a token budget while preserving structurally important messages like the system prompt.

from langchain_core.messages import trim_messages, SystemMessage

def call_model(state: State):
    trimmed = trim_messages(
        state["messages"],
        max_tokens=4000,
        strategy="last",
        token_counter=llm,
        include_system=True,
        start_on="human",
    )
    response = llm.invoke(trimmed)
    return {"messages": [response]}

The strategy="last" option keeps the most recent messages within the budget, which is usually what you want for conversational agents. The include_system=True flag guarantees the system message is never dropped, and start_on="human" ensures the trimmed history begins on a human turn rather than an orphaned AI message.

Note that trimming happens only at the point of model invocation. The full state is still preserved in the graph, which is important: you keep a complete audit trail while only sending a bounded slice to the model.

Strategy 2: Summarization Nodes for Long Conversations

Trimming discards information. When conversations are long and earlier context remains valuable, summarization is a better fit. The pattern is to add a dedicated node that compresses older messages into a single summary message once the history crosses a threshold.

from langchain_core.messages import HumanMessage, AIMessage, RemoveMessage

class State(TypedDict):
    messages: Annotated[list, add_messages]
    summary: str

def maybe_summarize(state: State):
    messages = state["messages"]
    if len(messages) <= 6:
        return state

    summary = state.get("summary", "")
    summary_prompt = (
        f"Here is the existing summary: {summary}\n\n"
        "Extend this summary with the new conversation below:\n"
        f"{messages[:-2]}"
    )
    new_summary = llm.invoke(summary_prompt).content

    delete_messages = [
        RemoveMessage(id=m.id) for m in messages[:-2]
    ]
    return {"messages": delete_messages, "summary": new_summary}

def call_model(state: State):
    summary = state.get("summary", "")
    system = SystemMessage(
        content=f"You are a helpful assistant. Conversation summary: {summary}"
    )
    messages = [system] + state["messages"][-2:]
    response = llm.invoke(messages)
    return {"messages": [response]}

The RemoveMessage reducer is the key mechanism here. When a node returns a RemoveMessage with a specific id, LangGraph's add_messages reducer deletes that message from state rather than appending. This lets you shrink persistent state, not just the model's view of it.

Wiring this into a graph with conditional routing gives you an agent that self-compresses:

graph = StateGraph(State)
graph.add_node("summarize", maybe_summarize)
graph.add_node("model", call_model)

graph.add_edge(START, "summarize")
graph.add_edge("summarize", "model")
graph.add_edge("model", END)
app = graph.compile()

Strategy 3: Selective Tool Output Handling

Tool calls are often the worst context offenders. A single web search or file read can return thousands of tokens, and most of that content is irrelevant after the model has reasoned about it. Two patterns help: truncating tool outputs at the source, and stripping tool messages from history after they have been consumed.

def truncate_tool_output(content: str, max_chars: int = 2000) -> str:
    if len(content) <= max_chars:
        return content
    return content[:max_chars] + "\n...[truncated]..."

def call_tool(state: State):
    last_msg = state["messages"][-1]
    tool_calls = last_msg.tool_calls
    results = []
    for tc in tool_calls:
        raw = run_my_tool(tc["args"])
        results.append({
            "role": "tool",
            "tool_call_id": tc["id"],
            "content": truncate_tool_output(raw),
        })
    return {"messages": results}

For even tighter control, you can add a cleanup node that removes old tool messages once the model has produced a follow-up response. This keeps the working context focused on the current reasoning step rather than the full artifact history.

Strategy 4: State Channels and Parallel Contexts

Not every node needs every piece of state. LangGraph lets you define multiple channels in your state, and you can route only the relevant channels to each node. This is especially powerful for multi-agent systems where a research agent produces large documents that a planner agent never needs to see in full.

class State(TypedDict):
    messages: Annotated[list, add_messages]
    research_notes: str
    plan: str

def researcher(state: State):
    notes = do_research(state["messages"][-1].content)
    return {"research_notes": notes}

def planner(state: State):
    # Planner sees only the summary, not raw research artifacts
    short_notes = state["research_notes"][:1500]
    plan = llm.invoke(f"Make a plan based on: {short_notes}")
    return {"plan": plan.content}

By keeping bulky artifacts in dedicated channels and only passing trimmed views into model calls, you decouple storage from inference cost. The graph retains rich state for downstream nodes or for the user, while each LLM call stays lean.

Strategy 5: Hierarchical and Recursive Summarization

For agents that run for hundreds of steps, a single summary layer is insufficient. Hierarchical summarization maintains summaries at multiple time scales: a short-term summary of the last few turns, a medium-term summary of the last task, and a long-term summary of the entire session.

def hierarchical_summarize(state: State):
    messages = state["messages"]
    if len(messages) < 20:
        return state

    short_term = llm.invoke(
        "Summarize the last 10 messages: " + str(messages[-10:])
    ).content

    medium_term = state.get("medium_summary", "")
    medium_term = llm.invoke(
        f"Existing medium summary: {medium_term}\n"
        f"Integrate this short-term summary: {short_term}"
    ).content

    delete_messages = [RemoveMessage(id=m.id) for m in messages[:-2]]
    return {
        "messages": delete_messages,
        "short_summary": short_term,
        "medium_summary": medium_term,
    }

This mirrors how humans handle long projects: you remember the gist of last week, the details of this morning, and you look up specifics only when needed. The model gets a compact, multi-resolution view of history at every step.

Best Practices

Conclusion

Context window optimization is not a single technique but a layered discipline: trim aggressively where you can, summarize where you must, isolate bulky artifacts in dedicated channels, and budget tokens per node based on what each step actually needs. LangGraph's stateful graph model makes these patterns natural to implement, because every node is an explicit opportunity to reshape what the model sees. By combining trim_messages for bounded recency, summarization nodes for long-running value retention, tool output truncation for artifact-heavy workflows, and channel separation for multi-agent architectures, you can build agents that stay fast, cheap, and accurate even as they run for dozens or hundreds of steps. The result is a system that scales not by buying a bigger context window, but by using the one you have with discipline.

— Ad —

Google AdSense will appear here after approval

← Back to all articles