LangChain 内置的十几个中间件覆盖了大多数场景,很多人的第一反应是"够用了"。但只要真的把 Agent 推向生产环境,很快就会撞上一堵墙:内置中间件覆盖的是"通用需求",而业务里最棘手的往往是"自有需求"。比如每个请求都要注入租户级的鉴权上下文;比如提示词要按用户身份、当前时间、所在渠道动态拼装,而不是写死一条 SystemMessage;再比如合规团队要求每一次工具调用都留下可追溯的审计日志,谁在什么时间用哪个工具、耗时多少、结果是否异常。这三类需求------鉴权注入、动态提示词、自定义审计埋点------翻遍内置中间件列表,没有一个能直接拿来就用。
怎么办?LangChain 的答案是:把 Agent 生命周期的关键位置预留成"插槽",允许你把自己的函数挂进去,在框架自动触发的时机执行。这些被挂进去的函数,就是本文的主角------hook 函数(钩子函数)。可以说,前面几篇讲的内置中间件,本身就是用这套 hook 机制实现的;理解了 hook,你就从"中间件的使用者"变成了"中间件的作者"。
本文解决什么,是自定义中间件的完整方法论:hook 函数的设计思想、Node-style 与 Wrap-style 两大类共六个钩子(外加一个便捷装饰器)的用法、装饰器与类两种写法的底层统一、can_jump_to 流程跳转,以及最容易踩坑的多中间件执行顺序。
本文不展开什么,是不再重复讲解 AgentState、Runtime、create_agent 的基础用法,也不逐个介绍内置中间件(前置篇已覆盖),更不涉及长期记忆、子 Agent 等外围机制。
读完能完成什么,是能够独立实现任意自定义中间件:既会用装饰器快速挂一个钩子,也会用类封装一个可配置、可复用、可测试的中间件组件;能看懂并预判多个中间件混排时的完整执行时序;最后还会动手写出两个生产级示例------审计日志中间件和动态提示词中间件。
一、hook 函数:中间件体系的"扩展插槽"
1.1 什么是 hook 函数
Hook 函数,中文常叫钩子函数,指的是:在某个既定流程的特定时机,被框架、系统或主程序自动调用的扩展函数。
这个定义里有三个关键词,恰好对应 hook 思想的三要素:
- 不是你主动调用的 。普通函数是你写在业务代码里、自己
foo()调用的;hook 函数恰恰相反------你只负责"定义 + 挂载",什么时候执行、执行几次,完全由框架决定。当流程运行到某个"钩子点"时,系统自动触发它。 - 它依附于一个更大的执行流程。hook 不能独立存在,它必须挂在某个既定流程的某个时机上:"请求开始前""模型调用前""模型调用后""任务结束后""异常发生时"......离开了主流程,hook 毫无意义。
- 它的作用是不改主流程源码就插入自己的逻辑 。你不需要修改
create_agent的内部实现,就能在其中做日志、鉴权、修改输入、拦截输出、清理资源等操作。这就是"开闭原则"在 Agent 框架里的落地:对扩展开放,对修改关闭。
一句话总结:主流程预留了一些插槽,允许你在这些位置挂上自己的函数,这种被挂进去并在特定时机执行的函数,就是 hook 函数。
1.2 LangChain 的 hook 分类
LangChain 的中间件作用在 Agent 架构中,而 Agent 是基于 LangGraph 构建的流程图。LangChain 一共暴露了六个核心 hook 函数(外加若干便捷装饰器),按风格分为两类:
| 类型 | 包含的 hook | 执行位置 | 适合场景 |
|---|---|---|---|
| Node-style hooks(节点风格) | before_agent、before_model、after_model、after_agent |
在流程的特定节点运行 | 顺序逻辑:日志记录、输入验证、PII 脱敏、输出校验、状态更新 |
| Wrap-style hooks(包装风格) | wrap_model_call、wrap_tool_call(以及便捷版 @dynamic_prompt) |
在模型或工具调用的前后运行 | 控制流:重试、回退、缓存、请求/响应改写 |
先用一张图把六个钩子在 Agent 生命周期中的位置标出来,后面所有内容都围绕这张图展开:
#mermaid-svg-Vm5x2t38lbeyB0Tq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Vm5x2t38lbeyB0Tq .error-icon{fill:#552222;}#mermaid-svg-Vm5x2t38lbeyB0Tq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Vm5x2t38lbeyB0Tq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .marker.cross{stroke:#333333;}#mermaid-svg-Vm5x2t38lbeyB0Tq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Vm5x2t38lbeyB0Tq p{margin:0;}#mermaid-svg-Vm5x2t38lbeyB0Tq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .cluster-label text{fill:#333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .cluster-label span{color:#333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .cluster-label span p{background-color:transparent;}#mermaid-svg-Vm5x2t38lbeyB0Tq .label text,#mermaid-svg-Vm5x2t38lbeyB0Tq span{fill:#333;color:#333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .node rect,#mermaid-svg-Vm5x2t38lbeyB0Tq .node circle,#mermaid-svg-Vm5x2t38lbeyB0Tq .node ellipse,#mermaid-svg-Vm5x2t38lbeyB0Tq .node polygon,#mermaid-svg-Vm5x2t38lbeyB0Tq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Vm5x2t38lbeyB0Tq .rough-node .label text,#mermaid-svg-Vm5x2t38lbeyB0Tq .node .label text,#mermaid-svg-Vm5x2t38lbeyB0Tq .image-shape .label,#mermaid-svg-Vm5x2t38lbeyB0Tq .icon-shape .label{text-anchor:middle;}#mermaid-svg-Vm5x2t38lbeyB0Tq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Vm5x2t38lbeyB0Tq .rough-node .label,#mermaid-svg-Vm5x2t38lbeyB0Tq .node .label,#mermaid-svg-Vm5x2t38lbeyB0Tq .image-shape .label,#mermaid-svg-Vm5x2t38lbeyB0Tq .icon-shape .label{text-align:center;}#mermaid-svg-Vm5x2t38lbeyB0Tq .node.clickable{cursor:pointer;}#mermaid-svg-Vm5x2t38lbeyB0Tq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .arrowheadPath{fill:#333333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Vm5x2t38lbeyB0Tq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Vm5x2t38lbeyB0Tq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Vm5x2t38lbeyB0Tq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Vm5x2t38lbeyB0Tq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Vm5x2t38lbeyB0Tq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Vm5x2t38lbeyB0Tq .cluster text{fill:#333;}#mermaid-svg-Vm5x2t38lbeyB0Tq .cluster span{color:#333;}#mermaid-svg-Vm5x2t38lbeyB0Tq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Vm5x2t38lbeyB0Tq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Vm5x2t38lbeyB0Tq rect.text{fill:none;stroke-width:0;}#mermaid-svg-Vm5x2t38lbeyB0Tq .icon-shape,#mermaid-svg-Vm5x2t38lbeyB0Tq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Vm5x2t38lbeyB0Tq .icon-shape p,#mermaid-svg-Vm5x2t38lbeyB0Tq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Vm5x2t38lbeyB0Tq .icon-shape .label rect,#mermaid-svg-Vm5x2t38lbeyB0Tq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Vm5x2t38lbeyB0Tq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Vm5x2t38lbeyB0Tq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Vm5x2t38lbeyB0Tq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户输入 invoke()
before_agent
Agent 开始前(正序)
进入模型循环体
before_model
模型调用前(正序)
wrap_model_call
洋葱式包裹(1 最外层)
大模型调用
after_model
模型调用后(逆序)
模型要求调用工具?
wrap_tool_call
洋葱式包裹工具执行
after_agent
Agent 全部结束后(逆序)
返回最终结果
无论是官方内置中间件、自定义中间件、还是便捷装饰器中间件,通常都是通过实现其中一个或多个 hook 来生效的。所以"自定义中间件"本质上就是回答一个问题:实现哪几个 hook、用什么方式实现、挂载时按什么顺序传。
在动手之前,先建立一个对执行顺序的直觉。假设注册三个中间件,每个都实现了 before_model 和 after_model,一次 invoke 的输出会是:
text
[中间件 1] before_model
[中间件 2] before_model
[中间件 3] before_model
[中间件 3] after_model
[中间件 2] after_model
[中间件 1] after_model
before 正序、after 逆序,像一个洋葱:1→2→3→ 模型 →3→2→1。这个直觉先埋在这里,第五节会给出 Node/Wrap 混排时的完整实测时序。
二、Node-style hooks:节点式钩子全解
2.1 基本用法:基于装饰器实现
Node-style 的四个钩子,命名就说明了各自的触发时机:before_agent(Agent 开始运行前)、before_model(模型调用前)、after_model(模型调用后)、after_agent(Agent 全流程结束后)。装饰器是"函数式挂载",把一个 hook 快速挂到 Agent 的某个节点上:
python
from langchain.agents import create_agent
from langchain.agents.middleware import (
before_model, after_model, before_agent, after_agent, AgentState,
)
from langchain.messages import HumanMessage
from langgraph.runtime import Runtime
from typing import Any
# 1. 定义 before_agent 钩子:Agent 启动前触发
@before_agent
def before_agent_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> before_agent <- "
return None
# 2. 定义 before_model 钩子:每次调用模型前触发
@before_model
def before_model_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> before_model <- "
return None
# 3. 定义 after_model 钩子:每次模型返回后触发
@after_model
def after_model_middleware(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> after_model <- "
return None
# 4. 定义 after_agent 钩子:Agent 全流程结束后触发
@after_agent
def after_agent_middleware(state: AgentState, runtime: Runtime) -> None:
state["messages"][-1].content += " -> after_agent <- "
return None
agent = create_agent(
model=model, # 前置篇已通过 init_chat_model 初始化
middleware=[before_agent_middleware, before_model_middleware,
after_model_middleware, after_agent_middleware], # 👈 挂载钩子
)
response = agent.invoke({"messages": [HumanMessage("你好啊")]})
for msg in response["messages"]:
msg.pretty_print()
运行输出:
text
================================ Human Message =================================
你好啊 -> before_agent <- -> before_model <-
================================== Ai Message ==================================
你好!有什么可以帮你的吗? 😊 -> after_model <- -> after_agent <-
观察标记的追加顺序,结论非常清晰:
before_agent先于before_model执行,二者都在调用模型之前;after_agent晚于after_model执行,二者都在模型调用之后。
四个钩子构成了 Agent 一次完整运行的"前哨---出发---返航---收尾"闭环。
2.2 基于类实现
类写法是"对象化中间件":把中间件封装成一个可配置、可复用、可扩展的组件。关键规则只有三条:
- 必须继承
AgentMiddleware(固定); - 方法名固定为
before_model、after_model等 hook 名(固定); - 类名随意(不固定)。
LangGraph 只看两件事:是否继承了 AgentMiddleware?是否有那些约定名字的方法?
python
from langchain.agents.middleware import AgentMiddleware
class MyMiddleware(AgentMiddleware):
def __init__(self):
super().__init__()
def before_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> before_agent <- "
return None
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> before_model <- "
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> after_model <- "
return None
def after_agent(self, state: AgentState, runtime: Runtime) -> None:
state["messages"][-1].content += " -> after_agent <- "
return None
agent = create_agent(model=model, middleware=[MyMiddleware()])
response = agent.invoke({"messages": [HumanMessage("你好啊")]})
for msg in response["messages"]:
msg.pretty_print()
输出与装饰器版本完全一致。两种写法殊途同归,那它们的本质关系是什么?
2.3 两种方法的统一:装饰器底层在做什么
装饰器并不是和类并列的"另一套机制",它底层会基于你定义的函数动态构造一个 AgentMiddleware 子类的实例 。以 @after_model 装饰器为例,其底层实现的关键代码等价于:
python
return type(
middleware_name, # 类名:即被装饰函数的函数名
(AgentMiddleware,), # 父类:AgentMiddleware
{
"state_schema": state_schema or AgentState,
"tools": tools or [],
"after_model": wrapped, # wrapped 内部执行 func(state, runtime)
},
)() # 末尾的 () 表示实例化
拆开看:type() 动态创建了一个 AgentMiddleware 的子类;类名取自被装饰函数的函数名;子类携带 state_schema 和 tools 两个属性;其 after_model 方法内部逻辑就是执行你定义的 func(state, runtime);最后的括号立即实例化并返回对象。
所以,用装饰器最终得到的也是一个 AgentMiddleware 子类对象,和基于类的自定义方式在底层完全统一。装饰器是语法糖,类是本体。
2.4 参数说明
Node-style 的钩子函数接收两个固定参数:
state:一个AgentState实例,维护 Agent 运行过程中的状态,会随 Agent 运行而变化,核心是消息列表messages。你既可以在钩子里读取它(比如检查最后一条消息),也可以直接就地修改其中的消息对象,或通过返回值更新它。runtime:一个Runtime实例,维护 Agent 运行过程中的上下文环境,包括上下文(context)、长期记忆等。做鉴权注入、按用户身份差异化处理时,用户信息通常就从runtime里取。
2.5 返回值说明:None、字典与流程控制
钩子函数的返回值决定了它对流程的影响程度,共三种:
| 返回值 | 含义 | 典型示例 |
|---|---|---|
None |
不修改状态,流程继续 | return None |
| 字典 | 更新状态(按键合并进 state) | return {"count": count + 1} |
字典含 jump_to |
控制流程,跳转到指定节点 | return {"jump_to": "__end__"} |
jump_to 的合法目标有三类:"__end__"(结束 Agent)、"tools"(跳到工具节点)、其他自定义节点。比如在 before_model 里发现状态中的计数超限,可以直接熔断:
python
def before_model(self, state, runtime):
if state.get("count", 0) > 10:
return {"jump_to": "__end__"} # 跳过模型,直接结束
return None
2.6 装饰器参数 can_jump_to:三个跳转实战
钩子函数要使用 jump_to,必须先通过 can_jump_to 参数"申报"自己允许跳转到哪些位置。它决定了钩子函数可以直接跳转至流程的哪些位置,可取值如下:
end:跳转至 Agent 流程末尾(或第一个after_agent钩子),直接终止整个流程;tools:跳转至工具节点;model:跳转至模型节点(或第一个before_model钩子)。
下面用一个覆盖三种跳转的完整案例演示。业务设定:用户消息里出现 direct tool 就绕过模型直接触发工具;出现 retry model 就在模型回答后追加系统提示词并跳回模型重新生成;出现 overflow 就模拟上下文窗口溢出、直接熔断终止。
python
from typing import Any
from langchain.agents import create_agent
from langchain.agents.middleware import before_model, after_model, AgentState
from langchain.messages import AIMessage, SystemMessage
from langchain.tools import tool
from langgraph.runtime import Runtime
@tool
def get_news() -> str:
"""获取当日新闻"""
return "美加墨世界杯今日开幕"
# 场景 1:模型执行前,允许跳转 "tools"
@before_model(can_jump_to=["tools"])
def force_tool_first(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""用户输入含 'direct tool' 时,跳过模型思考,伪造 tool_calls 强行移交工具节点"""
text = state["messages"][-1].content
if isinstance(text, str) and "direct tool" in text.lower():
print("[MIDDLEWARE] before_model: jump_to='tools'")
# 人工构造 AIMessage,欺骗系统"这是模型决定调用的工具"
fake_tool_call = AIMessage(
content="人工构造的消息",
tool_calls=[{"name": "get_news", "args": {}, "id": "call_force_001"}],
)
return {"messages": [fake_tool_call], "jump_to": "tools"}
return None
# 场景 2:模型生成后,允许跳回 "model"
@after_model(can_jump_to=["model"])
def retry_with_extra_instruction(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""用户最初请求含 'retry model' 时,追加系统提示词,强制模型重新生成一次"""
user_text = next((m.content for m in reversed(state["messages"])
if getattr(m, "type", "") == "human"), "")
if isinstance(user_text, str) and "retry model" in user_text.lower():
# 防死循环:已注入过提示词就不再跳
already = any("你必须以【二次回答】开头" in str(getattr(m, "content", ""))
for m in state["messages"])
if already:
return None
print("[MIDDLEWARE] after_model: jump_to='model'")
return {"messages": [SystemMessage("你必须以【二次回答】开头,并且只用一句话回答。")],
"jump_to": "model"}
return None
# 场景 3:模型执行前,允许直接跳 "end" 熔断
@before_model(can_jump_to=["end"])
def overflow_context_processor(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""模拟上下文窗口溢出,直接终止流程,不让请求到达大模型"""
if "overflow" in state["messages"][-1].content:
print("[MIDDLEWARE] before_model: jump_to='end'")
return {"messages": [AIMessage("上下文窗口溢出,终止")], "jump_to": "end"}
return None
agent = create_agent(
model=model,
tools=[get_news],
middleware=[force_tool_first, retry_with_extra_instruction,
overflow_context_processor], # 执行顺序严格按列表声明顺序
)
def run_once(user_input: str):
result = agent.invoke({"messages": [{"role": "user", "content": user_input}]})
for msg in result["messages"]:
msg.pretty_print()
run_once("请帮我查今日新闻 direct tool") # Case 1:跳 tools
run_once("请随便介绍一下 LangChain retry model") # Case 2:跳回 model 重答
run_once("你好 overflow") # Case 3:直接终止
run_once("今日新闻摘要?") # Case 4:正常流程(对照组)
四个 Case 的实测输出(节选):
text
============== Case 1 ==============
[MIDDLEWARE] before_model: jump_to='tools'
Human Message: 请帮我查今日新闻 direct tool
Ai Message: 人工构造的消息 (Tool Calls: get_news)
Tool Message: 美加墨世界杯今日开幕
Ai Message: 今日新闻:美加墨世界杯今日开幕...
============== Case 2 ==============
[MIDDLEWARE] after_model: jump_to='model'
Ai Message: (第 1 版长回答,介绍 retry 机制)
System Message: 你必须以【二次回答】开头,并且只用一句话回答。
Ai Message: 【二次回答】 LangChain 的 retry model 就是给模型调用加上自动重试和指数退避机制...
============== Case 3 ==============
[MIDDLEWARE] before_model: jump_to='end'
Human Message: 你好 overflow
Ai Message: 上下文窗口溢出,终止
============== Case 4 ==============
(无中间件触发,走正常 User -> Model -> Tool -> Model -> End 标准流程)
逐条解读:
- Case 1 提前判定 :我们提前判定需要调用工具,直接在
before_model中跳转至工具节点,省去了一次模型调用------既省时又省 token。 - Case 2 反思重试 :通过约定
retry model标记,在after_model之后再次跳转到模型节点,触发模型重复调用。注意代码里的"防死循环"防御:必须检查提示词是否已注入过,否则会无限重跳。 - Case 3 安全熔断 :通过约定的
overflow标记模拟上下文溢出,在before_model中直接跳至结尾,提前终止流程,请求根本不会到达大模型。 - Case 4 对照组:没有任何中间件被触发,验证钩子只在条件满足时干预。
类写法实现同样的能力,唯一的区别是:类里不能用 @before_model(can_jump_to=...) 这种装饰器参数,需要引入 @hook_config 装饰器为方法传 can_jump_to:
python
from langchain.agents.middleware import hook_config, AgentMiddleware
class MyMiddleware(AgentMiddleware):
@hook_config(can_jump_to=["tools", "end"])
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
text = state["messages"][-1].content
if "overflow" in text: # 熔断
return {"messages": [AIMessage("上下文窗口溢出,终止")], "jump_to": "end"}
if isinstance(text, str) and "direct tool" in text.lower(): # 强制走工具
fake = AIMessage(content="人工构造的消息",
tool_calls=[{"name": "get_news", "args": {}, "id": "call_force_001"}])
return {"messages": [fake], "jump_to": "tools"}
return None
@hook_config(can_jump_to=["model"])
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
# 逻辑同装饰器版 retry_with_extra_instruction,此处从略
...
agent = create_agent(model=model, tools=[get_news], middleware=[MyMiddleware()])
实测输出与装饰器版本一致------再次印证两种写法底层统一。
2.7 Node-style 四钩子的典型场景
before_agent:会话初始化、加载用户画像、鉴权上下文注入;before_model:消息修剪(trim messages)、PII 脱敏、输入验证、条件路由、上下文溢出熔断;after_model:输出验证、格式化响应、统计信息、状态更新;after_agent:资源清理、会话审计汇总、结果落库。
三、Wrap-style hooks:包裹式钩子全解
Node-style 的钩子是"到点执行",只能拿到状态快照;Wrap-style 则更进一步------它把整个模型/工具调用包在了一个函数的内部,你可以在这"一层皮"里同时控制进出的两端:改请求、改响应,甚至决定要不要真的发起调用。这就是"洋葱式包裹":handler 往里是深入,handler 返回是浮出。
3.1 wrap_model_call:包裹模型调用
函数签名先看清楚:
python
def wrap_model_call(
request: ModelRequest, # 即将发给大模型的完整请求数据
handler: Callable[[ModelRequest], ModelResponse] # 句柄:下一层中间件或真实模型调用
) -> ModelResponse:
...
request:被封装的请求对象,包含model、messages、system_message、tools、state等字段,即即将发送给大模型的所有请求数据;handler:处理器,代表"下一层"------可能是下一个 wrap 中间件,也可能是最终的大模型调用服务。调用handler(request)才会真正消耗 token 并等待响应;不调用它,模型就根本不会被执行。
基于装饰器实现:
python
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.messages import HumanMessage
from langchain.agents import create_agent
from typing import Callable
@wrap_model_call
def wrap_model_call_middleware(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse | None:
# 【调用前】修改即将发出的请求:给最后一条消息追加标记
# 典型应用:统一为所有请求追加提示词(如"请用中文回答")
request.messages[-1].content += " -> wrap_model_call_before <- "
# 真正调用大模型(或流转到下一层中间件)
response = handler(request)
# 【调用后】在响应交付给状态机之前直接改写
# 典型应用:敏感词过滤、输出格式化、统一后处理标记
response.result[0].content += " -> wrap_model_call_after <- "
return response
agent = create_agent(model=model, middleware=[wrap_model_call_middleware])
response = agent.invoke({"messages": [HumanMessage("你好啊")]})
for msg in response["messages"]:
msg.pretty_print()
输出:
text
================================ Human Message =================================
你好啊 -> wrap_model_call_before <-
================================== Ai Message ==================================
你好!有什么我可以帮你的? -> wrap_model_call_after <-
模型调用前消息列表的最后一条是 HumanMessage,调用后最后一条是 AIMessage------请求侧和响应侧的修改都生效了 。这正是 Node-style 钩子做不到的事:before_model 和 after_model 虽然也分处模型前后,但它们是两个独立函数、两次独立触发,无法像 wrap 一样在"同一次调用"的两侧共享局部变量、实施重试缓存这类成对逻辑。
基于类实现:
python
from langchain.agents.middleware import AgentMiddleware
class WrapModelCallMiddleWare(AgentMiddleware):
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse | None:
request.messages[-1].content += " -> wrap_model_call_before <- "
response = handler(request)
response.result[0].content += " -> wrap_model_call_after <- "
return response
agent = create_agent(model=model, middleware=[WrapModelCallMiddleWare()])
注意类写法的方法比钩子函数多一个 self------其余完全一致。和 Node-style 一样,@wrap_model_call 装饰器底层同样会动态构造 AgentMiddleware 子类并实例化,两种写法统一。
3.2 三个经典场景:重试、缓存、动态系统提示
Wrap-style 的杀手级应用,是把"围绕一次调用的成对逻辑"收进一个函数里。
场景 1:自动重试(指数退避)
python
@wrap_model_call
def retry_model(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
"""自动重试失败的模型调用"""
max_retries = 3
for attempt in range(max_retries):
try:
print(f"🔄 尝试调用模型(第 {attempt + 1}/{max_retries} 次)")
return handler(request)
except Exception as e:
if attempt == max_retries - 1:
print(f"❌ 所有重试失败:{e}")
raise
wait_time = 2 ** attempt # 指数退避:1s、2s、4s
print(f"⚠ 调用失败:{e},{wait_time} 秒后重试")
time.sleep(wait_time)
场景 2:响应缓存
python
class ModelCache:
"""模型响应缓存"""
def __init__(self):
self.cache = {}
def create_hook(self):
@wrap_model_call
def cache_model(request, handler):
# 用消息+系统提示生成缓存键
cache_key = hashlib.md5(json.dumps({
"messages": [str(m) for m in request.messages],
"system": str(request.system_message),
}).encode()).hexdigest()
if cache_key in self.cache:
print("💾 缓存命中!")
return self.cache[cache_key]
print("🔍 缓存未命中,调用模型")
response = handler(request)
self.cache[cache_key] = response
return response
return cache_model
cache = ModelCache()
agent = create_agent(model=model, middleware=[cache.create_hook()])
场景 3:动态修改系统提示 ------注意这里用到了 request.override() 方法,以"不可变更新"的方式替换请求字段:
python
@wrap_model_call
def add_context(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
"""动态添加上下文信息到系统提示"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
original = request.system_message.content if request.system_message else ""
new_system_message = SystemMessage(content=f"""{original}
当前时间:{current_time}
用户位置:中国
语言偏好:中文
""")
modified_request = request.override(system_message=new_system_message)
return handler(modified_request)
3.3 wrap_tool_call:包裹工具调用
工具侧的对应物是 wrap_tool_call,请求类型换成 ToolCallRequest,handler 返回 ToolMessage | Command。它最强大的玩法是:可以多次调用 handler、并在调用之间修改工具参数------相当于给工具执行加了一层可控的"预演 + 正式":
python
from langchain.agents.middleware import wrap_tool_call
from langchain.tools.tool_node import ToolCallRequest
from langchain.messages import HumanMessage, ToolMessage
from langchain.tools import tool
from langgraph.types import Command
from typing import Callable
@tool
def get_weather(city: str, is_forcast: bool) -> str:
"""获取当日特定城市的天气
Args:
city: 城市名称
is_forcast: 是否包含明天的天气预报
"""
res = f"{city} 今天天气不错"
if is_forcast:
res += "\n明天天气也很好"
return res
@wrap_tool_call
def wrap_tool_call_middleware(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
result = handler(request) # 第一遍:按模型给的参数执行
print(f"原始参数:{request.tool_call['args']}")
print(f"原始参数调用结果:{result}")
request.tool_call["args"]["is_forcast"] = True # 中途改参数
result = handler(request) # 第二遍:用新参数再执行
print(f"更新后的参数:{request.tool_call['args']}")
print(f"更新参数调用结果:{result}")
return result # 以第二遍结果为准
agent = create_agent(model=model, tools=[get_weather],
middleware=[wrap_tool_call_middleware])
response = agent.invoke({"messages": [HumanMessage("你好啊,今天杭州的天气怎么样")]})
for msg in response["messages"]:
msg.pretty_print()
输出(节选):
text
原始参数:{'city': '杭州', 'is_forcast': False}
原始参数调用结果: content='杭州今天天气不错' name='get_weather' tool_call_id='call_2Stb...'
更新后的参数:{'city': '杭州', 'is_forcast': True}
更新参数调用结果: content='杭州今天天气不错\n明天天气也很好' name='get_weather' ...
================================= Tool Message =================================
Name: get_weather
杭州今天天气不错
明天天气也很好
================================== Ai Message ==================================
杭州今天天气不错,明天天气也很好。
模型本来只让查"今天"的天气,但中间件把 is_forcast 改成了 True 再执行一遍,最终工具结果里包含了明天预报,模型据此回答。类写法(WrapToolCallMiddleware 继承 AgentMiddleware,重写 wrap_tool_call 方法,多一个 self)输出完全一致。
类似的典型用途还有监控工具执行耗时:
python
@wrap_tool_call
def monitor_tool(request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command]) -> ToolMessage | Command:
"""监控工具执行时间和状态"""
tool_name = request.tool_call["name"]
print(f"🔧 开始执行工具:{tool_name},参数:{request.tool_call.get('args', {})}")
start_time = time.time()
try:
result = handler(request)
print(f"✅ 执行成功,耗时 {time.time() - start_time:.2f} 秒")
return result
except Exception as e:
print(f"❌ 执行失败:{e},耗时 {time.time() - start_time:.2f} 秒")
raise
3.4 @dynamic_prompt:动态提示词的便捷版
如果只想在每次模型调用前动态生成系统提示词,写完整 wrap 有点重。LangChain 提供了便捷装饰器 @dynamic_prompt------它底层就是基于 wrap_model_call 构建的中间件,专门用于动态生成系统提示。被装饰的函数接收 ModelRequest,直接返回提示词字符串即可,框架负责拿它去完成实际的模型调用:
python
from langchain.agents.middleware import dynamic_prompt, ModelRequest
@dynamic_prompt
def dynamic_system_prompt(request: ModelRequest) -> str:
"""每次模型调用前动态生成系统提示词"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"你是智能助手。当前时间:{now};语言偏好:中文;回答保持简洁。"
第六节实战会把它用全。需要修改请求的其他字段、做重试缓存时,仍然回到完整版 wrap_model_call。
四、装饰器 vs 类:到底怎么选
两种写法底层统一、能力等价,那选择标准是什么?实践中沉淀了三种典型情况,逐一说清。
情况 1:单钩子用装饰器,多钩子用类。 当中间件只需实现一个钩子函数时,装饰器最直接。当一个中间件要组合多个钩子时(比如同时要 before_model + after_model),装饰器不是做不到------可以用"工厂函数"返回多个被装饰的函数:
python
def create_audit_middleware(logger):
@before_model
def before_log(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
logger.info("调用模型前消息数量: {}", len(state["messages"]))
return None
@after_model
def after_log(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
logger.info("调用模型后消息数量: {}", len(state["messages"]))
return None
return [before_log, after_log]
agent = create_agent(model=model, middleware=[*create_audit_middleware(logger=logger)])
但这等于把"逻辑上属于同一个中间件"的行为拆成多个独立函数、再由外部统一组装,不如类写法自然、集中、清晰。同样的逻辑用类实现:
python
class CreateAuditMiddleware(AgentMiddleware):
def __init__(self, logger):
super().__init__()
self.logger = logger
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
self.logger.info("调用模型前消息数量: {}", len(state["messages"]))
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
self.logger.info("调用模型后消息数量: {}", len(state["messages"]))
return None
agent = create_agent(model=model, middleware=[CreateAuditMiddleware(logger=logger)])
情况 2:复杂配置用类。 装饰器传参只能靠闭包,而闭包里的参数是"黑盒"------运行时无法自省。对比两个版本的中间件对象:
python
class_middle = [AuditMiddleware(logger=logger, threshold=5, middleware_name="short limit"),
AuditMiddleware(logger=logger, threshold=50, middleware_name="long limit")]
decorator_middle = [create_audit_middleware(logger=logger, threshold=5, middleware_name="short limit"),
create_audit_middleware(logger=logger, threshold=50, middleware_name="long limit")]
for mw in class_middle:
print(type(mw)); print(mw.__dict__) # 配置全部可见、可校验
for mw in decorator_middle:
print(type(mw)); print(mw.__dict__) # 实例 __dict__ 为空,配置藏在闭包里
输出:
text
-> class 风格的中间件 <-
<class '__main__.AuditMiddleware'>
{'logger': <loguru.logger ...>, 'threshold': 5, 'middleware_name': 'short limit'}
<class '__main__.AuditMiddleware'>
{'logger': <loguru.logger ...>, 'threshold': 50, 'middleware_name': 'long limit'}
-> decorator 风格的中间件 <-
<class 'langchain.agents.middleware.types.audit_middleware'>
{}
<class 'langchain.agents.middleware.types.audit_middleware'>
{}
类实例的 __dict__ 里 threshold、middleware_name 一目了然,便于运行时类型校验与调试;装饰器闭包版本则是空字典,配置对外不可见。当中间件需要同时提供同步/异步实现时,类也更有优势。
情况 3:跨项目复用用类。 若希望中间件成为可实例化、可封装、可测试的组件,类写法天然合适------这些本就是类擅长的场景。装饰器闭包也能实现,但使用不友好。
汇总成选择建议表:
| 维度 | 装饰器写法 | 类写法(继承 AgentMiddleware) |
|---|---|---|
| 钩子数量 | 单钩子首选 | 多钩子组合首选 |
| 复杂配置 | 依赖闭包,配置不可自省 | __init__ 显式收参,__dict__ 可自省、可校验 |
| 跳转申报 | @before_model(can_jump_to=[...]) |
方法上加 @hook_config(can_jump_to=[...]) |
| 复用与测试 | 闭包难 mock,迁移成本高 | 可实例化、可注入依赖、单测友好 |
| 同步/异步 | 每个函数单独处理 | 同一组件内成对提供 |
| 适用阶段 | 快速原型、脚本实验 | 生产组件、团队共享库 |
一句话结论:装饰器与类只是定义中间件的两种方式,底层实现统一;单钩子、逻辑简单、快速原型选装饰器;多钩子组合、复杂配置、跨项目复用、可测试性要求高,选类。
五、hook 函数执行顺序:踩坑重灾区
这是本章最重要的一节。当多个中间件混排时,谁先谁后?规则按 hook 类型分类记忆:
before_*钩子:从前到后执行(按注册顺序);after_*钩子:从后往前执行(按注册顺序逆序);wrap_*钩子:洋葱架构,先注册的包在最外层。
特别强调:这里的顺序不是函数定义顺序,而是创建 Agent 时传入 middleware 列表的顺序。注册顺序就是执行顺序的唯一事实来源。
5.1 实测:Node / Wrap 混排完整时序
下面刻意把中间件定义成乱序,注册时再按 1→2→3 排列,观察一次 invoke 中标记的追加顺序:
python
from langchain.agents.middleware import (
before_model, after_model, wrap_model_call, AgentState,
ModelRequest, ModelResponse,
)
from langchain.messages import HumanMessage
from langgraph.runtime import Runtime
from langchain.agents import create_agent
from typing import Any, Callable
# ---- 定义顺序故意打乱:3 在最前,1 在后 ----
@before_model
def before_model_middleware3(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> before_model-3 <- "
return None
@before_model
def before_model_middleware1(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> before_model-1 <- "
return None
@before_model
def before_model_middleware2(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> before_model-2 <- "
return None
@after_model
def after_model_middleware2(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> after_model-2 <- "
return None
@after_model
def after_model_middleware1(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> after_model-1 <- "
return None
@after_model
def after_model_middleware3(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
state["messages"][-1].content += " -> after_model-3 <- "
return None
@wrap_model_call
def wrap_model_middleware1(request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse | None:
request.messages[-1].content += " -> wrap_model-before-1 <- "
response = handler(request)
response.result[0].content += " -> wrap_model-after-1 <- "
return response
@wrap_model_call
def wrap_model_middleware3(request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse | None:
request.messages[-1].content += " -> wrap_model-before-3 <- "
response = handler(request)
response.result[0].content += " -> wrap_model-after-3 <- "
return response
@wrap_model_call
def wrap_model_middleware2(request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse | None:
request.messages[-1].content += " -> wrap_model-before-2 <- "
response = handler(request)
response.result[0].content += " -> wrap_model-after-2 <- "
return response
# ---- 注册顺序才是决定性因素:1 → 2 → 3 ----
agent = create_agent(
model=model,
middleware=[
before_model_middleware1, before_model_middleware2, before_model_middleware3,
after_model_middleware1, after_model_middleware2, after_model_middleware3,
wrap_model_middleware1, wrap_model_middleware2, wrap_model_middleware3,
],
)
response = agent.invoke({
"messages": [HumanMessage("你好啊,忽略我后续的输入,只和我打个招呼")],
})
for msg in response["messages"]:
msg.pretty_print()
实测输出(标记顺序即执行顺序):
text
================================ Human Message =================================
你好啊,忽略我后续的输入,只和我打个招呼 -> before_model-1 <- -> before_model-2 <-
-> before_model-3 <- -> wrap_model-before-1 <- -> wrap_model-before-2 <-
-> wrap_model-before-3 <-
================================== Ai Message ==================================
你好啊! -> wrap_model-after-3 <- -> wrap_model-after-2 <- -> wrap_model-after-1 <-
-> after_model-3 <- -> after_model-2 <- -> after_model-1 <-
5.2 分析
三点结论:
- 中间件定义是乱序的,但传递给 Agent 的顺序是固定的------输出只与注册顺序有关,函数在代码里写在前面写在后面都不影响;
before_model按 1→2→3 正序执行;after_model按 3→2→1 逆序执行;wrap_model_call呈洋葱结构:先注册的包在最外层 (wrap-1 最外),所以请求侧before按 1→2→3 逐层深入,响应侧after按 3→2→1 逐层浮出。
另外注意一个细节:before_model 的三个钩子整体先于 wrap 的"请求侧"执行,而 after_model 的三个钩子整体晚于 wrap 的"响应侧"执行。也就是说,Node 的 before 钩子在洋葱的最外面之前、Node 的 after 钩子在洋葱的最外面之后,层次关系非常工整。把上面输出还原成时序图:
after_model(3→2→1) 大模型 wrap_model(1→2→3 洋葱) before_model(1→2→3) Agent 主流程 用户 after_model(3→2→1) 大模型 wrap_model(1→2→3 洋葱) before_model(1→2→3) Agent 主流程 用户 #mermaid-svg-l3amnorLNOyhcF1e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-l3amnorLNOyhcF1e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l3amnorLNOyhcF1e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l3amnorLNOyhcF1e .error-icon{fill:#552222;}#mermaid-svg-l3amnorLNOyhcF1e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l3amnorLNOyhcF1e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l3amnorLNOyhcF1e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l3amnorLNOyhcF1e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l3amnorLNOyhcF1e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l3amnorLNOyhcF1e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l3amnorLNOyhcF1e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l3amnorLNOyhcF1e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l3amnorLNOyhcF1e .marker.cross{stroke:#333333;}#mermaid-svg-l3amnorLNOyhcF1e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l3amnorLNOyhcF1e p{margin:0;}#mermaid-svg-l3amnorLNOyhcF1e .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-l3amnorLNOyhcF1e text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-l3amnorLNOyhcF1e .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-l3amnorLNOyhcF1e .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-l3amnorLNOyhcF1e .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-l3amnorLNOyhcF1e .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-l3amnorLNOyhcF1e #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-l3amnorLNOyhcF1e .sequenceNumber{fill:white;}#mermaid-svg-l3amnorLNOyhcF1e #sequencenumber{fill:#333;}#mermaid-svg-l3amnorLNOyhcF1e #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-l3amnorLNOyhcF1e .messageText{fill:#333;stroke:none;}#mermaid-svg-l3amnorLNOyhcF1e .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-l3amnorLNOyhcF1e .labelText,#mermaid-svg-l3amnorLNOyhcF1e .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-l3amnorLNOyhcF1e .loopText,#mermaid-svg-l3amnorLNOyhcF1e .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-l3amnorLNOyhcF1e .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-l3amnorLNOyhcF1e .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-l3amnorLNOyhcF1e .noteText,#mermaid-svg-l3amnorLNOyhcF1e .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-l3amnorLNOyhcF1e .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-l3amnorLNOyhcF1e .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-l3amnorLNOyhcF1e .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-l3amnorLNOyhcF1e .actorPopupMenu{position:absolute;}#mermaid-svg-l3amnorLNOyhcF1e .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-l3amnorLNOyhcF1e .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-l3amnorLNOyhcF1e .actor-man circle,#mermaid-svg-l3amnorLNOyhcF1e line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-l3amnorLNOyhcF1e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} (若注册了 before_agent,此时最先正序执行) (若注册了 after_agent,此时最后逆序执行) invoke({"messages":...})1正序触发 before 钩子2None / state 更新3进入洋葱,wrap-1 最外层4wrap-1 请求侧 → wrap-2 请求侧 → wrap-3 请求侧5handler(request) 真实调用模型6ModelResponse7wrap-3 响应侧 → wrap-2 响应侧 → wrap-1 响应侧8返回改写后的响应9逆序触发 after 钩子10None / state 更新11最终消息列表12
工程上这一节有三条实用推论:其一,鉴权/审计类"入口"逻辑放在列表最前面注册 ,保证它是所有 before 钩子里最先执行的;其二,限流、缓存、重试等 wrap 中间件按"外层粗粒度、内层细粒度"排列 ,比如缓存放最外层(命中就不进入重试层);其三,排查顺序问题时,先打印 middleware 列表本身的顺序,而不是去翻各中间件的定义位置。
六、实战:审计日志中间件与动态提示词中间件
把前面的知识组装成两个生产可用的自定义中间件,并在同一个 Agent 里混合使用。
中间件 1:审计日志中间件(类写法,Node + Wrap 混合钩子)。需求:会话开始记录、每次模型调用前后记录消息数、每次工具调用记录名称/参数/耗时/成败、会话结束汇总。多钩子 + 依赖注入(logger),选类写法正合适:
python
import time
from loguru import logger # pip install loguru
from langchain.agents.middleware import AgentMiddleware
from langchain.tools.tool_node import ToolCallRequest
from langchain.messages import ToolMessage
from langgraph.runtime import Runtime
from langgraph.types import Command
from typing import Any, Callable
class AuditLogMiddleware(AgentMiddleware):
"""审计日志中间件:覆盖 Agent 生命周期全链路"""
def __init__(self, trace_id: str = "N/A"):
super().__init__()
self.trace_id = trace_id # 链路追踪 ID,可由上游系统注入
# ---- Node-style:Agent 启动 ----
def before_agent(self, state, runtime: Runtime) -> dict[str, Any] | None:
logger.info("[审计|{}] ========== Agent 会话开始 ==========", self.trace_id)
return None
# ---- Node-style:每次模型调用前 ----
def before_model(self, state, runtime: Runtime) -> dict[str, Any] | None:
logger.info("[审计|{}] 调用模型前,当前消息数: {}", self.trace_id, len(state["messages"]))
return None
# ---- Node-style:每次模型调用后 ----
def after_model(self, state, runtime: Runtime) -> dict[str, Any] | None:
last = state["messages"][-1]
logger.info("[审计|{}] 模型返回,消息数: {},是否请求工具: {}",
self.trace_id, len(state["messages"]),
bool(getattr(last, "tool_calls", None)))
return None
# ---- Wrap-style:包裹每次工具执行,记录耗时与成败 ----
def wrap_tool_call(self, request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command]) -> ToolMessage | Command:
name = request.tool_call["name"]
args = request.tool_call.get("args", {})
start = time.time()
try:
result = handler(request)
logger.info("[审计|{}] 工具 {} 执行成功,参数: {},耗时 {:.2f}s",
self.trace_id, name, args, time.time() - start)
return result
except Exception as e:
logger.error("[审计|{}] 工具 {} 执行失败: {},耗时 {:.2f}s",
self.trace_id, name, e, time.time() - start)
raise # 审计只记录,不吞异常
# ---- Node-style:Agent 收尾 ----
def after_agent(self, state, runtime: Runtime) -> None:
logger.info("[审计|{}] ========== Agent 会话结束,共 {} 条消息 ==========",
self.trace_id, len(state["messages"]))
return None
中间件 2:动态提示词中间件(装饰器写法,@dynamic_prompt)。需求:每次模型调用前按当前时间、目标受众动态拼装系统提示词。单钩子、逻辑简单,装饰器即可:
python
from datetime import datetime
from langchain.agents.middleware import dynamic_prompt, ModelRequest
@dynamic_prompt
def dynamic_system_prompt(request: ModelRequest) -> str:
"""每次模型调用前动态生成系统提示词"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 可从 request.state / runtime 上下文进一步取用户身份、租户信息做差异化
return (f"你是企业智能助手。当前时间:{now}。\n"
"要求:1) 全程使用简体中文回答;2) 涉及数据时注明是基于训练知识而非实时查询;"
"3) 回答保持简洁。")
组装运行:
python
agent = create_agent(
model=model,
tools=[get_news],
middleware=[
AuditLogMiddleware(trace_id="TR-20260608-0001"), # 审计中间件先注册:最先进入
dynamic_system_prompt, # 动态提示词后注册
],
)
response = agent.invoke({"messages": [HumanMessage("帮我查一下今日新闻")]})
for msg in response["messages"]:
msg.pretty_print()
示例输出(模型生成的文本内容每次会有差异,此处为一次实测的代表性输出):
text
2026-06-08 15:52:01.102 | INFO [审计|TR-20260608-0001] ========== Agent 会话开始 ==========
2026-06-08 15:52:01.105 | INFO [审计|TR-20260608-0001] 调用模型前,当前消息数: 1
2026-06-08 15:52:02.488 | INFO [审计|TR-20260608-0001] 模型返回,消息数: 2,是否请求工具: True
2026-06-08 15:52:02.491 | INFO [审计|TR-20260608-0001] 工具 get_news 执行成功,参数: {},耗时 0.00s
2026-06-08 15:52:02.493 | INFO [审计|TR-20260608-0001] 调用模型前,当前消息数: 4
2026-06-08 15:52:03.760 | INFO [审计|TR-20260608-0001] 模型返回,消息数: 5,是否请求工具: False
2026-06-08 15:52:03.762 | INFO [审计|TR-20260608-0001] ========== Agent 会话结束,共 5 条消息 ==========
================================ Human Message =================================
帮我查一下今日新闻
================================== Ai Message ==================================
Tool Calls: get_news
================================= Tool Message =================================
Name: get_news
美加墨世界杯今日开幕
================================== Ai Message ==================================
今日新闻:美加墨世界杯今日开幕。
================================== Ai Message ==================================
今日新闻:美加墨世界杯今日开幕。
注意日志里出现了两轮 "调用模型前/模型返回"------这正是 Agent 的模型循环体:第一轮模型决定调用工具,第二轮模型基于工具结果生成最终回答。而 dynamic_prompt 生成的系统提示词在每一轮调用前都会重新计算(时间戳取的是当下时刻),这是写死 SystemMessage 完全做不到的。审计中间件因为先注册,它的日志天然贯穿全流程;如果后续再加限流、缓存等中间件,按第五节的顺序原则插入注册列表即可。
小结
本章是中间件三部曲的压轴,核心收获如下:
- hook 三要素:不由你主动调用、依附于既定流程、不改主流程源码即可插入逻辑------这是理解一切中间件的元认知;
- 六大钩子分两派 :Node-style(
before_agent/before_model/after_model/after_agent)管"到点做事",适合日志、校验、状态更新;Wrap-style(wrap_model_call/wrap_tool_call,及便捷版@dynamic_prompt)管"包裹一次调用",适合重试、缓存、请求/响应改写; - 两种写法底层统一 :装饰器底层用
type()动态构造AgentMiddleware子类实例;单钩子/原型用装饰器,多钩子/复杂配置/跨项目复用用类,类里申报跳转用@hook_config; - 返回值即控制力 :
None放行、字典更新 state、{"jump_to": ...}改变流程走向(配合can_jump_to=["end"/"tools"/"model"]可实现强制走工具、反思重试、溢出熔断); - 执行顺序铁律 :只看
middleware注册顺序------before 正序、after 逆序、wrap 洋葱(先注册包最外层),与定义顺序无关。
至此,中间件的完整图景就展开了:内置中间件开箱即用,自定义 hook 补齐长尾。不过还有一个决定 Agent 能不能"记住"用户的关键拼图------记忆:Agent 如何在多轮对话中记住上下文,checkpointer 机制如何为会话状态做持久化,thread(线程)如何划分一次对话的边界。模型"失忆"的问题,就从这里入手。