
自定义中间件
中间件(钩子)风格
接下来我们一起来学习自定义中间件,也就是如何从零创建属于我们自己的中间件、完整的操作步骤。在动手编写中间件代码之前,我们必须先搞懂中间件的两种 分类风格,行业里也把它们叫做钩子(Hook)风格。
先解释什么是钩子: 我们之前讲过,中间件的本质就是在 Agent 完整运行流程里的固定节点,插入我们自定义的业务逻辑,这种可插入逻辑的节点就叫做钩子(Hook)。
所以,我们已经能理解中间件(Middleware)是在 Agent 执行流程中特定节点插入逻辑的钩子(Hook),我们可以通过其实现日志、校验、重试、缓存、状态跟踪等横切关注点。它不影响 Agent 核心逻辑,但可以拦截、修改请求 / 响应,甚至改变执行流向。
不过想要自定义中间件,我们需要先了解下 LangChain 提供两种风格的钩子:节点风格 、包装风格,我们分开拆解讲解,他们分别适用于不同场景。
节点风格(Node-style Hooks)
先梳理 Agent 最基础的运行主线:用户输入内容 → 调用大模型推理 → 产出最终输出,整套逻辑组合起来就是完整 Agent 系统。 节点风格就是在这条主线流程的四个固定关键节点插入自定义逻辑,四个钩子点位分别是:
before_agent:整个Agent启动执行之前触发;before_model:每一次调用大模型之前触发;after_model:每一次大模型推理响应完成后触发;after_agent:整套Agent全部执行结束之后触发。
这里有两个关键注意点:
before_agent和after_agent在单次完整Agent调用中只会执行一次,分别对应流程最开头、流程最末尾;before_model和after_model会跟随模型调用次数重复执行,触发顺序严格从上到下、线性顺序执行,不会乱序。
完整执行时序:用户输入 → before_agent(仅一次) → before_model → 大模型调用 → after_model → 循环工具 / 二次模型调用(重复 before_model/after_model) → after_agent(仅一次) → 最终输出。
简单总结节点风格: 把整套 Agent 流程拆成四段固定节点,在四段节点分别挂载逻辑,全程线性顺序运行,适合日志打印、参数校验、计数统计这类简单线性任务。
**特点:**顺序执行,返回字典更新状态。适合日志、校验、计数等线性逻辑。
| 钩子 | 触发时机 |
|---|---|
before_agent |
Agent 开始前(仅一次) |
before_model |
每次调用模型前 |
after_model |
每次模型响应后 |
after_agent |
Agent 结束后(仅一次) |
包装风格(Wrap-style Hooks)
包装风格和节点风格完全不同,它可以完整接管、包裹目标函数的全部执行流程,我们能自主控制目标函数【大模型调用 / 工具调用】要不要执行、执行多少次、修改入参、替换返回结果。 包装风格只分为两类,分别对应两大核心执行单元:
wrap_model_call:包裹大模型调用的全过程;wrap_tool_call:包裹工具调用的全过程。
我们梳理简化后的 Agent 完整流程方便理解:用户输入 → 大模型推理 → 工具执行(可选) → 二次大模型整合结果 → 最终输出。 包装风格会把「大模型调用」「工具调用」单独封装成可包裹单元,在包裹内部自由插入前置、后置逻辑:
- 包裹模型调用时:可以在模型执行前写前置逻辑、模型执行后写后置逻辑,甚至重复调用模型(比如失败重试);
- 包裹工具调用时:同理,工具运行前做权限校验、工具运行后捕获异常、清洗返回数据。
两种钩子风格互不冲突,开发时可以只使用其中一种,也可以同时搭配使用。底层实现逻辑很简单:不管是节点风格还是包装风格,本质都是定义独立方法,再把这些方法注册、插入到 Agent 的执行链路中运行。
**特点:**完全控制被包裹的函数,可以决定是否调用、调用几次、修改参数 / 返回值。适合重试、缓存、动态选择模型 / 工具。
| 钩子 | 包裹对象 |
|---|---|
wrap_model_call |
模型调用 |
wrap_tool_call |
工具调用 |
如下图所示:

