news 2026/7/21 21:41:43

深入学 LangChain 官方文档(十六)Built-in 与 Custom Middleware

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入学 LangChain 官方文档(十六)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_agentafter_agent包住一次完整 invocation;before_modelafter_model会随循环重复;wrap_model_callwrap_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 像执行路径上的检查站,到点运行并可更新 State;Wrap-style 像包裹真实调用的控制层,可以短路、变换请求、调用一次或多次 handler。前者回答“在哪个时点检查”,后者回答“这次调用怎样发生”。

只想在 Agent 开始时验证租户状态,不需要包住每次模型调用;想实现模型降级或缓存,必须控制 handler,单纯before_model无法接住异常并再次调用另一个模型。

三、先按问题选择 Built-in

LangChain 已提供一组 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 把这些常见治理问题封装成标准策略模块;只有当前风险存在时才加入,并为每项配置明确范围与退出行为。

PIIMiddlewareredactmaskhashblock语义不同。客服聊天中的邮箱可以在送入模型前redact,信用卡可以mask,疑似 API key 可以直接block。开启apply_to_output=True时,当前文档还支持对流式 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_at=time.perf_counter()tool_name=request.tool_call["name"]try:result=handler(request)print(f"tool={tool_name}status=success")returnresultexceptException:print(f"tool={tool_name}status=error")raisefinally:elapsed_ms=(time.perf_counter()-started_at)*1000print(f"tool={tool_name}elapsed_ms={elapsed_ms:.1f}")model=ChatOpenAI(model="qwen3.7-plus",api_key=os.environ["MODEL_API_KEY"],base_url=os.environ["MODEL_BASE_URL"],)agent=create_agent(model=model,tools=[get_order],middleware=[PIIMiddleware("email",strategy="redact",apply_to_input=True,apply_to_output=True,),ModelCallLimitMiddleware(run_limit=6,exit_behavior="end"),ToolRetryMiddleware(max_retries=2,tools=["get_order"],retry_on=(ConnectionError,TimeoutError),on_failure="continue",),audit_tool_call,],)result=agent.invoke({"messages":[{"role":"user","content":"我的邮箱是 user@example.com,请查询订单 A-2048",}]})print(result["messages"][-1].content)

PII 层在内容进入和离开 Agent 时处理邮箱;模型调用上限阻止一次运行无限循环;ToolRetryMiddleware只重试get_order的连接和超时异常;audit_tool_call包住每次真实工具尝试并保留异常传播。


请求先经过 PII 处理,Agent loop 受模型调用上限约束;进入get_order后,Retry 决定是否再次调用 handler,Custom 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 是否合格,可以问四个问题:它处理哪个失败模式,运行在什么 Hook,handler 会调用几次,它与相邻层的先后顺序是否有测试。回答不了其中任何一个,代码即使能运行,也很难证明组合行为安全。

到这里,Agent 已经能够实时暴露运行、连接外部能力、审批高风险动作,并把脱敏、限额和韧性策略接入生命周期。下一步要解决的是另一类缺口:模型参数和流程都受控,却仍然不知道企业私有资料。下一篇将进入 Knowledge Base 与 RAG,追踪 Document、Embedding、Vector Store 和 Retriever 怎样把外部知识送进模型上下文。

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

2026最新类似Kimi Work企业Agent深度横评:哪款更适合国内团队协作?

作为一名企业AI工具选型顾问,我最近半年帮不下十家企业做了办公Agent工具的调研对比,不少客户都问我有没有类似Kimi Work的企业Agent推荐,经过一个多月的实测体验,我最终选择了字节跳动出品的TRAE Work作为企业团队的优先推荐方案…

作者头像 李华
网站建设 2026/7/21 21:34:15

TMS320F2807x USB端点寄存器深度解析与DMA传输实战

1. 项目概述与核心价值搞嵌入式USB开发,特别是用TI的C2000系列这类实时微控制器,最头疼的往往不是写业务逻辑,而是跟那一大堆控制器寄存器打交道。手册里每个位域都认识,连起来就不知道该怎么配了。尤其是USB这种协议栈相对复杂的…

作者头像 李华
网站建设 2026/7/21 21:27:45

数学不必独尊一套公理:论将1归为质数的合理性

数学不必独尊一套公理:论将1归为质数的合理性 在数学史上,“1是否为质数”并非一个自古不变的定论,而是一个经历了长期演变与争议的问题。古希腊数学家如欧几里得在《几何原本》中定义质数为“只能被一个单位所量尽者”,这一定义并…

作者头像 李华
网站建设 2026/7/21 21:26:53

mba硕士毕业论文格式模板范文

mba硕士毕业论文格式模板范文 凌晨三点,你对着电脑屏幕,第N次打开那个名为“MBA论文初稿”的空白文档。导师上周刚毙了你的选题,理由是“缺乏创新性,像十年前的研究”。你翻遍了知网,感觉所有能写的方向都被人写烂了&…

作者头像 李华
网站建设 2026/7/21 21:26:23

在AI编程时代,程序员的核心能力到底是什么

开篇:一场让我后背发凉的对比 2025年冬天,我去了一家做Google Cloud AI解决方案的创业公司做技术咨询。 公司里有个资深工程师,做了六年Java后端,架构能力很强。老板给他布置了个任务:给客户写一个多租户的计费系统&am…

作者头像 李华