LangChain middleware is the extension layer inside an agent where tracing, request shaping, counters, and guardrails can run outside prompt text and tool functions. A small custom hook keeps that behavior attached to the agent factory instead of scattering it through the application code that calls the agent.
The smoke test uses an AgentMiddleware subclass with a custom state_schema, a before_model hook, and a wrap_model_call hook. before_model runs just before each model request, while wrap_model_call receives the model request and a handler so the middleware can inspect or modify the request before handing it to the model.
A deterministic fake chat model keeps the test independent of provider credentials while still exercising create_agent() and agent.invoke(). The printed hook lines show that the middleware ran, and the returned state fields confirm that the hook update traveled through the agent state. Middleware can read raw user messages, so production traces should be sanitized before they reach logs or observability tools.
Related: How to install LangChain with pip
Related: How to create a LangChain agent
Related: How to redact PII with LangChain middleware
Steps to create custom LangChain agent middleware:
- Open an activated Python project environment.
Use a virtual environment so the middleware test imports the same LangChain package set that the project will use.
Related: How to create a virtual environment for LangChain - Install or upgrade LangChain in the project environment.
$ python -m pip install --upgrade langchain
LangChain v1 requires Python 3.10 or newer. Provider integrations such as langchain-openai are separate packages when a real model provider is used.
Related: How to install LangChain with pip - Save the custom middleware smoke test.
- custom_middleware_agent.py
from collections.abc import Callable from typing import Any from langchain.agents import create_agent from langchain.agents.middleware import ( AgentMiddleware, AgentState, ModelRequest, ModelResponse, ) from langchain.messages import SystemMessage from langchain_core.language_models.fake_chat_models import FakeListChatModel from langgraph.runtime import Runtime from typing_extensions import NotRequired class AgentReadyFakeModel(FakeListChatModel): def bind_tools(self, tools, *, tool_choice=None, **kwargs): if tools: raise NotImplementedError("This smoke test does not exercise tools.") return self class TraceState(AgentState): model_call_count: NotRequired[int] last_user_message: NotRequired[str] class TraceModelCallMiddleware(AgentMiddleware[TraceState]): state_schema = TraceState def before_model( self, state: TraceState, runtime: Runtime ) -> dict[str, Any] | None: count = state.get("model_call_count", 0) + 1 user_messages = [ message for message in state["messages"] if getattr(message, "type", None) == "human" ] latest = str(user_messages[-1].content) if user_messages else "" print(f"middleware before_model: call={count}, latest={latest!r}") return { "model_call_count": count, "last_user_message": latest, } def wrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse], ) -> ModelResponse: content_blocks = list(request.system_message.content_blocks) content_blocks.append( {"type": "text", "text": "Add the middleware trace marker."} ) patched_request = request.override( system_message=SystemMessage(content=content_blocks) ) print(f"middleware wrap_model_call: messages={len(request.messages)}") return handler(patched_request) model = AgentReadyFakeModel( responses=["middleware trace marker: custom hook ran."] ) agent = create_agent( model=model, tools=[], system_prompt="Answer briefly.", middleware=[TraceModelCallMiddleware()], ) result = agent.invoke( { "messages": [ {"role": "user", "content": "Test the custom middleware."} ], "model_call_count": 0, } ) print(f"Agent reply: {result['messages'][-1].content}") print(f"Model calls recorded: {result['model_call_count']}") print(f"Last user message: {result['last_user_message']}")
TraceState declares the custom fields that the middleware writes. before_model() records the user message and call count, and wrap_model_call() modifies the model request before calling the handler.
- Run the middleware script.
$ python custom_middleware_agent.py middleware before_model: call=1, latest='Test the custom middleware.' middleware wrap_model_call: messages=1 Agent reply: middleware trace marker: custom hook ran. Model calls recorded: 1 Last user message: Test the custom middleware.
The two middleware lines come from the custom hooks. The final state fields show that the model_call_count and last_user_message updates were returned through agent.invoke().
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.