1. 中间件概述
在create_agent( )的底层运行机制中,有几个重要的组件,分别是:
- 模型(Model):Agent 的“大脑”,负责理解任务与决策推理
- 工具(Tools):Agent 的“手脚”,执行模型自己做不到的外部操作
- 系统提示词(System Prompt):Agent 的 “角色”,告诉模型该怎么想、参考什么上下文
- 中间件(Middleware):Agent 的 “中枢”,在执行流程的关键节点进行拦截、控制和增强
语法如下:
1.1 什么是中间件?
Middleware(中间件):是 Agent 执行过程中的钩子函数
钩子:框架或系统在某些关键执行点暴露的扩展接口;开发者可以“挂上”自己的逻辑,在那些点上插入、修改或替换行为,而无需改变主流程代码;就像在流水线上某个环节设置了一个“检查点”或 “插入器”
借助中间件,开发者可以高度定制和控制 Agent 运行的每一个环节,是处理 Agent 生命周期的标准方式
没有中间件的 agent 框架:
添加中间件之后的Agent架构:
在 LangChain 的 Agent 执行循环中,比如 “模型调用前”、“模型调用后”、“工具调用前后” 设置一些钩子 (hooks),可以在不改 Agent 主体逻辑的情况下实现策略与治理
1.2 为什么需要中间件?
如果没有中间件,Agent 的执行流程通常比较直接:
用户输入 → 拼接提示词/消息 → 调用模型 → 如有需要调用工具 → 返回结果这种方式只适用于简单需求,一旦进入真实项目,往往会遇到很多额外需求,这些需求有一个共同特点:它们不是 Agent 的核心业务逻辑,但又会影响 Agent 的执行过程;这些逻辑又不能全部直接写进主流程,会造成主程序混乱,代码难以复用等问题
中间件的价值:在于把这些与业务无关、但与执行过程强相关的横切逻辑,从 Agent 主流程中分离出来。让 Agent 主体代码聚焦业务 ,而借助中间件,实现“拦截流程、修改流程、增强流程”
目前,LangChain 1.x 的中间件能实现如下功能:
- ✅ 日志与分析 - 追踪行为、调试、性能监控
- ✅ 转换 - 修改提示词、工具选择、输出格式
- ✅ 容错 - 重试、降级、早期终止
- ✅ 安* - 限流、守护规则、PII 检测
1.3 中间件的分类
分为:
- 自定义中间件:允许开发者自定义,从而实现更加灵活的 Agent 行为管理
- 内置中间件:LangChain 实现并提供的
- 模型供应商定制的中间件:依赖于特定模型服务的实现
- 和模型供应商无关的中间件:LangChain提供的与供应商无关的中间件,如下
链接:Middleware overview - Docs by LangChain
1.4 和模型供应商无关的内置中间件分类
LangChain 提供的和模型供应商无关的内置中间件分为六个类别
类型1:成本与资源控制类
核心目标:控成本、控配额、避免无限调用
解决问题:“ Agent太贵、太能跑、停不下来 ” 的问题
包含:
- Model call limit:限制模型调用次数,防止一次任务反复请求 LLM,导致费用失控
- Tool call limit:限制工具调用次数,避免 Agent 无限试错、死循环调工具
- Summarization:在上下文快满时自动总结历史,减少 token 消耗
- Context editing:裁剪上下文、清理工具调用痕迹,本质上也是为了节省上下文成本
业务场景理解:
适合生产环境的成本治理、配额治理、长会话优化、SaaS 产品控费
类型2:稳定性与容错保障类
核心目标:保证服务不中断、失败后尽量自动恢复
解决问题:“ 调用失败怎么办、模型挂了怎么办、工具超时怎么办 ”
包含:
- Model fallback:主模型失败时切换备用模型
- Model retry:模型调用失败后自动重试
- Tool retry:工具调用失败后自动重试
业务场景理解: 适合线上生产系统,尤其是多模型、多工具依赖的 Agent
本质上是在做高可用、容灾、鲁棒性建设
类型3:安全与合规风控类
核心目标:让 Agent 可控、可审、合规
包含:
- Human-in-the-loop(人在环):在关键工具调用前暂停,等人工审批
- PII detection:检测和处理个人敏感信息
业务场景理解:
适合企业内部系统、客服系统、审批流、数据查询类 Agent。
尤其是涉及:发邮件、调数据库、调财务/人事系统、导出敏感信息、执行外部动作等
类型4:决策增强与智能编排类
核心目标:提升 Agent 的决策质量和任务拆解能力
解决问题:“Agent不够聪明、不会规划、不会筛选工具” 的问题
包含:
- To-do list:给 Agent 增加任务规划、分步骤执行和状态跟踪能力
- LLM tool selector:当工具太多时,用子模型筛选最相关的几个工具交给主模型
- Subagent:允许生成子Agent,把复杂任务拆给不同角色处理
业务场景理解:
适合复杂任务流,比如:研究型 Agent、多步骤分析、报告生成、多角色协作、长链路任务编排等
这类本质上是在增强 Agent的“脑子”与“组织能力”
类型5:执行能力扩展类
核心目标:给 Agent 更多“手脚”
解决问题:“Agent 只能聊天,不能真正操作环境”的问题
包含:
- Shell tool:给 Agent 持久 shell,会执行命令
- File search:给 Agent 文件搜索能力,能做 Glob/Grep
- Filesystem:给 Agent 文件系统读写与长期存储能力
业务场景理解:
适合工程 Agent、代码 Agent、本地自动化 Agent、运维 Agent
本质上是把 Agent 从“纯推理”扩展成“能操作环境的执行体”
类型6:开发调试与测试辅助类
核心目标:方便开发、测试、验证 Agent 行为
这类中间件主要不是直接服务业务,而是服务于研发和调试阶段
包含:
- LLM tool emulator:用 LLM 模拟工具执行,便于测试(最典型)
- Summarization:有时也可辅助调试长会话表现
- Context editing:可用于测试上下文裁剪效果
- Human-in-the-loop:也常用于调试高风险步骤
业务场景理解:
适合开发阶段快速验证流程、做 mock、减少真实工具依赖
2. 常用内置中间件的使用
2.1 SummarizationMiddleware 中间件
作用:对历史消息列表进行摘要&总结,达到压缩上下文的效果
原理:在达到触发条件时,调用大模型对历史消息进行摘要,将摘要的结果作为HumanMessage, 放到消息列表最开始的位置
参数说明如下:
参数1:model — 用于生成摘要的模型
可以是模型名称也可以是模型对象,如果传递的是模型名称,底层会调用init_chat_model初始化模型
参数2:trigger — 摘要触发条件
是一个列表,每个元素对应一个条件,当任一条件满足时,触发摘要:
- tokens:token 的数量,历史 token 的累计数量达到该值触发摘要
- messages:历史消息数量,历史消息条数达到该值触发摘要
- fraction:上下文长度比例,历史 token 的累计数量达到模型的 max_input_tokens*fraction 触发摘要
注意:如果条件包含 fraction ,要求模型的 profile 包含max_input_tokens
参数3:keep — 摘要时保留的原始消息
支持三种条件,但和 trigger 不同,keep同一时间只接收一种条件
- tokens:摘要时保留的token数量
- messages:摘要时保留的历史消息条数
- fraction:摘要时保留 max_input_tokens*fraction 个 token
参数4:token_counter — 统计token数量的函数
默认使用 LangChain 提供的 count_tokens_approximately ,一般不用更改
参数5:summary_prompt — 摘要时的自定义提示词
该提示词需要包含{messages} 占位符,使得历史消息列表可以被插入;不指定则使用内置提示词
参数6:trim_token_to_summarize — 摘要时历史消息的最大token数
如果历史消息 token 数大于该值,则会被裁剪;默认为" 4000 "
如果 trigger 用 token 作为度量,调大触发阈值时,当前配置项应相应调整,否则会丢失信息
案例1:
输出结果如下:
案例2:测试 summary_prompt 参数
输出结果如下:
2.2 HumanInTheLoopMiddleware 中间件
HumanInTheLoopMiddleware(人在环中间件、人工审核中间件):在工具调用前中断Agent运行,等待用户对工具调用请求决策:
- approve(同意执行)
- edit(编辑调用配置后执行)
- reject(拒绝执行)
参数1:interrupt_on — 工具名和对应的中断策略
策略可以是True、False或InterruptOnConfig对象,精细控制决策选项
1. True 表示需要中段,其所有决策 (approve, edit, reject) 都可以选择
2. False 表示不中断,即无需审批即可执行
3. InterruptOnConfig 是一个TypedDict 的子类,可以用字典直接赋值;支持的Key有:
- allowed_decisions:精细控制中断后允许的决策
- description:特定工具的中断描述信息,优先级高于 description_prefix,后者会更改所有工具中断的描述
参数2:description_prefix — 自定义中断描述
默认为 "Tool execution requires approval"
案例:
注意:本例需要从中断的位置让 Agent 继续运行,这就需要用到短期记忆,这里先使用即可
创建 Agent 时:通过checkpointer参数启用短期记忆
调用 Agent 时:通过传递相同的config加载记忆
结果大致如下:
案例2:指明工具调用请求决策
最终大致输出结果如下:
2.3 PIIMiddleware 中间件
PII 中间件用于检测和处理对话中的个人身份信息(Personally Identifiable Information,PII),支持自定义处理策略
参数说明如下:
参数1:pii_type — 检测的 PII 数据类型
可以是内置类型或自定义类型,自定义类型有
- email:电子邮箱地址
- credit_card:信用卡号
- url:网址
- mac_address:设备MAC地址
- ip:IP地址
参数2:strategy — 处理PII信息的策略
支持四种选项:
- redact:将检测到的 PII 信息用字符串 [REDACTED_[PII_TYPE]] 替换,其中的 PII_TYPE是上面提到的具体类型,比如 [REDACTED_EMAIL] 、 [REDACTED_CREDIT_CARD] 这样的标签。完全 “擦除/隐藏” 真实内容;适合日志清洗、合规需求、公开输出时隐藏敏感内容
- mask:用 *** 将 PII 信息的前面一部分信息遮蔽;比如信用卡号可能变成 ****-****- ****-1234 (只保留最后几位/部分可见),邮箱可能保留域名部分 + 隐藏用户名的一部分等 — 既隐藏大部分敏感信息,又保留了一点“可辨识性”(比如账号后四位、域名等),适合用户服务界面 / 前端显示 / 需要部分可识别但不泄露完整敏感内容的场景
- hash:用检测到的 PII 信息的哈希值替代原值;比如:<email_hash:a1b2c3d4>,适合 analytics、调试 (debug)、统计分析、匿名追踪等场景
- block:如果检测到 PII 信息, 直接抛出异常。适合对隐私要求极高、绝不允许泄露任何敏感信息的场景
参数3:detector — 自定义PII检测函数或者正则表达式
如果没有提供则使用内置的检测函数(一般不用手动修改)
LangChain 为每种 PII 信息定制了专门的检测函数,相关源码如下:
参数4:apply_to_input — 是否在调用模型前检测
默认为 True
参数5:apply_to_output — 是否在模型调用后检测
默认为 False
参数6:apply_to_tool_results — 是否在工具调用后检测其输出
默认为False
通常只在模型调用前检测;因为 PII 检测的主要目的是避免将敏感信息发送给模型服务导致信息泄露
案例如下:使用内置检测器
输出内容如下:
案例:自定义检测器 / 检测函数
接着:
输出结果如下:
2.4 TodoListMiddleware中间件
TodoListMiddleware 中间件赋予了 Agent任务规划和追踪进度的能力,可以应对复杂的多步任务
典型应用场景:
- 任务链路长、步骤多,且有严格的先后依赖关系
- 需要在前端 UI 界面实时展示 Agent 的 “思考与执行进度”
To-do list 的创建和维护是通过调用 write_todos 工具实现的
参数说明如下:
1. system_prompt — 自定义指导 todo 列表使用的提示词
不提供则使用内置提示词,通常不必提供
2. tool_description — 自定义 write_tools 工具的描述信息
不提供则使用内置描述,通常不必提供
案例:
设计一个较复杂任务:
1. 任务目标
扫描工作目录,测试并修复工作区下的my_add.py文件
2. 工具列表
为了实现上述任务,提供一系列工具:
- list_files:扫描工作目录,列出其中的所有文件
- read_file:扫描指定文件,返回文件内容
- write_file:向指定文件写入内容
- run_tests:运行测试,底层基于 pytest 实现
3.代码
3.1 环境准备
在项目根目录下创建todo_workspace作为工作空间
在该目录下创建my_add.py,写入以下内容:
在该目录下创建test_my_add.py,写入以下内容:
测试工具是基于 pytest 实现的
pytest 用法如下:
1.在PyCharm中打开终端,依次执行命令:切换到 todo_workspace 的工作目录
2. 然后执行:
3. 输出结果如下:
分析:
- pytest 会扫描目录下所有以 test_ 开头或以 _test 结尾的文件,视为测试文件
- 然后执行测试文件中所有以 test 开头的函数
- 执行出错会打印到控制台,如上所示
- 测试函数的逻辑是调用 my_add.py 中的 add 函数,得不到符合预期的结果则抛出异常
3.2 业务代码
# 模型初始化 from langchain.chat_models import init_chat_model from dotenv import load_dotenv import os from rich import print as rprint from langchain_core.messages import SystemMessage, HumanMessage, AIMessage # 1.加载配置信息 load_dotenv(override=True) api_key = os.getenv("BAILIAN_API_KEY") base_url = os.getenv("BAILIAN_API_BASE") # 2.初始化模型 llm = init_chat_model( model_provider="openai", # 模型提供商 model="deepseek-v4.1-flash", # 模型名称 api_key=api_key, # API Key base_url=base_url, # API Base URL ) # 提供工具列表 from langchain.tools import tool from pathlib import Path import subprocess WORKSPACE = Path("../todo_workspace") @tool def list_files(path: str = ".") -> str: """ 列出工作区指定目录下的文件和子目录, path 只能是相对路径 Args: path: 工作区下的相对路径,一定指向目录,默认为., 表示工作区根路径, 不能访问工作区外的目录 """ target = (WORKSPACE / path).resolve() workspace_root = WORKSPACE.resolve() if not str(target).startswith(str(workspace_root)): return "错误:只允许访问工作区内的目录。" if not target.exists(): return f"错误:目录不存在: {path}" if not target.is_dir(): return f"错误:不是目录: {path}" items = sorted(target.iterdir(), key=lambda p: (p.is_file(),p.name.lower())) if not items: return f"目录为空: {path}" lines = [] for item in items: rel = item.relative_to(workspace_root) kind = "[DIR]" if item.is_dir() else "[FILE]" lines.append(f"{kind} {rel.as_posix()}") return "\n".join(lines) @tool def read_file(path: str) -> str: """ 读取工作区中的文本文件内容。path 只能是相对路径。 Args: path: 工作区内的文件名 """ file_path = (WORKSPACE / path).resolve() if not str(file_path).startswith(str(WORKSPACE.resolve())): return "错误:只允许读取工作区内的文件。" if not file_path.exists(): return f"错误:文件不存在: {path}" return file_path.read_text(encoding="utf-8") @tool def write_file(path: str, content: str) -> str: """ 写入工作区中的文本文件。path 只能是相对路径 Args: path: 工作区内的文件名 content: 写入文件的内容 """ file_path = (WORKSPACE / path).resolve() if not str(file_path).startswith(str(WORKSPACE.resolve())): return "错误:只允许写入工作区内的文件。" file_path.write_text(content, encoding="utf-8") return f"已写入文件: {path}" @tool def run_tests() -> str: """ 在工作区运行 pytest -q,并返回输出。不接收任何参数,返回格式为 returncode=0|1 STDOUT: STDERR: """ try: result = subprocess.run( ["pytest", "-q"], cwd=str(WORKSPACE), capture_output=True, text=True, timeout=20, ) return ( f"returncode={result.returncode}\n\n" f"STDOUT:\n{result.stdout}\n\n" f"STDERR:\n{result.stderr}" ) except Exception as e: return f"运行测试失败: {e}" from langchain.agents import create_agent from langchain.agents.middleware import TodoListMiddleware from langchain.messages import HumanMessage from rich import print as rprint # 1. 初始化 Agent agent = create_agent( model=llm, # write_todos 等工具,TodoListMiddleware 需要配合这些工具使用 tools=[list_files, read_file, write_file, run_tests], # 引入 Todo 列表中间件 middleware=[TodoListMiddleware()], system_prompt=( "你是一个代码修复助手。遇到多步骤任务时,先使用 write_todos 制定待办事项;" "然后读取文件、修复代码并运行测试。工作全部在工作区下进行。" ), ) # 2. 使用invoke进行同步调用 print("正在执行 Agent 任务...") final_state = agent.invoke( { "messages": [ HumanMessage(content="请测试并修复工作区下 my_add.py 文件中的代码") ] } ) rprint(final_state)结果如下:
3. 其他内置中间件
3.1 ModelCallLimitMiddleware中间件
限制模型调用次数,避免无限循环,控制调用成本
案例:整个会话限制——优雅退出
案例:整个会话限制——抛异常
案例:单次调用限制——优雅退出
案例:单次调用限制——抛异常
3.2 ToolCallLimitMiddleware中间件
限制工具调用次数,可以限制所有工具调用的总次数,也可以限制特定工具的调用次数
退出行为有三种模式:
- error:直接抛异常
- end:结束整个会话
- continue:继续运行 Agent,这是默认行为,此时 Agent 会将工具调用超出限制的信息传递给模型,后者自主决定后续行为,如果模型能力不足,可能导致死循环,为了避免这种情况,我们实现的 fake server 会以 20% 的概率输出正确响应,从而能终止循环
案例:整个会话限制——优雅结束
案例:整个会话限制——抛异常
案例:单次调用限制——继续运行
3.3 ModelFallbackMiddleware中间件
用于故障转移,当主模型无法访问时,启用备用模型
案例:模型初始化时定义备用模型
案例:创建 agent 时定义备用模型
3.4 LLMToolSelectorMiddleware中间件
智能工具筛选: 当工具太多时,用子模型筛选最相关的几个工具
参数:
- model:用于工具筛选的子模型
- max_tools:限定可以调用的工具总数
- always_include:指定的工具不被计数
案例1:
案例2:
3.5 ToolRetryMiddleware中间件
基于指数退避算法,设置工具调用失败时的重试策略
3.6 ModelRetryMiddleware中间件
模型调用失败时重试,策略和工具调用的重试一样,都是基于指数退避算法
案例:继续运行
案例:抛异常
3.7 LLMToolEmulator中间件
某些情况下,工具尚未开发完成,我们希望先测试工具调用,可以用LLM tool emulator模拟工具
案例:
3.8 ContextEditingMiddleware中间件
上下文编辑中间件,该中间件提供了上下文管理的一种方式;通过更改发送给模型的消息列表来控制成本
案例:
3.9 FilesystemFileSearchMiddleware中间件
基于系统的 Glob 和 Grep 检索工具,为 Agent 赋予本地文件搜索和分析的能力:
- Glob 根据文件路径检索
- Grep 根据文件内容检索
3.10 Shell tool中间件
为 Agent 提供一个可以执行命令的 Shell 环境
3.11 Filesystem中间件
这是源自 deepagents(基于LangChain的另一个框架)的中间件,内置了四个工具,分别用于查看目录、读文件、写文件和改文件
4. 多个中间件组合及执行顺序
Middleware 可以叠加使用,其中中间件的书写顺序非常重要
案例:
5. 自定义中间件
某些复杂场景下,官方内置的中间件不能完全满足需求,此时可以通过实现 LangChain 暴露的中间件hook 函数构建自定义中间件
注意:尽量使用内置中间件
5.1 什么是 hook 函数(钩子函数)
hook 函数(钩子函数):在某个既定流程的特定时机,被框架、系统或主程序自动调用的扩展函数
核心特点:
- 不是主动在业务代码里随便调用的,而是当流程运行到某个“钩子点”时,系统自动触发
- 依附于一个更大的执行流程;比如“请求开始前”“模型调用前”“任务结束后”“异常发生时”等
- 作用是在不改主流程源码的前提下插入自己的逻辑。例如做日志 、 鉴权 、 修改输入 、 拦截输出 、 清理资源等
LangChain 的中间件作用在 Agent 架构中,后者是基于 LangGraph 构建的流程图
如下列出了六个hook 函数(钩子函数):
无论是官方内置中间件、自定义中间件、还是便捷装饰器中间件,通常都是通过实现其中的 一个或多个 hook 来生效的
5.2 LangChain 的 hook 函数分类
官方将六个钩子函数按照风格分为两类
类型1:Node-style hooks(节点风格钩子)
它们在流程的特定节点运行,适合顺序逻辑,如记录日志、验证
包括
- before_agent:在Agent开始运行之前执行
- before_model:在模型调用之前执行
- after_model:在模型调用之后执行
- after_agent:在Agent流程全部完成后执行
类型2:Wrap-style hooks(包装风格钩子)
它们在模型或工具调用前后运行,适合控制流,如重试、回退、缓存
包括
- wrap_model_call (包裹模型调用)
- wrap_tool_call (包裹工具调用)
5.3 Node-style hooks函数用法
支持两种用法
- 装饰器是函数式挂载,把一个hook快速挂载到Agent的某个节点
- 类写法是对象化中间件,把中间件封装为一个可配置、可复用、可扩展的组件
5.3.1 基本用法
1.基于装饰器实现
输出结果如下:
2.基于类实现
关键规则:
1.必须继承 AgentMiddleware← 这个固定
2.方法名固定 ( before_model , after_model )← 这个固定
3.类名随意← 这个不固定
案例:
输出如下:
1. before_model 通常的场景:
- 消息修剪(trim messages)
- PII 脱敏
- 输入验证
- 条件路由
2. after_model 通常的场景:
- 输出验证
- 格式化响应
- 统计信息
- 状态更新
5.3.2 两种方法的统一
装饰器是一种便捷的写法,其底层会基于我们重写的方法构造一个 AgentMiddleware 子类的实例
5.3.3 参数说明
Node-style hooks 函数有两个参数
- state:是一个 AgentState 实例,维护 Agent 运行过程中的状态,这类状态会随着 Agent 的运行而发生变化,包括消息列表
- runtime:是一个 Runtime 实例,维护 Agent 运行过程中的上下文环境,包括上下文 、 长期记忆等
5.3.4 返回值说明
返回 None:不修改状态(不修改Agent状态)
返回字典:更新状态
返回 {"jump_to": "..."}:控制流程
jump_to 目标:
- "__end__" :结束 Agent
- "tools" :跳到工具节点
- 其他自定义节点
5.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. 基于装饰器实现
2. 基于类实现
和基于装饰器实现的关键区别在于:需要引入额外的装饰器@hook_config为can_jump_to传参
5.4 Wrap-style hooks 函数用法
5.4.1 基本用法
1. wrap_model_call
① 基于装饰器实现
同时在模型调用前后做事,所以命名为 wrap_model_call ,wrap 意为包裹
案例:
② 基于类实现
2. wrap_tool_call
同时在工具调用前后做事,所以命名为 wrap_tool_call
① 基于装饰器实现
② 基于类实现
5.4.2 两种方式的统一
装饰器方法底层也会创建一个 AgentMiddleware 的实例
5.4.3 参数说明
- request:被封装的请求对象,可以是模型或工具调用请求
- handler:处理器,用于处理请求并返回调用结果
5.5 装饰器和类的选择
情况1:中间件只用一个钩子函数,推荐用装饰器,需要多个钩子函数推荐类写法
- 当一个中间件只需要实现一个钩子函数时,直接使用装饰器最简单
- 当一个中间件需要实现多个钩子函数时,类写法更合适
即:
- 装饰器写法适合把单个 hook 快速挂到 agent 生命周期的某个节点上
- 类写法更适合把多个 hook 组织为一个完整的中间件组件
- 当中间件同时涉及 before_model 、 after_model 等多个钩子时,虽然装饰器工厂也能实现,但类写法在结构表达、配置归属、可维护性上更好
情况2:复杂配置推荐用类实现
装饰器当然也可以通过函数闭包传递参数,但在自省(运行时类型校验)、调试等方面天然不如类写法方便
情况3:跨项目复用推荐用类写法
如果希望中间件成为一个可实例化、可封装、可测试的组件,类写法更加合适,因为这些本就是类擅长 的场景,装饰器的闭包也能实现,但使用不友好
总结如下:
装饰器写法和类写法都能实现 middleware hook,本质上只是两种定义中间件的方式,并不是能力上完全割裂的两套机制,底层实现是统一的
一般来说:
- 装饰器写法更适合单个 hook、逻辑简单、快速原型的场景
- 类写法更适合多个 hook 组合、复杂配置、需要同时提供同步 / 异步实现、以及更强复用与可测试性的场景
5.6 hook函数执行顺序
- before_* 钩子函数:从前到后执行
- after_* 钩子函数:从后往前执行
- wrap_* 钩子函数:洋葱架构,前面的包裹后面的,外层的先执行
这里的顺序并非定义顺序,而是创建Agent时传递中间件的顺序
案例: