* LangChain 自定义中间件详解:Hook、装饰器、类与执行顺序

一、自定义中间件

某些复杂场景下,官方内置的中间件不能完全满足需求,此时可以通过实现LangChain暴露的中间件

hook函数构建自定义中间件。

说明:尽可能使用内置中间件。

1.1 什么是hook函数(钩子函数)

Hook 函数,中文常叫钩子函数,指的是:在某个既定流程的特定时机,被框架、系统或主程序自动

调用的扩展函数。

因此,可以把它理解成:

主流程预留了一些插槽,允许你在这些位置挂上自己的函数,这种被挂进去并在特定时机执行的函

数,就是 hook 函数。

核心特点:

1、不是你主动在业务代码里随便调用的,而是当流程运行到某个"钩子点"时,系统自动触发它。

2、它依附于一个更大的执行流程。比如"请求开始前""模型调用前""任务结束后""异常发生时"等。

3、它的作用是让你在不改主流程源码的前提下插入自己的逻辑。例如做日志、鉴权、修改输入、

拦截输出、清理资源等。

LangChain的中间件作用在Agent架构中,后者是基于LangGraph构建的流程图。如下列出了六个hook

函数(钩子函数):

无论是官方内置中间件、自定义中间件、还是下文提到的便捷装饰器中间件,通常都是通过实现其中的

一个或多个hook来生效的。

1.2 LangChain的hook函数分类

官方将六个钩子函数按照风格分为两类

1.2.1 类型1:Node-style hooks(节点风格钩子)

顾名思义,它们在流程的特定节点运行。

适合顺序逻辑,如记录日志、验证

包括

  • before_agent:在Agent开始运行之前执行。

  • before_model:在模型调用之前执行。

  • after_model:在模型调用之后执行。

  • after_agent:在Agent流程全部完成后执行。

1.2.2 类型2:Wrap-style hooks(包装风格钩子)

顾名思义,它们在模型或工具调用前后运行。

适合控制流,如重试、回退、缓存。

包括

  • wrap_model_call (包裹模型调用)

  • wrap_tool_call (包裹工具调用)

1.3 Node-style hooks函数用法

支持两种用法

  • 装饰器是函数式挂载,把一个hook快速挂载到Agent的某个节点。

  • 类写法是对象化中间件,把中间件封装为一个可配置、可复用、可扩展的组件。

1.3.1 基本用法

1.3.1.1 基于装饰器实现
text 复制代码
  1  from langchain.chat_models import init_chat_model
  2  from dotenv import load_dotenv
  3  import os
  4
  5  # 从.env文件中加载环境变量
  6  load_dotenv(override=True)
  7
  8  model = init_chat_model(
  9      model="gpt-5.4-mini",
 10      model_provider="openai",
 11      api_key=os.getenv("CLOSEAI_API_KEY"),
 12      base_url=os.getenv("CLOSEAI_BASE_URL")
 13  )
