news 2026/10/16 3:06:26

LangChain 第7章 中间件与钩子函数:16 个官方中间件、洋葱顺序与 6 个 Hook

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangChain 第7章 中间件与钩子函数:16 个官方中间件、洋葱顺序与 6 个 Hook

摘要:本文系统梳理 LangChain 1.x 的中间件与钩子函数机制,涵盖 16 个官方中间件、洋葱模型执行顺序、6 个钩子函数(4 个节点风格 + 2 个包装风格),并通过自定义中间件实战、常见坑位排查、面试高频考点与课后练习,帮助读者从原理到生产级落地掌握这一可插拔扩展层。

目录

  • 一、本章定位
  • 二、常用官方中间件
  • 三、两个重点中间件实操
  • 四、多个中间件的洋葱执行顺序
  • 五、钩子函数 Hook
  • 六、知识点速查
  • 深度拓展一:核心原理与源码解读
  • 深度拓展二:代码实战——从最小可用到生产级
  • 深度拓展三:常见坑与调试技巧
  • 深度拓展四:最佳实践与性能优化
  • 深度拓展五:面试高频考点
  • 深度拓展六:课后练习与思考
  • 补充一:中间件与上下文裁剪的实战配合
  • 补充二:怎么测试一个中间件
  • 补充三:排查"中间件好像没生效"的三板斧
  • 补充四:中间件设计的三条经验法则
  • 补充五:常见 FAQ
  • 补充六:本章小结
  • 补充七:动手清单

LangChain 第7章 中间件与钩子函数:16 个官方中间件、洋葱顺序与 6 个 Hook

一句话总结:中间件是 Agent 的可插拔扩展层,在不改主流程源码的前提下插入裁剪、限流、兜底、人工审核等能力;多个中间件按洋葱模型执行,自定义中间件靠实现 6 个钩子函数。

📎 参考资料一:李沐深度学习191集课程全解析(模块拆解、学习路径)

李沐深度学习191集课程全解析:模块拆解、学习路径-CSDN博客

📎 参考资料二:吴恩达《面向开发者的提示词工程》

吴恩达《面向开发者的提示词工程》-CSDN博客

📎 参考资料三:吴恩达 MCP 教程(Model Context Protocol)

吴恩达 MCP 教程(Model Context Protocol)(一)-CSDN博客

📎 参考资料四:多模态大模型教程学习笔记(ViT · CLIP · SAM · GLIP · Stable Diffusion)

多模态大模型教程学习笔记 — ViT · CLIP · SAM · GLIP · Stable Diffusion

本章可视化图谱

📎 知识图谱在线链接(点击打开 / 一键复制 / 分享他人):

https://mcnj5s80gkfh.doubaoapps.com/app/app_17fqh4pbny0

一、本章定位

第 64–85 集共 22 集,是全课最硬核、最容易断片的分水岭,建议留一整段时间啃。LangChain 1.x 提供 16 个官方中间件(第二节 4 个 + 第三节 12 个),优先用现成的,满足不了再自定义;自定义中间件本质上就是实现一个或多个钩子函数。

二、常用官方中间件

中间件

作用

ToolCallLimitMiddleware(70)

限制工具调用次数,防止 Agent 无限循环烧 token

ModelFallbackMiddleware(71)

主模型失败时自动兜底切换到备用模型

LLMToolSelectorMiddleware(72)

由一个 LLM 先筛选工具,缩小候选、减少选错

SummarizationMiddleware

历史消息超长时自动摘要压缩

HumanInTheLoopMiddleware

敏感工具调用前中断,交人工审核确认

PIIRedactionMiddleware

对手机号、身份证等个人敏感信息脱敏

ContextEditingMiddleware(76)

上下文裁剪,控制 token 与成本

FilesystemFileSearchMiddleware(77)

赋予 Agent 本地文件 glob/grep 搜索分析能力

另有代码执行 / Shell 类中间件需要可执行命令的环境(Windows 下不便测试),以及与 deepagents 深度耦合的中间件(如待办、子智能体相关)。

三、两个重点中间件实操

ContextEditingMiddleware 上下文编辑

  • 目的:多轮对话历史随调用越滚越大、token 成本越来越高,用它给上下文“抽脂”。

  • 关键参数:trigger 触发编辑的 token 阈值;keep 控制保留条数(如 keep=0 表示工具消息一条不留全清),编辑工具可迭代配置多个。

  • 观察方式:用不用它,消息列表条数不变;要看每条 AIMessage 的 usage_metadata 里 input_tokens 的增长幅度——有裁剪的对照组增长明显更慢。

  • 常配合 InMemorySaver(短期记忆)在同一会话中维持连续性。

