Built-in 与 Custom Middleware)
深入学 LangChain 官方文档十六Built-in 与 Custom Middleware本篇对应的官方文档Middleware overview支撑 Middleware 在 Agent loop 与 compiled LangGraph 中的运行位置。Prebuilt middleware支撑 PII、调用限额、重试、错误处理等 Built-in 能力及配置边界。Custom middleware支撑 node-style / wrap-style Hook、handler 控制、自定义实现与多 Middleware 顺序。本篇讲解范围本篇用客服 Agent 串起敏感信息处理、调用限额、工具重试和自定义审计讲清通用治理能力怎样接入 Agent 生命周期以及组合顺序为什么属于运行合同。Human-in-the-loop 的四类审批决定不再重复知识库、Embedding、Vector Store 与 RAG 留给下一篇。客服 Agent 的第一版通常很好写system prompt 里提醒不要泄露隐私工具函数里捕获网络异常调用方再记录耗时。随着需求增加规则会散落到三处Prompt 负责一部分工具负责一部分外围服务再补一部分。问题不只是代码重复。同一封邮件在进入模型前脱敏了却可能在工具结果或流式事件中再次暴露一个查询工具自己重试三次外层调用方又重试两次最终可能发出六次请求。每条局部规则都“看起来正确”组合后却失去统一边界。左侧的 Prompt、Tool 和调用方各自维护治理逻辑策略覆盖面与执行顺序难以确认右侧把通用控制挂到 Agent 生命周期 Hook同一条消息、模型调用和工具调用都能在明确位置接受检查。Middleware 的价值不是把所有业务代码搬进一个列表而是为横切策略提供稳定接入点脱敏、限额、重试、降级、日志和 Guardrail 不再依赖每个工具作者自觉复制。一、Middleware 运行在 Agent loop 里面create_agent返回的是已编译的 LangGraph。Middleware 不是 Agent 外面的反向代理它的 Hook 会成为这张图内部的节点或调用包装层。Agent 开始一次 invocation 后会在模型与工具之间循环模型生成普通回答或 tool calls工具执行后用ToolMessage回传模型再决定是否继续。沿着这条循环观察 Hook 的包围范围和运行频率就能看出一次性校验与逐轮控制为什么不能放在同一位置。before_agent与after_agent包住一次完整 invocationbefore_model与after_model会随循环重复wrap_model_call和wrap_tool_call则直接包住真实调用。Hook 的频率不同放错位置会让一次性校验被重复执行或让逐次限制只检查一次。当整个 Agent 被放进更大的StateGraph作为节点或子图时这些 Hook 仍会运行。Middleware 与 Agent 是一个编译后的执行单元不需要在外层工作流里重新手工调用一次。二、Node-style 负责时点Wrap-style 控制调用Custom Middleware 提供两种 Hook 风格。Node-style Hook 在固定时点按顺序运行before_agent在一次调用开始前执行一次before_model在每次模型调用前执行after_model在每次模型响应后执行after_agent在 Agent 结束时执行一次。它们适合验证、日志、State 更新和jump_to跳转。Wrap-style Hook 环绕一次真实调用wrap_model_call包住模型wrap_tool_call包住工具。它接收 request 和 handler由 Middleware 决定是否、何时、调用几次 handler。Node-style 像执行路径上的检查站到点运行并可更新 StateWrap-style 像包裹真实调用的控制层可以短路、变换请求、调用一次或多次 handler。前者回答“在哪个时点检查”后者回答“这次调用怎样发生”。只想在 Agent 开始时验证租户状态不需要包住每次模型调用想实现模型降级或缓存必须控制 handler单纯before_model无法接住异常并再次调用另一个模型。三、先按问题选择 Built-inLangChain 已提供一组 provider-agnostic Middleware。选择时应从失败模式出发而不是看到类名就全部加入列表。对话即将超过上下文窗口使用 Summarization 或 Context Editing。模型或工具调用次数可能失控使用 Model Call Limit 或 Tool Call Limit。主模型暂时不可用使用 Model Fallback 或 Model Retry。外部 API 有短暂网络错误使用 Tool Retry想把最终异常变成模型可见的受控消息再组合 Tool Error。输入、输出或流式通道可能泄露邮箱、卡号与密钥使用 PII Detection。高风险工具必须等人批准使用上一篇的 Human-in-the-loop。把这些问题按行与对应 Built-in 对齐重点是先确认失败模式再选择只承担该职责的标准策略。上下文、成本、韧性、隐私和人工审批对应不同失败模式。Built-in Middleware 把这些常见治理问题封装成标准策略模块只有当前风险存在时才加入并为每项配置明确范围与退出行为。PIIMiddleware的redact、mask、hash、block语义不同。客服聊天中的邮箱可以在送入模型前redact信用卡可以mask疑似 API key 可以直接block。开启apply_to_outputTrue时当前文档还支持对流式 wire output 做脱敏这项能力要求对应 LangChain 版本不能只升级示例代码而不核对运行依赖。四、把 Built-in 与 Custom 放进同一个 Agent下面的代码组合四层策略邮箱输入输出脱敏、单次 Agent 运行的模型调用上限、只读订单查询的暂时性错误重试以及一个自定义工具审计 Hook。importosimporttimefromcollections.abcimportCallablefromlangchain.agentsimportcreate_agentfromlangchain.agents.middlewareimport(ModelCallLimitMiddleware,PIIMiddleware,ToolRetryMiddleware,wrap_tool_call,)fromlangchain.messagesimportToolMessagefromlangchain.toolsimporttoolfromlangchain.tools.tool_nodeimportToolCallRequestfromlangchain_openaiimportChatOpenAIfromlanggraph.typesimportCommand# 作用模拟只读订单查询真实实现应设置超时并返回受控字段。tooldefget_order(order_id:str)-str:returnf订单{order_id}当前状态为已支付。# 作用记录每次工具尝试的名称、结果和耗时不修改工具返回值。wrap_tool_calldefaudit_tool_call(request:ToolCallRequest,handler:Callable[[ToolCallRequest],ToolMessage|Command],)-ToolMessage|Command:started_attime.perf_counter()tool_namerequest.tool_call[name]try:resulthandler(request)print(ftool{tool_name}statussuccess)returnresultexceptException:print(ftool{tool_name}statuserror)raisefinally:elapsed_ms(time.perf_counter()-started_at)*1000print(ftool{tool_name}elapsed_ms{elapsed_ms:.1f})modelChatOpenAI(modelqwen3.7-plus,api_keyos.environ[MODEL_API_KEY],base_urlos.environ[MODEL_BASE_URL],)agentcreate_agent(modelmodel,tools[get_order],middleware[PIIMiddleware(email,strategyredact,apply_to_inputTrue,apply_to_outputTrue,),ModelCallLimitMiddleware(run_limit6,exit_behaviorend),ToolRetryMiddleware(max_retries2,tools[get_order],retry_on(ConnectionError,TimeoutError),on_failurecontinue,),audit_tool_call,],)resultagent.invoke({messages:[{role:user,content:我的邮箱是 userexample.com请查询订单 A-2048,}]})print(result[messages][-1].content)PII 层在内容进入和离开 Agent 时处理邮箱模型调用上限阻止一次运行无限循环ToolRetryMiddleware只重试get_order的连接和超时异常audit_tool_call包住每次真实工具尝试并保留异常传播。请求先经过 PII 处理Agent loop 受模型调用上限约束进入get_order后Retry 决定是否再次调用 handlerCustom Audit 记录每次尝试。每层只承担一个横切职责订单事实仍由工具连接的业务服务提供。这里故意只给只读查询加重试。若refund_order没有幂等键简单套用 Tool Retry 可能提交多次退款。Middleware 能发起多次 handler 调用却不能替外部系统生成正确的幂等合同。五、Handler 调用次数就是行为语义Wrap-style 的关键不在装饰器写法而在 handler 被调用几次。调用零次表示短路。缓存命中、策略阻断或已有结果时Middleware 可以直接返回不访问真实模型或工具。调用一次是正常路径。Middleware 可以先用request.override(...)生成新请求再把它交给 handler。请求对象应通过官方覆盖接口修改避免原地变更影响其他层。调用多次表示重试、候选比较或降级。每次 handler 都可能触发真实成本与副作用必须限定异常类型、最大次数、退避和最终失败行为。零次 handler 对应短路一次对应正常调用多次对应重试或降级。调用次数不是实现细节它直接决定外部请求次数、成本、日志数量和副作用风险。如果 Custom Middleware 只想监控不应吞掉异常或改变结果如果想重试优先使用 Built-in Retry并把可重试异常与副作用幂等写清。只有 Built-in 无法表达业务条件时才值得自己控制 handler。六、多 Middleware 的顺序会改变结果Middleware 列表不是无序集合。对middleware[m1, m2, m3]before_*按m1 → m2 → m3运行。wrap_*像函数调用一样嵌套m1包住m2m2再包住m3和真实调用。after_*按m3 → m2 → m1反向运行。将三段顺序放在同一条进入—调用—退出路径中外层与内层各自能观察到什么就会变得明确。Before 从外到内依次进入Wrap 形成嵌套调用栈After 再由内向外退出。外层能观察后续层的整体结果内层只能看到更靠近真实调用的请求与异常。改变列表顺序会改变日志覆盖范围、异常由谁接住以及短路发生在哪一层。例如审计放在 Retry 外层时一次业务调用可能只记录一个最终结果审计放在 Retry 内层时每次重试尝试都能留下记录。两种都可能合理但必须由审计目标决定而不是碰巧写成某个顺序。官方 Built-in 文档也给出了 Tool Retry 与 Tool Error 的组合要求Retry 耗尽后要把异常继续交给 Error 层Error 再将其转换为受控ToolMessage。若前一层提前把错误吞成普通字符串后一层就失去判断依据。七、Custom Middleware 只补业务差异装饰器适合单一、无复杂状态的 Hook继承AgentMiddleware更适合需要初始化配置、自定义 State schema、stream transformer 或多个 Hook 的可复用策略。无论哪种写法都应满足三个边界。第一Middleware 只保存横切策略不把订单计算、权限判定等权威业务逻辑搬出服务端。第二request、State 和返回值的修改使用明确接口并可被测试。第三短路、重试、跳转和异常处理都要有可观察结果。Custom Middleware 最常见的合理用途是补充组织特有的审计字段、租户路由、动态工具筛选或内部策略服务接入。若代码只是在重新实现邮箱正则、通用指数退避或模型调用计数应先回头检查 Built-in 是否已经覆盖。八、上线前按四步检查组合一套 Middleware 组合可以按以下顺序审查。先列失败模式究竟要处理隐私、成本、上下文、暂时性异常还是高风险副作用。没有风险对象就不要先选类名。再选 Built-in确认配置范围、版本要求、退出行为和异常类型。通用能力优先复用官方实现。然后补 Custom只实现 Built-in 无法表达的业务差异并写清 handler 调用次数、request 修改和 State 更新。最后验证顺序为正常、短路、重试耗尽、异常、流式输出和副作用工具分别建立测试确认每一层看到的请求与结果符合预期。生产决策从失败模式开始依次经过 Built-in 复用、Custom 补差、顺序设计和组合测试。跳过前面的风险定义Middleware 列表越长越难证明实际执行路径。这五步共同形成一份可复现的运行合同每层职责、顺序和异常路径都能被单独验证。总结把顺序当成可测试的运行合同Built-in Middleware 提供已经标准化的治理积木Custom Middleware 提供业务差异的扩展点。两者共同依赖同一套生命周期Node-style 在明确时点执行Wrap-style 通过 handler 控制真实模型或工具调用。判断一层 Middleware 是否合格可以问四个问题它处理哪个失败模式运行在什么 Hookhandler 会调用几次它与相邻层的先后顺序是否有测试。回答不了其中任何一个代码即使能运行也很难证明组合行为安全。到这里Agent 已经能够实时暴露运行、连接外部能力、审批高风险动作并把脱敏、限额和韧性策略接入生命周期。下一步要解决的是另一类缺口模型参数和流程都受控却仍然不知道企业私有资料。下一篇将进入 Knowledge Base 与 RAG追踪 Document、Embedding、Vector Store 和 Retriever 怎样把外部知识送进模型上下文。