创建中间件
讲完两种钩子风格,接下来我们直接写代码实操,创建自定义中间件分为两种实现方案:
装饰器方式 :使用 @ 装饰器标记方法,快速声明这个方法要挂载到哪个流程阶段,逻辑简单、单一功能场景优先用;
类实现方式 :创建 class 类,在同一个类里面实现多个不同钩子方法,适合需要同时处理多个节点、逻辑复杂的场景。
下面我们先实操第一种:装饰器实现自定义中间件,沿用之前演示过的天气查询 Agent 作为基础案例。 先保留好天气工具、基础 Agent 初始化代码,清空原本配置的预构建中间件,后面我们手动注册自定义钩子逻辑。
装饰器方式
我们依次实现四个节点钩子,先从 before_agent 开始: 使用 @before_agent 装饰器修饰自定义方法,这个方法固定携带两个入参:
第一个参数 state: AgentState 类型,是 Agent 内置的全局状态对象。 补充说明:LangGraph 有自定义状态体系,LangChain Agent 也自带一套标准化默认状态 AgentState。我们调用 agent.invoke() 时传入的 messages 消息列表,就存放在 state 对象内部,后续结构化输出、上下文数据也都存在 state 里,可以随时读取、修改。
第二个参数 runtime: 运行时上下文对象,源自 LangGraph,可以获取全局运行上下文、会话信息等拓展数据。
节点风格的钩子方法返回值只有两种规范:要么返回 None,要么返回字典(用于更新全局 state 状态)。
我们写一个日志打印示例方法:方法内部打印「即将执行 Agent」,直接返回 None。只要把这个方法注册到 Agent 的 middleware 列表,后续运行 Agent 时就会自动打印这句日志,证明自定义中间件挂载生效。
同理,@after_agent 装饰器写法完全一致,入参、返回规则和 before_agent 完全相同。在这个方法里我们可以读取、修改 state 里的消息列表,比如自动总结历史对话、修改系统提示词,大家课后可以自行调试 state 和 runtime 内部携带的各类数据,拓展自定义逻辑。
剩下 @before_model、@after_model 两个模型节点钩子,参数、返回规范和前面两个完全统一,我们分别实现日志打印:「即将执行大语言模型」「大语言模型执行完成」。
四个节点钩子方法全部定义完成后,只需要把四个方法名全部放进创建 Agent 时的 middleware 数组参数里,就完成全部挂载,相当于一次性注册了四个自定义中间件。
python
from typing import Callable, Any
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import (
before_model,
wrap_model_call,
ModelRequest,
ModelResponse,
after_model,
before_agent,
after_agent,
wrap_tool_call
)
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langchain_core.messages import ToolMessage
from langgraph.prebuilt.tool_node import ToolCallRequest
from langgraph.runtime import Runtime
from langgraph.types import Command
# ============================================
# 1. 定义工具
# ============================================
@tool
def get_weather_for_location(city: str) -> str:
"""获取指定城市的天气信息。"""
return f"{city}总是阳光明媚!"
# ============================================
# 2. 配置模型
# ============================================
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
)
# ============================================
# 3. Agent 生命周期钩子(Middleware)
# ============================================
# --------------------------------------------
# 3.1 Agent 生命周期钩子
# --------------------------------------------
@before_agent
def log_before_agent(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在 Agent 执行前触发"""
print("【before_agent】即将执行 Agent")
return None
@after_agent
def log_after_agent(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在 Agent 执行完成后触发"""
print("【after_agent】Agent 执行完成")
return None
# --------------------------------------------
# 3.2 模型调用钩子
# --------------------------------------------
@before_model
def log_before_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在调用模型前触发"""
print("【before_model】即将调用模型")
return None
@after_model
def log_after_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在调用模型完成后触发"""
print("【after_model】调用模型完成")
return None
# # --------------------------------------------
# # 3.3 模型调用包装器(带重试逻辑)
# # --------------------------------------------
# @wrap_model_call
# def retry_model(
# request: ModelRequest,
# handler: Callable[[ModelRequest], ModelResponse],
# ) -> ModelResponse:
# """
# 包装模型调用,添加自动重试机制(最多 3 次)。
#
# 执行流程:
# 1. 打印当前要发送给模型的最后一条消息
# 2. 尝试调用 handler(实际执行模型调用)
# 3. 成功则返回结果
# 4. 失败则重试,最多 3 次
# """
# for attempt in range(3):
# try:
# print(f"【wrap_model】第 {attempt + 1} 次尝试调用模型")
# print(f"【wrap_model】最新消息: {request.messages[-1].content[:50]}...")
#
# result = handler(request)
#
# print(f"【wrap_model】模型调用成功")
# return result
#
# except Exception as e:
# if attempt == 2: # 最后一次尝试也失败
# print(f"【wrap_model】模型调用失败,已重试 3 次,错误: {e}")
# raise # 重新抛出异常
# else:
# print(f"【wrap_model】模型调用出现错误,将重试 ({attempt + 1}/3),错误: {e}")
#
#
# # --------------------------------------------
# # 3.4 工具调用包装器(监控)
# # --------------------------------------------
# @wrap_tool_call
# def monitor_tool(
# request: ToolCallRequest,
# handler: Callable[[ToolCallRequest], ToolMessage | Command],
# ) -> ToolMessage | Command:
# """
# 包装工具调用,添加日志监控功能。
#
# 执行流程:
# 1. 打印工具名称和参数
# 2. 执行 handler(实际调用工具)
# 3. 成功则返回结果
# 4. 失败则捕获异常并重新抛出
# """
# print(f"【wrap_tool】执行工具: {request.tool_call['name']}")
# print(f"【wrap_tool】参数: {request.tool_call['args']}")
#
# try:
# result = handler(request)
# print(f"【wrap_tool】工具执行成功")
# return result
# except Exception as e:
# print(f"【wrap_tool】工具执行失败: {e}")
# raise # 重新抛出异常,让上层处理
# ============================================
# 4. 创建 Agent
# ============================================
agent = create_agent(
model=model,
tools=[get_weather_for_location],
system_prompt="你是一位乐于助人的助手。",
middleware=[
log_before_agent, # Agent 执行前钩子
log_after_agent, # Agent 执行后钩子
log_before_model, # 模型调用前钩子
log_after_model, # 模型调用后钩子
# retry_model, # 模型调用包装器(重试)
# monitor_tool, # 工具调用包装器(监控)
],
)
# ============================================
# 5. 执行 Agent
# ============================================
print("=" * 60)
print("开始执行 Agent - 完整中间件链路演示")
print("=" * 60)
response = agent.invoke({
"messages": [{"role": "user", "content": "北京的天气如何?"}]
})
print("\n" + "=" * 60)
print("最终响应结果")
print("=" * 60)
print(f"助手回复:{response['messages'][-1].content}")
print("\n" + "=" * 60)
print("完整消息列表")
print("=" * 60)
for i, msg in enumerate(response['messages']):
msg_type = type(msg).__name__
content_preview = msg.content[:80] + "..." if len(msg.content) > 80 else msg.content
has_tool_calls = hasattr(msg, 'tool_calls') and bool(msg.tool_calls)
print(f" [{i}] {msg_type}: {content_preview} (tool_calls={has_tool_calls})")
我们调用 agent.invoke(),传入用户提问「北京的天气如何?」,观察控制台打印日志顺序:
-
第一行打印:即将执行 Agent(
before_agent,全程仅执行 1 次) -
第二行打印:即将执行大语言模型(第一次模型调用,用于判断是否需要调用工具)
-
模型推理完成,打印:大语言模型执行完成
-
执行天气工具,再次进入二次模型调用,重复打印「即将执行大语言模型」「大语言模型执行完成」
-
整套流程全部结束,打印:Agent 执行完成(
after_agent,全程仅执行 1 次)
这里解释一下为什么模型前后钩子会打印两次:
-
第一次模型调用:用来分析用户问题,判断需要调用天气工具;
-
工具执行完成后,需要第二次调用大模型,整合工具返回的天气数据,生成完整自然语言回答; 因此
before_model、after_model会跟随模型调用次数重复触发,完全符合Agent原生循环执行逻辑。 而before_agent、after_agent只在整套对话流程首尾各执行一次,和我们之前讲解的节点规则完全匹配。
python
============================================================
开始执行 Agent - 完整中间件链路演示
============================================================
【before_agent】即将执行 Agent
【before_model】即将调用模型
【after_model】调用模型完成
【before_model】即将调用模型
【after_model】调用模型完成
【after_agent】Agent 执行完成
============================================================
最终响应结果
============================================================
助手回复:北京的天气是**阳光明媚**的!☀️
看来今天北京天气不错,是个适合外出活动的好日子。如果你需要更详细的天气信息(比如温度、湿度、风力等),也可以告诉我,我会尽力帮你查询。有什么其他需要帮忙的吗?😊
============================================================
完整消息列表
============================================================
[0] HumanMessage: 北京的天气如何? (tool_calls=False)
[1] AIMessage: 好的,我来帮你查询北京的天气情况。 (tool_calls=True)
[2] ToolMessage: 北京总是阳光明媚! (tool_calls=False)
[3] AIMessage: 北京的天气是**阳光明媚**的!☀️
看来今天北京天气不错,是个适合外出活动的好日子。如果你需要更详细的天气信息(比如温度、湿度、风力等),也可以告诉我,我会... (tool_calls=False)
接下来我们继续讲包装风格的钩子,上面只讲完了节点风格,还没有实操包装风格的代码。 包装风格同样可以用装饰器快速实现,分为两种装饰器,对应两种包裹对象:
-
如果想要包裹大语言模型执行流程 ,使用装饰器
@wrap_model_call; -
如果想要包裹工具执行流程 ,使用装饰器
@wrap_tool_call。
这两种就是包装风格仅有的两类装饰器。需要重点注意:用这两个装饰器定义的方法,入参结构和前面节点风格的方法完全不一样,我们分开拆解参数与代码逻辑。
@wrap_model_call 模型包装钩子
**先说一下适用场景:**包裹模型调用时,我们可以在模型执行前打印日志、执行后打印日志;同时大模型调用很容易出现网络超时、接口报错等失败情况,借助包装钩子我们能手动控制重试逻辑,调用失败后自动重新发起请求,这是节点风格做不到的能力。
python
@wrap_model_call
def retry_model(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
被 @wrap_model_call 修饰的自定义方法固定携带两个参数:
第一个参数 request,类型为 ModelRequest; ModelRequest 内部封装了本次模型调用的全部上下文: 绑定给 Agent 的聊天模型、完整消息列表、系统提示词(系统消息)、待使用的工具列表、结构化输出格式、全局状态、Runtime 运行上下文、各类模型配置参数。 我们可以通过这个 request 对象读取、修改任意上下文数据,比如动态切换本次调用使用的大模型、修改提示词、删减工具等,全部都能实现。
第二个参数 handler,是一个回调函数; 这个回调函数就是原生执行大模型的底层方法,入参是 ModelRequest,返回值是 ModelResponse。 handler(request) 这一行代码等价于直接发起一次大模型调用。
同时,整个自定义方法的返回值必须是 ModelResponse,也就是大模型调用后的返回结果,这是语法强制要求。
python
@wrap_model_call
def retry_model(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
"""
包装模型调用,添加自动重试机制(最多 3 次)。
执行流程:
1. 打印当前要发送给模型的最后一条消息
2. 尝试调用 handler(实际执行模型调用)
3. 成功则返回结果
4. 失败则重试,最多 3 次
"""
for attempt in range(3):
try:
print(f"【wrap_model】第 {attempt + 1} 次尝试调用模型")
print(f"【wrap_model】最新消息: {request.messages[-1].content[:50]}...")
result = handler(request)
print(f"【wrap_model】模型调用成功")
return result
except Exception as e:
if attempt == 2: # 最后一次尝试也失败
print(f"【wrap_model】模型调用失败,已重试 3 次,错误: {e}")
raise # 重新抛出异常
else:
print(f"【wrap_model】模型调用出现错误,将重试 ({attempt + 1}/3),错误: {e}")
我们给模型调用增加最多 3 次重试的容错逻辑:
循环 3 次发起模型调用,循环变量从 0 开始计数;
每次循环开头打印日志,从 request.messages 里取出最新一条消息并打印,方便调试查看用户输入;
执行 handler(request) 发起模型调用,用 try-except 捕获全部执行异常;
如果调用无异常,直接接收返回的 result 并 return,结束本次钩子执行;
如果捕获到异常,打印错误日志,判断当前重试次数:
-
若循环计数等于 2(代表 3 次机会全部用完:0、1、2),不再重试,抛出异常终止流程;
-
若次数未用尽,进入下一次循环,自动重新发起模型调用;
日志标注当前是第几次重试,格式为「模型调用出现错误,将重试 {次数 + 1}/3 次,错误信息为:{异常详情}」。
写完这个重试钩子方法后,和节点风格一样,把方法名放进 Agent 初始化的 middleware 数组中完成注册。
@wrap_tool_call 工具包装钩子
python
@wrap_tool_call
def monitor_tool(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
工具包装钩子写法和模型包装钩子高度相似,只需要修改参数、返回值类型,我们逐一区分:
第一个入参 request,类型改为 ToolCallRequest,存放本次工具调用的全部信息;
回调 handler 执行后,返回值不再是 ModelResponse,支持两种返回类型:ToolMessage、Command;
-
ToolMessage:常规场景,工具执行完成后返回的结果消息,绝大多数业务只用这个; -
Command:特殊跳转指令,执行完工具后手动修改执行流向、跳转流程。
python
@wrap_tool_call
def monitor_tool(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
"""
包装工具调用,添加日志监控功能。
执行流程:
1. 打印工具名称和参数
2. 执行 handler(实际调用工具)
3. 成功则返回结果
4. 失败则捕获异常并重新抛出
"""
print(f"【wrap_tool】执行工具: {request.tool_call['name']}")
print(f"【wrap_tool】参数: {request.tool_call['args']}")
try:
result = handler(request)
print(f"【wrap_tool】工具执行成功")
return result
except Exception as e:
print(f"【wrap_tool】工具执行失败: {e}")
raise # 重新抛出异常,让上层处理
方法开头打印日志,从 ToolCallRequest 中读取工具名称、工具入参并打印; 取值路径:request.tool_call["name"] 获取工具名,request.tool_call["args"] 获取传入参数,键名固定不能随意修改;
调用 handler(request) 执行原生工具逻辑,同样用 try-except 捕获工具执行异常;
无异常则打印「工具执行成功」日志,返回工具结果;
捕获异常后打印「工具调用出现错误」日志,可根据业务需求抛出异常或做兜底处理。
工具钩子编写完成后,同样添加到 middleware 中间件列表,和节点风格钩子、模型包装钩子共存。
全部节点风格、包装风格钩子注册完成后,我们再次执行 agent.invoke({"messages": [{"role": "user", "content": "北京的天气如何?"}]}),观察完整打印日志顺序,验证两类钩子的执行顺序互不冲突:
-
先执行节点风格
before_agent:打印「即将执行 Agent」; -
进入第一次模型调用,触发
wrap_model_call包装钩子:打印用户最新提问「北京的天气如何?」,执行模型调用,打印「模型调用完成」; -
退出包装钩子,触发节点风格
after_model:打印「大语言模型执行完成」; -
判定需要调用工具,进入
wrap_tool_call工具包装钩子:打印工具名称、入参,执行工具后打印「工具执行成功」; -
工具执行完毕,需要二次调用大模型整合结果,再次触发
wrap_model_call:此时最新消息是工具返回内容「在北京总是阳光明媚!」,执行模型、打印调用完成; -
二次模型执行结束,再次触发
after_model; -
整套对话全部结束,触发节点风格
after_agent:打印「Agent 执行完成」。
由此可以得出结论:节点风格钩子和包装风格钩子完全可以混合使用,二者负责拦截、处理流程里不同阶段的逻辑,我们可以根据业务需求自由选择对应的钩子类型编写自定义逻辑。
python
============================================================
开始执行 Agent - 完整中间件链路演示
============================================================
【before_agent】即将执行 Agent
【before_model】即将调用模型
【wrap_model】第 1 次尝试调用模型
【wrap_model】最新消息: 北京的天气如何?...
【wrap_model】模型调用成功
【after_model】调用模型完成
【wrap_tool】执行工具: get_weather_location
【wrap_tool】参数: {'city': '北京'}
【wrap_tool】工具执行成功
【before_model】即将调用模型
【wrap_model】第 1 次尝试调用模型
【wrap_model】最新消息: 北京总是阳光明媚!...
【wrap_model】模型调用成功
【after_model】调用模型完成
【after_agent】Agent 执行完成
============================================================
最终响应结果
============================================================
助手回复:北京的天气是**阳光明媚**的!☀️
看来今天北京是个好天气,阳光充足,非常适合外出活动。如果你有出行计划,可以放心安排哦!不过具体的气温、风力等详细信息,建议你查看当地的天气预报应用获取更准确的数据。
还有其他需要帮忙的吗?😊
============================================================
完整消息列表
============================================================
[0] HumanMessage: 北京的天气如何? (tool_calls=False)
[1] AIMessage: 好的,我来帮你查询北京的天气情况。 (tool_calls=True)
[2] ToolMessage: 北京总是阳光明媚! (tool_calls=False)
[3] AIMessage: 北京的天气是**阳光明媚**的!☀️
看来今天北京是个好天气,阳光充足,非常适合外出活动。如果你有出行计划,可以放心安排哦!不过具体的气温、风力等详细信息,建... (tool_calls=False)
接下来我们单独测试模型重试逻辑,模拟网络不通的报错场景: 关闭本地代理,让程序无法连接大模型接口,发起对话请求,观察钩子的异常捕获与自动重试流程:【或者可以就是在 handler 的参数中不传入 ModelRequest】
python
============================================================
开始执行 Agent - 完整中间件链路演示
============================================================
【before_agent】即将执行 Agent
【before_model】即将调用模型
【wrap_model】第 1 次尝试调用模型
【wrap_model】最新消息: 北京的天气如何?...
【wrap_model】模型调用出现错误,将重试(1/3),错误: create_agent.<locals>._execute_model_sync() missing 1 required positional argument: 'request'
【wrap_model】第 2 次尝试调用模型
【wrap_model】最新消息: 北京的天气如何?...
【wrap_model】模型调用出现错误,将重试(2/3),错误: create_agent.<locals>._execute_model_sync() missing 1 required positional argument: 'request'
【wrap_model】第 3 次尝试调用模型
【wrap_model】最新消息: 北京的天气如何?...
【wrap_model】模型调用失败,已重试 3 次,错误: create_agent.<locals>._execute_model_sync() missing 1 required positional argument: 'request'
-
打印前置日志,准备发起第一次模型调用;
-
接口连接超时 / 参数缺失,触发
except捕获异常,打印日志:模型调用超时,第 1 次重试(1/3); -
自动进入第二次循环,再次发起调用,依旧超时,打印日志:第 2 次重试(2/3);
-
第三次循环发起调用,仍然失败,判定 3 次重试机会耗尽,直接抛出异常,终止整套
Agent执行流程。
整套重试逻辑完全由我们自定义的 wrap_model_call 中间件接管,无需改动 Agent 核心代码,完美实现模型调用容错能力。
如果我们自定义的中间件通常逻辑单一、职责明确,适合用装饰器式快速实现。我们可以取消对应注释。
对于上述代码,构建中间件的核心类型需要进行说明:
节点风格方法参数中,AgentState 为 Agent 的默认状态。Runtime 可以获取上下文信息。
包装风格方法参数中,ModelRequest 为传递给模型的请求信息;ModelResponse 为模型调用的响应信息。
类方式
接下来我们学习用 class 类的方式来创建自定义中间件,先对比两种实现方式的核心区别: 之前用装饰器实现钩子时,每一个钩子方法都相当于一个独立中间件,绑定到 Agent 的时候,需要把所有钩子方法全部写到 middleware 列表里,数量一多会显得很零散。 那如果我们希望把多个不同阶段的钩子逻辑整合到同一个中间件 中,只需要在 middleware 列表注册一次,该怎么实现?答案就是类式中间件。
**也就是说:**如果需要在一个中间件里同时处理多个钩子(比如既要在调用模型前记录日志,又要在模型返回后记录结果),就可以用类式中间件把多个钩子方法放在同一个类中。
类方式中间件适用场景
当业务需求需要在一套中间件里同时处理多个流程节点时,比如既要在模型调用前打印日志、又要在模型响应完成后打印结果、还要给模型调用加重试逻辑,就非常适合用类方式实现。 它可以把全部相关的钩子方法封装到同一个类内部,统一管理,不用分散定义大量独立装饰器方法。
类中间件基础语法要求
自定义的类必须继承父类 AgentMiddleware ,这是强制规范;
在类内部按需实现对应钩子方法,钩子依旧只有两类:节点风格、包装风格,没有第三种 ;
不需要写任何装饰器,只需要严格固定钩子方法名 (before_agent、after_model、wrap_model_call 等),框架会自动识别 ;
类内所有方 法第一个参数必须加上 self,代表类实例本身,其余入参、返回值规范和装饰器写法完全一致。
我们沿用之前天气工具的案例,新建一个日志专用中间件类,命名为 LoggingMiddleware,继承 AgentMiddleware。
把之前装饰器写法里所有的钩子逻辑全部平移到这个类中,去掉所有 @xxx 装饰器;
每个钩子方法开头新增参数 self,其余参数、打印逻辑、返回规则完全不变;
-
节点风格:
before_agent、after_agent、before_model、after_model; -
包装风格:
wrap_model_call(带 3 次重试)、wrap_tool_call(工具日志打印与异常捕获);
按需删减钩子:如果不 需要监听工具调用流程,直接删掉 wrap_tool_call 方法即可,框架会自动跳过该节点拦截,只执行你定义了的钩子。本次演示我们把全部钩子完整保留。
装饰器写法需要在 middleware=[方法1,方法2,方法3...] 传入一堆钩子,类方式只需要实例化一次我们写好的类,放入列表:
python
middleware=[LoggingMiddleware()]
只写一行,就完成了全部多钩子逻辑的绑定,代码更加整洁。
python
from typing import Callable, Any
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import (
before_model,
wrap_model_call,
ModelRequest,
ModelResponse,
after_model,
before_agent,
after_agent,
wrap_tool_call,
AgentMiddleware
)
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langchain_core.messages import ToolMessage
from langgraph.prebuilt.tool_node import ToolCallRequest
from langgraph.runtime import Runtime
from langgraph.types import Command
# ============================================
# 1. 定义工具
# ============================================
@tool
def get_weather_for_location(city: str) -> str:
"""获取指定城市的天气信息。"""
return f"{city}总是阳光明媚!"
# ============================================
# 2. 配置模型
# ============================================
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
)
# ============================================
# 3. 自定义中间件(类方式)
# ============================================
class LoggingMiddleware(AgentMiddleware):
"""
日志中间件:记录 Agent 执行全链路的关键步骤。
功能:
- before_agent / after_agent:Agent 生命周期钩子
- before_model / after_model:模型调用钩子
- wrap_model_call:模型调用包装器(含重试逻辑)
- wrap_tool_call:工具调用包装器(含监控逻辑)
"""
# --------------------------------------------
# 3.1 Agent 生命周期钩子
# --------------------------------------------
def before_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在 Agent 执行前触发"""
print("【before_agent】即将执行 Agent")
return None
def after_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在 Agent 执行完成后触发"""
print("【after_agent】Agent 执行完成")
return None
# --------------------------------------------
# 3.2 模型调用钩子
# --------------------------------------------
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在调用模型前触发"""
print("【before_model】即将调用模型")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""在调用模型完成后触发"""
print("【after_model】调用模型完成")
return None
# --------------------------------------------
# 3.3 模型调用包装器(带重试逻辑)
# --------------------------------------------
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
"""
包装模型调用,添加自动重试机制(最多 3 次)。
执行流程:
1. 打印当前要发送给模型的最后一条消息
2. 尝试调用 handler(实际执行模型调用)
3. 成功则返回结果
4. 失败则重试,最多 3 次
"""
for attempt in range(3):
print(f"【wrap_model】第 {attempt + 1} 次尝试调用模型")
print(f"【wrap_model】最新消息: {request.messages[-1].content[:50]}...")
try:
result = handler(request)
print(f"【wrap_model】模型调用成功")
return result
except Exception as e:
if attempt == 2: # 最后一次尝试也失败
print(f"【wrap_model】模型调用失败,已重试 3 次,错误: {e}")
raise # 重新抛出异常
else:
print(f"【wrap_model】模型调用出现错误,将重试 ({attempt + 1}/3),错误: {e}")
# --------------------------------------------
# 3.4 工具调用包装器(监控)
# --------------------------------------------
def wrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command],
) -> ToolMessage | Command:
"""
包装工具调用,添加日志监控功能。
执行流程:
1. 打印工具名称和参数
2. 执行 handler(实际调用工具)
3. 成功则返回结果
4. 失败则捕获异常并重新抛出
"""
print(f"【wrap_tool】执行工具: {request.tool_call['name']}")
print(f"【wrap_tool】参数: {request.tool_call['args']}")
try:
result = handler(request)
print(f"【wrap_tool】工具执行成功")
return result
except Exception as e:
print(f"【wrap_tool】工具执行失败: {e}")
raise # 重新抛出异常,让上层处理
# ============================================
# 4. 创建 Agent
# ============================================
agent = create_agent(
model=model,
tools=[get_weather_for_location],
system_prompt="你是一位乐于助人的助手。",
middleware=[LoggingMiddleware()], # 使用类方式定义的中间件
)
# ============================================
# 5. 执行 Agent
# ============================================
print("=" * 60)
print("开始执行 Agent - 类方式中间件演示")
print("=" * 60)
response = agent.invoke({
"messages": [{"role": "user", "content": "北京的天气如何?"}]
})
print("\n" + "=" * 60)
print("最终响应结果")
print("=" * 60)
print(f"助手回复:{response['messages'][-1].content}")
print("\n" + "=" * 60)
print("完整消息列表")
print("=" * 60)
for i, msg in enumerate(response['messages']):
msg_type = type(msg).__name__
content_preview = msg.content[:80] + "..." if len(msg.content) > 80 else msg.content
has_tool_calls = hasattr(msg, 'tool_calls') and bool(msg.tool_calls)
print(f" [{i}] {msg_type}: {content_preview} (tool_calls={has_tool_calls})")
初次运行时会直接报错,报错原因很容易忽略: 装饰器写法的独立方法没有 self 参数,但类内的成员方法必须以 self 作为第一个入参。我们刚才平移代码时忘记给每个钩子方法加上 self,补齐所有方法的 self 参数后重新执行即可正常运行。
python
============================================================
开始执行 Agent - 类方式中间件演示
============================================================
【before_agent】即将执行 Agent
【before_model】即将调用模型
【wrap_model】第 1 次尝试调用模型
【wrap_model】最新消息: 北京的天气如何?...
【wrap_model】模型调用成功
【after_model】调用模型完成
【wrap_tool】执行工具: get_weather_for_location
【wrap_tool】参数: {'city': '北京'}
【wrap_tool】工具执行成功
【before_model】即将调用模型
【wrap_model】第 1 次尝试调用模型
【wrap_model】最新消息: 北京总是阳光明媚!...
【wrap_model】模型调用成功
【after_model】调用模型完成
【after_agent】Agent 执行完成
============================================================
最终响应结果
============================================================
助手回复:北京的天气是**阳光明媚**的!☀️
这是一个好天气,适合外出活动。不过如果您需要更具体的温度、湿度或风力等信息,也可以告诉我,我可以进一步帮您查询。有什么其他需要帮忙的吗?
============================================================
完整消息列表
============================================================
[0] HumanMessage: 北京的天气如何? (tool_calls=False)
[1] AIMessage: 好的,我来查询一下北京的天气情况。 (tool_calls=True)
[2] ToolMessage: 北京总是阳光明媚! (tool_calls=False)
[3] AIMessage: 北京的天气是**阳光明媚**的!☀️
这是一个好天气,适合外出活动。不过如果您需要更具体的温度、湿度或风力等信息,也可以告诉我,我可以进一步帮您查询。有什么其... (tool_calls=False)
执行后控制台打印日志顺序和装饰器混合写法完全一致:
先执行 before_agent 打印「即将执行 Agent」;
2. 进入模型流程,先执行 before_model,再进入 wrap_model_call 打印用户提问、完成模型调用;
-
模型执行完毕触发
after_model; -
判定调用工具,进入
wrap_tool_call打印工具名、参数、执行成功日志; -
工具结束后二次调用大模型,再次执行
before_model、wrap_model_call、after_model; -
整套对话结束执行
after_agent打印收尾日志。
所有节点风格、包装风格的钩子全部正常拦截流程,证明类式中间件生效。
这就是第二种自定义中间件实现方案 ------ 类写法。 它最大的优势:如果一套业务逻辑需要监听 Agent 多个执行阶段、编写多个钩子,全部封装到同一个类里统一维护,绑定 Agent 时只需要实例化一次,代码聚合度更高、更整洁,也是工业开发里推荐优先使用 的实现方式。