FilesystemFileSearchMiddleware 文件搜索

  • 基于系统 glob(按文件路径检索)与 grep(按文件内容检索),挂上中间件后工具会被自动注入,无需手动传 tools。

  • 参数:paths 检索路径、文件格式过滤、use_ripgrep(用更快的 ripgrep 引擎,前提是本机已装)、max_file_size_mb(单文件读入内存上限,示例 10MB;太大易 OOM,太小交互频繁、性能下降)。

四、多个中间件的洋葱执行顺序

middleware 参数是列表,可写多个且有顺序要求(例如先修剪消息、再做摘要、最后记日志)。执行顺序像剥洋葱:按列表 1、2、3 书写时,进入阶段的 before 钩子按 1→2→3 执行,核心调用后出来阶段的 after 钩子按 3→2→1 执行。顺序写错,裁剪和摘要就会互相打架。

五、钩子函数 Hook

钩子函数是在流程特定时机被框架自动调用的扩展函数:不是业务代码主动调用,而是在请求开始前、模型调用前后、任务结束后等时机自动触发;如同生产车间在主流程外预留的插槽,挂参数校验、日志、质检等逻辑,从而在不改主流程源码的前提下扩展能力。

分类

钩子

时机

节点风格 Node(4 个)

before_agent / before_model

Agent / 模型执行前

节点风格 Node(4 个)

after_model / after_agent

模型 / Agent 执行后

包装风格 Wrap(2 个)

wrap_model_call

包裹模型调用前后

包装风格 Wrap(2 个)

wrap_tool_call

包裹工具调用前后

  • 第 80 集:基于装饰器定义 Node-style 钩子;第 81 集:基于类定义;第 82 集:装饰器参数 can_jump_to 的使用(控制流程跳转)。

  • 无论官方中间件、自定义中间件还是装饰器中间件,通常都是通过实现其中一个或多个钩子生效。

第 64–69、73–75、83–85 集本地字幕未导出,本节据课程集目录与 LangChain 1.x 官方体系整理,涵盖摘要、人工审核、PII 脱敏等中间件;具体类名与参数以课件/视频为准。

六、知识点速查

主题

要点

数量

官方 16 个中间件,优先用现成

控成本

ContextEditing:trigger 阈值 + keep 保留,看 usage_metadata

文件搜索

glob 路径 / grep 内容,ripgrep 更快,max_file_size 防 OOM

执行顺序

洋葱模型:进入 123、出来 321

6 个钩子

节点 4(before/after × agent/model)+ 包装 2(wrap model/tool)

定义方式

装饰器(80)、类(81)、can_jump_to(82)

深度拓展:本章完整知识体系与实战进阶

上一章我们拆解了 create_agent 这个统一入口的内部循环。但一个真正能上生产的 Agent,光有"模型—工具"主干是不够的:你还需要在调用模型之前动态注入当前时间、在工具结果过长时做裁剪、在执行危险动作前找人确认、在出错时自动重试。这些横切关注点(cross-cutting concerns)如果全部硬编码进系统提示词或者塞进每个工具函数里,代码会迅速变成一团无法维护的面条。LangChain 1.0 给出的答案就是中间件(middleware)机制。本章深度拓展将从设计思想讲起:什么是洋葱模型、create_agent(middleware=[...]) 的执行顺序、官方中间件分哪几类、自定义中间件要实现哪些钩子、Hook 和 Middleware 到底是什么关系,以及为什么"用中间件"比"在 prompt 里写死规则"工程上更优。

深度拓展一:核心原理与源码解读

1.1 为什么需要中间件:横切关注点与"面条提示词"之痛

设想你要做一个企业问答 Agent。需求清单可能是这样:每次回答前要把"今天是某年某月某日"写进系统提示;工具返回的网页摘要动辄上万字,要裁剪到 500 字以内;执行"删除订单"这种工具前必须找人工确认;模型调用失败要自动重试两次;所有请求要记录日志。如果把这些规则一条条写进系统提示词,会出现两个问题:第一,提示词越来越长,模型注意力被稀释;第二,这些规则本质是"代码逻辑"(裁剪、重试、确认、打日志),根本不是语言模型能稳定执行的,写进 prompt 等于把工程责任甩给一个概率系统。

中间件的思路是:把这些横切关注点从主干流程里抽出来,做成可以层层包裹的"过滤器",按统一顺序挂在 Agent 上。主干逻辑只负责"调模型、跑工具",其余全部由中间件在旁边拦一道。