text 复制代码
  1  from langchain.agents.middleware import before_model, after_model,
     before_agent, after_agent, AgentState, AgentMiddleware
  2  from langchain.messages import HumanMessage
  3  from langgraph.runtime import Runtime
  4  from langchain.agents import create_agent
  5
  6  from typing import Any
  7
  8  # 1. 定义 before_model 钩子
  9  @before_model
 10  def before_model_middleware(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 11      state["messages"][-1].content += " -> before_model <- "
 12      return None
 13
 14  # 2. 定义 after_model 钩子
 15  @after_model
 16  def after_model_middleware(state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 17      state["messages"][-1].content += " -> after_model <- "
 18      return None
 19
 20  # 3. 定义 before_agent 钩子
 21  @before_agent
 22  def before_agent_middleware(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 23      state["messages"][-1].content += " -> before_agent <- "
 24      return None
 25
 26  # 4. 定义 after_agent 钩子
 27  @after_agent
 28  def after_agent_middleware(state: AgentState, runtime: Runtime) -> None:
 29      state["messages"][-1].content += " -> after_agent <- "
 30      return None
 31
 32  agent = create_agent(
 33      model = model,
 34      middleware = [before_model_middleware, after_model_middleware,
     before_agent_middleware, after_agent_middleware] # 👈 添加中间件
 35  )
 36
 37  response = agent.invoke({
 38      "messages": [HumanMessage("你好啊")],
 39  })
 40
 41  for msg in response["messages"]:
 42      msg.pretty_print()

输出

text 复制代码
 1  ================================ Human Message
    =================================
 2
 3  你好啊 -> before_agent <-  -> before_model <-
 4  ================================== Ai Message
    ==================================
 5
 6  你好!有什么可以帮你的吗?😊 -> after_model <-  -> after_agent <-
  1. 观察HumanMessage可以发现,before_agent钩子先于before_model被执行,二者都在调用模型

之前被执行

  1. after_agent晚于after_model执行,二者都在模型调用后执行。
1.3.1.2 基于类实现

关键规则:

  1. 必须继承 AgentMiddleware ← 这个固定
  2. 方法名固定 ( before_model , after_model ) ← 这个固定
  3. 类名随意 ← 这个不固定

LangGraph 只看:

  • 是否继承 AgentMiddleware?

  • 是否有 before_model / after_model 等方法?

举例:

text 复制代码
  1  from langchain.agents.middleware import AgentMiddleware, AgentState,
     hook_config
  2  from langchain.messages import HumanMessage
  3  from langgraph.runtime import Runtime
  4  from langchain.agents import create_agent
  5
  6  from typing import Any
  7
  8
  9  class MyMiddleware(AgentMiddleware):
 10      def __init__(self):
 11          super().__init__()
 12
 13      def before_model(self, state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 14          state["messages"][-1].content += " -> before_model <- "
 15          return None
 16
 17      def after_model(self, state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 18          state["messages"][-1].content += " -> after_model <- "
 19          return None
 20
 21      def before_agent(self, state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 22          state["messages"][-1].content += " -> before_agent <- "
 23          return None
 24
 25      def after_agent(self, state: AgentState, runtime: Runtime) -> None:
 26          state["messages"][-1].content += " -> after_agent <- "
 27          return None
 28
 29  my_middleware = MyMiddleware()
 30
 31
 32  agent = create_agent(
 33      model = model,
 34      middleware = [my_middleware]
 35  )
 36
 37  response = agent.invoke({
 38      "messages": [HumanMessage("你好啊")],
 39  })
 40
 41  for msg in response["messages"]:
 42      msg.pretty_print()

输出

text 复制代码
 1  ================================•[1m Human Message
    •[0m=================================
 2
 3  你好啊 -> before_agent <-  -> before_model <-
 4  ==================================•[1m Ai Message
    •[0m==================================
 5
 6  你好!看起来你发送的消息中包含一些特殊的标记,比如 `before_agent` 和
    `before_model`。请问你是想测试某种流程,还是需要我帮你处理特定的任务呢?
 7
 8  如果是技术测试或调试,请告诉我具体需求;如果是普通聊天,我们可以直接开始对话哦! 😊 ->
    after_model <-  -> after_agent <-
  1. before_model 通常的场景:

消息修剪(trim messages)

PII 脱敏

输入验证

条件路由

  1. after_model 通常的场景:

输出验证

格式化响应

统计信息

状态更新

1.3.2 两种方法的统一

装饰器底层会基于我们重写的方法构造一个AgentMiddleware子类的实例,以@after_model 装饰器

底层实现为例,关键代码如下

text 复制代码
 1  return type(
 2      middleware_name,
 3      (AgentMiddleware,),
 4      {
 5          "state_schema": state_schema or AgentState,
 6          "tools": tools or [],
 7          "after_model": wrapped,
 8      },
 9  )()

这是after_model最终返回的内容。

上述代码中的wrapped是after_model内部的装饰器,代码如下

text 复制代码
 1  def wrapped(
 2      _self: AgentMiddleware[StateT, ContextT],
 3      state: StateT,
 4      runtime: Runtime[ContextT],
 5  ) -> dict[str, Any] | Command[Any] | None:
 6      return func(state, runtime)  # type: ignore[return-value]

上述代码等价于

text 复制代码
 1  return type(
 2      middleware_name,
 3      (AgentMiddleware,),
 4      {
 5          "state_schema": state_schema or AgentState,
 6          "tools": tools or [],
 7          "after_model": func(state, runtime),
 8      },
 9  )()

而func(state, runtime) 正是我们定义的、被@after_model 修饰的函数,在上述案例中对应的是

after_model_middleware,

上述代码的含义是

  1. 创建一个AgentMiddleware的子类
  2. 类名为middleware_name,即创建agent时传递的中间件名称,上述案例中是

after_model_middleware

  1. 这个子类有两个属性state_schema 和tools
  2. 有一个方法:after_model ,逻辑等同于func(state, runtime) 。
  3. 最后的括号() 表示实例化子类,返回一个对象

所以,用装饰器最终返回的也是一个AgentMiddleware的子类对象,并且重写了after_model方法,和

基于类的自定义方式本质是一样的。

1.3.3 参数说明

Node-style hooks函数有两个参数

state: 是一个AgentState实例,维护Agent运行过程中的状态,这类状态会随着Agent的运行而发生变

化,包括消息列表。

runtime: 是一个Runtime实例,维护Agent运行过程中的上下文环境,包括上下文、长期记忆等。

1.3.4 返回值说明

返回 None:不修改状态(不修改Agent状态)

text 复制代码
 1  def before_model(self, state, runtime):
 2      print("日志记录")
 3      return None  # 不做任何修改,继续流程

返回字典:更新状态

text 复制代码
 1  def after_model(self, state, runtime):
 2      count = state.get("count", 0)
 3      return {"count": count + 1}  # 更新状态中的 count

返回 {"jump_to": "..."}:控制流程

text 复制代码
 1  def before_model(self, state, runtime):
 2      if state.get("count", 0) > 10:
 3          return {"jump_to": "__end__"}  # 跳过模型,直接结束
 4      return None

jump_to 目标:

  • "end" - 结束 Agent

  • "tools" - 跳到工具节点

  • 其他自定义节点

1.3.5 装饰器参数:can_jump_to

这里就涉及到Node-style的四个hook函数可以接收额外参数can_jump_to 。

钩子函数可以改变Agent正常的运行轨迹。比如:发现上下文窗口溢出,直接跳转至结尾,提前终止整

个Agent。

can_jump_to 决定了钩子函数可以直接跳转至流程的哪些位置,可取值如下:

  • end:跳转至Agent流程末尾,或第一个after_agent钩子,直接终止整个流程。

  • tools:跳转至工具节点。

  • model:跳转至模型节点,或第一个before_model钩子。

1.3.5.1 基于装饰器实现
text 复制代码
   1  from typing import Any
   2
   3  from langchain.agents import create_agent
   4  from langchain.agents.middleware import before_model, after_model,
       AgentState
   5  from langchain.messages import AIMessage, SystemMessage
   6  from langchain.tools import tool
   7  from langgraph.runtime import Runtime
   8
   9
  10  @tool
  11  def get_news() -> str:
  12      """获取当日新闻"""
  13      return f"美加墨世界杯今日开幕"
  14
  15
  16  # 在模型(LLM)执行前触发。允许跳转到 "tools" 节点。
  17  @before_model(can_jump_to=["tools"])
  18  def force_tool_first(state: AgentState, runtime: Runtime) -> dict[str, Any]
       | None:
  19      """
  20      【业务场景:强行拦截并触发工具】
  21      如果用户输入包含 "direct tool",则跳过本次大模型的思考/生成阶段,
  22      直接伪造一个大模型的 tool_calls 意图,强行把控制权移交给工具执行节点。
  23      """
  24      text = state["messages"][-1].content
  25      # 检查关键词,满足条件则强行干预流程
  26      if isinstance(text, str) and "direct tool" in text.lower():
  27          print("[MIDDLEWARE] before_model: jump_to='tools'")
  28
  29          # 人工构造一个大模型的消息对象(AIMessage)
  30          # 欺骗系统,让系统误以为这是模型自己决定要调用的工具
  31          fake_tool_call = AIMessage(
  32              content="人工构造的消息",
  33              tool_calls=[
  34                  {
  35                      "name": "get_news",
  36                      "args": {},
  37                      "id": "call_force_weather_001",
  38                  }
  39              ],
  40          )
  41
  42          # 返回更新后的状态:注入伪造的消息,并明确指定下一步跳转到 "tools" 节点
  43          return {
  44              "messages": [fake_tool_call],
  45              "jump_to": "tools",
  46          }
  47      # 如果不满足触发条件,返回 None,流程正常向下流转(继续让 LLM 思考)
  48      return None
  49
  50  # 在模型(LLM)执行生成之后触发。允许重新跳转回 "model" 节点。
  51  @after_model(can_jump_to=["model"])
  52  def retry_with_extra_instruction(state: AgentState, runtime: Runtime) ->
       dict[str, Any] | None:
  53      """
  54      【业务场景:反思/重试机制】
  55      如果大模型已经生成了回答,但发现用户最初的请求包含 "retry model",
  56      则动态追加一条系统提示词(SystemMessage),强行让模型重新生成(重试)一次。
  57      """
  58      # 倒序遍历消息历史,找到最近的一次用户输入(human 消息)
  59      user_text = ""
  60      for msg in reversed(state["messages"]):
  61          if getattr(msg, "type", "") == "human":
  62              user_text = getattr(msg, "content", "")
  63              break
  64
  65      # 检查用户输入是否包含触发重试的关键字
  66      if isinstance(user_text, str) and "retry model" in user_text.lower():
  67          # 【核心防御】:防止无限循环重跳(死循环)
  68          # 检查消息历史中是否已经注入过这条特殊的系统提示。如果有,说明已经重试过了,不再
       重复干预。
  69          already_injected = any(
  70              isinstance(getattr(msg, "content", None), str)
  71              and "你必须以【二次回答】开头" in msg.content
  72              for msg in state["messages"]
  73          )
  74          if already_injected:
  75              return None # 已注入过,直接放行,结束重试流程
  76
  77          print("[MIDDLEWARE] after_model: jump_to='model' with extra system
       instruction")
  78
  79          # 返回更新后的状态:追加强力约束的系统消息,并将指针跳回 "model" 节点重新执行
  80          return {
  81              "messages": [
  82                  SystemMessage("你必须以【二次回答】开头,并且只用一句话回答。")
  83              ],
  84              "jump_to": "model",
  85          }
  86
  87      return None
  88
  89  # 在模型(LLM)执行前触发。允许直接跳转到 "end" 节点(强行终止)。
  90  @before_model(can_jump_to=["end"])
  91  def overflow_context_processor(state: AgentState, runtime: Runtime) ->
       dict[str, Any] | None:
  92      """
  93      【业务场景:安全卫士/异常拦截】
  94      模拟上下文窗口溢出(Token超限)或其他严重的系统阻断情况。
  95      一旦触发,直接熔断流程,拒绝让大模型继续处理,直接报错或返回兜底文案。
  96      """
  97
  98      # 假装溢出,模拟检查最后一条消息是否包含 overflow 标识
  99      if "overflow" in state["messages"][-1].content:
 100          print("[MIDDLEWARE] before_model: jump_to='end' when contenxt
       window overflow")
 101
 102          # 构造兜底的结束消息,并直接指定跳转到 "end" 终止 Agent 运行
 103          return {
 104              "messages": [
 105                  AIMessage("上下文窗口溢出,终止")
 106              ],
 107              "jump_to": "end",
 108          }
 109
 110
 111  agent = create_agent(
 112      model=model,
 113      tools=[get_news],
 114      # # 将定义的中间件按照顺序挂载到 Agent 中(注意:执行顺序会严格按照列表声明顺序)
 115      middleware=[force_tool_first, retry_with_extra_instruction,
       overflow_context_processor],
 116  )
 117
 118
 119  def run_once(user_input: str):
 120      result = agent.invoke(
 121          {
 122              "messages": [
 123                  {"role": "user", "content": user_input}
 124              ]
 125          }
 126      )
 127
 128      for msg in result["messages"]:
 129          msg.pretty_print()
 130
 131
 132  if __name__ == "__main__":
 133      # Case 1: 直接跳 tools
 134      # 预期表现:
 135      # 1. 触发 force_tool_first,打印 "[MIDDLEWARE] before_model:
       jump_to='tools'"
 136      # 2. 绕过 LLM 的首轮思考,直接调用 `get_news` 工具
 137      # 3. 工具返回结果后,LLM 总结工具结果并输出
 138      print('=' * 30, '-> Case 1 <-', '=' * 30)
 139      run_once("请帮我查今日新闻 direct tool")
 140
 141      # Case 2: 输出后跳回 model
 142      # 预期表现:
 143      # 1. 正常进入 LLM 生成第 1 版回答
 144      # 2. 触发 retry_with_extra_instruction,打印 "[MIDDLEWARE] after_model:
       jump_to='model'..."
 145      # 3. 注入系统提示词后,LLM 被强行拉回并生成第 2 版回答
 146      # 4. 最终输出应带有"【二次回答】"前缀
 147      print('=' * 30, '-> Case 2 <-', '=' * 30)
 148      run_once("请随便介绍一下 LangChain retry model")
 149
 150      # Case 3:
 151      # 预期表现:
 152      # 1. 触发 overflow_context_processor 中间件
 153      # 2. 直接打印终止信息并退出,LLM 根本不会接收到这个请求
 154      print('=' * 30, '-> Case 3 <-', '=' * 30)
 155      run_once("你好 overflow")
 156
 157      # Case 4: 正常流程
 158      # 预期表现:
 159      # 1. 没有任何中间件被触发(不满足任何关键字)
 160      # 2. Agent 走正常的 OOTB(Out of the box)标准工作流:User -> Model -> Call
       Tool -> Model -> End
 161      print('=' * 30, '-> Case 4 <-', '=' * 30)
 162      run_once("今日新闻摘要?")

输出

text 复制代码
   1  ============================== -> Case 1 <-
       ==============================
   2  [MIDDLEWARE] before_model: jump_to='tools'
   3  ================================ Human Message
       =================================
   4
   5  请帮我查今日新闻 direct tool
   6  ================================== Ai Message
       ==================================
   7
   8  人工构造的消息
   9  Tool Calls:
  10  get_news (call_force_weather_001)
  11  Call ID: call_force_weather_001
  12  Args:
  13  ================================= Tool Message
       =================================
  14  Name: get_news
  15
  16  美加墨世界杯今日开幕
  17  ================================== Ai Message
       ==================================
  18
  19  今日新闻:
  20  - 美加墨世界杯今日开幕
  21
  22  如果你想,我也可以继续帮你整理成:
  23  1. 一句话摘要
  24  2. 3条要点
  25  3. 中文新闻解读
  26  ============================== -> Case 2 <-
       ==============================
  27  [MIDDLEWARE] after_model: jump_to='model' with extra system
       instruction
  28  ================================ Human Message
       =================================
  29
  30  请随便介绍一下 LangChain retry model
  31  ================================== Ai Message
       ==================================
  32
  33  可以,简单介绍一下 **LangChain 的 retry model(重试机制)**。
  34
  35  ## 1. 它是什么
  36  在 LangChain 里,retry model 指的是:
  37  当模型调用失败时,自动重试几次,而不是立刻报错。
  38
  39  常见失败原因包括:
  40  - 网络抖动
  41  - API 临时不可用
  42  - 限流(rate limit)
  43  - 超时
  44  - 服务端偶发错误
  45
  46  ---
  47
  48  ## 2. 它解决什么问题
  49  大模型调用并不是每次都稳定成功。
  50  重试机制的作用就是提高程序鲁棒性,让你的链路更"抗打"。
  51
  52  比如:
  53  - 第一次请求超时
  54  - 第二次请求成功
  55  这样用户就不需要手动刷新或重新触发。
  56
  57  ---
  58
  59  ## 3. 在 LangChain 中怎么理解
  60  LangChain 本身经常会包裹底层模型调用,比如 OpenAI、Anthropic、Hugging Face
       等。
  61  Retry model 通常意味着给这些调用加上一层 **自动重试包装**。
  62
  63  典型逻辑是:
  64  1. 发送请求
  65  2. 如果成功,直接返回
  66  3. 如果失败,等待一小段时间
  67  4. 再试一次
  68  5. 达到最大次数后,抛出错误
  69
  70  ---
  71
  72  ## 4. 常见特性
  73  重试机制一般会带这些能力:
  74
  75  - **最大重试次数**:比如 3 次、5 次
  76  - **指数退避**:每次失败后等待时间逐渐增加
  77  - **只对可重试错误生效**:比如网络错误、429、5xx
  78  - **不重试业务错误**:比如参数格式不对、提示词错误
  79
  80  ---
  81
  82  ## 5. 一个直观例子
  83  你可以把它理解成:
  84
  85  > "如果模型第一次没接通,等一下再拨;如果还不通,再等久一点继续拨。"
  86
  87  这比直接失败更适合生产环境。
  88
  89  ---
  90
  91  ## 6. 适合用在哪
  92  特别适合这些场景:
  93  - 在线问答系统
  94  - Agent 工具调用
  95  - 批量任务处理
  96  - 需要高可用的生产服务
  97
  98  ---
  99
 100  ## 7. 需要注意什么
 101  虽然重试很好用,但也不是越多越好:
 102
 103  - 重试太多会增加延迟
 104  - 失败请求会消耗更多 token / API 额度
 105  - 如果是逻辑错误,重试也没用
 106  - 要避免重复执行有副作用的操作
 107
 108  ---
 109
 110  ## 8. 一句话总结
 111  **LangChain retry model 本质上就是为大模型调用加"自动重试 + 退避"能力,提高调
       用稳定性和容错率。**
 112
 113  如果你愿意,我也可以继续给你讲:
 114  - LangChain 里具体怎么写 retry
 115  - 和 `with_retry()` 的关系
 116  - Python 里一个最小示例
 117  ================================ System Message
       ================================
 118
 119  你必须以【二次回答】开头,并且只用一句话回答。
 120  ================================== Ai Message
       ==================================
 121
 122  【二次回答】LangChain 的 retry model 就是给模型调用加上自动重试和指数退避机制,
       在网络抖动、限流或临时服务错误时提高调用成功率与稳定性。
 123  ============================== -> Case 3 <-
       ==============================
 124  [MIDDLEWARE] before_model: jump_to='end' when contenxt window overflow
 125  ================================ Human Message
       =================================
 126
 127  你好 overflow
 128  ================================== Ai Message
       ==================================
 129
 130  上下文窗口溢出,终止
 131  ============================== -> Case 4 <-
       ==============================
 132  ================================ Human Message
       =================================
 133
 134  今日新闻摘要?
 135  ================================== Ai Message
       ==================================
 136  Tool Calls:
 137    get_news (call_IYwXdmiTrkWDX5Zr6VM2RZOO)
 138   Call ID: call_IYwXdmiTrkWDX5Zr6VM2RZOO
 139    Args:
 140  ================================= Tool Message
       =================================
 141  Name: get_news
 142
 143  美加墨世界杯今日开幕
 144  ================================== Ai Message
       ==================================
 145
 146  今日新闻摘要:
 147
 148  - **美加墨世界杯今日开幕**
 149
 150  如果你需要,我也可以把这条新闻进一步整理成:
 151  1. **一句话摘要**
 152  2. **背景解读**
 153  3. **可能影响**
  1. 我们提前判定需要调用工具,直接在before_model中跳转至工具节点,省去了一次模型调用
  2. 通过约定的retry model标记,在after_model之后再次跳转到模型节点,触发模型重复调用
  3. 通过约定的overflow标记,模拟上下文窗口溢出,在before_model中直接跳转至结尾,提前终

止流程

  1. Case 4 是没有被干预的正常Agent流程,作为对照。
1.3.5.2 基于类实现

和基于装饰器实现的关键区别在于:需要引入额外的装饰器@hook_config 为can_jump_to 传参。

text 复制代码
   1  from typing import Any
   2
   3  from langchain.agents import create_agent
   4  from langchain.agents.middleware import hook_config, AgentState,
       AgentMiddleware
   5  from langchain.messages import AIMessage, SystemMessage
   6  from langchain.tools import tool
   7  from langgraph.runtime import Runtime
   8
   9
  10  @tool
  11  def get_news() -> str:
  12      """获取当日新闻"""
  13      return f"美加墨世界杯今日开幕"
  14
  15
  16  class MyMiddleware(AgentMiddleware):
  17      @hook_config(can_jump_to=["tools", "end"])
  18      def before_model(self, state: AgentState, runtime: Runtime) ->
       dict[str, Any] | None:
  19          text = state["messages"][-1].content
  20          # 假装溢出
  21          if "overflow" in text:
  22              print("[MIDDLEWARE] before_model: jump_to='end' when contenxt
       window overflow")
  23              return {
  24                  "messages": [
  25                      AIMessage("上下文窗口溢出,终止")
  26                  ],
  27                  "jump_to": "end",
  28              }
  29
  30          if isinstance(text, str) and "direct tool" in text.lower():
  31              print("[MIDDLEWARE] before_model: jump_to='tools'")
  32
  33              fake_tool_call = AIMessage(
  34                  content="人工构造的消息",
  35                  tool_calls=[
  36                      {
  37                          "name": "get_news",
  38                          "args": {},
  39                          "id": "call_force_weather_001",
  40                      }
  41                  ],
  42              )
  43
  44              return {
  45                  "messages": [fake_tool_call],
  46                  "jump_to": "tools",
  47              }
  48
  49          return None
  50
  51      @hook_config(can_jump_to=["model"])
  52      def after_model(self, state: AgentState, runtime: Runtime) -> dict[str,
       Any] | None:
  53          user_text = ""
  54          for msg in reversed(state["messages"]):
  55              if getattr(msg, "type", "") == "human":
  56                  user_text = getattr(msg, "content", "")
  57                  break
  58
  59          if isinstance(user_text, str) and "retry model" in
       user_text.lower():
  60              # 防止无限重跳:如果已经加过提示,就不再跳
  61              already_injected = any(
  62                  isinstance(getattr(msg, "content", None), str)
  63                  and "你必须以【二次回答】开头" in msg.content
  64                  for msg in state["messages"]
  65              )
  66              if already_injected:
  67                  return None
  68
  69              print("[MIDDLEWARE] after_model: jump_to='model' with extra
       system instruction")
  70
  71              return {
  72                  "messages": [
  73                      SystemMessage("你必须以【二次回答】开头,并且只用一句话回答。")
  74                  ],
  75                  "jump_to": "model",
  76              }
  77
  78          return None
  79
  80
  81  agent = create_agent(
  82      model=model,
  83      tools=[get_news],
  84      middleware=[MyMiddleware()],
  85  )
  86
  87
  88  def run_once(user_input: str):
  89      result = agent.invoke(
  90          {
  91              "messages": [
  92                  {"role": "user", "content": user_input}
  93              ]
  94          }
  95      )
  96
  97      for msg in result["messages"]:
  98          msg.pretty_print()
  99
 100
 101  if __name__ == "__main__":
 102      # Case 1: 直接跳 tools
 103      print('=' * 30, '-> Case 1 <-', '=' * 30)
 104      run_once("请帮我查今日新闻 direct tool")
 105
 106      # Case 2: 输出后跳回 model
 107      print('=' * 30, '-> Case 2 <-', '=' * 30)
 108      run_once("请随便介绍一下 LangChain retry model")
 109
 110      # Case 3:
 111      print('=' * 30, '-> Case 3 <-', '=' * 30)
 112      run_once("你好 overflow")
 113
 114      # Case 4: 正常流程
 115      print('=' * 30, '-> Case 4 <-', '=' * 30)
 116      run_once("今日新闻摘要?")

输出

text 复制代码
   1  ============================== -> Case 1 <-
       ==============================
   2  [MIDDLEWARE] before_model: jump_to='tools'
   3  ================================ Human Message
       =================================
   4
   5  请帮我查今日新闻 direct tool
   6  ================================== Ai Message
       ==================================
   7
   8  人工构造的消息
   9  Tool Calls:
  10  get_news (call_force_weather_001)
  11  Call ID: call_force_weather_001
  12  Args:
  13  ================================= Tool Message
       =================================
  14  Name: get_news
  15
  16  美加墨世界杯今日开幕
  17  ================================== Ai Message
       ==================================
  18
  19  今日新闻:
  20  - 美加墨世界杯今日开幕
  21
  22  如果你愿意,我也可以帮你继续整理成:
  23  1. 简短版要闻
  24  2. 详细新闻摘要
  25  3. 按国内 / 国际 / 财经分类整理
  26
  27  ============================== -> Case 2 <-
       ==============================
  28  [MIDDLEWARE] after_model: jump_to='model' with extra system
       instruction
  29  ================================ Human Message
       =================================
  30
  31  请随便介绍一下 LangChain retry model
  32  ================================== Ai Message
       ==================================
  33
  34  当然可以。下面我用比较通俗的方式介绍一下 **LangChain 的 retry model(重试机制)
       **。
  35
  36  ## 1. 它是什么
  37  在调用大模型或外部工具时,可能会遇到一些临时性失败,比如:
  38
  39  - 网络超时
  40  - API 限流
  41  - 服务端偶发错误
  42  - 返回内容格式不符合预期
  43
  44  **retry model** 的作用就是:
  45  当第一次调用失败时,自动再试几次,直到成功或者达到最大重试次数。
  46
  47  ---
  48
  49  ## 2. 为什么需要它
  50  大模型调用不像普通本地函数,很多问题不是"代码错了",而是"环境不稳定"。
  51  比如:
  52
  53  - 第一次请求超时,第二次却成功了
  54  - 某次返回了空内容,再请求一次就正常
  55  - 短时间内触发 rate limit,稍等一下再调用就行
  56
  57  所以重试机制能提高程序的稳定性和容错率。
  58
  59  ---
  60
  61  ## 3. LangChain 里怎么用
  62  LangChain 通常会结合底层模型客户端或自己的 runnable 机制来做重试。常见思路是:
  63
  64  - 设置最大重试次数
  65  - 设置重试间隔
  66  - 指定哪些错误需要重试
  67  - 失败后自动重新调用
  68
  69  有时你会看到类似"包装一个 chain / runnable / model,让它带 retry 行为"。
  70
  71  ---
  72
  73  ## 4. 它通常会做什么
  74  一个典型 retry model 会包含:
  75
  76  - **重试次数上限**:比如最多 3 次
  77  - **退避策略**:每次失败后等更久一点再试,比如 1s、2s、4s
  78  - **错误筛选**:只对临时错误重试,不对参数错误重试
  79  - **日志记录**:方便知道为什么重试
  80
  81  ---
  82
  83  ## 5. 简单理解
  84  你可以把它理解成:
  85
  86  > "如果大模型这次没答好,不要立刻放弃,给它几次机会。"
  87
  88  ---
  89
  90  ## 6. 注意事项
  91  重试虽然有用,但不是越多越好:
  92
  93  - 会增加延迟
  94  - 会增加调用成本
  95  - 如果错误是逻辑错误,重试也没用
  96
  97  所以一般只对**短暂性、可恢复错误**启用重试。
  98
  99  ---
 100
 101  ## 7. 一个直观例子
 102  比如你问模型一个问题:
 103
 104  1. 第一次请求:超时
 105  2. 第二次请求:返回 429(限流)
 106  3. 第三次请求:成功
 107
 108  这就是 retry model 在起作用。
 109
 110  ---
 111
 112  如果你愿意,我还可以继续给你讲:
 113  1. **LangChain 里 retry 的代码示例**
 114  2. **retry 和 fallback 的区别**
 115  3. **Python 里怎么给 LLM 调用加重试**
 116  ================================ System Message
       ================================
 117
 118  你必须以【二次回答】开头,并且只用一句话回答。
 119  ================================== Ai Message
       ==================================
 120
 121  【二次回答】LangChain 的 retry model 就是给大模型调用加"自动重试"机制:遇到超
       时、限流或临时错误时按次数和退避策略重新请求,从而提高稳定性和成功率。
 122
 123  ============================== -> Case 3 <-
       ==============================
 124  [MIDDLEWARE] before_model: jump_to='end' when contenxt window overflow
 125  ================================ Human Message
       =================================
 126
 127  你好 overflow
 128  ================================== Ai Message
       ==================================
 129
 130  上下文窗口溢出,终止
 131
 132  ============================== -> Case 4 <-
       ==============================
 133  ================================ Human Message
       =================================
 134
 135  今日新闻摘要?
 136  ================================== Ai Message
       ==================================
 137  Tool Calls:
 138    get_news (call_R4eUqp1uGgAGvYQLqDpUv6CW)
 139   Call ID: call_R4eUqp1uGgAGvYQLqDpUv6CW
 140    Args:
 141  ================================= Tool Message
       =================================
 142  Name: get_news
 143
 144  美加墨世界杯今日开幕
 145  ================================== Ai Message
       ==================================
 146
 147  今日新闻摘要:
 148
 149  - **美加墨世界杯今日开幕**
 150
 151  如果你愿意,我也可以把这条新闻再整理成**一句话版**、**要点版**,或者帮你补充**背景
       解读**。

1.4 Wrap-style hooks函数用法

1.4.1 基本用法

1.4.1.1 wrap_model_call
1.4.1.1.1 基于装饰器实现

我们可以同时在模型调用前后做事,所以命名为wrap_model_call ,wrap意为包裹。

源码:

text 复制代码
 1  def wrap_model_call(
 2      request: ModelRequest,
 3      handler: Callable[[ModelRequest], ModelResponse]
 4  ) -> ModelResponse:
 5      """
 6      request: 包含 model, messages, system_message, tools, state
 7      handler: 执行实际模型调用的函数
 8      返回:ModelResponse
 9      """

代码实现:

text 复制代码
  1  from langchain.chat_models import init_chat_model
  2  from dotenv import load_dotenv
  3  import os
  4
  5  # 从.env文件中加载环境变量
  6  load_dotenv(override=True)
  7
  8  model = init_chat_model(
  9      model="gpt-5.4-mini",
 10      model_provider="openai",
 11      api_key=os.getenv("CLOSEAI_API_KEY"),
 12      base_url=os.getenv("CLOSEAI_BASE_URL")
 13  )
text 复制代码
  1  from langchain.agents.middleware import wrap_model_call, ModelRequest,
     ModelResponse
  2  from langchain.messages import HumanMessage
  3  from langchain.agents import create_agent
  4  from typing import Callable
  5
  6  @wrap_model_call
  7  def wrap_model_call_middleware(
  8      request: ModelRequest, # 包含即将发送给大模型的所有请求数据(如消息列表、温度等)
  9      handler: Callable[[ModelRequest], ModelResponse], # 核心句柄:代表下一个中间
     件或最终的大模型调用服务
 10  ) -> ModelResponse | None:
 11      # 动态篡改用户发出的最后一条消息的内容,悄悄往里面追加字符串。
 12      # 典型应用:统一在底层为所有请求追加特殊的 Prompt 提示词(例如:"请用中文回答"、"禁
     止透漏公司机密"等)。
 13      request.messages[-1].content += " -> wrap_model_call_before <- "
 14      # 将修改后的请求传递给 handler,真正去调用大模型(或者流转到下一个拦截器)
 15      # 这一步会产生真实的 Token 消耗并等待大模型响应
 16      response = handler(request)
 17      # 大模型返回响应后,在将响应交付给 Agent 状态机之前,对其内容进行直接篡改
 18      # `response.result` 是一个消息列表,修改其第一条返回消息的内容
 19      # 典型应用:做底层的文本敏感词过滤、输出格式强行格式化、或是统一添加某些后处理标记。
 20      response.result[0].content += " -> wrap_model_call_after <- "
 21      # 将修改完的响应体返回,继续维持 Agent 生命周期流转
 22      return response
 23
 24  agent = create_agent(
 25      model = model,
 26      middleware = [wrap_model_call_middleware]
 27  )
 28
 29  response = agent.invoke({
 30      "messages": [HumanMessage("你好啊")],
 31  })
 32
 33  for msg in response["messages"]:
 34      msg.pretty_print()

输出

text 复制代码
 1  ================================ Human Message
    =================================
 2
 3  你好啊 -> wrap_model_call_before <-
 4  ================================== Ai Message
    ==================================
 5
 6  你好!有什么我可以帮你的? -> wrap_model_call_after <-

模型调用前消息列表的最后一条是HumanMessage,调用后最后一条是AIMessage,可以看到,模型调

用前后的更改都生效了。

1.4.1.1.2 基于类实现
text 复制代码
  1  from langchain.agents.middleware import AgentMiddleware, ModelRequest,
     ModelResponse
  2  from langchain.messages import HumanMessage
  3  from langchain.agents import create_agent
  4  from typing import Callable
  5
  6
  7  class WrapModelCallMiddleWare(AgentMiddleware):
  8      def wrap_model_call(
  9          self,
 10          request: ModelRequest,
 11          handler: Callable[[ModelRequest], ModelResponse],
 12      ) -> ModelResponse | None:
 13          request.messages[-1].content += " -> wrap_model_call_before <- "
 14          response = handler(request)
 15          response.result[0].content += " -> wrap_model_call_after <- "
 16
 17          return response
 18
 19  agent = create_agent(
 20      model = model,
 21      middleware = [WrapModelCallMiddleWare()]
 22  )
 23
 24  response = agent.invoke({
 25      "messages": [HumanMessage("你好啊")],
 26  })
 27
 28  for msg in response["messages"]:
 29      msg.pretty_print()

输出

text 复制代码
 1  ================================ Human Message
    =================================
 2
 3  你好啊 -> wrap_model_call_before <-
 4  ================================== Ai Message
    ==================================
 5
 6  你好!`wrap_model_call_before` 这部分是想做什么呢?如果你是在测试消息包裹、提示词
    注入,或者想让我按某种格式回复,可以直接告诉我规则。 -> wrap_model_call_after <-

使用场景:用于拦截、重试、缓存模型调用。

场景1:重试逻辑

text 复制代码
  1  from langchain.agents.middleware import wrap_model_call, ModelRequest,
     ModelResponse
  2  from typing import Callable
  3  import time
  4
  5  @wrap_model_call
  6  def retry_model(
  7      request: ModelRequest,
  8      handler: Callable[[ModelRequest], ModelResponse]
  9  ) -> ModelResponse:
 10      """自动重试失败的模型调用"""
 11      max_retries = 3
 12
 13      for attempt in range(max_retries):
 14          try:
 15              print(f"🔄 尝试调用模型(第 {attempt + 1}/{max_retries} 次)")
 16              return handler(request)
 17          except Exception as e:
 18              if attempt == max_retries - 1:
 19                  print(f"❌ 所有重试失败:{e}")
 20                  raise
 21
 22              # 指数退避
 23              wait_time = 2 ** attempt
 24              print(f"⚠  调用失败:{e},{wait_time} 秒后重试")
 25              time.sleep(wait_time)

场景2:响应缓存

text 复制代码
  1  from langchain.agents.middleware import wrap_model_call, ModelRequest,
     ModelResponse
  2  from typing import Callable
  3  import hashlib
  4  import json
  5
  6  class ModelCache:
  7      """模型响应缓存"""
  8      def __init__(self):
  9          self.cache = {}
 10
 11      def create_hook(self):
 12          @wrap_model_call
 13          def cache_model(
 14              request: ModelRequest,
 15              handler: Callable[[ModelRequest], ModelResponse]
 16          ) -> ModelResponse:
 17              # 生成缓存键
 18              cache_key = hashlib.md5(
 19                  json.dumps({
 20                      "messages": [str(m) for m in request.messages],
 21                      "system": str(request.system_message)
 22                  }).encode()
 23              ).hexdigest()
 24
 25              # 检查缓存
 26              if cache_key in self.cache:
 27                  print("💾 缓存命中!")
 28                  return self.cache[cache_key]
 29
 30              # 调用模型
 31              print("🔍 缓存未命中,调用模型")
 32              response = handler(request)
 33
 34              # 存入缓存
 35              self.cache[cache_key] = response
 36              return response
 37
 38          return cache_model
 39
 40  # 使用
 41  cache = ModelCache()
 42  agent = create_agent(
 43      model=model,
 44      middleware=[cache.create_hook()]
 45  )

场景3:修改系统提示

text 复制代码
  1  from langchain.agents.middleware import wrap_model_call, ModelRequest,
     ModelResponse
  2  from langchain_core.messages import SystemMessage
  3  from typing import Callable
  4
  5  @wrap_model_call
  6  def add_context(
  7      request: ModelRequest,
  8      handler: Callable[[ModelRequest], ModelResponse]
  9  ) -> ModelResponse:
 10      """动态添加上下文信息到系统提示"""
 11
 12      # 获取当前时间
 13      from datetime import datetime
 14      current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 15
 16      # 构建新的系统消息
 17      original_content = request.system_message.content if
     request.system_message else ""
 18      new_content = f"""{original_content}
 19
 20        当前时间:{current_time}
 21        用户位置:中国
 22        语言偏好:中文
 23        """
 24
 25      # 创建新的系统消息
 26      new_system_message = SystemMessage(content=new_content)
 27
 28      # 使用 override 方法修改请求
 29      modified_request = request.override(system_message=new_system_message)
 30
 31      return handler(modified_request)
1.4.1.2 wrap_tool_call

我们可以同时在工具调用前后做事,所以命名为wrap_tool_call 。

1.4.1.2.1 基于装饰器实现
text 复制代码
  1  from langchain.agents.middleware import wrap_tool_call
  2  from langchain.tools.tool_node import ToolCallRequest
  3  from langchain.messages import HumanMessage, ToolMessage
  4  from langchain.agents import create_agent
  5  from langchain.tools import tool
  6  from langgraph.types import Command
  7  from typing import Callable
  8
  9
 10  @tool
 11  def get_weather(city: str, is_forcast: bool) -> str:
 12      """
 13      获取当日特定城市的天气
 14
 15      Args:
 16          city: 城市名称
 17          is_forcast: 是否包含明天的天气预报
 18      """
 19      res = f"{city}今天天气不错"
 20      if is_forcast:
 21          res += "\n明天天气也很好"
 22      return res
 23
 24
 25  @wrap_tool_call
 26  def wrap_tool_call_middleware(
 27          request: ToolCallRequest,
 28          handler: Callable[[ToolCallRequest], ToolMessage | Command],
 29  ) -> ToolMessage | Command:
 30      result = handler(request)
 31      print(f"原始参数:{request.tool_call['args']}")
 32      print(f"原始参数调用结果: {result}")
 33
 34      request.tool_call["args"]["is_forcast"] = True
 35      result = handler(request)
 36      print(f"更新后的参数:{request.tool_call['args']}")
 37      print(f"更新参数调用结果: {result}")
 38      return result
 39
 40
 41  agent = create_agent(
 42      model=model,
 43      tools=[get_weather],
 44      middleware=[wrap_tool_call_middleware]
 45  )
 46
 47  response = agent.invoke({
 48      "messages": [HumanMessage("你好啊,今天杭州的天气怎么样")],
 49  })
 50
 51  for msg in response["messages"]:
 52      msg.pretty_print()

输出

text 复制代码
  1  原始参数:{'city': '杭州', 'is_forcast': False}
  2  原始参数调用结果: content='杭州今天天气不错' name='get_weather'
     tool_call_id='call_2StbfD50EA3AWleNShXhefbf'
  3  更新后的参数:{'city': '杭州', 'is_forcast': True}
  4  更新参数调用结果: content='杭州今天天气不错\n明天天气也很好'
     name='get_weather' tool_call_id='call_2StbfD50EA3AWleNShXhefbf'
  5  ================================ Human Message
     =================================
  6
  7  你好啊,今天杭州的天气怎么样
  8  ================================== Ai Message
     ==================================
  9  Tool Calls:
 10  get_weather (call_2StbfD50EA3AWleNShXhefbf)
 11  Call ID: call_2StbfD50EA3AWleNShXhefbf
 12   Args:
 13    city: 杭州
 14     is_forcast: True
 15  ================================= Tool Message
     =================================
 16  Name: get_weather
 17
 18  杭州今天天气不错
 19  明天天气也很好
 20  ================================== Ai Message
     ==================================
 21
 22  杭州今天天气不错,明天天气也很好。

在@wrap_tool_call装饰的函数中两次调用函数并更改参数。

1.4.1.2.2 基于类实现
text 复制代码
  1  from langchain.agents.middleware import AgentMiddleware
  2  from langchain.tools.tool_node import ToolCallRequest
  3  from langchain.messages import HumanMessage, ToolMessage
  4  from langchain.agents import create_agent
  5  from langchain.tools import tool
  6  from langgraph.types import Command
  7  from typing import Callable
  8
  9  @tool
 10  def get_weather(city: str, is_forcast: bool) -> str:
 11      """
 12      获取当日特定城市的天气
 13
 14      Args:
 15          city: 城市名称
 16          is_forcast: 是否包含明天的天气预报
 17      """
 18      res = f"{city}今天天气不错"
 19      if is_forcast:
 20          res += "\n明天天气也很好"
 21      return res
 22
 23  class WrapToolCallMiddleware(AgentMiddleware):
 24      def wrap_tool_call(
 25          self,
 26          request: ToolCallRequest,
 27          handler: Callable[[ToolCallRequest], ToolMessage | Command],
 28      ) -> ToolMessage | Command:
 29          result = handler(request)
 30          print(f"原始参数:{request.tool_call['args']}")
 31          print(f"原始参数调用结果: {result}")
 32
 33          request.tool_call["args"]["is_forcast"] = True
 34          result = handler(request)
 35          print(f"更新后的参数:{request.tool_call['args']}")
 36          print(f"更新参数调用结果: {result}")
 37          return result
 38
 39  agent = create_agent(
 40      model = model,
 41      tools = [get_weather],
 42      middleware = [WrapToolCallMiddleware()]
 43  )
 44
 45  response = agent.invoke({
 46      "messages": [HumanMessage("你好啊,今天杭州的天气怎么样")],
 47  })
 48
 49  for msg in response["messages"]:
 50      msg.pretty_print()

输出

text 复制代码
  1  原始参数:{'city': '杭州', 'is_forcast': False}
  2  原始参数调用结果: content='杭州今天天气不错' name='get_weather'
     tool_call_id='call_yU58ykg0IY1paVlGqTBuU8Ie'
  3  更新后的参数:{'city': '杭州', 'is_forcast': True}
  4  更新参数调用结果: content='杭州今天天气不错\n明天天气也很好'
     name='get_weather' tool_call_id='call_yU58ykg0IY1paVlGqTBuU8Ie'
  5  ================================ Human Message
     =================================
  6
  7  你好啊,今天杭州的天气怎么样
  8  ================================== Ai Message
     ==================================
  9  Tool Calls:
 10  get_weather (call_yU58ykg0IY1paVlGqTBuU8Ie)
 11  Call ID: call_yU58ykg0IY1paVlGqTBuU8Ie
 12   Args:
 13    city: 杭州
 14     is_forcast: True
 15  ================================= Tool Message
     =================================
 16  Name: get_weather
 17
 18  杭州今天天气不错
 19  明天天气也很好
 20  ================================== Ai Message
     ==================================
 21
 22  杭州今天天气不错,明天天气也很好。

使用场景:用于监控、重试、修改工具执行。

比如:

text 复制代码
  1  from langchain.agents.middleware import wrap_tool_call
  2  from langchain.tools.tool_node import ToolCallRequest
  3  from langchain_core.messages import ToolMessage
  4  from langgraph.types import Command
  5  from typing import Callable
  6  import time
  7
  8  @wrap_tool_call
  9  def monitor_tool(
 10      request: ToolCallRequest,
 11      handler: Callable[[ToolCallRequest], ToolMessage | Command]
 12  ) -> ToolMessage | Command:
 13      """监控工具执行时间和状态"""
 14
 15      tool_name = request.tool_call["name"]
 16      tool_args = request.tool_call.get("args", {})
 17
 18      print(f"🔧 开始执行工具:{tool_name}")
 19      print(f"   参数:{tool_args}")
 20
 21      start_time = time.time()
 22      try:
 23          result = handler(request)
 24          elapsed = time.time() - start_time
 25          print(f"✅ 工具执行成功,耗时:{elapsed:.2f}秒")
 26          return result
 27      except Exception as e:
 28          elapsed = time.time() - start_time
 29          print(f"❌ 工具执行失败:{e},耗时:{elapsed:.2f}秒")
 30          raise

1.4.2 两种方法的统一

同上,装饰器方法底层也会创建一个AgentMiddleware的实例。

1.4.3 参数说明

request:被封装的请求对象,可以是模型或工具调用请求

handler:处理器,用于处理请求并返回调用结果。

1.5 装饰器和类的选择

情况1:中间件只用一个钩子函数,推荐用装饰器,需要多个钩子函数推荐类写法

  • 当一个中间件只需要实现一个钩子函数时,直接使用装饰器最简单。

  • 当一个中间件需要实现多个钩子函数时,类写法更合适。

装饰器也不是不能实现,多数情况下可以像下面的示例里那样通过工厂函数返回多个装饰器函数来完

成;但这种方式本质上是把一个"逻辑上属于同一个中间件"的行为拆成多个独立函数,再由外部统一组

装,因此不如类写法自然、集中、清晰。

  1. 使用装饰器实现
text 复制代码
  1  from langchain.agents import create_agent
  2  from langchain.agents.middleware import before_model, after_model,
     AgentState
  3  from langchain.messages import HumanMessage
  4  from langgraph.runtime import Runtime
  5  from loguru import logger
  6  from typing import Any
  7
  8
  9  def create_audit_middleware(logger):
 10      @before_model
 11      def before_log(state: AgentState, runtime: Runtime) -> dict[str, Any] |
     None:
 12          logger.info("调用模型前消息数量: {}", len(state["messages"]))
 13          return None
 14
 15      @after_model
 16      def after_log(state: AgentState, runtime: Runtime) -> dict[str, Any] |
     None:
 17          logger.info("调用模型后消息数量:{}", len(state["messages"]))
 18          return None
 19
 20      return [before_log, after_log]
 21
 22  agent = create_agent(
 23      model = model,
 24      middleware = [*create_audit_middleware(logger=logger)],
 25  )
 26
 27  response = agent.invoke({
 28      "messages": [HumanMessage("你好~")]
 29  })
 30
 31  for msg in response["messages"]:
 32      msg.pretty_print()

输出

text 复制代码
 1  2026-06-08 15:27:20.879 | INFO     | __main__:before_log:14 - 调用模型前消息数
    量: 1
 2  2026-06-08 15:27:23.181 | INFO     | __main__:after_log:19 - 调用模型后消息数
    量:2
 3  ================================ Human Message
    =================================
 4
 5  你好~
 6  ================================== Ai Message
    ==================================
 7
 8  你好呀~有什么我可以帮你的吗?
 9
  1. 使用类实现
text 复制代码
  1  from langchain.agents import create_agent
  2  from langchain.agents.middleware import before_model, after_model,
     AgentState, AgentMiddleware
  3  from langchain.messages import HumanMessage
  4  from langgraph.runtime import Runtime
  5  from loguru import logger
  6  from typing import Any
  7
  8
  9  class CreateAuditMiddleware(AgentMiddleware):
 10      def __init__(self, logger):
 11          super().__init__()
 12          self.logger = logger
 13
 14      def before_model(self, state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 15          self.logger.info("调用模型前消息数量: {}", len(state["messages"]))
 16          return None
 17
 18      def after_model(self, state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 19          self.logger.info("调用模型后消息数量:{}", len(state["messages"]))
 20          return None
 21
 22
 23  agent = create_agent(
 24      model=model,
 25      middleware=[CreateAuditMiddleware(logger=logger)],
 26  )
 27
 28  response = agent.invoke({
 29      "messages": [HumanMessage("你好~")]
 30  })
 31
 32  for msg in response["messages"]:
 33      msg.pretty_print()

输出

text 复制代码
 1  2026-06-08 15:29:20.185 | INFO     | __main__:before_model:15 - 调用模型前
    消息数量: 1
 2  2026-06-08 15:29:22.322 | INFO     | __main__:after_model:19 - 调用模型后
    消息数量:2
 3  ================================ Human Message
    =================================
 4
 5  你好~
 6  ================================== Ai Message
    ==================================
 7
 8  你好!有什么我可以帮你的吗?

结合上面的两个示例,可以得出结论:

  • 装饰器写法适合把单个 hook 快速挂到 agent 生命周期的某个节点上;

  • 类写法更适合把多个 hook 组织为一个完整的中间件组件;

  • 当中间件同时涉及 before_model 、after_model 等多个钩子时,虽然装饰器工厂也能实现,但

    类写法在结构表达、配置归属、可维护性上更好。

总结:

单钩子场景下,装饰器即可;多钩子场景下,类不是唯一可行方案,但通常是更自然、更推荐的实现方

式。

情况2:复杂配置推荐用类实现

装饰器当然也可以通过函数闭包传递参数,但在自省(运行时类型校验)、调试等方面天然不如类写法

方便。

text 复制代码
  1  from langchain.agents.middleware import before_model, AgentState,
     AgentMiddleware
  2  from langgraph.runtime import Runtime
  3  from typing import Any
  4  from loguru import logger
  5
  6
  7  # 基于类的方法
  8  class AuditMiddleware(AgentMiddleware):
  9      def __init__(self, logger, threshold: int, middleware_name: str):
 10          self.logger = logger
 11          self.threshold = threshold
 12          self.middleware_name = middleware_name
 13
 14      def before_model(self, state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 15          self.logger.info("current name: {}, threshold: {}",
     self.middleware_name, self.threshold)
 16          return None
 17
 18
 19  # 基于装饰器的方法,传参要通过闭包完成
 20  def create_audit_middleware(logger, threshold: int, middleware_name: str):
 21      @before_model
 22      def audit_middleware(state: AgentState, runtime: Runtime) -> dict[str,
     Any] | None:
 23          logger.info("current name: {}, threshold: {}", middleware_name,
     threshold)
 24          return None
 25
 26      return audit_middleware
 27
 28
 29  class_middle = [
 30      AuditMiddleware(logger=logger, threshold=5, middleware_name="short
     limit"),
 31      AuditMiddleware(logger=logger, threshold=50, middleware_name="long
     limit"),
 32  ]
 33
 34  decorator_middle = [
 35      create_audit_middleware(logger=logger, threshold=5,
     middleware_name="short limit"),
 36      create_audit_middleware(logger=logger, threshold=50,
     middleware_name="long limit"),
 37  ]
 38
 39  print("=" * 30, "-> class风格的中间件 <-", "=" * 30)
 40  for mw in class_middle:
 41      print(type(mw))
 42      print(mw.__dict__)
 43
 44  print("=" * 30, "-> decorator风格的中间件 <-", "=" * 30)
 45  for mw in decorator_middle:
 46      print(type(mw))  # 基于类的方法
 47      print(mw.__dict__)

输出

text 复制代码
  1  ============================== -> class风格的中间件 <-
     ==============================
  2  <class '__main__.AuditMiddleware'>
  3  {'logger': <loguru.logger handlers=[(id=0, level=10, sink=stderr)]>,
     'threshold': 5, 'middleware_name': 'short limit'}
  4  <class '__main__.AuditMiddleware'>
  5  {'logger': <loguru.logger handlers=[(id=0, level=10, sink=stderr)]>,
     'threshold': 50, 'middleware_name': 'long limit'}
  6  ============================== -> decorator风格的中间件 <-
     ==============================
  7  <class 'langchain.agents.middleware.types.audit_middleware'>
  8  {}
  9  <class 'langchain.agents.middleware.types.audit_middleware'>
 10  {}

基于类的写法可以随时打印参数信息,而基于装饰器的闭包实现则难以做到。

情况3:跨项目复用推荐用类写法

如果希望中间件成为一个可实例化、可封装、可测试的组件,类写法更加合适,因为这些本就是类擅长

的场景,装饰器的闭包也能实现,但使用不友好。

总结:

装饰器写法和类写法都能实现 middleware hook,本质上只是两种定义中间件的方式,并不是能力上完

全割裂的两套机制。底层实现是统一的。

一般来说:

  • 装饰器写法更适合单个 hook、逻辑简单、快速原型的场景;

  • 类写法更适合多个 hook 组合、复杂配置、需要同时提供同步/异步实现、以及更强复用与可测试

    性的场景;

1.6 hook函数执行顺序(重要)

分类讨论

before_* 钩子函数:从前到后执行

after_* 钩子函数:从后往前执行

wrap_* 钩子函数:洋葱架构,前面的包裹后面的

这里的顺序并非定义顺序,而是创建Agent时传递中间件的顺序。

1. 代码

text 复制代码
  1  from langchain.chat_models import init_chat_model
  2  from dotenv import load_dotenv
  3  import os
  4
  5  # 从.env文件中加载环境变量
  6  load_dotenv(override=True)
  7
  8  model = init_chat_model(
  9      model="gpt-5.4-mini",
 10      model_provider="openai",
 11      api_key=os.getenv("CLOSEAI_API_KEY"),
 12      base_url=os.getenv("CLOSEAI_BASE_URL")
 13  )
text 复制代码
  1  from langchain.agents.middleware import (
  2      before_model,
  3      after_model,
  4      AgentState,
  5      wrap_model_call,
  6      ModelRequest,
  7      ModelResponse,
  8  )
  9  from langchain.messages import HumanMessage
 10  from langgraph.runtime import Runtime
 11  from langchain.agents import create_agent
 12  from typing import Any, Callable
 13
 14
 15  @before_model
 16  def before_model_middleware3(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 17      state["messages"][-1].content += " -> before_model-3 <- "
 18      return None
 19
 20
 21  @before_model
 22  def before_model_middleware1(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 23      state["messages"][-1].content += " -> before_model-1 <- "
 24      return None
 25
 26
 27  @before_model
 28  def before_model_middleware2(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 29      state["messages"][-1].content += " -> before_model-2 <- "
 30      return None
 31
 32
 33  @after_model
 34  def after_model_middleware2(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 35      state["messages"][-1].content += " -> after_model-2 <- "
 36      return None
 37
 38
 39  @after_model
 40  def after_model_middleware1(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 41      state["messages"][-1].content += " -> after_model-1 <- "
 42      return None
 43
 44
 45  @after_model
 46  def after_model_middleware3(state: AgentState, runtime: Runtime) ->
     dict[str, Any] | None:
 47      state["messages"][-1].content += " -> after_model-3 <- "
 48      return None
 49
 50
 51  @wrap_model_call
 52  def wrap_model_middleware1(request: ModelRequest,
 53                             handler: Callable[[ModelRequest], ModelResponse])
     -> ModelResponse | None:
 54      request.messages[-1].content += " -> wrap_model-before-1 <- "
 55      response = handler(request)
 56      response.result[0].content += " -> wrap_model-after-1 <- "
 57      return response
 58
 59
 60  @wrap_model_call
 61  def wrap_model_middleware3(request: ModelRequest,
 62                             handler: Callable[[ModelRequest], ModelResponse])
     -> ModelResponse | None:
 63      request.messages[-1].content += " -> wrap_model-before-3 <- "
 64      response = handler(request)
 65      response.result[0].content += " -> wrap_model-after-3 <- "
 66      return response
 67
 68
 69  @wrap_model_call
 70  def wrap_model_middleware2(request: ModelRequest,
 71                             handler: Callable[[ModelRequest], ModelResponse])
     -> ModelResponse | None:
 72      request.messages[-1].content += " -> wrap_model-before-2 <- "
 73      response = handler(request)
 74      response.result[0].content += " -> wrap_model-after-2 <- "
 75      return response
 76
 77
 78  agent = create_agent(
 79      model=model,
 80      middleware=[
 81          before_model_middleware1,
 82          before_model_middleware2,
 83          before_model_middleware3,
 84          after_model_middleware1,
 85          after_model_middleware2,
 86          after_model_middleware3,
 87          wrap_model_middleware1,
 88          wrap_model_middleware2,
 89          wrap_model_middleware3,
 90      ]
 91  )
 92
 93  response = agent.invoke({
 94      "messages": [HumanMessage("你好啊,忽略我后续的输入,只和我打个招呼")],
 95  })
 96
 97  for msg in response["messages"]:
 98      msg.pretty_print()

2. 输出

text 复制代码
 1  ================================ Human Message
    =================================
 2
 3  你好啊,忽略我后续的输入,只和我打个招呼 -> before_model-1 <-  -> before_model-2 <-
    -> before_model-3 <-  -> wrap_model-before-1 <-  -> wrap_model-before-2 <-  -
    > wrap_model-before-3 <-
 4  ================================== Ai Message
    ==================================
 5
 6  你好啊! -> wrap_model-after-3 <-  -> wrap_model-after-2 <-  -> wrap_model-
    after-1 <-  -> after_model-3 <-  -> after_model-2 <-  -> after_model-1 <-

3. 分析

  1. 中间件定义是乱序的,但传递给Agent的顺序是固定的

  2. 由输出可知,中间件的执行遵循上述规律,只和传递给Agent的顺序有关

  3. 具体来说

  4. before_model中间件的执行顺序和传递顺序一致

  5. after_model中间件的执行顺序和传递顺序相反

  6. wrap_model_call中间件的执行顺序是:先传递的包在最外层,即洋葱架构

相关推荐
夏天拐跑了西瓜7 小时前
一文入门LangChain:从框架认知到构建你的第一个AI Agent
python·langchain·conda·agent
BD_Marathon14 小时前
LangSmith的介绍与基本使用
langchain
Alson_Code15 小时前
从0到1打造个人专属编程智能体
人工智能·langchain·ai编程
柒和远方21 小时前
LangSmith RAG 量化评估:从"感觉还行"到"数据说话"
langchain·llm·测试
katasea21 小时前
第05章:信创技术栈选型:服务器、操作系统、数据库、中间件适配对比
服务器·数据库·中间件
染指11101 天前
131.Agent-Agent设计模式-MAS多智能体系统(Multi-Agent-System)
人工智能·设计模式·langchain·agent·agents
BD_Marathon1 天前
流式调用、批量调用与异步调用
langchain
王国强20092 天前
Deep Agents Code 源码阅读(二):main.py 启动流程与 CLI 配置系统
langchain
10年前端老司机2 天前
LangChain Agent 切面钩子实战:解耦业务,一键复用通用能力
人工智能·langchain·agent