中间件 [ Agent 的拦截器 ] [ 2 ]

自定义中间件

中间件(钩子)风格

接下来我们一起来学习自定义中间件,也就是如何从零创建属于我们自己的中间件、完整的操作步骤。在动手编写中间件代码之前,我们必须先搞懂中间件的两种 分类风格,行业里也把它们叫做钩子(Hook)风格

先解释什么是钩子: 我们之前讲过,中间件的本质就是在 Agent 完整运行流程里的固定节点,插入我们自定义的业务逻辑,这种可插入逻辑的节点就叫做钩子(Hook)。

所以,我们已经能理解中间件(Middleware)是在 Agent 执行流程中特定节点插入逻辑的钩子(Hook),我们可以通过其实现日志、校验、重试、缓存、状态跟踪等横切关注点。它不影响 Agent 核心逻辑,但可以拦截、修改请求 / 响应,甚至改变执行流向。

不过想要自定义中间件,我们需要先了解下 LangChain 提供两种风格的钩子:节点风格包装风格,我们分开拆解讲解,他们分别适用于不同场景。

节点风格(Node-style Hooks)

先梳理 Agent 最基础的运行主线:用户输入内容 → 调用大模型推理 → 产出最终输出,整套逻辑组合起来就是完整 Agent 系统。 节点风格就是在这条主线流程的四个固定关键节点插入自定义逻辑,四个钩子点位分别是:

  1. before_agent:整个 Agent 启动执行之前触发;
  2. before_model:每一次调用大模型之前触发;
  3. after_model:每一次大模型推理响应完成后触发;
  4. after_agent:整套 Agent 全部执行结束之后触发。

这里有两个关键注意点:

  1. before_agentafter_agent 在单次完整 Agent 调用中只会执行一次,分别对应流程最开头、流程最末尾;
  2. before_modelafter_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)

包装风格和节点风格完全不同,它可以完整接管、包裹目标函数的全部执行流程,我们能自主控制目标函数【大模型调用 / 工具调用】要不要执行、执行多少次、修改入参、替换返回结果。 包装风格只分为两类,分别对应两大核心执行单元:

  1. wrap_model_call:包裹大模型调用的全过程;
  2. wrap_tool_call:包裹工具调用的全过程。

我们梳理简化后的 Agent 完整流程方便理解:用户输入 → 大模型推理 → 工具执行(可选) → 二次大模型整合结果 → 最终输出。 包装风格会把「大模型调用」「工具调用」单独封装成可包裹单元,在包裹内部自由插入前置、后置逻辑:

  1. 包裹模型调用时:可以在模型执行前写前置逻辑、模型执行后写后置逻辑,甚至重复调用模型(比如失败重试);
  2. 包裹工具调用时:同理,工具运行前做权限校验、工具运行后捕获异常、清洗返回数据。

两种钩子风格互不冲突,开发时可以只使用其中一种,也可以同时搭配使用。底层实现逻辑很简单:不管是节点风格还是包装风格,本质都是定义独立方法,再把这些方法注册、插入到 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。只要把这个方法注册到 Agentmiddleware 列表,后续运行 Agent 时就会自动打印这句日志,证明自定义中间件挂载生效。

同理,@after_agent 装饰器写法完全一致,入参、返回规则和 before_agent 完全相同。在这个方法里我们可以读取、修改 state 里的消息列表,比如自动总结历史对话、修改系统提示词,大家课后可以自行调试 stateruntime 内部携带的各类数据,拓展自定义逻辑。

剩下 @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(),传入用户提问「北京的天气如何?」,观察控制台打印日志顺序:

  1. 第一行打印:即将执行 Agent(before_agent,全程仅执行 1 次)

  2. 第二行打印:即将执行大语言模型(第一次模型调用,用于判断是否需要调用工具)

  3. 模型推理完成,打印:大语言模型执行完成

  4. 执行天气工具,再次进入二次模型调用,重复打印「即将执行大语言模型」「大语言模型执行完成」

  5. 整套流程全部结束,打印:Agent 执行完成(after_agent,全程仅执行 1 次)

这里解释一下为什么模型前后钩子会打印两次:

  • 第一次模型调用:用来分析用户问题,判断需要调用天气工具;

  • 工具执行完成后,需要第二次调用大模型,整合工具返回的天气数据,生成完整自然语言回答; 因此 before_modelafter_model 会跟随模型调用次数重复触发,完全符合 Agent 原生循环执行逻辑。 而 before_agentafter_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,类型为 ModelRequestModelRequest 内部封装了本次模型调用的全部上下文: 绑定给 Agent 的聊天模型、完整消息列表、系统提示词(系统消息)、待使用的工具列表、结构化输出格式、全局状态、Runtime 运行上下文、各类模型配置参数。 我们可以通过这个 request 对象读取、修改任意上下文数据,比如动态切换本次调用使用的大模型、修改提示词、删减工具等,全部都能实现。

