Module 4: Middleware and Customization
Introduction: Customizing Agents Without Rewriting Them
Overview
In Module 3 you built complete agents with create_agent. One line of code and you had an autonomous agent: it reasons, calls tools, observes results, and repeats until it has the answer. You added system prompts, custom state, streaming, structured output. Everything worked.
Until you needed something create_agent doesn't solve with a parameter: you want to know how long each model call takes. Or you need it to use a cheap model for simple questions and a powerful one for complex questions. Or to filter the available tools based on the user's permissions. Or to retry automatically when a tool fails.
You could rewrite the agent from scratch for each case. But that defeats the whole point of create_agent: a high-level abstraction you shouldn't have to open up in order to modify.
The answer is middleware — functions that intercept what happens between the agent and the model (or between the agent and the tools) without touching the agent's internal logic. Think of them as filters you can snap on or off without ever opening the agent's code.
Where are we in the guide?
This is Module 4 of the guide LangChain & LangGraph: From Chains to Agents. It's the last module of Block 1 (LangChain Core) before we move on to Block 2 (LangGraph Fundamentals).
Block 1: LangChain Core (Modules 1-4) ← YOU ARE HERE (Module 4)
Block 2: LangGraph Fundamentals (Modules 5-7)
Block 3: Advanced LangGraph (Modules 8-10)
Block 4: Production (Modules 11-12)
Your progress through Block 1:
Module 1: Models and Providers ✅ Done
│
▼
Module 2: Tools and Tool Calling ✅ Done
│
▼
Module 3: Agents (create_agent) ✅ Done
│
▼
Module 4: Middleware and Customization ← YOU ARE HERE
In Module 1 you learned to connect to models. In Module 2 you gave them tools. In Module 3 you automated the whole loop with create_agent. Now you're going to customize that agent without rewriting it: intercept its calls, change its behavior, and add cross-cutting capabilities.
The bridge: from building agents to customizing them
What you already know
In Module 3 you got here:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the web for information."""
return f"Results for '{query}': LangChain is a framework for LLMs."
agent = create_agent(
"openai:gpt-4.1-mini",
tools=[search],
system_prompt="You are a research assistant."
)
result = agent.invoke({"messages": [("user", "What is LangChain?")]})
print(result["messages"][-1].content)
# Expected output: LangChain is a framework for building applications with LLMs.
Works perfectly. But now ask yourself these questions:
- How long did the model call take? You don't know.
- How many tokens did it burn? You don't know.
- Can you switch to a more powerful model when the question is complex? Not without rewriting.
- Can you filter the tools based on who's asking? Not without rewriting.
- Can you retry if the model fails? Not without rewriting.
What you'll learn here
With middleware, you add every one of those capabilities without touching the agent:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
import time
@tool
def search(query: str) -> str:
"""Search the web for information."""
return f"Results for '{query}': LangChain is a framework for LLMs."
model = init_chat_model("openai:gpt-4.1-mini")
def before_model(messages):
print(f"[LOG] Sending {len(messages)} messages to the model")
def after_model(response):
print(f"[LOG] Model replied with {len(response.content)} characters")
if response.tool_calls:
print(f"[LOG] Tool calls: {[tc['name'] for tc in response.tool_calls]}")
agent = create_agent(
model,
tools=[search],
before_model=before_model,
after_model=after_model,
system_prompt="You are a research assistant."
)
result = agent.invoke({"messages": [("user", "What is LangChain?")]})
print(result["messages"][-1].content)
# Expected output:
# [LOG] Sending 2 messages to the model
# [LOG] Model replied with 0 characters
# [LOG] Tool calls: ['search']
# [LOG] Sending 4 messages to the model
# [LOG] Model replied with 62 characters
# LangChain is a framework for building applications with LLMs.
The agent behaves exactly the same. But now you can see what it does. And this is only the beginning — the more advanced middleware doesn't just observe, it changes the behavior.
The problem: customization without rewriting
As your agents go from prototypes to production, you need capabilities that cut across multiple agents:
| Need | Without middleware | With middleware |
|---|---|---|
| Logging | print() scattered through the code | One centralized hook that intercepts everything |
| Latency monitoring | Timing each call by hand | @wrap_model_call with automatic time.time() |
| Token counting | Checking response.usage_metadata manually | An automatic accumulator in after_model |
| Model routing | if/else before creating the agent | Middleware that decides per request |
| Tool filtering | Tools hardcoded into create_agent | Dynamic tools per user or context |
| Retry on tools | try/except inside every tool | @wrap_tool_call with reusable retry logic |
| Custom error handling | Error logic in every agent | One middleware that applies to all of them |
Without middleware, each of these needs means modifying the agent directly — or worse, copy-pasting logic between agents. With middleware, you write the logic once and apply it wherever you need it.
An analogy: airport security checkpoints
Picture an airport:
Passenger ──▶ [Check-in] ──▶ [Security] ──▶ [Immigration] ──▶ Plane
│ │ │
Verifies Inspects Validates
identity luggage permissions
- The passenger is the message headed to the model (or the tool call).
- The plane is the model (or the tool).
- The checkpoints are middleware.
Each checkpoint can:
- Inspect what passes through (logging — "this passenger has a carry-on")
- Modify what passes through (change the gate — "your flight moved to gate 12")
- Block what passes through (security — "this item can't go through")
- Redirect what passes through (immigration — "no visa, go to another line")
And the key part: the airline doesn't change. The plane takes off the same way. The checkpoints operate independently — you can add a new one (a health screening) or remove one (drop the liquids check) and the plane never finds out.
That's middleware: interceptors that sit between your code and the services it calls, without either side having to change.
What middleware can do
create_agent middleware operates at two interception points:
1. Intercepting model calls
Every time the agent is about to call the LLM, middleware can step in:
Messages ──▶ [before_model] ──▶ LLM ──▶ [after_model] ──▶ Response
│
[wrap_model_call]
(wraps the whole cycle)
before_model— Runs before the messages go to the model. It can inspect them, log them, or modify them.after_model— Runs after the model's response comes back. It can inspect the response, log metrics, or modify it.@wrap_model_call— Wraps the entire cycle. It receives the request, can modify it, calls the handler (which runs the model), and can modify the response. It's the most powerful of the three.
2. Intercepting tool calls
Every time the agent is about to run a tool, middleware can step in:
Tool call ──▶ [wrap_tool_call] ──▶ Tool ──▶ Result
@wrap_tool_call— Wraps every tool execution. It can inspect the arguments, modify them, retry on failure, log results, or replace the tool entirely.
The hooks at a glance
| Hook | When it runs | What it receives | What it's for |
|---|---|---|---|
before_model | Before each LLM call | The message list | Logging, validation, modifying messages |
after_model | After each LLM response | The model's response | Logging, metrics, modifying the response |
@wrap_model_call | Wraps the whole model cycle | Request + handler | Model routing, retry, full transformation |
@wrap_tool_call | Wraps each tool execution | Tool call + handler | Retry, error handling, tool-level logging |
Module map
| Capsule | Topic | What you'll learn |
|---|---|---|
| 02 | Your first middleware: logging and monitoring | before_model, after_model, timing, token counting, callbacks |
| 03 | @wrap_model_call: intercepting model calls | ModelRequest, the handler pattern, modifying requests, reading state |
| 04 | @wrap_tool_call: customizing tool execution | ToolCallRequest, custom error handling, retry logic, tool-level logging |
| 05 | Dynamic models: smart selection | Routing by complexity, by cost, by latency requirements |
| 06 | Dynamic tools and dynamic prompts | Filtering tools by permissions, registering tools at runtime, advanced dynamic prompts |
| 07 | AgentMiddleware class: composed middleware | Combining state_schema + tools + hooks, middleware as reusable modules |
| 08 | Project: Agent with dynamic routing | Agent with cheap/powerful models, logging middleware, dynamic tools |
How the learning flows: You start by observing what the agent does (logging in capsule 02). Then you learn to modify the model calls (03) and the tool calls (04). With that foundation, you build the advanced capabilities: dynamic model selection (05) and dynamic tools (06). Then you learn to package it all into reusable modules (07). Finally, you integrate everything into the project (08).
Connection to the project
This module's mini-project: Agent with Dynamic Model Routing
In Capsule 08 you'll build an agent that:
- Uses
@wrap_model_callto automatically pick between a cheap model (gpt-4.1-mini) and a powerful one (gpt-4.1) based on the complexity of the question - Includes logging middleware that records every model call with a timestamp, duration, and tokens consumed
- Implements dynamic tools — the available tools change based on the user's role
- Has retry middleware on tools that automatically retries when an external API fails
Every concept from capsules 02-07 comes together in this project.
Connection to the guide as a whole
Middleware closes out Block 1. Once you've got it, you'll understand how to customize agents at the LangChain level — the high-level layer. What comes next operates lower down:
- Module 5 (LangGraph Fundamentals): After mastering the high-level abstractions (
create_agent+ middleware), you'll drop down to LangGraph to build custom workflows. If middleware lets you intercept, LangGraph lets you redesign the whole flow. - Modules 6-7: Conditionals, loops, and subgraphs. These are the building blocks
create_agentuses internally — now you'll build them yourself. - Modules 8-10: Persistent memory, human-in-the-loop, and multi-agent. Middleware complements all of these — for instance, you can use middleware to log the decisions of a multi-agent system.
- Modules 11-12: Production and deployment. The logging and monitoring middleware you build here is the foundation of observability in production.
Boundaries: what this module does NOT cover
- ❌ Custom LangGraph nodes — Covered in Modules 5-7. Here you customize agents with middleware; you don't build graphs from scratch.
- ❌ Multi-agent systems — Covered in Module 10. Here you work with a single agent and its interceptors.
- ❌ Production deployment — Covered in Modules 11-12. Here you build middleware for development; the production infrastructure comes later.
- ❌ LangSmith / LangFuse — External observability tools. The logging middleware you build here is the manual version; observability platforms are complementary.
- ❌ Advanced guardrails — Content validation with dedicated frameworks. Middleware can do basic validation, but full guardrails are their own topic.
Technical setup
Prerequisites
Before you go on, make sure you have:
- ✅ Module 3 finished — you know how to create agents with
create_agent, system prompts, state, streaming, and structured output - ✅ Comfort with Python decorators — you understand
@decoratorand functions that take and return functions - ✅ Python 3.11+ installed
- ✅ At least one API key from a provider that supports tool calling (OpenAI or Anthropic recommended)
Installation
You use the same packages as in Module 3. Nothing new to install:
pip install langchain langgraph langchain-openai python-dotenv
If you already have everything installed from Module 3, check that your langchain version is v1.2+ (that's the release where the middleware system landed):
import langchain
print(langchain.__version__)
# You need >= 1.2.0
Check that everything works
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def ping(message: str) -> str:
"""Reply with a pong."""
return f"pong: {message}"
def before_model(messages):
print(f"[TEST] Middleware active — {len(messages)} messages")
agent = create_agent(
"openai:gpt-4.1-mini",
tools=[ping],
before_model=before_model
)
result = agent.invoke({"messages": [("user", "Ping with 'hello'")]})
print(result["messages"][-1].content)
# Expected output:
# [TEST] Middleware active — 2 messages
# [TEST] Middleware active — 4 messages
# pong: hello (or similar)
If you see the [TEST] messages followed by the agent's answer, your setup is ready for middleware.
If something breaks:
| Error | Cause | Fix |
|---|---|---|
ImportError: cannot import name 'create_agent' | Old langchain version | pip install --upgrade langchain (you need v1.2+) |
TypeError: create_agent() got an unexpected keyword argument 'before_model' | Your langchain version doesn't support middleware | pip install --upgrade langchain (you need v1.2+) |
ModuleNotFoundError: No module named 'langgraph' | langgraph isn't installed | pip install langgraph |
Signs it worked
By the end of this module, you'll know you got it if:
- ✅ You can add logging to an existing agent without modifying its code
- ✅ You understand the difference between
before_model,after_model,@wrap_model_call, and@wrap_tool_call - ✅ Your agent automatically picks the right model based on the complexity of the question
- ✅ You can filter the available tools based on the user's permissions or context
- ✅ You know how to compose several middleware into one reusable
AgentMiddleware - ✅ Your project shows dynamic routing, logging, and dynamic tools all working together
A look ahead: from middleware to LangGraph
In this module you customize agents with middleware — interceptors that change behavior without changing structure. But the agent is still a linear ReAct loop: model → tools → model → tools → answer.
What happens when you need a different flow? For example:
- An agent that plans first and executes second (two sequential phases)
- A workflow that branches based on the type of input (classify → route A or route B)
- A process that requires human approval before a critical action runs
- A system with several agents collaborating
For that you need LangGraph — the low-level orchestration framework. In Module 5, you'll leave the high-level abstractions (create_agent + middleware) behind and build graphs from scratch: nodes, edges, conditionals, and custom state management.
# Module 4: Middleware (customize the ReAct loop)
agent = create_agent(
model, tools,
middleware=[logging_middleware, routing_middleware]
)
# Module 5: LangGraph (design your own flow)
from langgraph.graph import StateGraph
graph = StateGraph(MyState)
graph.add_node("classify", classify_input)
graph.add_node("simple_agent", handle_simple)
graph.add_node("complex_agent", handle_complex)
graph.add_conditional_edges("classify", route_by_complexity)
Middleware gives you control over the agent. LangGraph gives you control of the entire flow.
Summary
- In Module 3 you learned to create autonomous agents with
create_agent— but customizing them meant modifying the agent's code directly - LangChain v1.2+'s middleware system lets you intercept and modify the agent's behavior without rewriting it
- Middleware works like airport security checkpoints: it inspects, modifies, or blocks what passes between the agent and the services it calls
- There are two interception points: model calls (
before_model,after_model,@wrap_model_call) and tool calls (@wrap_tool_call) - The simple hooks (
before_model,after_model) are for observing; the wrappers (@wrap_model_call,@wrap_tool_call) are for modifying - Middleware solves cross-cutting needs: logging, monitoring, model routing, dynamic tools, retry, error handling — all without copying logic between agents
- This is the last module of Block 1 (LangChain Core). Next comes LangGraph for low-level custom workflows
- The capstone project is an agent with dynamic model routing — a cheap model for simple questions, a powerful one for complex ones
Further reading
- create_agent API Reference — Full parameter reference, including the middleware hooks
- LangChain Agents Overview — The official conceptual guide to agents and customization
- Middleware Pattern — Wikipedia — The general concept of middleware in software engineering
- LangGraph Agents — How create_agent builds graphs internally (useful context for understanding where middleware gets injected)
- Python Decorators — Real Python — A refresher on Python decorators (prerequisite for
@wrap_model_calland@wrap_tool_call) - What's New in LangChain v1.2 — The release notes that introduced the middleware system
Module 4 — LangChain & LangGraph: From Chains to Agents
Next capsule: Your First Middleware: Logging and Monitoring — you'll learn to observe what your agent does with before_model and after_model.