首页 / 资讯中心 / 文章详情

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

LangChain 第7章 中间件与钩子函数:16 个官方中间件、洋葱顺序与 6 个 Hook ★ FEATURED ARTICLE
摘要本文系统梳理 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 个优先用现成的满足不了再自定义自定义中间件本质上就是实现一个或多个钩子函数。二、常用官方中间件中间件作用ToolCallLimitMiddleware70限制工具调用次数防止 Agent 无限循环烧 tokenModelFallbackMiddleware71主模型失败时自动兜底切换到备用模型LLMToolSelectorMiddleware72由一个 LLM 先筛选工具缩小候选、减少选错SummarizationMiddleware历史消息超长时自动摘要压缩HumanInTheLoopMiddleware敏感工具调用前中断交人工审核确认PIIRedactionMiddleware对手机号、身份证等个人敏感信息脱敏ContextEditingMiddleware76上下文裁剪控制 token 与成本FilesystemFileSearchMiddleware77赋予 Agent 本地文件 glob/grep 搜索分析能力另有代码执行 / Shell 类中间件需要可执行命令的环境Windows 下不便测试以及与 deepagents 深度耦合的中间件如待办、子智能体相关。三、两个重点中间件实操ContextEditingMiddleware 上下文编辑目的多轮对话历史随调用越滚越大、token 成本越来越高用它给上下文“抽脂”。关键参数trigger 触发编辑的 token 阈值keep 控制保留条数如 keep0 表示工具消息一条不留全清编辑工具可迭代配置多个。观察方式用不用它消息列表条数不变要看每条 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钩子函数是在流程特定时机被框架自动调用的扩展函数不是业务代码主动调用而是在请求开始前、模型调用前后、任务结束后等时机自动触发如同生产车间在主流程外预留的插槽挂参数校验、日志、质检等逻辑从而在不改主流程源码的前提下扩展能力。分类钩子时机节点风格 Node4 个before_agent / before_modelAgent / 模型执行前节点风格 Node4 个after_model / after_agent模型 / Agent 执行后包装风格 Wrap2 个wrap_model_call包裹模型调用前后包装风格 Wrap2 个wrap_tool_call包裹工具调用前后第 80 集基于装饰器定义 Node-style 钩子第 81 集基于类定义第 82 集装饰器参数 can_jump_to 的使用控制流程跳转。无论官方中间件、自定义中间件还是装饰器中间件通常都是通过实现其中一个或多个钩子生效。第 64–69、73–75、83–85 集本地字幕未导出本节据课程集目录与 LangChain 1.x 官方体系整理涵盖摘要、人工审核、PII 脱敏等中间件具体类名与参数以课件/视频为准。六、知识点速查主题要点数量官方 16 个中间件优先用现成控成本ContextEditingtrigger 阈值 keep 保留看 usage_metadata文件搜索glob 路径 / grep 内容ripgrep 更快max_file_size 防 OOM执行顺序洋葱模型进入 123、出来 3216 个钩子节点 4before/after × agent/model 包装 2wrap model/tool定义方式装饰器80、类81、can_jump_to82深度拓展本章完整知识体系与实战进阶上一章我们拆解了 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 生态围绕这些挂载点提供了一批常用中间件按用途大致分五类人工确认类Human-in-the-loop在执行高风险工具前暂停把请求抛给人类批准或修改常见于发邮件、删数据、转账等动作。上下文裁剪类tool result trimming / summarization工具返回超长文本时自动截断或用小模型摘要防止撑爆上下文窗口。动态系统提示类dynamic system prompt每次调用模型前往系统提示里注入运行时信息比如当前时间、用户身份、当天汇率。提问改写类query rewriting把用户口语化、指代不清的问题结合历史改写成模型更容易处理的完整问法。容错重试类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(contentf当前时间{now}。回答涉及时令的问题时以此为准。) # 把时间提示插到最前面不改动已有消息只新增一条 return {messages: [time_note] messages} tool def note(text: str) - str: 记一条笔记 return f已记录{text} model ChatOpenAI(modelgpt-4o-mini, temperature0) agent create_agent( modelmodel, 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) gt; 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(modelgpt-4o-mini) agent create_agent( modelmodel, tools[fetch_web], prompt你是网页阅读助手。, middleware[TrimToolResultMiddleware(max_len500)], ) print(agent.invoke({messages: [(user, 读一下 https://example.com 这篇文章。)]}))2.3 组合多个中间件看洋葱顺序如何叠加当你把多个中间件放进列表它们会按顺序层层包裹。下面这个例子同时挂了注入时间和裁剪结果两个中间件注意列表顺序决定了谁在外层。agent create_agent( modelmodel, tools[fetch_web], prompt你是一个助手。, # 顺序注入时间在外层裁剪在内层 # 请求时先注入时间、再裁剪响应时方向相反 middleware[InjectTimeMiddleware(), TrimToolResultMiddleware(max_len500)], )调试时你可以在每个钩子入口打印一行日志观察请求和响应的穿过顺序就能直观验证洋葱模型是不是如你所想。深度拓展三常见坑与调试技巧坑 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中间件按列表顺序层层包裹请求从外到内穿过每一层到达核心响应再从内到外原路返回。每层都能在动作前和动作后各处理一次。Q3create_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执行工具前后用于改消息、做重试、裁剪、确认。具体方法名以所用版本文档为准。Q6Hook 和 Middleware 有什么区别AMiddleware 是完整包裹能在动作前后各插手、还能决定是否继续Hook 更像固定节点上的轻量事件回调通常只做单边动作如打日志。Hook 可视为极简的单边中间件。Q7官方中间件大致分哪几类A人工确认类、上下文裁剪/摘要类、动态系统提示类、提问改写类、容错重试类。Q8为什么不把裁剪、重试这些规则直接写进系统提示词A因为它们是确定性的工程逻辑模型并不可靠执行而且提示词越长越稀释注意力规则也无法跨 Agent 复用。中间件把它们变成可测试、可替换的代码组件。Q9人工确认中间件要配合什么才能跨会话恢复A要配合 checkpointer如 InMemorySaver 或持久化实现用 thread_id 在暂停后恢复完整状态。Q10重试中间件有什么坑A不能盲目重试非幂等工具发邮件、删数据、转账否则会重复执行造成副作用。重试应只对查询、计算等幂等操作开启。Q11多个中间件叠加可能产生什么问题A消息被重复加工时间注入多次、结果被裁剪多次或者顺序写反导致某层被绕过。解决办法是让每个中间件幂等、单一职责并理清洋葱顺序。Q12怎么验证中间件真的按预期执行了A在每个钩子入口加日志打印配合 debug 流式或 trace画出请求/响应穿过各层的顺序图。深度拓展六课后练习与思考写一个记录每轮耗时的中间件在每次调用模型前后计时并把耗时打印出来。提示用 before_model 记开始时间after_model 算差值。写一个敏感词拦截中间件在 after_tool 里检查工具结果如果出现手机号、身份证号就打码。提示正则替换后替换最后一条 ToolMessage 内容。把注入时间中间件改成幂等如果历史里已经有时间消息就更新它而不是再塞一条。思考这样做对上下文长度有什么好处。设计一个写操作人工确认中间件当工具名属于 {delete_order, send_email, transfer} 时暂停并请求确认。思考它该挂在 before_tool 还是 after_tool。故意把两个中间件的列表顺序对调观察最终输出有什么不同并用日志画出两种情况下的洋葱穿过图。思考如果让你把工具结果摘要做成 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的直觉就建立起来了。
阅读完成 · 觉得有帮助?
咨询建站