第二个参数 handler,是一个回调函数; 这个回调函数就是原生执行大模型的底层方法,入参是 ModelRequest,返回值是 ModelResponsehandler(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 捕获全部执行异常;

如果调用无异常,直接接收返回的 resultreturn,结束本次钩子执行;

如果捕获到异常,打印错误日志,判断当前重试次数:

  • 若循环计数等于 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,支持两种返回类型:ToolMessageCommand

  • 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'
  1. 打印前置日志,准备发起第一次模型调用;

  2. 接口连接超时 / 参数缺失,触发 except 捕获异常,打印日志:模型调用超时,第 1 次重试(1/3);

  3. 自动进入第二次循环,再次发起调用,依旧超时,打印日志:第 2 次重试(2/3);

  4. 第三次循环发起调用,仍然失败,判定 3 次重试机会耗尽,直接抛出异常,终止整套 Agent 执行流程。

整套重试逻辑完全由我们自定义的 wrap_model_call 中间件接管,无需改动 Agent 核心代码,完美实现模型调用容错能力。

如果我们自定义的中间件通常逻辑单一、职责明确,适合用装饰器式快速实现。我们可以取消对应注释。

对于上述代码,构建中间件的核心类型需要进行说明:

节点风格方法参数中,AgentStateAgent 的默认状态。Runtime 可以获取上下文信息。

包装风格方法参数中,ModelRequest 为传递给模型的请求信息;ModelResponse 为模型调用的响应信息。

类方式

接下来我们学习用 class 类的方式来创建自定义中间件,先对比两种实现方式的核心区别: 之前用装饰器实现钩子时,每一个钩子方法都相当于一个独立中间件,绑定到 Agent 的时候,需要把所有钩子方法全部写到 middleware 列表里,数量一多会显得很零散。 那如果我们希望把多个不同阶段的钩子逻辑整合到同一个中间件 中,只需要在 middleware 列表注册一次,该怎么实现?答案就是类式中间件。

**也就是说:**如果需要在一个中间件里同时处理多个钩子(比如既要在调用模型前记录日志,又要在模型返回后记录结果),就可以用类式中间件把多个钩子方法放在同一个类中。

类方式中间件适用场景

当业务需求需要在一套中间件里同时处理多个流程节点时,比如既要在模型调用前打印日志、又要在模型响应完成后打印结果、还要给模型调用加重试逻辑,就非常适合用类方式实现。 它可以把全部相关的钩子方法封装到同一个类内部,统一管理,不用分散定义大量独立装饰器方法。

类中间件基础语法要求

自定义的类必须继承父类 AgentMiddleware ,这是强制规范;

在类内部按需实现对应钩子方法,钩子依旧只有两类:节点风格、包装风格,没有第三种

不需要写任何装饰器,只需要严格固定钩子方法名 (before_agent、after_model、wrap_model_call 等),框架会自动识别

类内所有方 法第一个参数必须加上 self,代表类实例本身,其余入参、返回值规范和装饰器写法完全一致。

我们沿用之前天气工具的案例,新建一个日志专用中间件类,命名为 LoggingMiddleware,继承 AgentMiddleware

把之前装饰器写法里所有的钩子逻辑全部平移到这个类中,去掉所有 @xxx 装饰器;

每个钩子方法开头新增参数 self,其余参数、打印逻辑、返回规则完全不变;

  • 节点风格: before_agentafter_agentbefore_modelafter_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 打印用户提问、完成模型调用;

  1. 模型执行完毕触发 after_model

  2. 判定调用工具,进入 wrap_tool_call 打印工具名、参数、执行成功日志;

  3. 工具结束后二次调用大模型,再次执行 before_modelwrap_model_callafter_model

  4. 整套对话结束执行 after_agent 打印收尾日志。

所有节点风格、包装风格的钩子全部正常拦截流程,证明类式中间件生效。

这就是第二种自定义中间件实现方案 ------ 类写法。 它最大的优势:如果一套业务逻辑需要监听 Agent 多个执行阶段、编写多个钩子,全部封装到同一个类里统一维护,绑定 Agent 时只需要实例化一次,代码聚合度更高、更整洁,也是工业开发里推荐优先使用 的实现方式。