1.2 洋葱模型:请求从外到内,响应从内到外

中间件最核心的心智模型是洋葱模型。你把 middleware=[A, B, C] 传给 create_agent,整个调用就像穿过一个三层洋葱:请求进来时,先经过 A,再经过 B,再经过 C,然后才到达最核心的"调用模型/执行工具";当结果返回时,方向正好反过来——先经过 C 的出口处理,再经过 B 的出口处理,再经过 A 的出口处理,最后才交给你。换句话说,每个中间件都有机会在"动作发生前"和"动作发生后"各做一次手脚。

层次

请求方向(外→内)

响应方向(内→外)

最外层 A

进入前:打日志、计时开始

返回后:结束计时、记日志

中间层 B

进入前:裁剪过长上下文

返回后:对输出做格式化

最内层 C

进入前:注入当前时间

返回后:—

核心

真正调用模型 / 执行工具

产生原始结果

这个模型带来两个关键性质:第一,顺序是显式的——列表里越靠前的中间件越靠外,越先拦截请求、越后处理响应;第二,每个中间件是独立可插拔的——你可以单独替换 A 或 B,而不必动主干。

1.3 create_agent(middleware=[...]) 的执行顺序

在 1.0 里,中间件通过 create_agent 的 middleware 参数注入。它不是只在"整个 Agent 调用"外面包一层,而是会包裹住循环里的两类关键动作:调用模型(model call)和执行工具(tool call)。也就是说,在 ReAct 循环的每一轮"模型想"和"工具跑"前后,中间件都会被触发一次。理解这一点很重要:你写的"动态注入时间"中间件,会在每一轮调用模型前都被执行,而不是只在最外层执行一次。

注意:不同版本里中间件的具体挂载点和钩子命名可能略有差异。学习时要抓住"洋葱包裹 model 调用和 tool 调用"这个本质,而不是死记某个方法名。下面用概念性接口讲解。

1.4 中间件可以拦截哪些点:概念性钩子

从设计上看,一个中间件至少能在以下几个点插手(命名为概念示意,实际以你所用版本的文档为准):

  • before_model / wrap_model_call:在每次调用大模型之前,修改消息历史(比如注入时间、裁剪旧消息、加上反思反馈);也可以包住整个模型调用,做重试或缓存。

  • after_model:模型返回之后、把结果送进下一轮之前,检查它有没有发起危险工具调用、有没有格式问题。

  • before_tool / wrap_tool_call:每次执行工具之前,检查参数、必要时找人确认、裁剪超长输入。

  • after_tool:工具返回之后,对结果做摘要、截断、脱敏,再喂回模型。

这种"在动作前后各拦一道"的结构,正好对应洋葱模型的入口和出口。

1.5 官方常用中间件类别

1.0 生态围绕这些挂载点提供了一批常用中间件,按用途大致分五类:

  1. 人工确认类(Human-in-the-loop):在执行高风险工具前暂停,把请求抛给人类批准或修改,常见于"发邮件、删数据、转账"等动作。

  2. 上下文裁剪类(tool result trimming / summarization):工具返回超长文本时,自动截断或用小模型摘要,防止撑爆上下文窗口。

  3. 动态系统提示类(dynamic system prompt):每次调用模型前,往系统提示里注入运行时信息,比如当前时间、用户身份、当天汇率。

  4. 提问改写类(query rewriting):把用户口语化、指代不清的问题,结合历史改写成模型更容易处理的完整问法。

  5. 容错重试类(retry / fallback):模型或工具调用失败时,按指数退避自动重试,或切换到备用模型。

1.6 Hook 与 Middleware 的关系

初学者经常把 Hook 和 Middleware 混为一谈。简单说:Middleware 是"包裹一圈"的完整洋葱,既能在动作前插手,也能在动作后插手,还能决定要不要继续往下走;Hook 更像一个"事件回调",是在某个固定节点被触发一次的轻量函数。在很多实现里,Hook 可以理解成"只实现了 before 或 after 单边、没有包裹逻辑的极简中间件"。工程上,能用一个 Middle件统一表达的横切逻辑就用 Middleware,只有纯粹的"打个日志、记个指标"这种单边动作才更适合写成轻量 Hook。

1.7 为什么中间件比在 prompt 里硬编码更好

