🎯 课程主题
- 装饰器方式 vs 类方式的选择决策:不同场景下如何选型
- Hook 函数执行顺序深入解析:before / after / wrap 三类钩子的调用顺序及"洋葱模型"
📝 核心知识点
一、装饰器 vs 类方式的选择指南
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 单钩子、逻辑简单、快速原型 | 装饰器 | 代码简洁,一个装饰器即一个中间件 |
| 多钩子(同一中间件含多个钩子) | 类 | 多个方法自然聚合在一个类中,易读易维护 |
| 复杂配置(需要实例变量) | 类 | 可通过 __init__ 初始化属性,钩子方法内部直接访问;装饰器需依赖闭包 |
| 跨项目复用 | 类 | 类天然支持封装、导包、实例化、单元测试 |
装饰器方式的本质:底层为每个装饰器创建一个仅含单一钩子的 AgentMiddleware 子类实例。
二、Hook 函数执行顺序(核心)
三种类型的钩子函数按照固定的洋葱模型依次执行:
┌─────────────────────┐
│ before_agent │ ①
│ │ │
│ ┌────▼───────────┐ │
│ │ before_model │ │ ② (声明顺序: 1→2→3)
│ │ │ │ │
│ │ ┌────▼──────┐ │ │
│ │ │wrap_model │ │ │ ③ 进入: 声明顺序 1→2→3
│ │ │ call │ │ │
│ │ │ ┌───┐ │ │ │
│ │ │ │模型│ │ │ │
│ │ │ └───┘ │ │ │
│ │ │wrap_model │ │ │ ④ 退出: 逆序 3→2→1 (洋葱剥皮)
│ │ └────────────┘ │ │
│ │ after_model │ │ ⑤ 逆序 (声明顺序的反向: 3→2→1)
│ └──────────────────┘ │
│ after_agent │ ⑥
└─────────────────────┘
| 钩子类型 | 进入顺序 | 退出/执行顺序 |
|---|---|---|
before_agent | 声明顺序 | —(无退出阶段) |
before_model | 声明顺序(如 1→2→3) | —(无退出阶段) |
wrap_model_call(进入) | 声明顺序(如 1→2→3) | 逆序(如 3→2→1) |
wrap_model_call(退出) | — | 逆序(洋葱剥皮) |
after_model | — | 逆序(声明顺序的反向,如 3→2→1) |
after_agent | — | 末尾执行 |
关键结论:
before_*和wrap_*的进入阶段:按声明顺序wrap_*的退出阶段和after_*:逆序执行
三、中间件声明顺序的重要性
传递给 create_agent(middleware=[...]) 时的顺序是有意义的,决定了同类型钩子的执行先后。
🏗️ 架构与工作流
多钩子排列模式
middleware 列表 = [m1, m2, m3]
↓ ↓ ↓
before_model: m1 → m2 → m3 (正序)
handler 调用: [模型执行]
after_model: m3 → m2 → m1 (逆序)
装饰器方式下的中间件对应关系
@before_model → AgentMiddleware 子类实例 A (仅含 before_model)
@after_model → AgentMiddleware 子类实例 B (仅含 after_model)
@wrap_model_call → AgentMiddleware 子类实例 C (仅含 wrap_model_call)
middleware = [A, B, C] ← 顺序决定了执行先后
💻 代码实战
执行顺序验证代码
from langchain.agents import create_agent
from langchain.agents.middleware import (
before_model, after_model, wrap_model_call
)
from langchain_core.messages import HumanMessage
# ---- 定义测试用的多个钩子函数 ----
# before_model: 声明 3 个 (命名 1/2/3 仅标识身份)
@before_model
def bm_1(state, runtime):
print("[before_model] 1")
return None
@before_model
def bm_3(state, runtime):
print("[before_model] 3")
return None
@before_model
def bm_2(state, runtime):
print("[before_model] 2")
return None
# after_model: 声明 3 个 (乱序声明)
@after_model
def am_2(state, runtime):
print("[after_model] 2")
return None
@after_model
def am_1(state, runtime):
print("[after_model] 1")
return None
@after_model
def am_3(state, runtime):
print("[after_model] 3")
return None
# wrap_model_call: 声明 3 个
@wrap_model_call
def wm_2(request, handler):
print("[wrap_model_call] 进入 2")
response = handler(request)
print("[wrap_model_call] 退出 2")
return response
@wrap_model_call
def wm_3(request, handler):
print("[wrap_model_call] 进入 3")
response = handler(request)
print("[wrap_model_call] 退出 3")
return response
@wrap_model_call
def wm_1(request, handler):
print("[wrap_model_call] 进入 1")
response = handler(request)
print("[wrap_model_call] 退出 1")
return response
# ---- 创建 Agent ----
# 注意:middleware 列表的排列顺序即为 before/wrap 进入顺序
agent = create_agent(
model=model,
middleware=[
bm_1, bm_3, bm_2, # before_model 组: 1→3→2
am_2, am_1, am_3, # after_model 组: 2→1→3 声明
wm_2, wm_3, wm_1, # wrap_model_call 组: 2→3→1 声明
]
)
# ---- 执行 ----
response = agent.invoke({"messages": [HumanMessage(content="你好")]})
实际输出分析
[before_model] 1 ← before 按声明顺序: 1→3→2
[before_model] 3
[before_model] 2
[wrap_model_call] 进入 2 ← wrap 进入按声明顺序: 2→3→1
[wrap_model_call] 进入 3
[wrap_model_call] 进入 1
[模型实际调用...]
[wrap_model_call] 退出 1 ← wrap 退出逆序: 1→3→2 (洋葱剥皮)
[wrap_model_call] 退出 3
[wrap_model_call] 退出 2
[after_model] 3 ← after 逆序(声明2,1,3 → 执行3,1,2)
[after_model] 1
[after_model] 2
闭包 vs 类方式(配置传递对比)
# ❌ 装饰器 + 闭包方式:需要外层函数捕获配置
def create_counter_middleware(initial_count: int):
"""通过闭包捕获 initial_count"""
count = [initial_count] # 用列表实现可变引用
@before_model
def counter(state, runtime):
count[0] += 1
print(f"计数: {count[0]}")
return None
return counter
# ✅ 类方式:通过实例属性管理配置(更直观)
class CounterMiddleware(AgentMiddleware):
def __init__(self, initial_count: int = 0):
self.count = initial_count
def before_model(self, state, runtime):
self.count += 1
print(f"计数: {self.count}")
return None
# 类方式天然支持 __init__ 配置,且属性可通过对象访问
mw = CounterMiddleware(initial_count=10)
print(mw.count) # 可读取
⚠️ 常见问题与避坑指南
| 问题 | 说明 |
|---|---|
| after 顺序与预期相反 | after_model / after_agent 是逆序执行的,不要误以为是声明顺序 |
| wrap 的洋葱模型 | 进入和退出顺序相反,理解"剥洋葱":先进入的最后退出 |
| 闭包中可变变量陷阱 | 闭包方式传递配置时,注意 Python 闭包对不可变类型的处理(需用列表等可变容器) |
| middleware 顺序随意 | 声明时虽然可以乱序,但传入 middleware 列表的顺序决定了执行先后 |
| 装饰器方式多钩子可读性差 | 用解包/闭包强行在一个装饰器函数里塞多个钩子逻辑虽然可行,但不推荐 |
| 跨项目复用时不要用闭包 | 闭包无法方便地 import、实例化、单元测试;类方式天然支持这些 |
💡 个人总结与延伸
- 选择决策一句话:单钩子用装饰器,多钩子/复杂配置/跨项目复用用类
- 执行顺序记忆法:
before/wrap-进入是"先来先服务"(FIFO),wrap-退出/after是"后来先服务"(LIFO) - 洋葱模型在中间件领域是通用模式(Django Middleware、Koa、Express、ASP.NET Core 都是如此),LangChain Agent 中间件也不例外
- 装饰器底层 = 类方式:这是框架设计的优雅之处——给开发者提供便捷语法糖,同时保持底层实现的一致性
- 理解执行顺序对调试至关重要:当多个中间件相互影响时,错误的顺序假设会导致难以排查的 bug