
文章目录
- [LangChain 内置中间件详解](#LangChain 内置中间件详解)
-
- [Agent 循环与中间件钩子](#Agent 循环与中间件钩子)
- 内置中间件总览
- 内置中间件详解
-
- [SummarizationMiddleware --- 对话摘要](#SummarizationMiddleware — 对话摘要)
- [HumanInTheLoopMiddleware --- 人工审批](#HumanInTheLoopMiddleware — 人工审批)
- [ModelCallLimitMiddleware --- 模型调用次数限制](#ModelCallLimitMiddleware — 模型调用次数限制)
- [ToolCallLimitMiddleware --- 工具调用次数限制](#ToolCallLimitMiddleware — 工具调用次数限制)
- [ModelFallbackMiddleware --- 模型降级](#ModelFallbackMiddleware — 模型降级)
- [PIIMiddleware --- PII 检测与脱敏](#PIIMiddleware — PII 检测与脱敏)
- [LLMToolSelectorMiddleware --- LLM 工具选择器](#LLMToolSelectorMiddleware — LLM 工具选择器)
- [ToolRetryMiddleware --- 工具调用重试](#ToolRetryMiddleware — 工具调用重试)
- [ModelRetryMiddleware --- 模型调用重试](#ModelRetryMiddleware — 模型调用重试)
- [ContextEditingMiddleware --- 上下文编辑](#ContextEditingMiddleware — 上下文编辑)
- [TodoListMiddleware --- 待办列表](#TodoListMiddleware — 待办列表)
- [ShellToolMiddleware --- Shell 工具](#ShellToolMiddleware — Shell 工具)
- [FilesystemFileSearchMiddleware --- 文件搜索](#FilesystemFileSearchMiddleware — 文件搜索)
- [FilesystemMiddleware --- 文件系统](#FilesystemMiddleware — 文件系统)
- [SubagentMiddleware --- 子 Agent](#SubagentMiddleware — 子 Agent)
- [ProviderToolSearchMiddleware --- Provider 工具搜索](#ProviderToolSearchMiddleware — Provider 工具搜索)
- 自定义中间件开发
- [中间件与 LangGraph 工作流集成](#中间件与 LangGraph 工作流集成)
- [Callbacks 和 Middleware 怎么选](#Callbacks 和 Middleware 怎么选)
LangChain 内置中间件详解
LangChain 的中间件系统让你在 Agent 执行的每一步插入自定义逻辑。你可以修改消息、阻止调用、重试失败、注入工具------本质上是在 Agent 的"大脑"和"手脚"之间架了一层可控的拦截器。
中间件通过 create_agent() 的 middleware 参数传入,多个中间件会按列表顺序依次执行。
python
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware, HumanInTheLoopMiddleware
agent = create_agent(
model="openai:gpt-4o",
tools=[your_tool],
middleware=[
SummarizationMiddleware(model="gpt-4o-mini"),
HumanInTheLoopMiddleware(interrupt_on={"send_email": True}),
],
)
Agent 循环与中间件钩子
Agent 的核心循环很简单:调用模型,模型决定调哪个工具,执行工具,把结果送回模型,如此往复直到模型不再调工具。中间件在每一步前后都留了钩子。
工具 LLM Middleware Agent User 工具 LLM Middleware Agent User #mermaid-svg-7CRPKf4FY9gyIwYl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7CRPKf4FY9gyIwYl .error-icon{fill:#552222;}#mermaid-svg-7CRPKf4FY9gyIwYl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7CRPKf4FY9gyIwYl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7CRPKf4FY9gyIwYl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7CRPKf4FY9gyIwYl .marker.cross{stroke:#333333;}#mermaid-svg-7CRPKf4FY9gyIwYl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7CRPKf4FY9gyIwYl p{margin:0;}#mermaid-svg-7CRPKf4FY9gyIwYl .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7CRPKf4FY9gyIwYl text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7CRPKf4FY9gyIwYl .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7CRPKf4FY9gyIwYl .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7CRPKf4FY9gyIwYl .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7CRPKf4FY9gyIwYl .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7CRPKf4FY9gyIwYl #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7CRPKf4FY9gyIwYl .sequenceNumber{fill:white;}#mermaid-svg-7CRPKf4FY9gyIwYl #sequencenumber{fill:#333;}#mermaid-svg-7CRPKf4FY9gyIwYl #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7CRPKf4FY9gyIwYl .messageText{fill:#333;stroke:none;}#mermaid-svg-7CRPKf4FY9gyIwYl .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7CRPKf4FY9gyIwYl .labelText,#mermaid-svg-7CRPKf4FY9gyIwYl .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7CRPKf4FY9gyIwYl .loopText,#mermaid-svg-7CRPKf4FY9gyIwYl .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7CRPKf4FY9gyIwYl .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7CRPKf4FY9gyIwYl .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7CRPKf4FY9gyIwYl .noteText,#mermaid-svg-7CRPKf4FY9gyIwYl .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7CRPKf4FY9gyIwYl .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7CRPKf4FY9gyIwYl .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7CRPKf4FY9gyIwYl .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7CRPKf4FY9gyIwYl .actorPopupMenu{position:absolute;}#mermaid-svg-7CRPKf4FY9gyIwYl .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7CRPKf4FY9gyIwYl .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7CRPKf4FY9gyIwYl .actor-man circle,#mermaid-svg-7CRPKf4FY9gyIwYl line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7CRPKf4FY9gyIwYl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} before_agent before_model after_model wrap_tool_call alt 模型请求调用工具 模型返回最终答案 loop Agent 循环 after_agent invoke() 调用模型 返回响应 执行工具 返回结果 返回结果
钩子分两类:
Node 型钩子 --- 在特定时间点运行,返回一个 dict 来更新状态,返回 None 表示什么都不改。
| 钩子 | 运行时机 |
|---|---|
before_agent |
Agent 开始前,每次 invoke 跑一次 |
before_model |
每次模型调用前 |
after_model |
每次模型返回响应后 |
after_agent |
Agent 结束后,每次 invoke 跑一次 |
Wrap 型钩子 --- 包裹模型或工具调用。你可以决定调用 handler 零次(短路)、一次(正常)、或多次(重试)。
| 钩子 | 运行时机 |
|---|---|
wrap_model_call |
包裹每次模型调用 |
wrap_tool_call |
包裹每次工具调用 |
内置中间件总览
#mermaid-svg-oMBvOFlqQFK6d0l7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oMBvOFlqQFK6d0l7 .error-icon{fill:#552222;}#mermaid-svg-oMBvOFlqQFK6d0l7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oMBvOFlqQFK6d0l7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .marker.cross{stroke:#333333;}#mermaid-svg-oMBvOFlqQFK6d0l7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oMBvOFlqQFK6d0l7 p{margin:0;}#mermaid-svg-oMBvOFlqQFK6d0l7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .cluster-label text{fill:#333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .cluster-label span{color:#333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .cluster-label span p{background-color:transparent;}#mermaid-svg-oMBvOFlqQFK6d0l7 .label text,#mermaid-svg-oMBvOFlqQFK6d0l7 span{fill:#333;color:#333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .node rect,#mermaid-svg-oMBvOFlqQFK6d0l7 .node circle,#mermaid-svg-oMBvOFlqQFK6d0l7 .node ellipse,#mermaid-svg-oMBvOFlqQFK6d0l7 .node polygon,#mermaid-svg-oMBvOFlqQFK6d0l7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oMBvOFlqQFK6d0l7 .rough-node .label text,#mermaid-svg-oMBvOFlqQFK6d0l7 .node .label text,#mermaid-svg-oMBvOFlqQFK6d0l7 .image-shape .label,#mermaid-svg-oMBvOFlqQFK6d0l7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-oMBvOFlqQFK6d0l7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oMBvOFlqQFK6d0l7 .rough-node .label,#mermaid-svg-oMBvOFlqQFK6d0l7 .node .label,#mermaid-svg-oMBvOFlqQFK6d0l7 .image-shape .label,#mermaid-svg-oMBvOFlqQFK6d0l7 .icon-shape .label{text-align:center;}#mermaid-svg-oMBvOFlqQFK6d0l7 .node.clickable{cursor:pointer;}#mermaid-svg-oMBvOFlqQFK6d0l7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .arrowheadPath{fill:#333333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oMBvOFlqQFK6d0l7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oMBvOFlqQFK6d0l7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oMBvOFlqQFK6d0l7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oMBvOFlqQFK6d0l7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oMBvOFlqQFK6d0l7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oMBvOFlqQFK6d0l7 .cluster text{fill:#333;}#mermaid-svg-oMBvOFlqQFK6d0l7 .cluster span{color:#333;}#mermaid-svg-oMBvOFlqQFK6d0l7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oMBvOFlqQFK6d0l7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oMBvOFlqQFK6d0l7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-oMBvOFlqQFK6d0l7 .icon-shape,#mermaid-svg-oMBvOFlqQFK6d0l7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oMBvOFlqQFK6d0l7 .icon-shape p,#mermaid-svg-oMBvOFlqQFK6d0l7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oMBvOFlqQFK6d0l7 .icon-shape .label rect,#mermaid-svg-oMBvOFlqQFK6d0l7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oMBvOFlqQFK6d0l7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oMBvOFlqQFK6d0l7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oMBvOFlqQFK6d0l7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 能力扩展
TodoListMiddleware
待办列表
LLMToolSelectorMiddleware
LLM 工具选择
ShellToolMiddleware
Shell 工具
FilesystemFileSearchMiddleware
文件搜索
FilesystemMiddleware
文件系统
SubagentMiddleware
子 Agent
ProviderToolSearchMiddleware
Provider 工具搜索
安全合规
PIIMiddleware
PII 检测
HumanInTheLoopMiddleware
人工审批
可靠性保障
ModelFallbackMiddleware
模型降级
ModelRetryMiddleware
模型重试
ToolRetryMiddleware
工具重试
开发者防护
SummarizationMiddleware
对话摘要
ContextEditingMiddleware
上下文编辑
ModelCallLimitMiddleware
模型调用限制
ToolCallLimitMiddleware
工具调用限制
内置中间件详解
SummarizationMiddleware --- 对话摘要
对话消息越来越多,Token 消耗越来越大,上下文窗口迟早会爆。SummarizationMiddleware 的做法是:当 Token 数达到阈值,把旧消息送给一个小模型做摘要,摘出来的文本替换掉原始消息,最近的消息保持原样不动。
模块路径:langchain.agents.middleware
Main Model Summary Model Agent User Main Model Summary Model Agent User #mermaid-svg-MJZVRTVJ9CmNIXOI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MJZVRTVJ9CmNIXOI .error-icon{fill:#552222;}#mermaid-svg-MJZVRTVJ9CmNIXOI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MJZVRTVJ9CmNIXOI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MJZVRTVJ9CmNIXOI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MJZVRTVJ9CmNIXOI .marker.cross{stroke:#333333;}#mermaid-svg-MJZVRTVJ9CmNIXOI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MJZVRTVJ9CmNIXOI p{margin:0;}#mermaid-svg-MJZVRTVJ9CmNIXOI .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MJZVRTVJ9CmNIXOI text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-MJZVRTVJ9CmNIXOI .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MJZVRTVJ9CmNIXOI .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-MJZVRTVJ9CmNIXOI .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-MJZVRTVJ9CmNIXOI .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-MJZVRTVJ9CmNIXOI #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-MJZVRTVJ9CmNIXOI .sequenceNumber{fill:white;}#mermaid-svg-MJZVRTVJ9CmNIXOI #sequencenumber{fill:#333;}#mermaid-svg-MJZVRTVJ9CmNIXOI #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-MJZVRTVJ9CmNIXOI .messageText{fill:#333;stroke:none;}#mermaid-svg-MJZVRTVJ9CmNIXOI .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MJZVRTVJ9CmNIXOI .labelText,#mermaid-svg-MJZVRTVJ9CmNIXOI .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-MJZVRTVJ9CmNIXOI .loopText,#mermaid-svg-MJZVRTVJ9CmNIXOI .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-MJZVRTVJ9CmNIXOI .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MJZVRTVJ9CmNIXOI .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-MJZVRTVJ9CmNIXOI .noteText,#mermaid-svg-MJZVRTVJ9CmNIXOI .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-MJZVRTVJ9CmNIXOI .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MJZVRTVJ9CmNIXOI .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MJZVRTVJ9CmNIXOI .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MJZVRTVJ9CmNIXOI .actorPopupMenu{position:absolute;}#mermaid-svg-MJZVRTVJ9CmNIXOI .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-MJZVRTVJ9CmNIXOI .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MJZVRTVJ9CmNIXOI .actor-man circle,#mermaid-svg-MJZVRTVJ9CmNIXOI line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-MJZVRTVJ9CmNIXOI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt Token 达到阈值 发送消息 检查 Token 是否达到 trigger 阈值 保留最近 N 条消息(keep) 旧消息文本 → 请求摘要 返回摘要 摘要替换旧消息,注入上下文 发送压缩后的上下文 响应 返回结果
这个中间件只压缩文本。图片、音频、视频不会被压缩或缩放。keep 参数保留的最近消息仍然包含原始的多模态数据,被摘要替代的旧消息里多模态内容会随摘要丢失。
构造参数:
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
model |
`str | BaseChatModel` | 是 |
trigger |
`ContextSize | TriggerClause | list |
keep |
ContextSize |
否 | 摘要后保留多少条最近的消息原文。默认 ("messages", 20)。同样支持 tokens、messages、fraction 三种度量 |
token_counter |
function |
否 | 自定义 Token 计数函数,默认按字符数估算。需要精确计数可以传 tiktoken 编码器 |
summary_prompt |
str |
否 | 自定义摘要 Prompt,模板里要有 {messages} 占位符,对话历史会插入到这个位置 |
trim_tokens_to_summarize |
int |
否 | 送去摘要的消息最多包含多少 Token,默认 4000。超过就截断 |
trigger 条件组合逻辑:
单个条件最简单:("tokens", 4000) 表示 Token 数 >= 4000 就触发。("messages", 50) 表示消息数 >= 50 就触发。("fraction", 0.8) 表示达到模型上下文窗口 80% 时触发,这个需要 langchain>=1.1 且模型有 profile 数据。
多个条件可以组合。{"tokens": 4000, "messages": 10} 里的条件是 AND 关系------Token >= 4000 且消息 >= 10 才触发。[("tokens", 3000), ("messages", 50)] 是 OR 关系------Token >= 3000 或消息 >= 50 任一满足就触发。还可以混合:[{"tokens": 5000, "messages": 3}, {"tokens": 3000, "messages": 6}] 表示两个 AND 子句之间是 OR。
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
# 基本用法:Token 超过 4000 时自动摘要,保留最近 20 条消息
agent = create_agent(
model="openai:gpt-4o",
tools=[weather_tool, calculator_tool],
middleware=[
SummarizationMiddleware(
model="openai:gpt-4o-mini",
trigger=("tokens", 4000),
keep=("messages", 20),
),
],
)
# 进阶:AND/OR 组合 + 比例保留
agent = create_agent(
model="openai:gpt-4o",
tools=[...],
middleware=[
SummarizationMiddleware(
model="openai:gpt-4o-mini",
trigger=[
{"tokens": 5000, "messages": 3},
{"tokens": 3000, "messages": 6},
],
keep=("fraction", 0.3),
),
],
)
HumanInTheLoopMiddleware --- 人工审批
有些操作不能全交给 Agent 自动执行------发邮件、删数据、转账。HumanInTheLoopMiddleware 在 Agent 准备调用特定工具时暂停,等人来审批,人可以批准、修改参数、或者直接拒绝。
前提:必须配 checkpointer,否则中断状态没法保存。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 说明 |
|---|---|---|
interrupt_on |
dict |
工具名到审批配置的映射。值可以是 True(需要审批)、False(不需要)、或 {"allowed_decisions": ["approve", "edit", "reject"]} 指定允许的操作 |
Tool Human HITL Middleware Agent Tool Human HITL Middleware Agent #mermaid-svg-Sxe3ySXBTmJxIAj5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .error-icon{fill:#552222;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .marker.cross{stroke:#333333;}#mermaid-svg-Sxe3ySXBTmJxIAj5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sxe3ySXBTmJxIAj5 p{margin:0;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sxe3ySXBTmJxIAj5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Sxe3ySXBTmJxIAj5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Sxe3ySXBTmJxIAj5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .sequenceNumber{fill:white;}#mermaid-svg-Sxe3ySXBTmJxIAj5 #sequencenumber{fill:#333;}#mermaid-svg-Sxe3ySXBTmJxIAj5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .messageText{fill:#333;stroke:none;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .labelText,#mermaid-svg-Sxe3ySXBTmJxIAj5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .loopText,#mermaid-svg-Sxe3ySXBTmJxIAj5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Sxe3ySXBTmJxIAj5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .noteText,#mermaid-svg-Sxe3ySXBTmJxIAj5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .actorPopupMenu{position:absolute;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Sxe3ySXBTmJxIAj5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sxe3ySXBTmJxIAj5 .actor-man circle,#mermaid-svg-Sxe3ySXBTmJxIAj5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Sxe3ySXBTmJxIAj5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 批准 编辑 拒绝 准备调用 send_email 检查 interrupt_on 配置 暂停执行,等待审批 展示工具调用详情 approve / edit / reject 执行 send_email 返回结果 用修改后的参数执行 返回结果 返回拒绝信息
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
def read_email(email_id: str) -> str:
return f"邮件内容: {email_id}"
def send_email(recipient: str, subject: str, body: str) -> str:
return f"已发送给 {recipient}"
agent = create_agent(
model="openai:gpt-4o",
tools=[read_email, send_email],
checkpointer=InMemorySaver(),
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": {
"allowed_decisions": ["approve", "edit", "reject"],
},
"read_email": False,
},
),
],
)
# Agent 调用 send_email 时会中断,等人通过 Command 对象审批
ModelCallLimitMiddleware --- 模型调用次数限制
Agent 有时候会陷入死循环,反复调用模型停不下来。ModelCallLimitMiddleware 给模型调用次数设上限,到了就停,防止费用失控。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
thread_limit |
int |
无限制 | 一个对话线程(同一个 thread_id)里最多调用多少次模型。跨多次 invoke 累计计数,需要 checkpointer |
run_limit |
int |
无限制 | 单次 invoke 里最多调用多少次模型。每次 invoke 从零开始计数 |
exit_behavior |
str |
"end" |
达到限制后的行为。"end" 优雅终止,注入一条提示消息。"error" 抛出 ModelCallLimitExceededError 异常 |
thread_limit 和 run_limit 可以同时设。thread_limit 跨 invoke 累积,靠 checkpointer 持久化计数器。run_limit 每次 invoke 独立计数。
#mermaid-svg-YIEtPSKPbdFduey8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YIEtPSKPbdFduey8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YIEtPSKPbdFduey8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YIEtPSKPbdFduey8 .error-icon{fill:#552222;}#mermaid-svg-YIEtPSKPbdFduey8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YIEtPSKPbdFduey8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YIEtPSKPbdFduey8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YIEtPSKPbdFduey8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YIEtPSKPbdFduey8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YIEtPSKPbdFduey8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YIEtPSKPbdFduey8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YIEtPSKPbdFduey8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YIEtPSKPbdFduey8 .marker.cross{stroke:#333333;}#mermaid-svg-YIEtPSKPbdFduey8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YIEtPSKPbdFduey8 p{margin:0;}#mermaid-svg-YIEtPSKPbdFduey8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YIEtPSKPbdFduey8 .cluster-label text{fill:#333;}#mermaid-svg-YIEtPSKPbdFduey8 .cluster-label span{color:#333;}#mermaid-svg-YIEtPSKPbdFduey8 .cluster-label span p{background-color:transparent;}#mermaid-svg-YIEtPSKPbdFduey8 .label text,#mermaid-svg-YIEtPSKPbdFduey8 span{fill:#333;color:#333;}#mermaid-svg-YIEtPSKPbdFduey8 .node rect,#mermaid-svg-YIEtPSKPbdFduey8 .node circle,#mermaid-svg-YIEtPSKPbdFduey8 .node ellipse,#mermaid-svg-YIEtPSKPbdFduey8 .node polygon,#mermaid-svg-YIEtPSKPbdFduey8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YIEtPSKPbdFduey8 .rough-node .label text,#mermaid-svg-YIEtPSKPbdFduey8 .node .label text,#mermaid-svg-YIEtPSKPbdFduey8 .image-shape .label,#mermaid-svg-YIEtPSKPbdFduey8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-YIEtPSKPbdFduey8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YIEtPSKPbdFduey8 .rough-node .label,#mermaid-svg-YIEtPSKPbdFduey8 .node .label,#mermaid-svg-YIEtPSKPbdFduey8 .image-shape .label,#mermaid-svg-YIEtPSKPbdFduey8 .icon-shape .label{text-align:center;}#mermaid-svg-YIEtPSKPbdFduey8 .node.clickable{cursor:pointer;}#mermaid-svg-YIEtPSKPbdFduey8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YIEtPSKPbdFduey8 .arrowheadPath{fill:#333333;}#mermaid-svg-YIEtPSKPbdFduey8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YIEtPSKPbdFduey8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YIEtPSKPbdFduey8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YIEtPSKPbdFduey8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YIEtPSKPbdFduey8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YIEtPSKPbdFduey8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YIEtPSKPbdFduey8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YIEtPSKPbdFduey8 .cluster text{fill:#333;}#mermaid-svg-YIEtPSKPbdFduey8 .cluster span{color:#333;}#mermaid-svg-YIEtPSKPbdFduey8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YIEtPSKPbdFduey8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YIEtPSKPbdFduey8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-YIEtPSKPbdFduey8 .icon-shape,#mermaid-svg-YIEtPSKPbdFduey8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YIEtPSKPbdFduey8 .icon-shape p,#mermaid-svg-YIEtPSKPbdFduey8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YIEtPSKPbdFduey8 .icon-shape .label rect,#mermaid-svg-YIEtPSKPbdFduey8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YIEtPSKPbdFduey8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YIEtPSKPbdFduey8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YIEtPSKPbdFduey8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} run_limit 计数器重置
run_limit 计数器重置
thread_limit 计数器累积
thread_limit 计数器累积
invoke 1
LLM 调用 2 次
invoke 2
LLM 调用 2 次
invoke 3
❌ 超限
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
return f"{city}: 晴,25°C"
def calculate(expression: str) -> str:
return str(eval(expression))
# run_limit=2:单次 invoke 最多 2 次 LLM 调用
# 第1次调用 → 决定调工具 → 第2次调用 → 总结回复,刚好用完
agent_normal = create_agent(
model=model,
tools=[get_weather],
checkpointer=InMemorySaver(),
middleware=[
ModelCallLimitMiddleware(run_limit=2, exit_behavior="end"),
],
)
result = agent_normal.invoke(
{"messages": [{"role": "user", "content": "北京今天天气怎么样?"}]},
config={"configurable": {"thread_id": "run-normal"}},
)
print(result["messages"][-1].content)
# run_limit=1:第2次 LLM 调用时被截断
agent_cut = create_agent(
model=model,
tools=[get_weather],
checkpointer=InMemorySaver(),
middleware=[
ModelCallLimitMiddleware(run_limit=1, exit_behavior="end"),
],
)
result = agent_cut.invoke(
{"messages": [{"role": "user", "content": "北京今天天气怎么样?"}]},
config={"configurable": {"thread_id": "run-cut"}},
)
# 第1次 LLM 调用 → 决定调工具 → 工具执行 → 第2次 LLM 调用被拦截
# thread_limit=3:跨 invoke 累计
agent_thread = create_agent(
model=model,
tools=[get_weather],
checkpointer=InMemorySaver(),
middleware=[
ModelCallLimitMiddleware(thread_limit=3, exit_behavior="end"),
],
)
config = {"configurable": {"thread_id": "thread-demo"}}
# invoke-1:消耗约 2 次调用(决定工具 + 总结),剩余 1
agent_thread.invoke(
{"messages": [{"role": "user", "content": "北京天气怎么样?"}]}, config=config
)
# invoke-2:消耗约 2 次,累计 4 > 3,超限
agent_thread.invoke(
{"messages": [{"role": "user", "content": "上海呢?"}]}, config=config
)
# exit_behavior="error":超限抛异常
agent_error = create_agent(
model=model,
tools=[get_weather],
checkpointer=InMemorySaver(),
middleware=[
ModelCallLimitMiddleware(run_limit=1, exit_behavior="error"),
],
)
try:
agent_error.invoke(
{"messages": [{"role": "user", "content": "北京天气怎么样?"}]},
config={"configurable": {"thread_id": "error-demo"}},
)
except Exception as e:
print(f"捕获异常: {type(e).__name__}: {e}")
ToolCallLimitMiddleware --- 工具调用次数限制
跟 ModelCallLimitMiddleware 同一思路,但限制的是工具调用次数。可以全局限制所有工具,也可以只限制某个工具。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
tool_name |
str |
无(全局限制) | 要限制的工具名。不传则限制所有工具 |
thread_limit |
int |
无限制 | 一个对话线程内最多调用多少次(需要 checkpointer) |
run_limit |
int |
无限制 | 单次 invoke 内最多调用多少次 |
exit_behavior |
str |
"continue" |
达到限制后怎么做。"continue" 阻止超限调用但让其他工具继续,"error" 抛异常,"end" 立即停止(仅限单个工具时) |
exit_behavior 的三个选项差异很大。"continue" 最温和------阻止超限调用,但 Agent 还能继续,它可以根据错误信息调整策略。"error" 直接中断执行。"end" 只适用于限制了单个工具的场景,多工具同时待调用时会报 NotImplementedError。
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware
from langgraph.checkpoint.memory import InMemorySaver
def get_weather(city: str) -> str:
return f"{city}: 晴,25°C"
def calculate(expression: str) -> str:
return str(eval(expression))
# 只限制 get_weather,calculate 不受影响
agent = create_agent(
model=model,
tools=[get_weather, calculate],
middleware=[
ToolCallLimitMiddleware(tool_name="get_weather", run_limit=1),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "查北京天气、查上海天气、算 1+1"}]}
)
# get_weather 第2次被拦,calculate 正常执行
# 全局限制:所有工具加起来最多 1 次
agent_run = create_agent(
model=model,
tools=[get_weather],
middleware=[ToolCallLimitMiddleware(run_limit=1)],
)
result = agent_run.invoke(
{"messages": [{"role": "user", "content": "分别查北京和上海天气"}]}
)
# thread_limit:跨 invoke 累计
agent_th = create_agent(
model=model,
tools=[get_weather],
checkpointer=InMemorySaver(),
middleware=[ToolCallLimitMiddleware(thread_limit=2)],
)
cfg = {"configurable": {"thread_id": "th-1"}}
agent_th.invoke({"messages": [{"role": "user", "content": "查北京天气"}]}, config=cfg)
agent_th.invoke({"messages": [{"role": "user", "content": "查上海天气"}]}, config=cfg)
# 第三次 invoke 时超限被拦截
# exit_behavior="error"
agent_err = create_agent(
model=model,
tools=[get_weather],
middleware=[ToolCallLimitMiddleware(run_limit=1, exit_behavior="error")],
)
try:
agent_err.invoke(
{"messages": [{"role": "user", "content": "分别查北京和上海天气"}]}
)
except Exception as e:
print(f"{type(e).__name__}: {e}")
# exit_behavior="end"
agent_end = create_agent(
model=model,
tools=[get_weather],
middleware=[ToolCallLimitMiddleware(run_limit=1, exit_behavior="end")],
)
agent_end.invoke(
{"messages": [{"role": "user", "content": "分别查北京和上海天气"}]}
)
ModelFallbackMiddleware --- 模型降级
主模型挂了怎么办?ModelFallbackMiddleware 按顺序尝试备选模型,一个不行换下一个,都失败才抛异常。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 说明 |
|---|---|---|
first_model |
`str | BaseChatModel` |
*additional_models |
`str | BaseChatModel` |
#mermaid-svg-S0wq2cdrqKUauWfC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-S0wq2cdrqKUauWfC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S0wq2cdrqKUauWfC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S0wq2cdrqKUauWfC .error-icon{fill:#552222;}#mermaid-svg-S0wq2cdrqKUauWfC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S0wq2cdrqKUauWfC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S0wq2cdrqKUauWfC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S0wq2cdrqKUauWfC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S0wq2cdrqKUauWfC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S0wq2cdrqKUauWfC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S0wq2cdrqKUauWfC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S0wq2cdrqKUauWfC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S0wq2cdrqKUauWfC .marker.cross{stroke:#333333;}#mermaid-svg-S0wq2cdrqKUauWfC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S0wq2cdrqKUauWfC p{margin:0;}#mermaid-svg-S0wq2cdrqKUauWfC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-S0wq2cdrqKUauWfC .cluster-label text{fill:#333;}#mermaid-svg-S0wq2cdrqKUauWfC .cluster-label span{color:#333;}#mermaid-svg-S0wq2cdrqKUauWfC .cluster-label span p{background-color:transparent;}#mermaid-svg-S0wq2cdrqKUauWfC .label text,#mermaid-svg-S0wq2cdrqKUauWfC span{fill:#333;color:#333;}#mermaid-svg-S0wq2cdrqKUauWfC .node rect,#mermaid-svg-S0wq2cdrqKUauWfC .node circle,#mermaid-svg-S0wq2cdrqKUauWfC .node ellipse,#mermaid-svg-S0wq2cdrqKUauWfC .node polygon,#mermaid-svg-S0wq2cdrqKUauWfC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S0wq2cdrqKUauWfC .rough-node .label text,#mermaid-svg-S0wq2cdrqKUauWfC .node .label text,#mermaid-svg-S0wq2cdrqKUauWfC .image-shape .label,#mermaid-svg-S0wq2cdrqKUauWfC .icon-shape .label{text-anchor:middle;}#mermaid-svg-S0wq2cdrqKUauWfC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-S0wq2cdrqKUauWfC .rough-node .label,#mermaid-svg-S0wq2cdrqKUauWfC .node .label,#mermaid-svg-S0wq2cdrqKUauWfC .image-shape .label,#mermaid-svg-S0wq2cdrqKUauWfC .icon-shape .label{text-align:center;}#mermaid-svg-S0wq2cdrqKUauWfC .node.clickable{cursor:pointer;}#mermaid-svg-S0wq2cdrqKUauWfC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-S0wq2cdrqKUauWfC .arrowheadPath{fill:#333333;}#mermaid-svg-S0wq2cdrqKUauWfC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-S0wq2cdrqKUauWfC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-S0wq2cdrqKUauWfC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S0wq2cdrqKUauWfC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-S0wq2cdrqKUauWfC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S0wq2cdrqKUauWfC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-S0wq2cdrqKUauWfC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S0wq2cdrqKUauWfC .cluster text{fill:#333;}#mermaid-svg-S0wq2cdrqKUauWfC .cluster span{color:#333;}#mermaid-svg-S0wq2cdrqKUauWfC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-S0wq2cdrqKUauWfC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-S0wq2cdrqKUauWfC rect.text{fill:none;stroke-width:0;}#mermaid-svg-S0wq2cdrqKUauWfC .icon-shape,#mermaid-svg-S0wq2cdrqKUauWfC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S0wq2cdrqKUauWfC .icon-shape p,#mermaid-svg-S0wq2cdrqKUauWfC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-S0wq2cdrqKUauWfC .icon-shape .label rect,#mermaid-svg-S0wq2cdrqKUauWfC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S0wq2cdrqKUauWfC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-S0wq2cdrqKUauWfC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-S0wq2cdrqKUauWfC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 失败
失败
失败
成功
成功
成功
主模型
gpt-4o
备选 1
gpt-4o-mini
备选 2
claude-3.5-sonnet
抛出异常
返回结果
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelFallbackMiddleware
agent = create_agent(
model="openai:gpt-4o",
tools=[...],
middleware=[
ModelFallbackMiddleware(
"openai:gpt-4o-mini",
"claude-3-5-sonnet-20241022",
),
],
)
PIIMiddleware --- PII 检测与脱敏
PIIMiddleware 在消息传递过程中检测邮箱、信用卡号、IP 地址、API Key 等敏感信息,按你选择的策略处理------遮盖、哈希、删除、或直接拦截。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pii_type |
str |
必需(位置参数) | PII 类型名称。内置类型有 "email"、"credit_card"、"ip"、"mac_address"、"url"。自定义类型可以随便命名 |
detector |
`str | Callable` | None |
strategy |
str |
"mask" |
脱敏策略。"mask" 部分遮盖(z***@e***.com),"redact" 完全替换为 [REDACTED],"hash" 哈希处理(同值同哈希,可跨记录关联),"block" 检测到就抛异常阻止 |
apply_to_input |
bool |
True |
是否对用户输入做检测 |
apply_to_output |
bool |
False |
是否对 AI 输出做检测 |
apply_to_tool_results |
bool |
False |
是否对工具返回结果做检测。工具可能返回敏感数据,建议开启 |
内置 PII 类型:
| 类型 | 匹配内容 | 示例 |
|---|---|---|
"email" |
邮箱地址 | user@example.com |
"credit_card" |
信用卡号 | 4532-7891-2345-6789 |
"ip" |
IP 地址 | 192.168.1.100 |
"mac_address" |
MAC 地址 | aa:bb:cc:dd:ee:ff |
"url" |
URL 链接 | https://example.com |
四种策略的效果:
| 策略 | 原始值 | 处理后 | 特点 |
|---|---|---|---|
"mask" |
zhangsan@example.com |
z***@e***.com |
保留部分结构,不可逆 |
"redact" |
13812345678 |
[REDACTED] |
完全不可读 |
"hash" |
123-45-6789 |
a1b2c3d4... |
同值同哈希,可跨记录关联 |
"block" |
sk-abc123... |
抛出异常 | 直接阻止,不给 LLM 看到 |
#mermaid-svg-S6N8lWjijpixUrjN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-S6N8lWjijpixUrjN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S6N8lWjijpixUrjN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S6N8lWjijpixUrjN .error-icon{fill:#552222;}#mermaid-svg-S6N8lWjijpixUrjN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S6N8lWjijpixUrjN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S6N8lWjijpixUrjN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S6N8lWjijpixUrjN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S6N8lWjijpixUrjN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S6N8lWjijpixUrjN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S6N8lWjijpixUrjN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S6N8lWjijpixUrjN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S6N8lWjijpixUrjN .marker.cross{stroke:#333333;}#mermaid-svg-S6N8lWjijpixUrjN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S6N8lWjijpixUrjN p{margin:0;}#mermaid-svg-S6N8lWjijpixUrjN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-S6N8lWjijpixUrjN .cluster-label text{fill:#333;}#mermaid-svg-S6N8lWjijpixUrjN .cluster-label span{color:#333;}#mermaid-svg-S6N8lWjijpixUrjN .cluster-label span p{background-color:transparent;}#mermaid-svg-S6N8lWjijpixUrjN .label text,#mermaid-svg-S6N8lWjijpixUrjN span{fill:#333;color:#333;}#mermaid-svg-S6N8lWjijpixUrjN .node rect,#mermaid-svg-S6N8lWjijpixUrjN .node circle,#mermaid-svg-S6N8lWjijpixUrjN .node ellipse,#mermaid-svg-S6N8lWjijpixUrjN .node polygon,#mermaid-svg-S6N8lWjijpixUrjN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S6N8lWjijpixUrjN .rough-node .label text,#mermaid-svg-S6N8lWjijpixUrjN .node .label text,#mermaid-svg-S6N8lWjijpixUrjN .image-shape .label,#mermaid-svg-S6N8lWjijpixUrjN .icon-shape .label{text-anchor:middle;}#mermaid-svg-S6N8lWjijpixUrjN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-S6N8lWjijpixUrjN .rough-node .label,#mermaid-svg-S6N8lWjijpixUrjN .node .label,#mermaid-svg-S6N8lWjijpixUrjN .image-shape .label,#mermaid-svg-S6N8lWjijpixUrjN .icon-shape .label{text-align:center;}#mermaid-svg-S6N8lWjijpixUrjN .node.clickable{cursor:pointer;}#mermaid-svg-S6N8lWjijpixUrjN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-S6N8lWjijpixUrjN .arrowheadPath{fill:#333333;}#mermaid-svg-S6N8lWjijpixUrjN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-S6N8lWjijpixUrjN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-S6N8lWjijpixUrjN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S6N8lWjijpixUrjN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-S6N8lWjijpixUrjN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S6N8lWjijpixUrjN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-S6N8lWjijpixUrjN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S6N8lWjijpixUrjN .cluster text{fill:#333;}#mermaid-svg-S6N8lWjijpixUrjN .cluster span{color:#333;}#mermaid-svg-S6N8lWjijpixUrjN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-S6N8lWjijpixUrjN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-S6N8lWjijpixUrjN rect.text{fill:none;stroke-width:0;}#mermaid-svg-S6N8lWjijpixUrjN .icon-shape,#mermaid-svg-S6N8lWjijpixUrjN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S6N8lWjijpixUrjN .icon-shape p,#mermaid-svg-S6N8lWjijpixUrjN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-S6N8lWjijpixUrjN .icon-shape .label rect,#mermaid-svg-S6N8lWjijpixUrjN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S6N8lWjijpixUrjN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-S6N8lWjijpixUrjN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-S6N8lWjijpixUrjN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
是
是
否
否
否
消息到达
需要检测?
应用检测规则
检测到 PII?
策略 = block?
抛异常,阻止传递
执行 mask/redact/hash
脱敏后继续
代码示例:
python
import re
from langchain.agents import create_agent
from langchain.agents.middleware import PIIMiddleware
def fetch_ticket(ticket_id: str) -> str:
tickets = {
"T001": (
"工单 T001 --- 用户: zhangsan@example.com,"
"电话: 13812345678,信用卡: 4532-7891-2345-6789,"
"IP: 192.168.1.100,MAC: aa:bb:cc:dd:ee:ff,"
"API Key: sk-abc123def4567890abcdef1234567890,"
"SSN: 123-45-6789"
),
"T002": (
"工单 T002 --- 用户: lisi@company.cn,"
"电话: +86 139-9876-5432,信用卡: 5214-5678-9012-3456,"
"IP: 10.0.0.55,MAC: 00-11-22-33-44-55,"
"API Key: sk-xyz9876543210fedcba09876543210fed,"
"SSN: 987-65-4321"
),
}
return tickets.get(ticket_id, f"[系统] 未找到 {ticket_id}")
# 自定义 SSN 检测函数,带号段校验
def detect_ssn(content: str) -> list[dict[str, str | int]]:
matches = []
pattern = r"\d{3}-\d{2}-\d{4}"
for m in re.finditer(pattern, content):
ssn = m.group(0)
first_three = int(ssn[:3])
if first_three not in [0, 666] and not (900 <= first_three <= 999):
matches.append({"text": ssn, "start": m.start(), "end": m.end()})
return matches
# 内置类型脱敏:email → mask, credit_card → mask
agent_builtin = create_agent(
model=model,
tools=[fetch_ticket],
middleware=[
PIIMiddleware("email", strategy="mask", apply_to_tool_results=True),
PIIMiddleware("credit_card", strategy="mask", apply_to_tool_results=True),
],
)
result = agent_builtin.invoke(
{"messages": [{"role": "user", "content": "查工单 T001,列出关键信息"}]}
)
print(result["messages"][-1].content)
# 正则检测:API Key → block,检测到直接拦截
agent_regex_block = create_agent(
model=model,
tools=[fetch_ticket],
middleware=[
PIIMiddleware(
"api_key",
detector=r"sk-[a-zA-Z0-9]{32}",
strategy="block",
apply_to_tool_results=True,
),
],
)
try:
result = agent_regex_block.invoke(
{"messages": [{"role": "user", "content": "查工单 T001"}]}
)
except Exception as e:
print(f"block 拦截: {type(e).__name__}: {e}")
# 自定义函数:SSN → hash
agent_custom = create_agent(
model=model,
tools=[fetch_ticket],
middleware=[
PIIMiddleware(
"ssn",
detector=detect_ssn,
strategy="hash",
apply_to_tool_results=True,
),
],
)
result = agent_custom.invoke(
{"messages": [{"role": "user", "content": "查工单 T001 和 T002,对比 SSN"}]}
)
print(result["messages"][-1].content)
# 组合多种策略
agent_combo = create_agent(
model=model,
tools=[fetch_ticket],
middleware=[
PIIMiddleware("email", strategy="mask", apply_to_tool_results=True),
PIIMiddleware("credit_card", strategy="mask", apply_to_tool_results=True),
PIIMiddleware("ip", strategy="redact", apply_to_tool_results=True),
PIIMiddleware("mac_address", strategy="redact", apply_to_tool_results=True),
PIIMiddleware("ssn", detector=detect_ssn, strategy="hash", apply_to_tool_results=True),
],
)
result = agent_combo.invoke(
{"messages": [{"role": "user", "content": "查工单 T001,列出所有信息"}]}
)
print(result["messages"][-1].content)
# 用户输入中也含敏感信息
agent_input = create_agent(
model=model,
tools=[fetch_ticket],
middleware=[
PIIMiddleware("email", strategy="mask", apply_to_tool_results=True),
PIIMiddleware(
"api_key",
detector=r"sk-[a-zA-Z0-9]{32}",
strategy="block",
apply_to_input=True,
apply_to_tool_results=True,
),
],
)
try:
result = agent_input.invoke({
"messages": [{
"role": "user",
"content": "我的 API Key 是 sk-abcdefghijklmnopqrstuvwxyz123456,帮我查 T002",
}]
})
except Exception as e:
print(f"用户输入中的 API Key 被拦截: {type(e).__name__}: {e}")
使用正则检测时传字符串(如 r"sk-[a-zA-Z0-9]{32}"),不要传 re.compile() 对象。自定义检测函数返回的 list[dict] 里每个 dict 的 text、start、end 三个字段缺一不可。
LLMToolSelectorMiddleware --- LLM 工具选择器
Agent 注册了几十个工具,每次请求把所有工具 schema 发给模型,Token 哗哗地烧。LLMToolSelectorMiddleware 的做法是:在主模型调用之前,先用一个便宜的小模型筛一遍工具列表,只保留跟当前任务相关的几个,把筛选结果传给主模型。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
model |
`str | BaseChatModel` | 是 |
max_tools |
int |
None |
最多保留多少个工具。不设的话不做数量限制,只过滤明显不相关的 |
prompt |
str |
None |
自定义筛选 Prompt。不传就用内置的,内置 Prompt 会让模型判断每个工具是否与当前任务相关 |
每次模型调用前,中间件会拿当前消息历史去问筛选模型"这些工具里哪些跟当前任务有关",然后只把相关的工具发给主模型。如果筛选模型没返回任何工具(极端情况),会保留所有工具,确保 Agent 不会缺工具。
Main Model(主模型) Selector Model(小模型) LLMToolSelector MW Agent Main Model(主模型) Selector Model(小模型) LLMToolSelector MW Agent #mermaid-svg-zQiUsqoOLZoasRBL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zQiUsqoOLZoasRBL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zQiUsqoOLZoasRBL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zQiUsqoOLZoasRBL .error-icon{fill:#552222;}#mermaid-svg-zQiUsqoOLZoasRBL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zQiUsqoOLZoasRBL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zQiUsqoOLZoasRBL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zQiUsqoOLZoasRBL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zQiUsqoOLZoasRBL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zQiUsqoOLZoasRBL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zQiUsqoOLZoasRBL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zQiUsqoOLZoasRBL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zQiUsqoOLZoasRBL .marker.cross{stroke:#333333;}#mermaid-svg-zQiUsqoOLZoasRBL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zQiUsqoOLZoasRBL p{margin:0;}#mermaid-svg-zQiUsqoOLZoasRBL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zQiUsqoOLZoasRBL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zQiUsqoOLZoasRBL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zQiUsqoOLZoasRBL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zQiUsqoOLZoasRBL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zQiUsqoOLZoasRBL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zQiUsqoOLZoasRBL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zQiUsqoOLZoasRBL .sequenceNumber{fill:white;}#mermaid-svg-zQiUsqoOLZoasRBL #sequencenumber{fill:#333;}#mermaid-svg-zQiUsqoOLZoasRBL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zQiUsqoOLZoasRBL .messageText{fill:#333;stroke:none;}#mermaid-svg-zQiUsqoOLZoasRBL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zQiUsqoOLZoasRBL .labelText,#mermaid-svg-zQiUsqoOLZoasRBL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zQiUsqoOLZoasRBL .loopText,#mermaid-svg-zQiUsqoOLZoasRBL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zQiUsqoOLZoasRBL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zQiUsqoOLZoasRBL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zQiUsqoOLZoasRBL .noteText,#mermaid-svg-zQiUsqoOLZoasRBL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zQiUsqoOLZoasRBL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zQiUsqoOLZoasRBL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zQiUsqoOLZoasRBL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zQiUsqoOLZoasRBL .actorPopupMenu{position:absolute;}#mermaid-svg-zQiUsqoOLZoasRBL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zQiUsqoOLZoasRBL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zQiUsqoOLZoasRBL .actor-man circle,#mermaid-svg-zQiUsqoOLZoasRBL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zQiUsqoOLZoasRBL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 准备调用主模型(含 50 个工具) 发消息 + 全部工具列表,问哪些相关 返回 5 个相关工具 只传 5 个工具 + 消息 返回响应
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import LLMToolSelectorMiddleware
# 基本用法:用小模型筛选,最多保留 5 个工具
agent = create_agent(
model="openai:gpt-4o",
tools=[get_weather, send_email, lookup_order, query_database,
search_web, calculate, translate, generate_report],
middleware=[
LLMToolSelectorMiddleware(
model="openai:gpt-4o-mini",
max_tools=5,
),
],
)
# 不设 max_tools:只过滤不相关的,不限制数量
agent = create_agent(
model="openai:gpt-4o",
tools=[...],
middleware=[
LLMToolSelectorMiddleware(
model="openai:gpt-4o-mini",
),
],
)
这个中间件和 ProviderToolSearchMiddleware 解决的是同一个问题------工具太多浪费 Token。区别在于:LLMToolSelectorMiddleware 在客户端多调一次 LLM 做筛选,适用所有 Provider;ProviderToolSearchMiddleware 在 Provider 服务端完成,不增加客户端延迟,但只有 Anthropic 和 OpenAI 支持。
ToolRetryMiddleware --- 工具调用重试
工具调用失败了,自动重试,带指数退避。比方说调一个 HTTP API 超时了,等 1 秒重试,再失败等 2 秒,再失败等 4 秒,上限是 3 次。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_retries |
int |
3 |
最大重试次数。超过这次数还不成功就抛异常 |
backoff_factor |
float |
2.0 |
退避因子。每次重试的等待时间 = 上一次等待时间 × backoff_factor |
initial_delay |
float |
1.0 |
第一次重试前等待的秒数 |
max_delay |
float |
60.0 |
单次等待的上限。指数增长到一定程度就不要再加了 |
jitter |
bool |
True |
是否加随机抖动。加上后实际等待时间会在计算值的基础上随机浮动,避免多个请求同时重试造成的"惊群效应" |
retry_on |
tuple[type[Exception], ...] |
(Exception,) |
哪些异常类型触发重试。默认所有异常都重试。可以缩小范围,比如只重试 (ConnectionError, TimeoutError) |
on_failure |
str |
"error" |
所有重试都失败后的行为。"error" 抛异常,"continue" 返回一个错误信息让 Agent 继续 |
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ToolRetryMiddleware
# 默认配置:最多重试 3 次,指数退避,带抖动
agent = create_agent(
model="openai:gpt-4o",
tools=[unstable_api_tool],
middleware=[
ToolRetryMiddleware(),
],
)
# 自定义配置:只重试网络错误,最多 5 次,失败后让 Agent 继续
agent = create_agent(
model="openai:gpt-4o",
tools=[unstable_api_tool],
middleware=[
ToolRetryMiddleware(
max_retries=5,
retry_on=(ConnectionError, TimeoutError),
on_failure="continue",
initial_delay=0.5,
max_delay=30.0,
),
],
)
ModelRetryMiddleware --- 模型调用重试
跟 ToolRetryMiddleware 一个思路,但重试的是模型调用。模型 API 偶尔会超时或返回 429(限流),这时候重试通常能解决问题。
模块路径:langchain.agents.middleware
构造参数: 与 ToolRetryMiddleware 完全相同。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_retries |
int |
3 |
最大重试次数 |
backoff_factor |
float |
2.0 |
退避因子 |
initial_delay |
float |
1.0 |
初始等待秒数 |
max_delay |
float |
60.0 |
单次等待上限 |
jitter |
bool |
True |
是否加随机抖动 |
retry_on |
tuple[type[Exception], ...] |
(Exception,) |
触发重试的异常类型 |
on_failure |
str |
"error" |
全部失败后的行为 |
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRetryMiddleware
agent = create_agent(
model="openai:gpt-4o",
tools=[...],
middleware=[
ModelRetryMiddleware(
max_retries=3,
backoff_factor=2.0,
initial_delay=1.0,
jitter=True,
),
],
)
ToolRetryMiddleware 和 ModelRetryMiddleware 可以一起用,各自独立控制重试逻辑。
ContextEditingMiddleware --- 上下文编辑
对话历史里有些工具调用结果太长、太乱、或者换模型后不兼容。ContextEditingMiddleware 让你在消息传给模型之前,清理、修剪、或删除指定的工具调用结果。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
edits |
list[ContextEdit] |
是 | 编辑规则列表。每条规则指定要处理哪些工具调用,以及处理方式 |
trigger |
`ContextSize | TriggerClause | list |
每条 ContextEdit 规则的结构:
| 字段 | 类型 | 说明 |
|---|---|---|
tool_name |
str |
要编辑的工具名。可以使用 "*" 匹配所有工具,或用 "prefix*" 做前缀匹配 |
action |
str |
编辑动作。"trim" 修剪内容(保留头部和尾部),"clear" 清空内容,"remove" 删除整条消息 |
max_tokens |
int |
修剪时保留的最大 Token 数(仅 "trim" 动作有效) |
keep_head_ratio |
float |
修剪时头部保留的比例,默认 0.5。设为 0.8 表示 80% 保留头部,20% 保留尾部 |
#mermaid-svg-mlJXiXK9XYxPLE0O{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mlJXiXK9XYxPLE0O .error-icon{fill:#552222;}#mermaid-svg-mlJXiXK9XYxPLE0O .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mlJXiXK9XYxPLE0O .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mlJXiXK9XYxPLE0O .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mlJXiXK9XYxPLE0O .marker.cross{stroke:#333333;}#mermaid-svg-mlJXiXK9XYxPLE0O svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mlJXiXK9XYxPLE0O p{margin:0;}#mermaid-svg-mlJXiXK9XYxPLE0O .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mlJXiXK9XYxPLE0O .cluster-label text{fill:#333;}#mermaid-svg-mlJXiXK9XYxPLE0O .cluster-label span{color:#333;}#mermaid-svg-mlJXiXK9XYxPLE0O .cluster-label span p{background-color:transparent;}#mermaid-svg-mlJXiXK9XYxPLE0O .label text,#mermaid-svg-mlJXiXK9XYxPLE0O span{fill:#333;color:#333;}#mermaid-svg-mlJXiXK9XYxPLE0O .node rect,#mermaid-svg-mlJXiXK9XYxPLE0O .node circle,#mermaid-svg-mlJXiXK9XYxPLE0O .node ellipse,#mermaid-svg-mlJXiXK9XYxPLE0O .node polygon,#mermaid-svg-mlJXiXK9XYxPLE0O .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mlJXiXK9XYxPLE0O .rough-node .label text,#mermaid-svg-mlJXiXK9XYxPLE0O .node .label text,#mermaid-svg-mlJXiXK9XYxPLE0O .image-shape .label,#mermaid-svg-mlJXiXK9XYxPLE0O .icon-shape .label{text-anchor:middle;}#mermaid-svg-mlJXiXK9XYxPLE0O .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mlJXiXK9XYxPLE0O .rough-node .label,#mermaid-svg-mlJXiXK9XYxPLE0O .node .label,#mermaid-svg-mlJXiXK9XYxPLE0O .image-shape .label,#mermaid-svg-mlJXiXK9XYxPLE0O .icon-shape .label{text-align:center;}#mermaid-svg-mlJXiXK9XYxPLE0O .node.clickable{cursor:pointer;}#mermaid-svg-mlJXiXK9XYxPLE0O .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mlJXiXK9XYxPLE0O .arrowheadPath{fill:#333333;}#mermaid-svg-mlJXiXK9XYxPLE0O .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mlJXiXK9XYxPLE0O .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mlJXiXK9XYxPLE0O .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mlJXiXK9XYxPLE0O .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mlJXiXK9XYxPLE0O .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mlJXiXK9XYxPLE0O .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mlJXiXK9XYxPLE0O .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mlJXiXK9XYxPLE0O .cluster text{fill:#333;}#mermaid-svg-mlJXiXK9XYxPLE0O .cluster span{color:#333;}#mermaid-svg-mlJXiXK9XYxPLE0O div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mlJXiXK9XYxPLE0O .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mlJXiXK9XYxPLE0O rect.text{fill:none;stroke-width:0;}#mermaid-svg-mlJXiXK9XYxPLE0O .icon-shape,#mermaid-svg-mlJXiXK9XYxPLE0O .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mlJXiXK9XYxPLE0O .icon-shape p,#mermaid-svg-mlJXiXK9XYxPLE0O .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mlJXiXK9XYxPLE0O .icon-shape .label rect,#mermaid-svg-mlJXiXK9XYxPLE0O .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mlJXiXK9XYxPLE0O .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mlJXiXK9XYxPLE0O .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mlJXiXK9XYxPLE0O :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 满足
是
trim
clear
remove
否
消息列表
检查 trigger 条件
遍历 edits 规则
匹配 tool_name?
action = ?
修剪内容,保留头尾
清空内容
删除整条消息
跳过
组装后的消息列表
传给模型
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ContextEditingMiddleware
# 场景1:每次模型调用前,把 web_search 工具返回的内容修剪到 2000 Token
agent = create_agent(
model="openai:gpt-4o",
tools=[web_search, calculate, get_weather],
middleware=[
ContextEditingMiddleware(
edits=[
{"tool_name": "web_search", "action": "trim", "max_tokens": 2000},
],
),
],
)
# 场景2:Token 超过 4000 时触发,清理所有工具调用结果
agent = create_agent(
model="openai:gpt-4o",
tools=[web_search, calculate, get_weather],
middleware=[
ContextEditingMiddleware(
edits=[
{"tool_name": "*", "action": "clear"},
],
trigger=("tokens", 4000),
),
],
)
# 场景3:前缀匹配,trim 所有 search_ 开头的工具结果
agent = create_agent(
model="openai:gpt-4o",
tools=[web_search, image_search, code_search],
middleware=[
ContextEditingMiddleware(
edits=[
{"tool_name": "search_*", "action": "trim", "max_tokens": 1000},
],
),
],
)
和 SummarizationMiddleware 的区别:SummarizationMiddleware 是"压缩旧消息为摘要",ContextEditingMiddleware 是"直接清理工具调用结果"。Summarization 保留语义但多花一次模型调用,ContextEditing 零成本但粗暴。
TodoListMiddleware --- 待办列表
Agent 处理复杂任务时容易跑偏------忘了最初要干什么,或者跳过了某些步骤。TodoListMiddleware 给 Agent 注入一个 write_todos 工具,让 Agent 在开始执行前先列出计划,执行过程中更新进度,执行完确认完成。todo 列表状态会持久化到 Agent 的状态里。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
system_prompt |
str |
None |
自定义 System Prompt 中关于 todo 的说明。不传则用内置默认提示,默认提示会告诉 Agent 什么时候用 write_todos、怎么规划任务 |
write_todos 工具接收一个 JSON 数组,每个元素包含三个字段:content(任务描述)、status("pending" / "in_progress" / "completed")、priority(优先级,可选)。
write_todos TodoList MW Agent User write_todos TodoList MW Agent User #mermaid-svg-neG9vF7uN8jX55el{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-neG9vF7uN8jX55el .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-neG9vF7uN8jX55el .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-neG9vF7uN8jX55el .error-icon{fill:#552222;}#mermaid-svg-neG9vF7uN8jX55el .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-neG9vF7uN8jX55el .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-neG9vF7uN8jX55el .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-neG9vF7uN8jX55el .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-neG9vF7uN8jX55el .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-neG9vF7uN8jX55el .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-neG9vF7uN8jX55el .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-neG9vF7uN8jX55el .marker{fill:#333333;stroke:#333333;}#mermaid-svg-neG9vF7uN8jX55el .marker.cross{stroke:#333333;}#mermaid-svg-neG9vF7uN8jX55el svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-neG9vF7uN8jX55el p{margin:0;}#mermaid-svg-neG9vF7uN8jX55el .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-neG9vF7uN8jX55el text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-neG9vF7uN8jX55el .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-neG9vF7uN8jX55el .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-neG9vF7uN8jX55el .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-neG9vF7uN8jX55el .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-neG9vF7uN8jX55el #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-neG9vF7uN8jX55el .sequenceNumber{fill:white;}#mermaid-svg-neG9vF7uN8jX55el #sequencenumber{fill:#333;}#mermaid-svg-neG9vF7uN8jX55el #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-neG9vF7uN8jX55el .messageText{fill:#333;stroke:none;}#mermaid-svg-neG9vF7uN8jX55el .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-neG9vF7uN8jX55el .labelText,#mermaid-svg-neG9vF7uN8jX55el .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-neG9vF7uN8jX55el .loopText,#mermaid-svg-neG9vF7uN8jX55el .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-neG9vF7uN8jX55el .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-neG9vF7uN8jX55el .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-neG9vF7uN8jX55el .noteText,#mermaid-svg-neG9vF7uN8jX55el .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-neG9vF7uN8jX55el .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-neG9vF7uN8jX55el .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-neG9vF7uN8jX55el .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-neG9vF7uN8jX55el .actorPopupMenu{position:absolute;}#mermaid-svg-neG9vF7uN8jX55el .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-neG9vF7uN8jX55el .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-neG9vF7uN8jX55el .actor-man circle,#mermaid-svg-neG9vF7uN8jX55el line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-neG9vF7uN8jX55el :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 帮我准备技术分享:React 19 新特性 before_model 注入 todo 提示到 system prompt 调用模型 write_todos({content:"调研 React Compiler",status:"pending"},...) todos 已更新 继续执行任务 write_todos({content:"调研 React Compiler",status:"completed"},...) 进度已更新 最终结果
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
def web_search(query: str) -> str:
return f"搜索结果: {query}"
def generate_report(topic: str) -> str:
return f"报告: {topic}"
agent = create_agent(
model="openai:gpt-4o",
tools=[web_search, generate_report],
middleware=[
TodoListMiddleware(),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "帮我准备一个 React 19 技术分享"}]}
)
# Agent 会先调用 write_todos 列出步骤,然后逐步执行
# 自定义 system prompt
agent = create_agent(
model="openai:gpt-4o",
tools=[web_search, generate_report],
middleware=[
TodoListMiddleware(
system_prompt=(
"在开始任何复杂任务之前,先用 write_todos 列出计划。"
"每个任务完成后立即更新状态。"
"如果任务超过 5 步,拆分成子任务。"
),
),
],
)
ShellToolMiddleware --- Shell 工具
给 Agent 开一个持久化的 Shell 会话,Agent 可以在里面执行命令。适合需要操作文件系统、运行脚本、管理进程的场景。Shell 会话在整个对话线程中保持,环境变量、工作目录、已安装的包都跨 invoke 累积。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
workspace_dir |
str |
None |
工作目录。Shell 的 cd 只能在这个目录及其子目录内活动。不传则用临时目录 |
allow_list |
list[str] |
None |
允许执行的命令白名单。例如 ["ls", "cat", "python", "pip"]。不传则不限制(危险) |
deny_list |
list[str] |
None |
禁止执行的命令黑名单。例如 ["rm", "sudo", "shutdown"]。不传则不限制 |
timeout |
int |
30 |
单次命令执行的超时秒数。超过这个时间命令会被终止 |
max_output_size |
int |
10000 |
命令输出最大字符数。超过会被截断 |
environment |
dict[str, str] |
None |
额外的环境变量 |
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ShellToolMiddleware
# 基本用法:带工作目录和安全限制
agent = create_agent(
model="openai:gpt-4o",
tools=[],
middleware=[
ShellToolMiddleware(
workspace_dir="/tmp/agent-workspace",
allow_list=["ls", "cat", "echo", "python", "pip", "git", "mkdir"],
deny_list=["rm", "rmdir", "sudo", "shutdown"],
timeout=60,
max_output_size=20000,
),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "列一下当前目录,然后创建一个 hello.py 文件"}]}
)
Shell 会话是持久的。同一个 thread_id 下,第一次 invoke 创建的虚拟环境和安装的包,第二次 invoke 还能用。工作目录 workspace_dir 如果不指定,中间件会创建一个临时目录,Agent 只能在这个沙箱里操作。
FilesystemFileSearchMiddleware --- 文件搜索
给 Agent 注入两个文件搜索工具:glob(按文件名模式匹配)和 grep(按内容搜索)。Agent 就能在文件系统里找文件了。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
base_dir |
str |
是 | 搜索的根目录。Agent 只能在这个目录及子目录内搜索,不能越界 |
max_results |
int |
50 |
单次搜索最多返回多少结果 |
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import FilesystemFileSearchMiddleware
agent = create_agent(
model="openai:gpt-4o",
tools=[],
middleware=[
FilesystemFileSearchMiddleware(
base_dir="/workspace/project",
max_results=100,
),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "找一下项目里所有定义 API 路由的文件"}]}
)
# Agent 会用 glob 搜 *.py,再用 grep 搜 @router 或 @app.get
base_dir 是强制的安全边界,Agent 的搜索请求会被限制在这个目录下,任何试图访问外部路径的搜索都会被拒绝。
FilesystemMiddleware --- 文件系统
给 Agent 一个完整的文件系统,可以读写文件、创建目录、列出文件。文件系统有两种模式:短期(临时文件,Agent 结束后清理)和长期(持久化,跨对话保留)。这个中间件来自 deepagents 包。
模块路径:deepagents.middleware.filesystem
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
base_dir |
str |
None |
文件系统根目录。不传则用临时目录 |
max_file_size |
int |
1048576(1MB) |
单个文件最大字节数。超过会拒绝写入 |
allowed_extensions |
list[str] |
None |
允许的文件扩展名。不传则不限制 |
max_files |
int |
None |
最多创建多少个文件。不传则不限制 |
代码示例:
python
from deepagents.middleware.filesystem import FilesystemMiddleware
from langchain.agents import create_agent
# 短期文件系统:Agent 结束后文件自动清理
agent = create_agent(
model="openai:gpt-4o",
tools=[],
middleware=[
FilesystemMiddleware(
base_dir="/tmp/agent-fs",
max_file_size=1048576,
allowed_extensions=[".txt", ".py", ".json", ".md", ".csv"],
),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "创建一个 data.csv 文件,写入一些测试数据"}]}
)
短期和长期文件系统的区别:短期模式在 Agent 或对话线程结束后自动清理所有文件,长期模式文件会一直保留。长期模式适合需要跨对话持久化的场景,比如 Agent 维护一个知识库。
SubagentMiddleware --- 子 Agent
主 Agent 遇到复杂任务时,把子任务委派给子 Agent 处理。子 Agent 有独立的上下文窗口,主 Agent 只看到子 Agent 的最终结果,中间过程不污染主上下文。可以配置多个不同类型的子 Agent,每个子 Agent 有自己的工具集和系统提示。来自 deepagents 包。
模块路径:deepagents.middleware.subagents
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
default_model |
`str | BaseChatModel` | 是 |
default_tools |
list |
[] |
所有子 Agent 默认可用的工具。子 Agent 的 tools 会追加到这个列表后面 |
default_middleware |
list |
[] |
所有子 Agent 默认的中间件 |
default_system_prompt |
str |
None |
所有子 Agent 默认的系统提示 |
subagents |
list[dict] |
[] |
自定义子 Agent 列表。每个子 Agent 是一个 dict,包含 name(名称)、description(描述,主 Agent 根据这个选子 Agent)、system_prompt(系统提示)、tools(工具列表)、model(可选,覆盖默认模型)、middleware(可选,追加到默认中间件后面) |
general_purpose_agent |
bool |
True |
是否创建一个通用子 Agent。通用子 Agent 可以用默认工具处理任意任务 |
#mermaid-svg-NSGQzWjvIarenJcB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NSGQzWjvIarenJcB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NSGQzWjvIarenJcB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NSGQzWjvIarenJcB .error-icon{fill:#552222;}#mermaid-svg-NSGQzWjvIarenJcB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NSGQzWjvIarenJcB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NSGQzWjvIarenJcB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NSGQzWjvIarenJcB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NSGQzWjvIarenJcB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NSGQzWjvIarenJcB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NSGQzWjvIarenJcB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NSGQzWjvIarenJcB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NSGQzWjvIarenJcB .marker.cross{stroke:#333333;}#mermaid-svg-NSGQzWjvIarenJcB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NSGQzWjvIarenJcB p{margin:0;}#mermaid-svg-NSGQzWjvIarenJcB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NSGQzWjvIarenJcB .cluster-label text{fill:#333;}#mermaid-svg-NSGQzWjvIarenJcB .cluster-label span{color:#333;}#mermaid-svg-NSGQzWjvIarenJcB .cluster-label span p{background-color:transparent;}#mermaid-svg-NSGQzWjvIarenJcB .label text,#mermaid-svg-NSGQzWjvIarenJcB span{fill:#333;color:#333;}#mermaid-svg-NSGQzWjvIarenJcB .node rect,#mermaid-svg-NSGQzWjvIarenJcB .node circle,#mermaid-svg-NSGQzWjvIarenJcB .node ellipse,#mermaid-svg-NSGQzWjvIarenJcB .node polygon,#mermaid-svg-NSGQzWjvIarenJcB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NSGQzWjvIarenJcB .rough-node .label text,#mermaid-svg-NSGQzWjvIarenJcB .node .label text,#mermaid-svg-NSGQzWjvIarenJcB .image-shape .label,#mermaid-svg-NSGQzWjvIarenJcB .icon-shape .label{text-anchor:middle;}#mermaid-svg-NSGQzWjvIarenJcB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NSGQzWjvIarenJcB .rough-node .label,#mermaid-svg-NSGQzWjvIarenJcB .node .label,#mermaid-svg-NSGQzWjvIarenJcB .image-shape .label,#mermaid-svg-NSGQzWjvIarenJcB .icon-shape .label{text-align:center;}#mermaid-svg-NSGQzWjvIarenJcB .node.clickable{cursor:pointer;}#mermaid-svg-NSGQzWjvIarenJcB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NSGQzWjvIarenJcB .arrowheadPath{fill:#333333;}#mermaid-svg-NSGQzWjvIarenJcB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NSGQzWjvIarenJcB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NSGQzWjvIarenJcB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NSGQzWjvIarenJcB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NSGQzWjvIarenJcB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NSGQzWjvIarenJcB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NSGQzWjvIarenJcB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NSGQzWjvIarenJcB .cluster text{fill:#333;}#mermaid-svg-NSGQzWjvIarenJcB .cluster span{color:#333;}#mermaid-svg-NSGQzWjvIarenJcB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NSGQzWjvIarenJcB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NSGQzWjvIarenJcB rect.text{fill:none;stroke-width:0;}#mermaid-svg-NSGQzWjvIarenJcB .icon-shape,#mermaid-svg-NSGQzWjvIarenJcB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NSGQzWjvIarenJcB .icon-shape p,#mermaid-svg-NSGQzWjvIarenJcB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NSGQzWjvIarenJcB .icon-shape .label rect,#mermaid-svg-NSGQzWjvIarenJcB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NSGQzWjvIarenJcB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NSGQzWjvIarenJcB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NSGQzWjvIarenJcB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 子Agent集群
主Agent
委派代码任务
委派调研任务
委派其他任务
返回结果
返回结果
返回结果
主 Agent
gpt-4o
CodeAgent
写代码、调试
工具: shell, filesystem
ResearchAgent
搜索、总结
工具: web_search
GeneralAgent
通用任务
工具: 默认工具集
代码示例:
python
from deepagents.middleware.subagents import SubagentMiddleware
from deepagents.middleware.filesystem import FilesystemMiddleware
from langchain.agents.middleware import ShellToolMiddleware
from langchain.agents import create_agent
def web_search(query: str) -> str:
return f"搜索结果: {query}"
# 创建带子 Agent 的主 Agent
agent = create_agent(
model="openai:gpt-4o",
tools=[web_search],
middleware=[
SubagentMiddleware(
default_model="openai:gpt-4o-mini",
default_tools=[web_search],
subagents=[
{
"name": "code-helper",
"description": "编写和调试代码,操作文件,执行 Shell 命令",
"system_prompt": "你是一个代码助手,擅长 Python 和 Shell。",
"tools": [],
"middleware": [
ShellToolMiddleware(workspace_dir="/tmp/agent"),
FilesystemMiddleware(),
],
},
{
"name": "researcher",
"description": "搜索信息、总结内容、对比分析",
"system_prompt": "你是一个研究助手,擅长搜索和总结。",
"tools": [web_search],
"model": "openai:gpt-4o",
},
],
general_purpose_agent=True,
),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "帮我调研 React 19 的新特性,然后写一篇总结到文件里"}]}
)
# 主 Agent 先把调研任务委派给 researcher,然后把写文件任务委派给 code-helper
SubagentMiddleware 来自 deepagents 包,需要单独安装:pip install deepagents。子 Agent 有独立的上下文窗口,中间过程不会污染主 Agent。子 Agent 的 model 可以不同于主 Agent,简单任务用便宜模型能省不少钱。default_middleware 和子 Agent 的 middleware 是追加关系,子 Agent 的 middleware 在 default_middleware 之后执行。
ProviderToolSearchMiddleware --- Provider 工具搜索
工具太多,请求体太大。ProviderToolSearchMiddleware 把选定的工具标记为"延迟加载",由 Provider 在服务端按需检索工具 schema,而不是每次都把全部工具定义发给模型。只有 Anthropic 和 OpenAI 支持。
模块路径:langchain.agents.middleware
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
searchable_tools |
`list[ToolIdentifier] | None` | None |
服务端工具搜索 Provider(Anthropic/OpenAI) ProviderToolSearch MW Agent 服务端工具搜索 Provider(Anthropic/OpenAI) ProviderToolSearch MW Agent #mermaid-svg-f2dsIU0SyEYCaQby{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-f2dsIU0SyEYCaQby .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-f2dsIU0SyEYCaQby .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-f2dsIU0SyEYCaQby .error-icon{fill:#552222;}#mermaid-svg-f2dsIU0SyEYCaQby .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-f2dsIU0SyEYCaQby .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-f2dsIU0SyEYCaQby .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-f2dsIU0SyEYCaQby .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-f2dsIU0SyEYCaQby .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-f2dsIU0SyEYCaQby .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-f2dsIU0SyEYCaQby .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-f2dsIU0SyEYCaQby .marker{fill:#333333;stroke:#333333;}#mermaid-svg-f2dsIU0SyEYCaQby .marker.cross{stroke:#333333;}#mermaid-svg-f2dsIU0SyEYCaQby svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-f2dsIU0SyEYCaQby p{margin:0;}#mermaid-svg-f2dsIU0SyEYCaQby .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-f2dsIU0SyEYCaQby text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-f2dsIU0SyEYCaQby .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-f2dsIU0SyEYCaQby .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-f2dsIU0SyEYCaQby .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-f2dsIU0SyEYCaQby .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-f2dsIU0SyEYCaQby #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-f2dsIU0SyEYCaQby .sequenceNumber{fill:white;}#mermaid-svg-f2dsIU0SyEYCaQby #sequencenumber{fill:#333;}#mermaid-svg-f2dsIU0SyEYCaQby #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-f2dsIU0SyEYCaQby .messageText{fill:#333;stroke:none;}#mermaid-svg-f2dsIU0SyEYCaQby .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-f2dsIU0SyEYCaQby .labelText,#mermaid-svg-f2dsIU0SyEYCaQby .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-f2dsIU0SyEYCaQby .loopText,#mermaid-svg-f2dsIU0SyEYCaQby .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-f2dsIU0SyEYCaQby .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-f2dsIU0SyEYCaQby .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-f2dsIU0SyEYCaQby .noteText,#mermaid-svg-f2dsIU0SyEYCaQby .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-f2dsIU0SyEYCaQby .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-f2dsIU0SyEYCaQby .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-f2dsIU0SyEYCaQby .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-f2dsIU0SyEYCaQby .actorPopupMenu{position:absolute;}#mermaid-svg-f2dsIU0SyEYCaQby .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-f2dsIU0SyEYCaQby .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-f2dsIU0SyEYCaQby .actor-man circle,#mermaid-svg-f2dsIU0SyEYCaQby line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-f2dsIU0SyEYCaQby :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} searchable_tools 中有 30 个标记为延迟 准备调用模型(含 50 个工具) 对延迟工具设置 extras"defer_loading"=True 注入 Provider 工具搜索工具 请求(仅 20 个非延迟工具 + 搜索工具) 模型需要某个延迟工具 返回该工具的完整 schema 执行工具,返回结果
代码示例:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ProviderToolSearchMiddleware
agent = create_agent(
model="anthropic:claude-sonnet-4-5",
tools=[get_weather, send_email, lookup_order, query_database,
search_web, calculate, translate, generate_report],
middleware=[
ProviderToolSearchMiddleware(
searchable_tools=["lookup_order", "query_database",
"search_web", "translate", "generate_report"],
),
],
)
# 只有 get_weather, send_email, calculate 的 schema 每次发送
# 其他工具由 Provider 按需检索
代码示例 --- 完整 Demo:
python
from langchain.agents import create_agent
from langchain.agents.middleware import ProviderToolSearchMiddleware
def get_weather(city: str) -> str:
return f"{city}: 晴,25°C"
def calculate(expr: str) -> str:
return str(eval(expr))
def send_email(to: str, subject: str, body: str) -> str:
return f"已发送给 {to}"
def lookup_order(order_id: str) -> str:
return f"订单 {order_id}: 已完成"
def query_database(sql: str) -> str:
return f"查询结果: [{sql}]"
def search_web(query: str) -> str:
return f"搜索结果: {query}"
def translate(text: str, target: str) -> str:
return f"翻译: {text} -> {target}"
def generate_report(topic: str) -> str:
return f"报告: {topic}"
# 基本用法:将低频工具延迟到 Provider 服务端搜索
agent = create_agent(
model="anthropic:claude-sonnet-4-5",
tools=[get_weather, calculate, send_email, lookup_order,
query_database, search_web, translate, generate_report],
middleware=[
ProviderToolSearchMiddleware(
searchable_tools=[
"lookup_order", "query_database",
"search_web", "translate", "generate_report",
],
),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "帮我查天气,然后发邮件通知团队"}]}
)
# 请求体只包含 get_weather, calculate, send_email 三个工具 schema
# 全部工具延迟搜索
agent2 = create_agent(
model="anthropic:claude-sonnet-4-5",
tools=[get_weather, calculate, send_email, lookup_order,
query_database, search_web, translate, generate_report],
middleware=[
ProviderToolSearchMiddleware(
searchable_tools=[
"get_weather", "calculate", "send_email",
"lookup_order", "query_database", "search_web",
"translate", "generate_report",
],
),
],
)
result = agent2.invoke(
{"messages": [{"role": "user", "content": "帮我查北京天气并计算 100*50"}]}
)
仅在 Anthropic 和 OpenAI 上有效,其他 Provider 会抛 ValueError。工具数量不多(< 10 个)时收益不大。和 LLMToolSelectorMiddleware 可以组合------先用 LLMToolSelector 筛选,再用 ProviderToolSearch 延迟。
自定义中间件开发
两种方式:装饰器(快,适合单钩子),类(灵活,适合多钩子 + 配置)。
装饰器方式
python
from langchain.agents import create_agent
from langchain.agents.middleware import (
before_model,
after_model,
wrap_model_call,
AgentState,
ModelRequest,
ModelResponse,
)
from langgraph.runtime import Runtime
from typing import Any, Callable
# 日志:每次模型调用前记录消息数
@before_model
def log_before_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
msg_count = len(state["messages"])
print(f"[Middleware] 即将调用模型,当前消息数: {msg_count}")
return None
agent = create_agent(
model=model,
tools=[get_weather],
middleware=[log_before_model],
)
# 消息限制:超过 50 条时自动跳转结束
from langchain.messages import AIMessage
@before_model(can_jump_to=["end"])
def check_message_limit(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
if len(state["messages"]) >= 50:
return {
"messages": [AIMessage("对话已达到长度限制,已自动结束。")],
"jump_to": "end",
}
return None
# 模型重试:最多 3 次
@wrap_model_call
def retry_model(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
for attempt in range(3):
try:
return handler(request)
except Exception as e:
if attempt == 2:
raise
print(f" [重试 {attempt + 1}/3] 错误: {e}")
# 自定义状态追踪
from typing_extensions import NotRequired
class TrackingState(AgentState):
model_call_count: NotRequired[int]
@after_model(state_schema=TrackingState)
def increment_counter(state: TrackingState, runtime: Runtime) -> dict[str, Any] | None:
current = state.get("model_call_count", 0)
return {"model_call_count": current + 1}
agent = create_agent(
model=model,
tools=[get_weather, calculate],
middleware=[increment_counter],
)
result = agent.invoke({
"messages": [{"role": "user", "content": "北京天气怎么样?"}],
"model_call_count": 0,
})
print(f"模型调用次数: {result.get('model_call_count', 'N/A')}")
类方式
python
from langchain.agents.middleware import AgentMiddleware, AgentState, hook_config
from langchain.messages import AIMessage
from langgraph.runtime import Runtime
from typing import Any, Callable
class LoggingMiddleware(AgentMiddleware):
def __init__(self, verbose: bool = True):
super().__init__()
self.verbose = verbose
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
if self.verbose:
print(f" [Logging] before_model: {len(state['messages'])} 条消息")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
if self.verbose:
last_msg = state['messages'][-1]
content_preview = str(last_msg.content)[:100] if hasattr(last_msg, 'content') else 'N/A'
print(f" [Logging] after_model: {content_preview}...")
return None
class MessageLimitMiddleware(AgentMiddleware):
def __init__(self, max_messages: int = 50):
super().__init__()
self.max_messages = max_messages
@hook_config(can_jump_to=["end"])
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
if len(state["messages"]) >= self.max_messages:
return {
"messages": [AIMessage(f"对话已达到 {self.max_messages} 条限制。")],
"jump_to": "end",
}
return None
class ToolRetryMiddleware(AgentMiddleware):
def __init__(self, max_retries: int = 3):
super().__init__()
self.max_retries = max_retries
def wrap_tool_call(self, request: dict, handler: Callable) -> Any:
tool_name = request.get("tool", "unknown")
for attempt in range(self.max_retries):
try:
return handler(request)
except Exception as e:
if attempt == self.max_retries - 1:
raise
print(f" [ToolRetry] {tool_name} 重试 {attempt + 1}/{self.max_retries}: {e}")
agent = create_agent(
model=model,
tools=[get_weather, calculate],
middleware=[LoggingMiddleware(verbose=True), MessageLimitMiddleware(max_messages=50)],
)
装饰器适合快速原型,类方式适合多钩子、复杂配置、需要跨项目复用的情况。Node 型钩子返回 dict 更新状态,返回 None 表示不动。Wrap 型钩子控制 handler 调用次数------零次短路,一次正常,多次重试。多个中间件的执行顺序跟 middleware 列表顺序一致,先注册的先跑。
中间件与 LangGraph 工作流集成
中间件底层是 LangGraph 图,create_agent() 内部构建 StateGraph,中间件的钩子被编译成节点插入图中。理解这一点对高级用法有用。
#mermaid-svg-KfFJrIzh3BsToe8w{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KfFJrIzh3BsToe8w .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KfFJrIzh3BsToe8w .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KfFJrIzh3BsToe8w .error-icon{fill:#552222;}#mermaid-svg-KfFJrIzh3BsToe8w .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KfFJrIzh3BsToe8w .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KfFJrIzh3BsToe8w .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KfFJrIzh3BsToe8w .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KfFJrIzh3BsToe8w .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KfFJrIzh3BsToe8w .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KfFJrIzh3BsToe8w .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KfFJrIzh3BsToe8w .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KfFJrIzh3BsToe8w .marker.cross{stroke:#333333;}#mermaid-svg-KfFJrIzh3BsToe8w svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KfFJrIzh3BsToe8w p{margin:0;}#mermaid-svg-KfFJrIzh3BsToe8w .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KfFJrIzh3BsToe8w .cluster-label text{fill:#333;}#mermaid-svg-KfFJrIzh3BsToe8w .cluster-label span{color:#333;}#mermaid-svg-KfFJrIzh3BsToe8w .cluster-label span p{background-color:transparent;}#mermaid-svg-KfFJrIzh3BsToe8w .label text,#mermaid-svg-KfFJrIzh3BsToe8w span{fill:#333;color:#333;}#mermaid-svg-KfFJrIzh3BsToe8w .node rect,#mermaid-svg-KfFJrIzh3BsToe8w .node circle,#mermaid-svg-KfFJrIzh3BsToe8w .node ellipse,#mermaid-svg-KfFJrIzh3BsToe8w .node polygon,#mermaid-svg-KfFJrIzh3BsToe8w .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KfFJrIzh3BsToe8w .rough-node .label text,#mermaid-svg-KfFJrIzh3BsToe8w .node .label text,#mermaid-svg-KfFJrIzh3BsToe8w .image-shape .label,#mermaid-svg-KfFJrIzh3BsToe8w .icon-shape .label{text-anchor:middle;}#mermaid-svg-KfFJrIzh3BsToe8w .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KfFJrIzh3BsToe8w .rough-node .label,#mermaid-svg-KfFJrIzh3BsToe8w .node .label,#mermaid-svg-KfFJrIzh3BsToe8w .image-shape .label,#mermaid-svg-KfFJrIzh3BsToe8w .icon-shape .label{text-align:center;}#mermaid-svg-KfFJrIzh3BsToe8w .node.clickable{cursor:pointer;}#mermaid-svg-KfFJrIzh3BsToe8w .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KfFJrIzh3BsToe8w .arrowheadPath{fill:#333333;}#mermaid-svg-KfFJrIzh3BsToe8w .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KfFJrIzh3BsToe8w .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KfFJrIzh3BsToe8w .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KfFJrIzh3BsToe8w .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KfFJrIzh3BsToe8w .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KfFJrIzh3BsToe8w .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KfFJrIzh3BsToe8w .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KfFJrIzh3BsToe8w .cluster text{fill:#333;}#mermaid-svg-KfFJrIzh3BsToe8w .cluster span{color:#333;}#mermaid-svg-KfFJrIzh3BsToe8w div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KfFJrIzh3BsToe8w .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KfFJrIzh3BsToe8w rect.text{fill:none;stroke-width:0;}#mermaid-svg-KfFJrIzh3BsToe8w .icon-shape,#mermaid-svg-KfFJrIzh3BsToe8w .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KfFJrIzh3BsToe8w .icon-shape p,#mermaid-svg-KfFJrIzh3BsToe8w .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KfFJrIzh3BsToe8w .icon-shape .label rect,#mermaid-svg-KfFJrIzh3BsToe8w .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KfFJrIzh3BsToe8w .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KfFJrIzh3BsToe8w .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KfFJrIzh3BsToe8w :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
start
before_agent
before_model
model(LLM 调用)
after_model
有工具调用?
wrap_tool_call
tools(工具执行)
after_agent
end
需要跨调用持久化的中间件必须配 checkpointer:
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver
# 开发环境
checkpointer = InMemorySaver()
# 生产环境
checkpointer = SqliteSaver.from_conn_string("checkpoints.db")
agent = create_agent(
model="openai:gpt-4o",
tools=[...],
checkpointer=checkpointer,
middleware=[
ModelCallLimitMiddleware(thread_limit=10),
HumanInTheLoopMiddleware(interrupt_on={"send_email": True}),
],
)
config = {"configurable": {"thread_id": "user-session-123"}}
agent.invoke({"messages": [...]}, config=config)
agent.invoke({"messages": [...]}, config=config) # 共享计数器和中断状态
create_agent 返回的图可以直接作为 LangGraph 子图节点:
python
from langgraph.graph import StateGraph, MessagesState, START, END
weather_agent = create_agent(
model="openai:gpt-4o",
tools=[get_weather],
middleware=[ModelCallLimitMiddleware(run_limit=3)],
)
graph = StateGraph(MessagesState)
graph.add_node("weather_agent", weather_agent)
graph.add_edge(START, "weather_agent")
graph.add_edge("weather_agent", END)
app = graph.compile()
result = app.invoke({"messages": [{"role": "user", "content": "北京天气怎么样?"}]})
中间件是编译时扩展,在 create_agent() 时就已经编译到图中了。生产环境别用 InMemorySaver,用 SqliteSaver 或 PostgresSaver。
Callbacks 和 Middleware 怎么选
两者不是替代关系,是互补的。
| 你要做什么 | 用哪个 | 为什么 |
|---|---|---|
| 记录日志、调试输出 | Callbacks | 纯观察,不改流程 |
| Token 用量统计 | Callbacks | 被动收集数据 |
| 链路追踪 | Callbacks | 现有生态成熟(LangSmith 等) |
| 流式输出处理 | Callbacks | on_llm_new_token 天然支持 |
| 限制模型/工具调用次数 | Middleware | 需要主动阻止执行 |
| 模型故障切换 | Middleware | 需要改模型选择 |
| PII 检测脱敏 | Middleware | 需要改消息内容 |
| 对话摘要压缩 | Middleware | 需要改消息列表 |
| 人工审批 | Middleware | 需要暂停等输入 |
| 重试(指数退避) | Middleware | 需要控制重试逻辑 |
| 任务规划 | Middleware | 需要注入工具和状态 |
| 上下文编辑 | Middleware | 需要改消息内容 |
可以同时用:
python
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware, PIIMiddleware
from langchain_core.callbacks import StdOutCallbackHandler
agent = create_agent(
model="openai:gpt-4o",
tools=[get_weather, calculate, send_email],
middleware=[
SummarizationMiddleware(model="openai:gpt-4o-mini", trigger=("tokens", 4000)),
PIIMiddleware("email", strategy="mask"),
],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "北京天气怎么样?"}]},
config={"callbacks": [StdOutCallbackHandler()]},
)
Middleware 先执行(在 Agent 内部),Callbacks 后触发(观察 Agent 行为)。Middleware 的修改会反映在 Callbacks 观察到的事件中。
本文档基于 LangChain 官方文档(https://docs.langchain.com/oss/python/langchain/overview)和 API 参考(https://reference.langchain.com/python/langchain/)编写,涵盖所有内置中间件。适用于 LangChain >= 1.0 版本。