把规则写进系统提示词有三个致命问题:其一,规则是代码逻辑(裁剪、重试、确认),模型并不可靠执行,写进 prompt 等于把工程责任外包给概率;其二,提示词越长,模型对核心任务的注意力越被稀释;其三,规则无法复用——每个 Agent 都得重新写一遍提示词。中间件则把这些变成可单元测试、可独立替换、可跨项目复用的 Python 对象,这是它在工程上的根本优势。

深度拓展二:代码实战——从最小可用到生产级

2.1 自定义中间件一:动态注入当前时间到系统提示

下面这个例子实现一个概念性中间件:在每次调用模型前,把"现在是某年某月某日星期几"动态注入到消息列表最前面的系统消息里。注意:这里用概念性的 before_model 钩子示意,实际项目请对照你所用版本的中间件基类文档调整方法名。

from datetime import datetime from langchain.agents import create_agent from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain_core.messages import SystemMessage 概念性中间件基类:实际版本中可能叫 BaseMiddleware 或其他名字 class InjectTimeMiddleware: """每次调用模型前,把当前时间注入系统提示。""" def before_model(self, state): # state 里通常包含当前 messages 列表 messages = list(state["messages"]) now = datetime.now().strftime("%Y-%m-%d %A %H:%M") time_note = SystemMessage(content=f"当前时间:{now}。回答涉及时令的问题时以此为准。") # 把时间提示插到最前面(不改动已有消息,只新增一条) return {"messages": [time_note] + messages} @tool def note(text: str) -> str: """记一条笔记""" return f"已记录:{text}" model = ChatOpenAI(model="gpt-4o-mini", temperature=0) agent = create_agent( model=model, tools=[note], prompt="你是一个高效的助手。", middleware=[InjectTimeMiddleware()], # 挂上自定义中间件 ) print(agent.invoke({"messages": [("user", "今天星期几?帮我记一下。")]}))

2.2 自定义中间件二:给每次工具结果自动加摘要/裁剪

第二个例子更实用:工具经常返回超长文本(比如抓回来一整篇网页),直接喂给模型既费 token 又稀释注意力。我们写一个 after_tool 中间件,把超过长度的工具结果自动截断并加一句提示。

from langchain.agents import create_agent from langchain_openai import ChatOpenAI from langchain_core.tools import tool class TrimToolResultMiddleware: """工具结果过长时,自动截断到 max_len,并标注'已截断'。""" def __init__(self, max_len: int = 500): self.max_len = max_len def after_tool(self, state): messages = list(state["messages"]) last = messages[-1] # 只处理 ToolMessage(概念判断:消息类型是工具结果) if type(last).__name__ == "ToolMessage": content = last.content if isinstance(content, str) and len(content) > self.max_len: trimmed = content[: self.max_len] + "\n...[内容过长,已截断]" # 替换最后一条消息的内容(概念示意) last.content = trimmed messages[-1] = last return {"messages": messages} @tool def fetch_web(url: str) -> str: """抓取网页正文(这里用超长假数据模拟)""" return "正文内容。" * 200 # 故意造一段很长的返回 model = ChatOpenAI(model="gpt-4o-mini") agent = create_agent( model=model, tools=[fetch_web], prompt="你是网页阅读助手。", middleware=[TrimToolResultMiddleware(max_len=500)], ) print(agent.invoke({"messages": [("user", "读一下 https://example.com 这篇文章。")]}))

2.3 组合多个中间件:看洋葱顺序如何叠加

当你把多个中间件放进列表,它们会按顺序层层包裹。下面这个例子同时挂了"注入时间"和"裁剪结果"两个中间件,注意列表顺序决定了谁在外层。

