概述
Middleware 不是装饰器语法糖,而是被编译进 Agent 图里的节点和包装器
上一篇我们讲了 create_agent() 如何自动组装 LangGraph StateGraph。
这一篇继续往下看 Middleware。
从使用者视角看,Middleware 很像 Web 框架里的拦截器:
python
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, wrap_model_call
@before_model
def log_before_model(state, runtime):
print("before model")
return None
@wrap_model_call
def retry_model(request, handler):
try:
return handler(request)
except Exception:
return handler(request)
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[],
middleware=[log_before_model, retry_model],
)
看起来只是把几个函数塞进 middleware=[...]。
但源码里发生的事情更具体:
text
create_agent(...)
-> 收集中间件 hook
-> 把 before/after hook 变成 graph node
-> 把 wrap_model_call 组合成模型调用洋葱链
-> 把 wrap_tool_call 组合成工具调用洋葱链
-> 添加 StateGraph 边和条件边
-> compile 成 CompiledStateGraph
所以 Middleware 的本质不是"Agent 外面套一层函数",而是:
- Node-style hook:被注册成 LangGraph 节点,参与图的状态更新和跳转。
- Wrap-style hook:被组合成包装链,包住一次模型调用或一次工具调用。
- State reducer :负责把 hook 返回的状态增量合并回
AgentState。 - Conditional edge :负责根据
jump_to、tool calls、结构化输出等条件继续走图或结束。
Middleware 是 create_agent() 在编译图时插入的生命周期扩展点,它既有图节点,也有函数包装链。
源码里的六类 Hook
但根据当前 LangChain OSS Python 文档和本地 langchain==1.3.9 源码,公开的 Middleware hook 分成六类:
| 类型 | Hook | 执行时机 | 是否每轮循环都会执行 |
|---|---|---|---|
| Node-style | before_agent |
Agent 调用开始前 | 否,只执行一次 |
| Node-style | before_model |
每次模型调用前 | 是 |
| Node-style | after_model |
每次模型返回后 | 是 |
| Node-style | after_agent |
Agent 调用结束前 | 否,只执行一次 |
| Wrap-style | wrap_model_call |
包裹每次模型调用 | 是 |
| Wrap-style | wrap_tool_call |
包裹每次工具调用 | 是 |
有些旧资料会提到 before_agent_execute,但当前源码中的基类 AgentMiddleware 并没有这个公开方法。生命周期入口已经拆成了 before_agent 和 after_agent。
本文按当前源码写作,核心文件是:
text
langchain/agents/middleware/types.py
langchain/agents/factory.py
其中:
types.py:定义AgentState、AgentMiddleware、ModelRequest、ModelResponse、各类 decorator。factory.py:在create_agent()中收集 hook、组合 wrapper、添加 graph node 和 edge。
第一层源码:AgentState 是所有 Middleware 共享的状态底座
先看状态。
AgentState 位于:
text
langchain/agents/middleware/types.py
核心结构可以简化成:
python
class AgentState(TypedDict, Generic[ResponseT]):
messages: Required[Annotated[list[AnyMessage], add_messages]]
jump_to: NotRequired[Annotated[JumpTo | None, EphemeralValue, PrivateStateAttr]]
structured_response: NotRequired[Annotated[ResponseT, OmitFromInput]]
三个字段非常关键:
| 字段 | 作用 |
|---|---|
messages |
Agent 主上下文,保存用户消息、AI 消息、Tool 消息 |
jump_to |
Middleware 用来请求跳转到 model、tools 或 end |
structured_response |
结构化输出成功后的最终对象 |
这里最容易忽略的是 messages 上的 add_messages reducer。
这意味着 hook 返回:
python
return {"messages": [AIMessage(content="blocked")]}
并不是把历史消息整体替换掉,而是通过 reducer 追加或合并消息。
而 jump_to 使用了 EphemeralValue 和 PrivateStateAttr,表示它更像一次性的内部控制信号,不应该暴露成普通输入输出字段。
Middleware 能改变 Agent 行为,是因为它能返回状态增量,而这些状态增量会被 LangGraph reducer 合并进 AgentState。
第二层源码:AgentMiddleware 基类定义了六类生命周期入口
AgentMiddleware 也在 types.py。
它的形状可以简化成:
python
class AgentMiddleware(Generic[StateT, ContextT, ResponseT]):
state_schema: type[StateT] = _DefaultAgentState
tools: Sequence[BaseTool]
transformers: Sequence[TransformerFactory] = ()
def before_agent(self, state, runtime): ...
async def abefore_agent(self, state, runtime): ...
def before_model(self, state, runtime): ...
async def abefore_model(self, state, runtime): ...
def after_model(self, state, runtime): ...
async def aafter_model(self, state, runtime): ...
def wrap_model_call(self, request, handler): ...
async def awrap_model_call(self, request, handler): ...
def after_agent(self, state, runtime): ...
async def aafter_agent(self, state, runtime): ...
def wrap_tool_call(self, request, handler): ...
async def awrap_tool_call(self, request, handler): ...
它不强制你每个方法都实现。
create_agent() 会判断某个 middleware 类是否覆写了基类方法:
python
m.__class__.before_model is not AgentMiddleware.before_model
如果覆写了,就认为这个 middleware 提供了对应 hook。
这也是为什么 class-based middleware 可以只实现自己需要的部分:
python
from typing import Any
from langchain.agents.middleware import AgentMiddleware, AgentState
from langgraph.runtime import Runtime
class AuditMiddleware(AgentMiddleware):
def before_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print("agent starts")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print("model returned")
return None
源码里还有三个 class attribute 值得记住:
| 属性 | 作用 |
|---|---|
state_schema |
声明该 middleware 需要扩展哪些 state 字段 |
tools |
声明 middleware 自带的工具 |
transformers |
声明 stream transformer,用于流式事件投影 |
本篇文章将重点讲 hook 和执行顺序,transformers 后面讲流式系统时再展开。
AgentMiddleware 是一个可选择实现的生命周期接口,真正的"是否启用"由 create_agent() 检查方法是否被覆写来决定。
第三层源码:decorator 不是魔法,只是动态创建 AgentMiddleware 子类
我们常写:
python
from langchain.agents.middleware import before_model
@before_model
def check_input(state, runtime):
return None
这看起来像普通函数装饰器。
但 types.py 里的 decorator 会动态构造一个 AgentMiddleware 子类,并把你的函数放到对应方法上。
以 wrap_model_call 为例,源码逻辑可以简化成:
python
def wrap_model_call(func=None, *, state_schema=None, tools=None, name=None):
def decorator(func):
def wrapped(self, request, handler):
return func(request, handler)
middleware_name = name or func.__name__
return type(
middleware_name,
(AgentMiddleware,),
{
"state_schema": state_schema or AgentState,
"tools": tools or [],
"wrap_model_call": wrapped,
},
)()
if func is not None:
return decorator(func)
return decorator
before_model、after_model、before_agent、after_agent、wrap_tool_call 也是同一个思路:
text
函数
-> decorator 包装
-> 动态生成 AgentMiddleware 子类
-> 返回 middleware 实例
-> create_agent(middleware=[...]) 统一处理
所以 decorator-based middleware 和 class-based middleware 在进入 create_agent() 之后没有本质区别。
| 写法 | 适合场景 |
|---|---|
| decorator-based | 单个 hook、快速实验、逻辑简单 |
| class-based | 多个 hook、需要配置、需要同步/异步双实现、需要复用 |
装饰器只是把函数适配成 AgentMiddleware 实例,核心运行机制仍然是 AgentMiddleware。
执行时序:before 顺序执行,after 逆序执行,wrap 洋葱嵌套
假设我们这样注册 middleware:
python
agent = create_agent(
model="openai:gpt-4o-mini",
tools=[search, calculator],
middleware=[
auth_middleware,
cache_middleware,
retry_middleware,
],
)
执行顺序不是简单地从上到下全部跑完。
当前规则是:
text
before_agent:
auth.before_agent
cache.before_agent
retry.before_agent
每一轮 Agent loop:
before_model:
auth.before_model
cache.before_model
retry.before_model
wrap_model_call:
auth.wrap_model_call(
cache.wrap_model_call(
retry.wrap_model_call(
model.invoke(...)
)
)
)
after_model:
retry.after_model
cache.after_model
auth.after_model
wrap_tool_call:
auth.wrap_tool_call(
cache.wrap_tool_call(
retry.wrap_tool_call(
tool.invoke(...)
)
)
)
after_agent:
retry.after_agent
cache.after_agent
auth.after_agent
可以用一张图理解:
text
START
|
v
auth.before_agent -> cache.before_agent -> retry.before_agent
|
v
auth.before_model -> cache.before_model -> retry.before_model
|
v
auth.wrap_model_call
|
+-- cache.wrap_model_call
|
+-- retry.wrap_model_call
|
+-- model.invoke()
|
<-----+
<-----------+
|
v
retry.after_model -> cache.after_model -> auth.after_model
|
+-- 有工具调用 --> tools -> 回到 before_model
|
+-- 无工具调用 --> retry.after_agent -> cache.after_agent -> auth.after_agent -> END
这个顺序和很多 Web 中间件类似:
- 进入阶段:从外到内。
- 核心调用:最内层执行真实模型或工具。
- 返回阶段:从内到外。
区别是 LangChain 还有显式的 LangGraph 节点和条件边。
before_* 按注册顺序跑,after_* 按注册逆序跑,wrap_* 是第一项包住后面所有项的洋葱模型。
factory.py 第一步:收集哪些 middleware 实现了哪些 hook
create_agent() 在 factory.py 里会先把 middleware 分组。
源码可以简化为:
python
middleware_w_before_agent = [
m for m in middleware
if m.__class__.before_agent is not AgentMiddleware.before_agent
or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent
]
middleware_w_before_model = [
m for m in middleware
if m.__class__.before_model is not AgentMiddleware.before_model
or m.__class__.abefore_model is not AgentMiddleware.abefore_model
]
middleware_w_after_model = [...]
middleware_w_after_agent = [...]
middleware_w_wrap_model_call = [...]
middleware_w_wrap_tool_call = [...]
这里有两个设计点:
第一,它同时检查同步和异步版本。
比如只实现了 awrap_model_call,也会被认为提供了 model wrap hook。之后如果你用同步 agent.invoke() 调用,基类同步方法会抛出 NotImplementedError,提示你当前 middleware 只支持异步路径。
第二,它会检查 middleware 名称是否重复:
python
if len({m.name for m in middleware}) != len(middleware):
raise AssertionError("Please remove duplicate middleware instances.")
因为 graph node 名称会使用:
text
{m.name}.before_model
{m.name}.after_model
如果同名,图节点会冲突。
所以自定义 middleware 时,最好保证每个实例的 name 唯一。
python
class NamedMiddleware(AgentMiddleware):
def __init__(self, name: str):
self._name = name
@property
def name(self) -> str:
return self._name
create_agent() 不是盲目执行所有 middleware,而是先按覆写的方法把它们分组,再分别接入图节点或包装链。
factory.py 第二步:把 wrap_model_call 组合成洋葱链
wrap_model_call 的核心能力是控制模型调用:
- 可以修改请求。
- 可以调用
handler(request)执行真实模型。 - 可以不调用 handler,直接短路返回。
- 可以多次调用 handler,实现重试。
- 可以捕获异常后切换模型。
- 可以改写模型响应。
factory.py 使用 _chain_model_call_handlers() 把多个 wrapper 组合起来。
简化逻辑是:
python
def _chain_model_call_handlers(handlers):
if not handlers:
return None
def compose_two(outer, inner):
def composed(request, handler):
def inner_handler(req):
inner_result = inner(req, handler)
return normalize(inner_result)
outer_result = outer(request, inner_handler)
return normalize(outer_result)
return composed
composed_handler = compose_two(handlers[-2], handlers[-1])
for h in reversed(handlers[:-2]):
composed_handler = compose_two(h, composed_handler)
return composed_handler
假设有三个 wrapper:
text
[auth, cache, retry]
最终得到:
text
auth(
cache(
retry(
model
)
)
)
请求进入时:
text
auth -> cache -> retry -> model
响应返回时:
text
model -> retry -> cache -> auth
这就是"洋葱模型"。
示例:模型 fallback middleware。
python
from collections.abc import Callable
from langchain.agents.middleware import ModelRequest, ModelResponse, wrap_model_call
from langchain.chat_models import init_chat_model
fallback = init_chat_model("openai:gpt-4o-mini")
@wrap_model_call
def fallback_on_error(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
try:
return handler(request)
except Exception:
return handler(request.override(model=fallback))
request.override(...) 很重要。
ModelRequest 当前源码已经不推荐直接改属性,而是推荐创建一个带覆盖值的新请求:
python
new_request = request.override(model=fallback)
这样 wrapper 层之间的数据传递更清晰,也更不容易产生隐式副作用。
wrap_model_call 的组合方式是"第一个 middleware 最外层",它拿到控制权最大,适合鉴权、缓存、fallback、重试和动态模型选择。
ModelRequest 和 ModelResponse:wrapper 能改什么、返回什么
wrap_model_call 的入参是 ModelRequest。
它包含:
| 字段 | 含义 |
|---|---|
model |
当前要调用的 BaseChatModel |
messages |
不包含 system message 的对话消息 |
system_message |
系统消息 |
tool_choice |
工具选择策略 |
tools |
当前模型可见的工具 |
response_format |
结构化输出配置 |
state |
当前 Agent state |
runtime |
LangGraph runtime |
model_settings |
模型调用参数 |
真实模型调用发生在 factory.py 的 _execute_model_sync():
python
def _execute_model_sync(request):
model_, effective_response_format = _get_bound_model(request)
messages = request.messages
if request.system_message:
messages = [request.system_message, *messages]
output = model_.invoke(messages)
handled_output = _handle_model_output(output, effective_response_format)
return ModelResponse(
result=handled_output["messages"],
structured_response=handled_output.get("structured_response"),
)
也就是说:
text
wrap_model_call
-> handler(request)
-> _execute_model_sync(request)
-> model.invoke(messages)
-> ModelResponse
wrap_model_call 可以返回三类结果:
| 返回类型 | 适合场景 |
|---|---|
ModelResponse |
完整保留 messages 和 structured_response |
AIMessage |
简单短路或简单改写 |
ExtendedModelResponse |
除模型响应外,还要返回一个 Command 更新 state |
比如记录 token 用量:
python
from collections.abc import Callable
from typing_extensions import NotRequired
from langchain.agents.middleware import (
AgentState,
ExtendedModelResponse,
ModelRequest,
ModelResponse,
wrap_model_call,
)
from langgraph.types import Command
class UsageState(AgentState):
last_model_tokens: NotRequired[int]
@wrap_model_call(state_schema=UsageState)
def track_usage(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ExtendedModelResponse:
response = handler(request)
usage = response.result[0].usage_metadata or {}
total_tokens = usage.get("total_tokens", 0)
return ExtendedModelResponse(
model_response=response,
command=Command(update={"last_model_tokens": total_tokens}),
)
这里不能直接 return {"last_model_tokens": total_tokens},因为 wrap_model_call 不是 graph node,它是模型调用的包装函数。
要从 wrapper 层更新 state,需要通过:
python
ExtendedModelResponse(
model_response=response,
command=Command(update={...}),
)
Node-style hook 直接返回 dict 更新 state;wrap_model_call 要更新 state,需要把 Command 包进 ExtendedModelResponse。
factory.py 第三步:把 wrap_tool_call 交给 ToolNode
工具调用的 wrapper 走另一条路径。
create_agent() 会先收集实现了 wrap_tool_call 或 awrap_tool_call 的 middleware,然后组合:
python
wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)
组合规则同样是第一个最外层。
接着创建 ToolNode:
python
tool_node = ToolNode(
tools=available_tools,
wrap_tool_call=wrap_tool_call_wrapper,
awrap_tool_call=awrap_tool_call_wrapper,
)
所以 wrap_tool_call 实际不是 model node 的一部分,而是 tools 节点执行每个工具时的包装逻辑。
示例:工具调用监控。
python
from collections.abc import Callable
from langchain.agents.middleware import wrap_tool_call
from langchain_core.messages import ToolMessage
from langgraph.prebuilt.tool_node import ToolCallRequest
@wrap_tool_call
def monitor_tool(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage],
) -> ToolMessage:
print(f"tool starts: {request.tool_call['name']}")
result = handler(request)
print(f"tool ends: {request.tool_call['name']}, status={result.status}")
return result
wrap_tool_call 可以:
- 修改 tool call 参数。
- 捕获工具异常。
- 重试工具调用。
- 返回一个替代
ToolMessage。 - 返回
Command来更新 state 或控制图行为。
但要注意一个边界:
text
wrap_model_call 控制模型调用。
wrap_tool_call 控制工具调用。
如果你在 wrap_model_call 里动态给 request.tools 增加工具,但这个工具没有注册到 create_agent(tools=[...]) 或 middleware 的 tools 属性里,ToolNode 默认不知道怎么执行它。
源码里也有对应的错误提示,大意是:
text
Middleware added tools that the agent doesn't know how to execute.
解决方式通常有两个:
| 方式 | 说明 |
|---|---|
| 启动时注册 | 把工具放进 create_agent(tools=[...]) 或 middleware 的 tools |
| 动态处理 | 在 wrap_tool_call 里识别动态工具并自己执行 |
wrap_tool_call 不是包住整个 Agent,而是被传给 ToolNode,在每一次具体工具执行时生效。
factory.py 第四步:Node-style hook 被注册成 LangGraph 节点
before_agent、before_model、after_model、after_agent 不是 wrapper,而是图节点。
factory.py 里会给每个 hook 加节点:
python
graph.add_node(
f"{m.name}.before_model",
RunnableCallable(sync_before, async_before, trace=False),
input_schema=resolved_state_schema,
)
节点名形如:
text
MyMiddleware.before_agent
MyMiddleware.before_model
MyMiddleware.after_model
MyMiddleware.after_agent
然后 create_agent() 决定四个关键位置:
python
entry_node = first_before_agent or first_before_model or "model"
loop_entry_node = first_before_model or "model"
loop_exit_node = first_after_model or "model"
exit_node = last_after_agent or END
这四个位置决定了整张图的骨架:
| 名称 | 含义 |
|---|---|
entry_node |
图从 START 进入的第一个节点 |
loop_entry_node |
每一轮 Agent loop 的入口 |
loop_exit_node |
每一轮模型调用后的出口 |
exit_node |
Agent 结束前要走的节点 |
如果存在 before_agent,入口是第一个 before_agent:
text
START -> middleware1.before_agent
如果不存在 before_agent,但存在 before_model:
text
START -> middleware1.before_model
如果都没有:
text
START -> model
Node-style hook 的本质就是 StateGraph 节点,create_agent() 通过 entry_node、loop_entry_node、loop_exit_node、exit_node 把它们插进 Agent 主循环。
图编排细节:为什么 after_model 和 after_agent 是逆序
before_model 的连边逻辑是顺序:
text
m1.before_model -> m2.before_model -> m3.before_model -> model
但 after_model 是反向接:
text
model -> m3.after_model -> m2.after_model -> m1.after_model
这和 wrapper 的返回路径一致。
假设 middleware 列表是:
python
[auth, cache, retry]
那么:
text
before_model:
auth -> cache -> retry -> model
after_model:
model -> retry -> cache -> auth
这样的设计可以让一组 middleware 的进入和退出更像成对括号:
text
auth enter
cache enter
retry enter
model
retry exit
cache exit
auth exit
after_agent 也是逆序:
text
retry.after_agent -> cache.after_agent -> auth.after_agent -> END
这对资源释放、审计收尾、耗时统计很有用。
比如:
python
class TimerMiddleware(AgentMiddleware):
def before_agent(self, state, runtime):
return {"started_at": time.perf_counter()}
def after_agent(self, state, runtime):
cost = time.perf_counter() - state["started_at"]
print(f"agent cost: {cost:.2f}s")
return None
如果外层 middleware 在进入时先建立上下文,退出时最后收尾,逆序就很自然。
after_* 逆序不是偶然,而是为了让 middleware 的进入和退出形成结构化嵌套。
Agent 跳转:jump_to 如何从状态字段变成条件边
Middleware 可以提前结束 Agent,或者要求回到模型、跳到工具节点。
示例:
python
from typing import Any
from langchain.agents.middleware import AgentState, before_model
from langchain_core.messages import AIMessage
from langgraph.runtime import Runtime
@before_model(can_jump_to=["end"])
def stop_if_too_long(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
if len(state["messages"]) > 50:
return {
"messages": [AIMessage(content="Conversation limit reached.")],
"jump_to": "end",
}
return None
关键是 can_jump_to=["end"]。
decorator 会把可跳转目标记录到 hook 函数上,class-based middleware 可以用:
python
from langchain.agents.middleware import hook_config
class LimitMiddleware(AgentMiddleware):
@hook_config(can_jump_to=["end"])
def before_model(self, state, runtime):
...
factory.py 里 _add_middleware_edge() 会根据 can_jump_to 决定添加普通边还是条件边。
简化逻辑:
python
if can_jump_to:
def jump_edge(state):
return resolve_jump(state.get("jump_to")) or default_destination
graph.add_conditional_edges(name, jump_edge, destinations)
else:
graph.add_edge(name, default_destination)
jump_to 的解析规则是:
jump_to |
目标 |
|---|---|
"model" |
回到 loop_entry_node,通常是第一个 before_model 或 model |
"tools" |
跳到 tools 节点 |
"end" |
跳到 exit_node,如果有 after_agent 会先走 after_agent |
这里有一个容易踩坑的点:
text
返回 {"jump_to": "end"} 之前,hook 必须声明 can_jump_to=["end"]。
否则 factory.py 不会为这个 middleware 节点建立对应条件边。
jump_to 是状态里的跳转意图,can_jump_to 是编译图时允许哪些跳转的声明,两者必须配合。
Agent 主循环:模型节点如何决定去工具、回模型还是结束
模型节点执行完之后,不是固定去 tools。
factory.py 里 _make_model_to_tools_edge() 会看当前 state。
逻辑可以简化成:
python
def model_to_tools(state):
if state.get("jump_to"):
return resolve_jump(state["jump_to"])
last_ai_message, tool_messages = fetch_last_ai_and_tool_messages(state["messages"])
if last_ai_message is None:
return end
if len(last_ai_message.tool_calls) == 0:
return end
pending_tool_calls = [
call for call in last_ai_message.tool_calls
if call["id"] not in executed_tool_message_ids
]
if pending_tool_calls:
return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]
if "structured_response" in state:
return end
return model
这个条件边对应 Agent 的核心循环:
text
model
|
+-- 没有 tool_calls -------------> end
|
+-- 有未执行 tool_calls ----------> tools
|
+-- structured_response 已生成 ---> end
|
+-- 有人工注入 ToolMessage 等情况 -> model
工具节点执行完之后,_make_tools_to_model_edge() 决定下一步:
text
tools
|
+-- 工具 return_direct=True ------> end
|
+-- 结构化输出工具完成 -----------> end
|
+-- 默认 ------------------------> model
这解释了为什么 after_model hook 很强:
它运行在模型返回之后、路由到工具之前。
因此它可以:
- 检查模型是否产生危险 tool call。
- 注入一条人工审批后的 ToolMessage。
- 设置
jump_to="end"提前结束。 - 设置
jump_to="model"让模型重新思考。
Agent 是否继续循环,不是由 while True 写死,而是由 LangGraph 条件边根据 state 动态路由。
异常处理:默认传播,控制权交给 wrap hook 和预置 middleware
Middleware 的异常处理要分两层看。
第一层:Node-style hook。
before_model、after_model 这类 hook 本身就是 graph node。如果 hook 内部抛异常,默认会沿着图执行传播出去。
所以不要在 node-style hook 里写脆弱逻辑:
python
@after_model
def unsafe_log(state, runtime):
# 如果最后一条消息不是你预期的结构,这里可能抛异常
print(state["messages"][-1].response_metadata["token_usage"])
return None
更稳妥:
python
@after_model
def safe_log(state, runtime):
last = state["messages"][-1]
usage = getattr(last, "usage_metadata", None)
if usage:
print(usage)
return None
第二层:Wrap-style hook。
wrap_model_call 和 wrap_tool_call 明确把 handler 交给你,因此异常控制权也交给你:
python
@wrap_model_call
def retry_model(request, handler):
for attempt in range(3):
try:
return handler(request)
except Exception:
if attempt == 2:
raise
工具调用同理:
python
@wrap_tool_call
def retry_tool(request, handler):
for attempt in range(3):
try:
return handler(request)
except Exception:
if attempt == 2:
raise
这也是 LangChain 提供预置 middleware 的原因。
例如:
ModelRetryMiddleware:模型调用重试。ModelFallbackMiddleware:模型故障切换。HumanInTheLoopMiddleware:工具调用前中断审批。SummarizationMiddleware:长上下文摘要。PIIMiddleware:敏感信息检测和处理。
它们不是特殊运行时能力,本质上也是基于这些 hook 做的组合。
LangChain 不会自动吞掉所有 middleware 异常;真正需要重试、fallback、降级时,应放进 wrap hook 或使用预置 middleware。
ExtendedModelResponse 的细节:多个 wrapper 同时更新 state 怎么合并
wrap_model_call 返回 ExtendedModelResponse 时,可以带一个 Command(update=...)。
问题来了:如果多个 wrapper 都返回 Command,谁先应用?
factory.py 里 _chain_model_call_handlers() 会把 commands 收集起来,并保持一个重要规则:
text
inner-first, then outer
也就是内层先应用,外层后应用。
对 reducer 字段,例如 messages:
text
inner message + outer message
对非 reducer 字段,例如普通字符串字段:
text
外层覆盖内层
示例:
python
from typing import Annotated
from typing_extensions import NotRequired
from langchain.agents.middleware import AgentMiddleware, AgentState
def last_wins(_old: str, new: str) -> str:
return new
class TraceState(AgentState):
trace_layer: NotRequired[Annotated[str, last_wins]]
如果:
text
inner -> {"trace_layer": "inner"}
outer -> {"trace_layer": "outer"}
最终是:
text
trace_layer = "outer"
这符合洋葱模型的直觉:最外层 middleware 拥有最终解释权。
但有一个限制要注意。
factory.py 在构建 model node 返回命令时,会拒绝 wrap_model_call 里带 goto、resume、graph 的 Command:
text
Command goto is not yet supported in wrap_model_call middleware.
Use the jump_to state field with before_model/after_model hooks instead.
所以:
- 想在
wrap_model_call里补充 state:可以用ExtendedModelResponse(command=Command(update=...))。 - 想控制图跳转:更推荐用
before_model或after_model返回{"jump_to": ...}。
ExtendedModelResponse 适合让 wrapper 更新 state,不适合让 wrapper 直接改图跳转;跳转应交给 node-style hook。
实战讲解:一个"审计 + 限流 + fallback"的组合 Middleware
下面写一个组合例子,把本文讲的机制串起来:
before_agent:记录请求开始。before_model:超过最大模型调用次数则结束。wrap_model_call:模型失败时切 fallback。after_model:统计调用次数。after_agent:输出审计日志。
python
import time
from collections.abc import Callable
from typing import Any
from langchain.agents.middleware import (
AgentMiddleware,
AgentState,
ModelRequest,
ModelResponse,
hook_config,
)
from langchain.chat_models import init_chat_model
from langchain_core.messages import AIMessage
from langgraph.runtime import Runtime
from typing_extensions import NotRequired
class AuditState(AgentState):
started_at: NotRequired[float]
model_call_count: NotRequired[int]
class AuditLimitFallbackMiddleware(AgentMiddleware[AuditState]):
state_schema = AuditState
def __init__(self, max_model_calls: int = 5):
self.max_model_calls = max_model_calls
self.fallback_model = init_chat_model("openai:gpt-4o-mini")
def before_agent(
self,
state: AuditState,
runtime: Runtime,
) -> dict[str, Any] | None:
return {
"started_at": time.perf_counter(),
"model_call_count": state.get("model_call_count", 0),
}
@hook_config(can_jump_to=["end"])
def before_model(
self,
state: AuditState,
runtime: Runtime,
) -> dict[str, Any] | None:
if state.get("model_call_count", 0) >= self.max_model_calls:
return {
"messages": [AIMessage(content="模型调用次数已达到上限。")],
"jump_to": "end",
}
return None
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
try:
return handler(request)
except Exception:
return handler(request.override(model=self.fallback_model))
def after_model(
self,
state: AuditState,
runtime: Runtime,
) -> dict[str, Any] | None:
return {
"model_call_count": state.get("model_call_count", 0) + 1,
}
def after_agent(
self,
state: AuditState,
runtime: Runtime,
) -> dict[str, Any] | None:
started_at = state.get("started_at")
if started_at is not None:
print(f"agent cost: {time.perf_counter() - started_at:.2f}s")
print(f"model calls: {state.get('model_call_count', 0)}")
return None
注册:
python
from langchain.agents import create_agent
agent = create_agent(
model="openai:gpt-4o",
tools=[],
middleware=[
AuditLimitFallbackMiddleware(max_model_calls=5),
],
)
这段代码背后对应的图大致是:
text
START
|
v
Audit.before_agent
|
v
Audit.before_model
|
+-- jump_to=end --> Audit.after_agent -> END
|
v
model node
|
+-- wrap_model_call 包裹 model.invoke
|
v
Audit.after_model
|
+-- 没有工具调用 --> Audit.after_agent -> END
如果有工具,还会多一段:
text
Audit.after_model
|
+-- 有 pending tool calls --> tools -> Audit.before_model -> model
复杂生产能力不是写进 Agent 主循环,而是拆成多个 hook,再由 create_agent() 编译进图。
常见误区
误区一:以为 Middleware 是 Agent 外层的一层普通函数。
不准确。
Node-style hook 是 graph node,wrap-style hook 是调用包装链。它们都在 create_agent() 编译时被接入。
误区二:以为 before_agent 每一轮都会执行。
不会。
before_agent 是每次 agent invocation 开始前一次;每轮模型调用前执行的是 before_model。
误区三:以为 after_agent 在 after_model 后面每轮都会执行。
不会。
after_agent 是结束前一次。每轮模型返回后执行的是 after_model。
误区四:以为 wrap_model_call 可以直接返回 dict 更新 state。
不行。
wrap_model_call 返回的是模型调用结果,更新 state 要用 ExtendedModelResponse(command=Command(update=...))。
误区五:以为 jump_to 写了就一定生效。
不一定。
必须通过 can_jump_to 或 hook_config(can_jump_to=[...]) 声明可跳转目标,让 factory.py 在编译图时添加条件边。
误区六:以为动态添加工具后 ToolNode 自动会执行。
不一定。
如果工具没有在 create_agent(tools=[...]) 或 middleware tools 里注册,ToolNode 默认不知道怎么执行。要么提前注册,要么用 wrap_tool_call 自己处理。
源码阅读路线:建议按这 5 步看
如果你想自己 debug Middleware,建议按这个顺序读源码。
第一步:看 types.py 的数据结构。
重点看:
text
ModelRequest
ModelResponse
ExtendedModelResponse
AgentState
AgentMiddleware
hook_config
before_model / after_model / wrap_model_call / wrap_tool_call decorators
先弄清楚 hook 入参、返回值和状态字段。
第二步:看 factory.py 的 hook 收集。
搜索:
text
middleware_w_before_agent
middleware_w_before_model
middleware_w_after_model
middleware_w_after_agent
middleware_w_wrap_model_call
middleware_w_wrap_tool_call
这一段告诉你哪些 middleware 会参与哪些阶段。
第三步:看 wrapper 组合。
搜索:
text
_chain_model_call_handlers
_chain_tool_call_wrappers
这一段是洋葱模型的核心。
第四步:看 graph node 和 edge。
搜索:
text
graph.add_node
graph.add_edge
graph.add_conditional_edges
重点理解:
text
entry_node
loop_entry_node
loop_exit_node
exit_node
第五步:看条件边。
搜索:
text
_make_model_to_tools_edge
_make_tools_to_model_edge
_add_middleware_edge
_resolve_jump
这一段解释 Agent 什么时候继续调用工具、什么时候回模型、什么时候结束。
读 Middleware 源码不要从预置 middleware 开始,先看 types.py 的接口,再看 factory.py 的编排。
总结
最后用一张总图收束:
text
create_agent(middleware=[m1, m2, m3])
|
v
检查 hook 覆写情况
|
+-- before_agent -> StateGraph node: m1.before_agent -> m2.before_agent -> m3.before_agent
|
+-- before_model -> StateGraph node: m1.before_model -> m2.before_model -> m3.before_model
|
+-- wrap_model -> m1(m2(m3(model.invoke)))
|
+-- after_model -> StateGraph node: m3.after_model -> m2.after_model -> m1.after_model
|
+-- wrap_tool -> m1(m2(m3(tool.invoke)))
|
+-- after_agent -> StateGraph node: m3.after_agent -> m2.after_agent -> m1.after_agent
|
v
添加 model/tools 节点和条件边
|
v
graph.compile(...)
记住三个结论:
- Node-style hook 是图节点 :返回
dict更新 state,配合jump_to和条件边控制流程。 - Wrap-style hook 是洋葱链 :通过
handler(request)控制模型或工具调用,可重试、短路、fallback、改写响应。 - 异常默认传播:真正的重试和降级要写在 wrap hook 里,或使用 LangChain 预置 middleware。