深入学LangChain 官方文档(十)Middleware 首讲 精读 LangChain 官方文档十Middleware 首讲本篇对应的官方文档Middleware overviewMiddleware 在 Agent 执行中的位置、适用场景与内置/自定义入口。Custom middlewarenode-style、wrap-style hooks状态更新、执行顺序与短路规则。Prebuilt middleware摘要、审批、调用限制、fallback、PII、重试等内置能力。本篇讲解范围本篇主要解释 Middleware 为什么存在、六类核心 hook 如何进入 Agent loop、模型与工具调用分别怎样被治理以及多层 Middleware 的组合顺序。自定义 stream transformer、provider-specific middleware、每个内置类的全部参数和生产部署细节留给后续专题。上一篇把信息保存和再次读取的边界理清了真正上线时却还有一层麻烦规则写明了不代表每次执行都一定守得住。日志该在哪里记录敏感信息由谁过滤模型超时后要不要重试高金额退款又该怎样在真实执行前停下来等待审批把这些要求继续写进 system prompt或者逐个塞进工具函数小型演示通常还能应付。工具一多问题就暴露出来了模型可能漏掉“必须先审批”的文字要求十几个工具开始重复日志与异常转换规则升级还得逐处排查。业务逻辑和执行政策纠缠在一起测试也只能从整条 Agent 链路下手。Middleware 就放在这个缺口里。它不替代 Agent loop也不创造新的业务能力而是在模型调用、工具调用和整个 Agent 生命周期的明确位置接过一部分控制权。退款工具仍然负责退款能否继续、怎样记录、失败后返回什么以及本轮模型能看到哪些 prompt 或工具则由对应的 Middleware 处理。这篇仍用退款客服 Agent 串起整条链路。先看横切规则为什么容易散落再定位 Middleware 的 hooks随后分别进入模型调用和工具调用最后把组合顺序、Human-in-the-loop 与错误包装接成一套可以验证的治理闭环。模型与工具仍在 Agent loop 中往返日志、权限、重试、审批和上下文调整则落在调用边界。Middleware 改变的是“这一步怎样执行”业务工具本身的领域职责没有被重新包装。先确认这层控制权放在哪里后面的 hook 才不会只剩一组 API 名字。一、Middleware 解决的是横切规则不是业务功能一个退款 Agent 可能同时使用查订单、读政策、计算金额和提交退款四个工具。现在给系统加上三条要求所有外部调用都记录耗时金额超过 1000 元必须人工确认接口超时要转换成模型能够处理的错误结果。直接改工具看起来最快代价会随着工具数量一起增长。职责首先开始重复。查订单和提交退款都要复制计时、日志与异常处理新工具接入时还得记得照搬。更危险的是规则并不完整prompt 里的“高金额先审批”只是给模型的指令不能代替执行边界上的强制拦截。等审批阈值、日志字段或重试策略变化多个工具、多个 Agent 又要同步修改遗漏几乎不可避免。这时需要把三种职责分开工具回答“这项业务动作怎样完成”prompt 回答“模型应该怎样思考和表达”Middleware 回答“执行经过某个关键边界时必须应用什么政策”。公共代码也不是都要搬进 Middleware。订单格式转换、退款资格判断仍然属于领域服务。只有一项策略横跨多个模型调用、业务工具或 Agent并且需要统一生效时才值得提升为拦截层。审批、日志和错误处理一旦散落在 prompt、工具与业务接口里任意遗漏都可能形成绕行路径。把执行政策收回统一边界后工具只保留业务输入与结果同一策略也能独立测试并被多个 Agent 复用。遇到新需求时可以先问三个问题它会不会跨越多个业务工具它是否必须在模型之外强制执行它是否需要观察或改变 Agent 的执行状态三个答案里有两个为“是”通常就已经进入 Middleware 的职责范围。二、六类 hook 把控制权放到 Agent 生命周期中官方自定义 Middleware 提供两种 hook 风格。node-style hooks 在固定执行节点运行包括before_agent、before_model、after_model、after_agent。wrap-style hooks 包裹每一次具体调用包括wrap_model_call和wrap_tool_call。六类 hook 分别对应不同的生命周期语义before_agent一次invoke刚开始适合初始化运行级字段或做入口检查before_model每轮模型调用前都会经过适合校验消息、更新 State 或决定跳转wrap_model_call真正包住模型调用可修改请求、短路、缓存、重试或 fallbackafter_model模型响应已经进入执行链适合统计、校验或根据结果更新 Statewrap_tool_call包住每个工具调用可做权限、审批、重试和错误转换after_agent本次 Agent 执行结束适合收口审计和运行级汇总。假设退款 Agent 处理一次请求时调用模型三次、工具两次。before_agent和after_agent各运行一次。before_model、wrap_model_call、after_model会跟着三次模型调用重复wrap_tool_call只跟着两次工具执行进入。hook 选错以后最先出问题的往往不是语法而是执行次数。一次运行只该做一次的初始化可能在循环里反复发生原本想统计每次工具调用的逻辑也可能只在入口记了一笔。before_agent / after_agent包住整次 invocation模型相关 hooks 随 Agent loop 重复wrap_tool_call则只在模型真正发起工具调用时进入。执行次数来自生命周期语义不能靠代码排列顺序猜。这也把 Middleware 与第 07、08 篇接了起来。State、Runtime Context 和 Store 负责承载信息Context Engineering 决定哪些信息在什么时点进入模型。Middleware 则给这些决定提供可执行的生命周期入口。比如before_model可以根据 State 更新摘要wrap_model_call可以按 Runtime Context 过滤工具容器职责并没有因此被替换。三、node-style 管固定节点wrap-style 管一次调用两种风格都能在模型前后运行代码所以很容易被当成两套相似写法。真正的分界不在“谁先谁后”而在它是否掌握被包裹调用的控制权。node-style hook 接收 State 与 Runtime在固定节点按顺序运行。它可以返回字典让字段通过 State reducer 合并配置允许时也可以返回jump_to转向model、tools或end。消息数量检查、状态计数、输入验证和运行日志通常适合放在这里因为它们只需要在节点到达时观察或更新状态。wrap-style hook 接收 request 和handler。这个handler代表下一层 Middleware 或真正的模型、工具当前层可以决定是否调用它。调用一次是正常透传多次调用可以实现重试完全不调用则意味着缓存命中、拒绝或短路。正因为握有这层控制权wrap-style 更适合 fallback、动态模型选择、工具筛选、超时处理和权限拦截。node-style 在 Agent 图确定的固定站点读取或合并 Statewrap-style 把下一层handler包在内部可以继续、重试、替换或停止调用。前者适合顺序性的状态动作后者才拥有真实调用的控制权。两种 hook 的状态更新合同也不完全相同。node-style 可以直接返回字典。wrap_model_call若要同时写 State需要按官方合同返回带Command的ExtendedModelResponsewrap_tool_call则可以直接返回Command。如果只因为它们都属于 Middleware 就随手返回字典代码看起来可能没问题更新却不会按预期进入图的 reducer。所以写自定义 Middleware 之前先把四件事问清楚它按整次运行、每次模型调用还是每次工具调用触发是否要控制handler是否更新 State异常继续向外抛还是转换成 Agent 能处理的结果答案确定以后hook 类型通常也就确定了。四、Model Middleware 改的是本次模型请求模型侧的横切需求大多围绕四类对象展开system message、messages、model 和 tools。官方示例通过ModelRequest.override(...)构造本次调用的新请求再交给handlerAgent 的原始配置不需要被永久改写。拿客服权限来说同一个 Agent 既服务普通用户也服务企业管理员。管理员可以看到内部工单工具普通用户只能使用公开查询工具。没有必要为此维护两套几乎相同的 Agent可以在wrap_model_call中读取 Runtime Context 的角色只把本次请求允许使用的工具传给模型。不过“模型没看到工具”只能减少误选不能成为最终权限边界。真实工具仍然要在服务端校验身份和权限。动态 prompt 走的也是同一条路径。Middleware 读取 State 或 Runtime Context在现有SystemMessage.content_blocks后追加当前租户的规则再把 override 后的 request 交给下一层。这次改动只属于瞬时 model context如果顺手写入长期 State下一轮就可能再次追加相同内容。这里真正要盯住的是两件事请求改动只对当前调用生效控制权还会继续向内传递。Middleware 根据 State 与 Runtime Context 改写本次ModelRequesthandler向内执行并返回ModelResponse。动态 prompt、工具筛选和模型切换不会改写 Agent 的永久配置。如果只想记录模型返回了什么after_model已经够用需要在调用失败时切换模型才轮到wrap_model_call因为它持有handler和异常边界。把所有逻辑都塞进 wrap hook 的确能跑后果却是职责混在一起、嵌套越来越难测。能用权限更小的 hook 解决就不要默认选择功能最强的那个。五、Tool Middleware 守住真实动作的边界模型调用和工具调用承担的风险并不相同。模型说得不理想通常还有机会重试或改写工具却可能已经发出邮件、扣减库存甚至提交退款。wrap_tool_call正好站在 Agent 从推理走向真实动作的边界上它不只是一个通用异常装饰器。ToolCallRequest里带着工具名、参数与调用标识。执行前Middleware 可以检查用户权限、参数范围和审批状态再决定是否调用handler(request)。执行后它可以记录结果、转换已知异常或者返回ToolMessage让模型拿到可以继续处理的失败信息。退款场景里查询类工具可以自动执行issue_refund遇到高风险条件则必须暂停。官方内置的HumanInTheLoopMiddleware会按工具名匹配interrupt_on配置并依靠 checkpointer 保存中断状态。审批完成后恢复的是原来的线程不需要再让模型猜一遍是否应该退款。真正决定风险的是顺序先审批再执行真实动作最后把合法结果送回循环。工具请求先经过权限与审批策略再抵达真实业务函数成功结果以ToolMessage返回 Agent loop已知错误也能转换成带原调用 ID 的工具结果。高风险写操作一旦可能已经发生重试就必须服从业务幂等合同。“自动重试”在这里尤其要克制。查询接口遇到明确超时或限流时做有限重试通常合理提交退款却不同——服务端可能已经成功只是客户端没有收到响应。此时盲目再调一次就可能制造重复动作。Middleware 能控制调用次数却无法凭空知道业务副作用是否已经发生。幂等键、事务状态和结果查询接口仍然要由业务系统提供。六、内置 Middleware 是经过命名的常见政策LangChain 已经把一批高频需求做成内置 Middleware包括 Summarization、Human-in-the-loop、Model/Tool call limit、Model fallback 和 PII detection。此外还有 Tool retry、Model retry、Tool error、工具选择与上下文编辑等能力。这些名字对应的是已经被识别出来的常见执行政策不是要求全部装进同一个 Agent。使用内置能力当然能少写代码但更重要的是策略的输入、状态字段和退出行为有了明确配置。比如ModelCallLimitMiddleware可以分别限制单次运行和线程累计调用。ToolCallLimitMiddleware可以面向全部工具或指定工具还能决定超限后继续、报错还是结束。Human-in-the-loop 则明确依赖 checkpointer因为没有持久状态中断就无从恢复。上下文过长交给摘要和编辑失控循环交给调用限制外部不稳定交给 retry 与 fallback敏感动作交给 PII 与审批。先从失败类型定位政策再选择组件比按类名逐个试用更容易形成可解释的组合。不过标注为“production-ready”并不等于加进列表就完成了生产治理。调用限制的阈值要经过压测PII 规则得匹配业务地区和字段fallback 模型要验证结构化输出兼容性审批还需要超时、撤销与审计。内置 Middleware 给出了稳定落点策略参数和执行后果依然由业务负责。自定义能力也最好保持同样的单一职责。一个 Middleware 如果同时包办动态 prompt、权限、计费、重试和日志很快又会变成另一个难以拆解的 Agent。拆开以后每层只回答一个问题也更容易独立验证给定 request 与 State它会不会继续调用 handler会返回什么异常又怎样传播。七、组合顺序决定谁先进入、谁最后收口Middleware 列表并不是简单地从上到下各执行一次。官方的顺序合同很明确before_*从前往后运行wrap hooks 像函数一样逐层嵌套第一个 Middleware 包住后面的全部层after_*与返回路径则从后往前展开。假设列表是[audit, approval, error_adapter]。请求先进入 audit再进入 approval随后抵达 error adapter 和真实工具。返回时方向相反内层结果先交给 error adapter再逐层回到 approval 与 audit。因此外层 audit 能观察包含内部处理在内的总耗时内层 error adapter 则更靠近原始异常。进入路径按列表顺序向内wrap hook 形成洋葱式嵌套after 和返回路径反向展开。外层覆盖的调用范围更完整内层更接近真实模型或工具任意一层短路、重试或吞掉异常都会改变外层最终看到的结果。顺序设计至少要核对三组相互作用日志是在敏感信息脱敏前还是后记录计数限制统计原始请求还是把重试后的真实调用也算进去缓存命中以后权限和审计是否仍然生效。没有明确答案时这个列表只是在正常路径上碰巧工作错误路径很可能绕开关键政策。短路也不是写一句“直接 return”就结束了。node-style 通过允许的jump_to改变图流向wrap-style 可以不调用 handler直接返回缓存或拒绝结果。无论走哪条路径下游都要收到合法的消息或状态结构审计层也应能区分真实执行、缓存命中、审批拒绝和策略阻断。八、用一个退款 Agent 串起审批与错误边界前面的边界放到一起后代码需要证明两件事issue_refund在真实执行前会进入人工审批工具发生已知超时时会转换成与原 tool call 配对的ToolMessage。示例没有自动重试退款写操作因为能否重试取决于业务幂等合同。fromcollections.abcimportCallablefromlangchain.agentsimportcreate_agentfromlangchain.agents.middlewareimportHumanInTheLoopMiddleware,wrap_tool_callfromlangchain.messagesimportToolMessagefromlangchain.toolsimporttoolfromlangchain.tools.tool_nodeimportToolCallRequestfromlangchain_openaiimportChatOpenAIfromlanggraph.checkpoint.memoryimportInMemorySaverfromlanggraph.typesimportCommandtooldefissue_refund(order_id:str,amount:float)-str:提交退款生产实现必须使用 order_id 或独立幂等键防止重复写入。returnf退款已受理{order_id}金额{amount:.2f}元wrap_tool_calldefconvert_known_tool_errors(request:ToolCallRequest,handler:Callable[[ToolCallRequest],ToolMessage|Command],)-ToolMessage|Command:执行工具并把已知超时转换为可被 Agent 继续处理的工具结果。try:returnhandler(request)exceptTimeoutError:returnToolMessage(content退款服务暂时无响应请先查询订单状态不要直接重复提交。,tool_call_idrequest.tool_call[id],)modelChatOpenAI(modelqwen3.7-plus,api_keyYOUR_API_KEY,base_urlYOUR_OPENAI_COMPATIBLE_ENDPOINT,)agentcreate_agent(modelmodel,tools[issue_refund],checkpointerInMemorySaver(),middleware[convert_known_tool_errors,HumanInTheLoopMiddleware(interrupt_on{issue_refund:{allowed_decisions:[approve,edit,reject],}}),],)resultagent.invoke({messages:[{role:user,content:为订单 A-2048 退款 1280 元}]},config{configurable:{thread_id:refund-A-2048}},)执行从用户消息进入 Agent loop。模型若选择issue_refund工具请求先经过外层错误适配器再被 Human-in-the-loop 拦下。执行暂停在审批位置checkpointer 保存当前线程审批方批准、编辑参数或拒绝以后恢复过程才可能进入真实退款工具。如果工具抛出明确的TimeoutError外层 Middleware 会生成带原tool_call_id的结果工具调用与返回消息仍然保持配对。这里有三个边界不能被“代码能跑”掩盖。首次调用返回的result可能表示中断状态并不等于退款已经成功应用层必须读取并呈现 interrupt。错误转换以后模型可以解释下一步但订单状态仍要回业务系统查询。InMemorySaver也只适合本地演示生产审批要使用能够跨进程恢复的 checkpointer。把执行顺序连起来就是“请求进入—策略拦截—人工决策—业务执行—合法结果—审计收口”。tool call 只是模型提出的执行请求审批决定是否放行业务工具负责幂等写入错误适配器维持消息合同checkpointer 保存可恢复的中断状态。少了任何一层都可能把“模型想退款”误当成“退款已完成”。这套闭环可靠不可靠要去拒绝、异常和恢复路径里验证不能只看正常退款是否走通。九、生产验收要检查正常路径也要检查绕行路径Middleware 上线前只跑一次成功示例几乎发现不了真正危险的问题。拒绝、超时、重试、短路和恢复才是执行政策最容易被绕开的地方。可以沿着五组问题复核触发范围策略按 invocation、model call 还是 tool call 生效次数是否与预期一致组合顺序脱敏、日志、计数、缓存、重试和审批的先后是否会产生绕行状态合同返回字典、Command、ExtendedModelResponse或ToolMessage是否匹配当前 hookreducer 是否会正确合并副作用边界写工具失败后能否重试是否存在幂等键超时究竟代表失败还是结果未知恢复与审计interrupt 能否跨请求恢复拒绝和编辑是否留痕用户是否能看到真实的等待与失败状态单元测试不必每次都拉起完整模型。直接构造 request、State 和假的 handler就能检查 handler 被调用几次、override 是否正确、异常有没有转换以及短路返回的结构是否合法。到了集成测试再覆盖真实 Agent loop 和 checkpointer 恢复。这样一来策略本身的问题和整条执行链的问题不会混在一起。十、回到主线把执行政策放回它该在的位置Middleware 并不是为了让 Agent 代码显得更“框架化”。日志、权限、审批、重试、上下文调整和调用限制同时出现时系统需要一个不依赖模型记忆、也不用污染每个业务工具的执行位置这才是它要解决的问题。把全文收回一条判断链先确认需求是否属于横切政策再按触发粒度选择生命周期 hook。只观察或更新固定节点时用 node-style需要控制真实调用时用 wrap-style。能复用内置能力就先复用自定义层保持单一职责最后从顺序、状态合同、异常传播、副作用和恢复路径验证组合。做到这里Agent loop 仍然是“模型决定—工具执行—结果返回”的循环但执行政策已经不再散落。接下来还有一个同样现实的问题模型需要的答案并不总在当前上下文里。外部知识怎样进入这条循环又怎样避免把“检索到了内容”误当成“拿到了可靠答案”下一篇进入 Retrieval继续处理这条知识接入边界。