agent = create_agent( model=model, tools=[fetch_web], prompt="你是一个助手。", # 顺序:注入时间在外层,裁剪在内层 # 请求时先注入时间、再裁剪;响应时方向相反 middleware=[InjectTimeMiddleware(), TrimToolResultMiddleware(max_len=500)], )

调试时,你可以在每个钩子入口打印一行日志,观察请求和响应的穿过顺序,就能直观验证洋葱模型是不是如你所想。

深度拓展三:常见坑与调试技巧

坑 1:把中间件顺序写反,效果完全不对。现象:明明挂了裁剪中间件,超长内容还是被喂给了模型。原因:裁剪中间件被放在了内层,而外层某个中间件又重新构造了消息,绕过了裁剪。解决:记住"列表越靠前越靠外",把"最后兜底"的中间件放最外层。

坑 2:以为中间件只在最外层执行一次。现象:注入时间的中间件只在第一轮生效,后续轮次时间没更新。原因:它确实会在每轮 model 调用前触发,但如果你只在"整个 Agent 入口"做了一次注入,就会失效。解决:把注入逻辑放在 before_model 这种"每次调用模型前"的钩子上。

坑 3:在中间件里直接修改了原始消息对象,污染了历史。现象:下一轮看到的历史和预期不一致。原因:中间件返回时应该返回新的消息列表或明确替换某条,而不是原地乱改共享对象。解决:遵循"返回一个新的 state 片段"的约定,不要偷偷改输入对象。

坑 4:把本该是代码的逻辑写回了 prompt。现象:提示词里写"如果工具结果太长就总结一下",模型时而照做时而不做。原因:这是确定性逻辑,不该交给概率模型。解决:用 after_tool 中间件做确定性裁剪/摘要。

坑 5:人工确认中间件和 checkpointer 没配合好,暂停后状态丢了。现象:找人确认后,Agent 不记得之前聊到哪了。原因:没有用 checkpointer 持久化暂停时的状态。解决:配 InMemorySaver 或持久化 checkpointer,用 thread_id 恢复。

坑 6:重试中间件把非幂等工具也重试了。现象:"发邮件"工具失败后被重试,结果发了两封。原因:重试逻辑没有区分幂等和非幂等工具。解决:重试中间件只对幂等工具(查询、计算)生效,对写操作/发操作默认不重试。

坑 7:钩子方法名记错,中间件静默不生效。现象:代码跑通了,但中间件好像没起作用。原因:你用的钩子名和当前版本的基类对不上,被当成普通方法忽略了。解决:对照版本文档确认基类和方法名,加一行打印日志验证钩子真的被调用。

坑 8:中间件层层叠加后,消息历史被重复加工。现象:同一条工具结果被裁剪了两次,或时间被注入了好几遍。原因:多个中间件都在改同一批消息,互相叠加产生副作用。解决:每个中间件只负责一件事,改动要幂等——比如注入时间前先检查有没有已存在的时间消息。

调试技巧:在每个钩子第一行加 print(f"[middleware] before_model"),配合 stream 的 debug 模式,就能画出完整的洋葱穿过图,定位到底哪一层没按预期执行。

深度拓展四:最佳实践与性能优化

单一职责:一个中间件只做一件事。"注入时间"和"裁剪结果"应该是两个中间件,而不是一个大而全的 God Middleware,否则难以测试和复用。

幂等性:写进消息历史的中间件要能重复执行不出错。比如注入时间前先检查是否已有时间消息,避免每轮都往历史里塞一条,把上下文越撑越长。

性能:after_tool 里如果调用小模型做摘要,要注意这会增加一次模型调用的延迟。可以先用"纯截断"这种零成本方案,只在确实需要时才上"小模型摘要"。

可观测:每个中间件都应该打点:记录它在什么时候被触发、改了多少消息、耗时多少。出问题时,trace 里能一眼看出是哪一层中间件改坏了状态。

分层配置:把中间件分成"全局层"(日志、计时)、"业务层"(注入用户身份、裁剪)、"安全层"(人工确认、写操作拦截),按层管理启停,方便在不同环境(开发/测试/生产)用不同组合。

别过度设计:不是每个 Agent 都需要五个中间件。简单任务挂一两个就够,中间件越多,调试成本越高。

深度拓展五:面试高频考点

Q1:什么是中间件?它解决什么问题?

A:中间件是包裹在 Agent 主干(模型调用、工具执行)外围的可插拔组件,用来处理横切关注点(日志、裁剪、重试、人工确认、动态提示注入),让主干逻辑保持干净,规则可复用、可测试。

Q2:什么是洋葱模型?

A:中间件按列表顺序层层包裹,请求从外到内穿过每一层到达核心,响应再从内到外原路返回。每层都能在动作前和动作后各处理一次。

Q3:create_agent(middleware=[A, B, C]) 里谁在外层?

A:列表越靠前越靠外。A 最先拦截请求、最后处理响应;C 最靠近核心。

Q4:中间件是只在 Agent 最外层执行一次吗?

A:不是。像 before_model 这种钩子,会在 ReAct 循环每一轮调用模型前都触发;after_tool 则在每一次工具执行后触发。

Q5:自定义中间件常见要实现哪些钩子?

A:概念上包括 before_model / wrap_model_call(调用模型前后)和 before_tool / after_tool(执行工具前后),用于改消息、做重试、裁剪、确认。具体方法名以所用版本文档为准。

Q6:Hook 和 Middleware 有什么区别?

A:Middleware 是完整包裹,能在动作前后各插手、还能决定是否继续;Hook 更像固定节点上的轻量事件回调,通常只做单边动作(如打日志)。Hook 可视为极简的单边中间件。

Q7:官方中间件大致分哪几类?

A:人工确认类、上下文裁剪/摘要类、动态系统提示类、提问改写类、容错重试类。

Q8:为什么不把裁剪、重试这些规则直接写进系统提示词?

A:因为它们是确定性的工程逻辑,模型并不可靠执行;而且提示词越长越稀释注意力,规则也无法跨 Agent 复用。中间件把它们变成可测试、可替换的代码组件。

Q9:人工确认中间件要配合什么才能跨会话恢复?

A:要配合 checkpointer(如 InMemorySaver 或持久化实现),用 thread_id 在暂停后恢复完整状态。

Q10:重试中间件有什么坑?

A:不能盲目重试非幂等工具(发邮件、删数据、转账),否则会重复执行造成副作用。重试应只对查询、计算等幂等操作开启。

Q11:多个中间件叠加可能产生什么问题?

A:消息被重复加工(时间注入多次、结果被裁剪多次),或者顺序写反导致某层被绕过。解决办法是让每个中间件幂等、单一职责,并理清洋葱顺序。

Q12:怎么验证中间件真的按预期执行了?

A:在每个钩子入口加日志打印,配合 debug 流式或 trace,画出请求/响应穿过各层的顺序图。

深度拓展六:课后练习与思考

  1. 写一个"记录每轮耗时"的中间件:在每次调用模型前后计时,并把耗时打印出来。提示:用 before_model 记开始时间,after_model 算差值。

  2. 写一个"敏感词拦截"中间件:在 after_tool 里检查工具结果,如果出现手机号、身份证号就打码。提示:正则替换后替换最后一条 ToolMessage 内容。

  3. 把"注入时间"中间件改成幂等:如果历史里已经有时间消息,就更新它而不是再塞一条。思考这样做对上下文长度有什么好处。

  4. 设计一个"写操作人工确认"中间件:当工具名属于 {delete_order, send_email, transfer} 时,暂停并请求确认。思考它该挂在 before_tool 还是 after_tool。

  5. 故意把两个中间件的列表顺序对调,观察最终输出有什么不同,并用日志画出两种情况下的洋葱穿过图。

  6. 思考:如果让你把"工具结果摘要"做成 after_tool 中间件,摘要用大模型还是小模型?在什么情况下纯截断比摘要更划算?

深度拓展补充:进阶案例与 FAQ

前面六节我们把中间件的洋葱模型、可挂载的钩子点、官方中间件分类和自定义写法讲清楚了。这一节作为补充,集中讨论几个在真实项目里绕不开的进阶问题:中间件和记忆裁剪怎么配合、中间件链怎么测试、怎么排查"中间件好像没生效",以及一批高频 FAQ。读完这一节,你应该能独立为自己的 Agent 设计一套干净、可维护的中间件组合。

补充一:中间件与上下文裁剪的实战配合

上一章反复强调,ReAct 循环里消息历史会越滚越长,迟早撞上下文窗口。中间件正好是解决这个问题的最佳位置:你可以写一个"上下文管理"中间件,在每次调用模型之前检查消息列表长度,一旦超过阈值,就把最早的若干条 ToolMessage 用小模型总结成一段摘要,或者直接截断只保留最近几轮。这样做的好处是,裁剪逻辑和业务主干完全解耦——Agent 本身不知道自己的历史被压缩过,它只是每次收到一段"刚刚好"的消息。

设计这种中间件时有几个细节要注意。第一,不要把最近一两轮的消息裁掉,否则模型刚做完工具调用就丢了结果,会莫名其妙。一般只压缩"足够老"的部分。第二,摘要本身也是一条消息,要把它放在合适的位置(通常是系统消息之后、对话开始之前),并标注清楚"以下是对早期对话的总结"。第三,这种操作要幂等,别每轮都重新总结一遍,那样既费 token 又会让历史抖动。一个常见做法是:只在历史长度超过阈值时触发一次压缩,压缩后打上标记,下轮不再重复压。

补充二:怎么测试一个中间件

中间件之所以比"把规则写进提示词"工程上更优,一个重要原因就是它可以被单元测试。你不需要真的起一个 Agent、烧 token 去验证一个裁剪中间件对不对——只要构造一条假的消息列表,直接调用中间件的钩子方法,断言返回的消息历史符合预期即可。比如测试"裁剪工具结果"中间件:造一条超长的 ToolMessage,调用 after_tool,断言结果被截到了指定长度并带上了"已截断"标记。这种测试又快又稳,不依赖网络和模型。

对于"注入时间"这类中间件,测试时要注意它依赖系统时钟。可以把当前时间作为参数注入,测试时传一个固定时间,断言生成的系统消息里包含这段时间,而不是真的去读墙上时钟。把副作用(时间、随机数、网络)从中间件核心逻辑里抽出来,是让它可测试的关键。这也是为什么我们一再强调"中间件要单一职责、要幂等"——只有这样,它才是一个输入明确、输出可断言的纯函数式组件。

补充三:排查"中间件好像没生效"的三板斧

新手最常遇到的挫败是:代码看起来对,结果中间件就是不起作用。这里给三板斧。第一斧,先确认钩子真的被调用了——在钩子方法第一行打一条日志,跑一次看控制台有没有输出。如果连日志都没有,说明你的中间件要么没被挂上,要么钩子方法名和当前版本的基类对不上,被当成了普通方法。第二斧,确认洋葱顺序——列表越靠前越靠外,如果你把"最后兜底"的中间件放在了最内层,它的出口处理可能被外层某个中间件的输出覆盖掉了。第三斧,确认你改的是不是同一份状态——中间件约定返回一个新的状态片段,如果你原地改了输入对象又没正确返回,或者多个中间件都在改同一批消息,就会出现"改了等于没改"的诡异现象。

把这三步走完,绝大多数"中间件静默失效"都能定位。再配合 stream 的 debug 模式或 trace,你能看到每一步消息历史到底长什么样,比凭空猜高效得多。

补充四:中间件设计的三条经验法则

第一条法则:越通用的中间件越要放外层。日志、计时、错误兜底这类"无论发生什么都要执行"的中间件,应该在最外层,这样它们才能包住内部所有其他中间件,确保任何异常都被记到。第二条法则:越业务相关的中间件越要放内层。注入用户身份、裁剪本业务的工具结果,这类和具体业务紧耦合的逻辑,靠近核心放置,方便针对不同业务替换。第三条法则:写之前先问"这是不是确定性逻辑"。如果是(裁剪、格式化、打码、限流),就该用中间件做成代码;如果它本质是"希望模型怎么想"(比如"回答要友好"),那它属于系统提示词的范畴,硬做成中间件反而别扭。分清这两类,是用好中间件的分水岭。

补充五:常见 FAQ

问:中间件和 Hook 到底选哪个?答:如果你需要在动作前后各处理一次、还能决定是否继续往下走,用中间件。如果你只是想在某个节点"顺手记一笔日志、打个指标",用轻量的 Hook 就够了,没必要为它包一整层洋葱。

问:我能不能不用中间件,自己在 Agent 外面包一层 Python 函数?答:能,但很快会失控。因为 ReAct 循环是多轮的,你在整个 Agent 外面包一层,只能处理"第一次进入"和"最终返回",管不到循环内部每一轮的模型调用和工具执行。中间件的价值恰恰在于它能插到循环内部的每个节点上。

问:中间件会拖慢 Agent 吗?答:纯内存操作(截断、加字段、打日志)几乎不耗时。但如果中间件里又调了一次小模型(比如做摘要),那确实会增加一次模型调用的延迟和成本。所以对延迟敏感的环节,优先用零成本的确定性处理(截断、正则),把模型摘要用在真正需要的地方。

问:多个中间件都想改系统提示,会冲突吗?答:会,这正是要理清洋葱顺序的原因。如果 A 注入时间、B 注入用户身份,它们都往系统消息里加内容,顺序会影响最终系统提示长什么样。解决办法是约定每个中间件只负责往系统提示"追加一段自己的内容",而不是重写整个系统消息,并且让它们幂等——加之前先检查有没有加过。

问:怎么给不同环境配不同中间件?答:把中间件列表做成配置。开发环境挂日志和计时,测试环境加更多校验,生产环境挂人工确认和持久化检查点。这样你可以在不动 Agent 主干代码的前提下,按环境灵活组装中间件组合。

问:中间件能用来做权限控制吗?答:能,而且应该这么做。在 before_tool 里检查当前用户有没有权限执行这个工具、参数越不越界,比靠提示词提醒模型"别乱调"可靠得多。权限是确定性规则,天生属于中间件层。

补充六:本章小结

这一章我们把中间件这套机制从头到尾梳理了一遍。核心心智模型只有一个:洋葱。你把 middleware=[A, B, C] 挂上去,请求从外到内穿过 A、B、C 到达核心的"调模型、跑工具",响应再从内到外原路返回。每一层都能在动作发生前和发生后各处理一次,这就是中间件强大表达力的来源。要记住,它包裹的不只是整个 Agent 的入口,更是 ReAct 循环内部每一轮的模型调用和工具执行——所以"注入当前时间"这种逻辑,会在每一轮调用模型前都触发,而不是只在最外层跑一次。

我们还理清了几个容易混淆的概念。官方中间件大致分五类:人工确认、上下文裁剪、动态系统提示、提问改写、容错重试。自定义中间件要实现的钩子,概念上就是 before_model / after_model 管模型调用前后、before_tool / after_tool 管工具执行前后,具体方法名以你所用版本文档为准,不必死记。Hook 和 Middleware 的区别在于:Middleware 是完整包裹,能前后插手、还能决定是否继续;Hook 更像固定节点上的轻量单边回调。而"把规则写进提示词"之所以是下策,是因为裁剪、重试、确认这类确定性工程逻辑,模型并不可靠执行,写进 prompt 既稀释注意力又无法复用,做成中间件才是可测试、可替换、可跨项目复用的工程组件。

最后给你三条能直接落地的经验法则:越通用的逻辑(日志、计时、兜底)越放外层,越业务相关的逻辑(注入身份、裁剪结果)越放内层;写之前先判断"这是不是确定性逻辑",是就用中间件做成代码,不是就留给系统提示词;每个中间件保持单一职责、保持幂等,这样它才输入明确、输出可断言,能用单元测试稳稳验证。掌握了这些,你就拥有了把一个裸 Agent 打磨成生产级系统的最后一块拼图——把横切关注点从主干里干净地剥离出来。

补充七:动手清单

学完本章,建议你按这个顺序动手练一遍:先写一个只挂"注入时间"中间件的最小 Agent,用日志确认它在每轮模型调用前都触发;再叠一个"裁剪工具结果"中间件,故意返回一段超长文本,观察它如何被截断;然后把两个中间件的列表顺序对调,用日志画出两种情况下洋葱穿过的顺序图;最后试着重构一段你之前硬写在系统提示词里的规则(比如"回答要简洁"或"引用来源"),判断它到底属于确定性逻辑(该用中间件)还是属于希望模型怎么想(该留在提示词)。这个区分练习做过几次,你对"什么该进中间件、什么该进 prompt"的直觉就建立起来了。

"中间件的最小 Agent,用日志确认它在每轮模型调用前都触发;再叠一个"裁剪工具结果"中间件,故意返回一段超长文本,观察它如何被截断;然后把两个中间件的列表顺序对调,用日志画出两种情况下洋葱穿过的顺序图;最后试着重构一段你之前硬写在系统提示词里的规则(比如"回答要简洁"或"引用来源"),判断它到底属于确定性逻辑(该用中间件)还是属于希望模型怎么想(该留在提示词)。这个区分练习做过几次,你对"什么该进中间件、什么该进 prompt"的直觉就建立起来了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 3:06:03

信号量原理与实战:从P/V操作到连接池限流

不知道你有没有遇到过这种灵异事件:开了十个线程去抓数据,逻辑写得挺小心,结果数据不是少几条,就是偶尔重复;又或者模块明明限制了并发数,高峰期服务还是被打穿,连接数一路往上飙。排查了半天&a…

作者头像 李华
网站建设 2026/10/16 3:05:19

【恋练有词】背单词

thinkreckonJuly twenty fifth然而:whereas,nonethelessThe economic recessionhave an operationhave surgery

作者头像 李华
网站建设 2026/10/16 3:05:18

在 Python 编程语言中,函数是组织代码、实现模块化设计的核心机制

在 Python 编程语言中,函数是组织代码、实现模块化设计的核心机制。函数的执行结果通常通过 return 语句返回给调用者。然而,一个常被初学者忽视但至关重要的特性是:当函数中没有显式使用 return 语句时,Python 会自动返回 None。…

作者头像 李华
网站建设 2026/10/16 3:05:16

LangChain Agent 进阶:错误处理、重试与人工介入

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录LangChain Agen…

作者头像 李华
网站建设 2026/10/16 3:05:07

电器元件-指示灯

简介指示灯是一种用于显示设备状态或操作状态的发光装置&#xff0c;通过颜色和亮灭状态向用户提供直观的信号信息。它在电气控制、电子设备及工业自动化中广泛使用。电源指示灯用于表示系统的电源状态&#xff0c;可选电压为 6.3V、12V、24V、36V、110V、220V、380V。应用场景…

作者头像 李华