一、中间件是什么
中间件是一段挂在 Agent 图上、在固定时机执行的代码,作用是给 Agent 加能力。
加能力的方式只有三种:
| 加能力的方式 | 做什么 | 例子 |
|---|---|---|
| 注入工具 | 往工具列表里塞新工具,模型自己决定什么时候调 | write_todos、task、eval、read_file |
| 注入提示词 | 往系统提示词里拼一段说明,告诉模型有这些工具、该怎么用 | TodoList、Skills、Memory |
| 改消息 / 拦调用 | 在消息进模型前、工具执行前动手 | Summarization 压缩历史、HITL 打断执行 |
挂载方式就是创建 Agent 时传 middleware=:
python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware, TodoListMiddleware
agent = create_agent(
model=model,
# 中间件按列表顺序依次应用,参数都在构造时传进去
middleware=[
TodoListMiddleware(
tool_description="创建和管理本次会话的待办清单。", # 不填用内置描述
),
HumanInTheLoopMiddleware(
interrupt_on={"write_file": True}, # 哪些工具要中断
),
],
)
create_deep_agent 的参数完全一样。它另外开了几个便利参数,skills=、memory=、subagents=、interrupt_on=,不用你手写中间件实例,内部会自动组好对应的那个。
全局对照表
| 中间件 | 功能 | 需要后端 | deepagent 内置 |
|---|---|---|---|
FilesystemMiddleware |
提供文件读写、查找、执行命令等工具 | ✅ | ✅ |
HumanInTheLoopMiddleware |
提供在调用工具前中断的能力 | ❌ | ✅(interrupt_on=) |
SkillsMiddleware |
提供 skills 能力 | ✅ | ✅(skills=) |
MemoryMiddleware |
提供记忆能力 | ✅ | ✅(memory=) |
SummarizationMiddleware |
提供上下文压缩的能力 | ✅ | ✅ |
TodoListMiddleware |
先计划再行动 | ❌ | ❌ |
RubricMiddleware |
轮次末尾评审目标 | ❌ | ❌ |
CodeInterpreterMiddleware |
代码解释器 | ❌ | ❌ |
SubAgentMiddleware |
提供子代理功能 | ✅ | ✅(subagents=) |
表里的「需要后端」指需要文件系统后端(backend):要么有文件可读,要么有地方存东西。标 ❌ 的那几个,纯靠图状态和提示词就能工作。
「deepagent 内置」指
create_deep_agent的默认栈里已经带了它。标 ❌ 的要自己写进middleware=。
按用途分成四类,后文就按这个顺序讲:
要读出来的是:这张图只是分类,不是依赖关系 ,没有「得先挂哪一类」的说法,同一个中间件也能按不同参数挂多份。真正要盯的是「需要后端」那一列,标 ✅ 的几个中间件共用同一个 backend 参数;而且 Memory 的写回要靠 Filesystem 提供的 edit_file 工具,这两个通常会一起出现。
二、计划与流程控制
2.1 TodoListMiddleware:先列清单再动手
它给 Agent 加了 write_todos 工具,并在系统提示词里要求它:复杂任务先拆成待办,每完成一步立刻改状态。
| 导入 | from langchain.agents.middleware import TodoListMiddleware |
| 注入 | write_todos 工具 + 一段使用说明 |
| 状态 | state["todos"],每项是 {content, status},status 取 pending / in_progress / completed |
| 后端 | 不需要 |
python
from langchain.agents import create_agent
from langchain.agents.middleware import TodoListMiddleware
agent = create_agent(
model=model,
middleware=[
TodoListMiddleware(
# 模型看到的工具描述,决定它什么时候想起来用清单
tool_description="创建和管理本次会话的待办清单。",
# 系统提示词里那段使用说明,不填就用内置的
system_prompt="复杂任务先列出待办,每完成一步立刻把状态改成 completed。",
)
],
)
agent.invoke({"messages": [{"role": "user", "content": "设计一个 7 天 LangGraph 训练营"}]})
write_todos 这个工具名,和 todos 这个状态字段,都不用自己配,中间件会带上。上面两个参数不填就走内置文案,模型用清单的节奏不合你意时再改。
适合需要多步骤、中途还要调整的任务。默认提示词里明确写了「简单任务别用这个工具」,一两个步骤能做完的事硬套清单反而多花 token。
2.2 HumanInTheLoopMiddleware:工具执行前先问人
它让指定工具的调用先停下来等人批,人来决定批准、改参数、拒绝,或者直接替工具回答。
| 导入 | from langchain.agents.middleware import HumanInTheLoopMiddleware |
| 注入 | 中断点,不是工具 |
| 后端 | 不需要,但中断要能存档,得配检查点(AgentServer 自带) |
python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
agent = create_agent(
model=model,
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
# True:调用时必经审批,四种决定都允许
"write_file": True,
# False:不问,直接执行
"read_file": False,
# 字典:逐项精细配置
"delete": {
# 允许哪些决定(这里放开了改参数,所以下面要给 args_schema)
"allowed_decisions": ["approve", "edit", "reject"],
# 给审批人看的说明
"description": "删除文件前需要人工确认。",
# 只在本次参数命中时才中断(这里:删 workspace 之外的路径)
"when": lambda req: not req.tool_call["args"].get("path", "").startswith("./workspace"),
# 人工改参数时给出的表单结构
"args_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
},
},
# 某个工具没单独配 description 时,用这段前缀生成审批说明
description_prefix="工具执行前需要人工审批",
)
],
)
interrupt_on 的 key 是工具名,value 有三种写法:True、False,或者一个配置字典。字典能填四项:
| 字段 | 作用 |
|---|---|
allowed_decisions |
允许哪些决定:approve edit reject respond |
description |
给审批人看的说明,可以传函数,按本次参数动态生成 |
when |
传函数,按本次工具调用的参数决定这一回要不要中断 |
args_schema |
人工改参数时给出的表单结构 |
四种决定的结果:
| 决定 | 结果 |
|---|---|
approve |
用原参数继续执行 |
edit |
用改过的参数执行 |
reject |
不执行,把拒绝原因作为失败的 ToolMessage 交回模型 |
respond |
不执行,把人的回答当作工具的成功结果交回模型 |
deepagent 里不用手写这个类,传 interrupt_on= 就自动组上:
python
from deepagents import create_deep_agent
agent = create_deep_agent(
model=model,
# 写法和平时的 HumanInTheLoopMiddleware 完全一样
interrupt_on={
"write_file": True,
"read_file": False,
"delete": {"allowed_decisions": ["approve", "reject"]},
},
)
2.3 RubricMiddleware:答完再按标准自评
每一轮回答结束后,它用一个 grader 模型按你给的标准打分,不达标就带着评审意见再来一轮。
| 导入 | from deepagents import RubricMiddleware |
| 触发 | 调用时在 state 里传 rubric,不传就是普通 Agent |
| 后端 | 不需要 |
python
from deepagents import RubricMiddleware, create_deep_agent
def record(evaluation):
"""每评一次回调一次,想看清每轮为什么没通过就接在这里。"""
print(evaluation)
agent = create_deep_agent(
model=model,
middleware=[
RubricMiddleware(
model=model, # grader 用的模型,可以换个便宜的
max_iterations=3, # 最多评 3 轮,到顶就以当前状态收手
system_prompt=None, # 不填就用内置的评审提示词
on_evaluation=record, # 每评一次回调一次
tools=None, # grader 的辅助工具,不填就只看对话记录打分
)
],
)
result = agent.invoke(
{
"messages": [{"role": "user", "content": "写一份 Python 学习建议"}],
"rubric": "最终答案必须包含:1. 学习目标;2. 三条具体行动;3. 一个可执行的今日练习。",
}
)
tools 是给 grader 用的辅助工具,不填就只看对话记录打分。
两点要注意:它不在 deepagent 的默认栈里 ,得自己写进 middleware=;而且评审没过而收手时,messages 里留下的是最后一版草稿 ,不会额外告诉你没过。要判断状态得看返回 state 里的 _rubric_status,或者用 on_evaluation 把每次评审记下来。
三、文件与执行
3.1 FilesystemMiddleware:给 Agent 一套文件工具
它让模型把中间产物、长内容、结果写到文件里,而不是全堆在对话里。
| 导入 | from deepagents import FilesystemMiddleware |
| 注入 | ls read_file write_file edit_file delete glob grep,后端支持执行时再加 execute |
| 后端 | 需要 |
python
from langchain.agents import create_agent
from deepagents import FilesystemMiddleware
from deepagents.backends import FilesystemBackend
agent = create_agent(
model=model,
middleware=[
FilesystemMiddleware(
# root_dir 是 Agent 眼里的根目录
backend=FilesystemBackend(root_dir="./workspace"),
# 只暴露其中几个工具,不填就是全部(等同于 "all")
tools=["ls", "read_file", "write_file", "edit_file"],
# 逐条换掉工具描述,键是工具名
custom_tool_descriptions={"write_file": "把内容写入指定文件。"},
# 单条命令最长执行秒数,只在沙箱后端上有效
max_execute_timeout=3600,
# 工具结果超过这么多 token 就从上下文卸到文件里
tool_token_limit_before_evict=20000,
# grep 一次最多返回多少条匹配
grep_max_count=1000,
)
],
)
tools 里必须带上 read_file ,少一个都不行,会直接抛 ValueError: read_file must be included in tools; it is required by FilesystemMiddleware。
最后两个参数一般不用动,上面写的就是默认值:tool_token_limit_before_evict 是工具结果超过多少 token 就从上下文卸到文件(20000),grep_max_count 是 grep 一次最多返回多少条匹配(1000)。
后端选型:
| 后端 | 文件存在哪 | 适合 |
|---|---|---|
StateBackend |
图状态里,不传 backend 时的默认值 |
临时文件,跑完就丢 |
FilesystemBackend |
真实磁盘,root_dir 是根 |
本地开发、CLI 工具 |
StoreBackend |
持久化存储 | 跨会话保留文件 |
CompositeBackend |
按路径前缀分流,一部分磁盘一部分内存 | 记忆文件要落盘、临时文件不要 |
| 沙箱后端 | 沙箱里 | 需要 execute 时只能用它 |
execute 只在实现了沙箱协议的后端上可用 ,普通后端下调用它只会拿到一段错误信息。另外 FilesystemBackend 默认 virtual_mode=True,会挡掉 ..、~ 和 root_dir 之外的绝对路径,但这是路径护栏,不是沙箱。
3.2 CodeInterpreterMiddleware:让模型写代码处理数据
它注入一个 eval 工具,跑的是隔离的 QuickJS,模型可以写 JavaScript 处理数据、批量调工具,中间结果不用全带进上下文。
| 导入 | from langchain_quickjs import CodeInterpreterMiddleware |
| 注入 | eval,工具名可改 |
| 后端 | 不需要 |
python
from langchain.agents import create_agent
from langchain_python.tools import web_search
from langchain_quickjs import CodeInterpreterMiddleware
agent = create_agent(
model=model,
middleware=[
CodeInterpreterMiddleware(
tool_name="eval", # 暴露给模型的工具名
timeout=5.0, # 单次 eval 最长跑 5 秒
memory_limit=64 * 1024 * 1024, # 堆内存上限,64 MiB
max_result_chars=4000, # 结果和 stdout 各自最多返回多少字符
capture_console=True, # 抓 console.log,以 stdout 返回
mode="thread", # thread 跨轮次 / turn 只在一轮内 / call 每次重开
ptc=[web_search], # 允许 JS 里用 tools.webSearch(...) 调这个工具
max_ptc_calls=256, # 单次 eval 里最多调几次工具
subagents=True, # JS 里给不给 task(...)
max_snapshot_bytes=None, # 快照大小上限,None 表示跟随 memory_limit
)
],
)
它是 JavaScript 解释器,不是 Python。 还有一点,ptc 和 JS 里的 task(...) 不走普通工具节点,所以不会触发 2.2 小节那套人工审批。
四、上下文管理
4.1 SummarizationMiddleware:把旧消息压成摘要
上下文快到上限时,它把较早的消息交给模型压成一段摘要,用摘要替换掉原文。
| 导入 | from langchain.agents.middleware import SummarizationMiddleware |
| 触发 | 每次调模型前检查长度 |
| 后端 | deepagent 版需要,被换掉的历史要卸载到文件里 |
python
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langchain_core.messages.utils import count_tokens_approximately
agent = create_agent(
model=model,
middleware=[
SummarizationMiddleware(
model=model, # 用哪个模型写摘要
trigger=("fraction", 0.8), # 用到上下文窗口的 80% 就触发
keep=("messages", 20), # 最近的 20 条原样保留
trim_tokens_to_summarize=4000, # 送去写摘要的那段历史最多取多少 token
# 写摘要用的提示词,必须带 {messages} 占位符;不填就用内置那份
summary_prompt="把下面的对话压成要点,保留关键结论和未决事项:\n\n{messages}",
# 估 token 数的函数,默认按字符数粗略估
token_counter=count_tokens_approximately,
)
],
)
trigger 和 keep 都是「单位 + 数值」的写法:
| 单位 | 含义 | 例 |
|---|---|---|
("tokens", n) |
到 n 个 token 触发 | ("tokens", 80000) |
("fraction", f) |
到模型窗口的 f 比例触发 | ("fraction", 0.8) |
("messages", n) |
到 n 条消息触发 | ("messages", 100) |
deepagent 内置的那份不是这个类 ,是 create_summarization_middleware(model, backend) 造出来的,多一个能力:被压缩的历史不是删掉,而是写成 markdown 存到后端的 /conversation_history/{session_id}.md,模型后续需要细节还能 read_file 读回来。所以它依赖后端。
python
from deepagents import create_deep_agent
# 默认就带压缩,不用你传 middleware
agent = create_deep_agent(model=model)
4.2 MemoryMiddleware:跨会话的记忆
启动时把记忆文件读出来拼进系统提示词,模型在对话里学到新东西时,用文件工具写回这些文件。
| 导入 | from deepagents import MemoryMiddleware |
| 注入 | 记忆文件内容 + 一段「什么时候该更新记忆」的说明 |
| 后端 | 需要 |
python
from deepagents import create_deep_agent
agent = create_deep_agent(
model=model,
# 加这个参数就等于挂上这个中间件,可以给多个文件,按顺序读
memory=["/memories/AGENTS.md", "/memories/preferences.md"],
)
手写:
python
from deepagents import MemoryMiddleware
from deepagents.backends import FilesystemBackend
MemoryMiddleware(
backend=FilesystemBackend(root_dir="./backup"),
# 按顺序加载,可以给多份,内容拼在一起
# 这些是虚拟路径,会落在 root_dir 下面,实际读的是
# ./backup/memories/AGENTS.md 和 ./backup/memories/preferences.md
sources=["/memories/AGENTS.md", "/memories/preferences.md"],
# 给提示词打缓存断点,只在 Anthropic 模型上生效
add_cache_control=False,
# 包住记忆内容的模板,必须留 {agent_memory} 这个占位符才合法;不填就用内置的
system_prompt="以下是你的长期记忆,用到时自行维护:\n\n{agent_memory}",
)
system_prompt 是包住记忆内容的那段模板,要保留 {agent_memory} 这个占位符才合法,不填就用内置的。
它和 Summarization 的分工:Summarization 管的是这一轮对话太长 ,Memory 管的是跨会话要记住的东西 。记忆是模型自己调 edit_file 写的,所以文件得放在可写的后端上。
五、能力扩展
5.1 SkillsMiddleware:按需加载技能包
扫描技能目录,把每个技能的「名字 + 描述」先给模型看;模型觉得用得上,再去读完整的 SKILL.md。
| 导入 | from deepagents.middleware import SkillsMiddleware |
| 注入 | 技能清单 + 读 SKILL.md 的引导 |
| 后端 | 需要,至少要能读技能文件 |
技能目录的结构:
bash
skills/
└── pdf-report/
├── SKILL.md # 必需:头部的 name / description + 正文说明
├── scripts/ # 可选:可执行脚本
├── references/ # 可选:需要时再读的补充资料
└── assets/ # 可选:图片、模板
python
from deepagents import create_deep_agent
agent = create_deep_agent(
model=model,
# 加这个参数就等于挂上这个中间件,可以给多个技能目录
skills=["/home/user/skills/", "/shared/skills/"],
)
这套做法的关键是按需展开 :系统提示词里只放技能名和一句话描述,完整说明书等模型判断用得着时再 read_file 读。技能再多也不会把上下文撑爆。
手写时要给 sources,字符串或 (路径, 显示名) 元组都行,同名技能后面的覆盖前面的:
python
from deepagents.middleware import SkillsMiddleware
SkillsMiddleware(
backend=backend,
sources=[("/home/user/skills/", "我的技能"), ("/shared/skills/", "团队技能")],
# 技能说明的模板,不填用内置那份(靠 {skills_list} 这些占位符拼出来);
# 传 None 表示连这段说明也不追加
system_prompt=None,
)
system_prompt 是那段技能说明的模板:不填就用内置那份(靠 {skills_list} 这些占位符拼出来),传 None 则连这段说明也不追加,一般不用改。
| 能力 | 要什么后端 |
|---|---|
| 读技能文件、按技能指引写文件 | 普通后端就行 |
| 执行技能里的脚本 | 沙箱后端 |
5.2 SubAgentMiddleware:把子任务丢给子代理
它注入一个 task 工具:主 Agent 把子任务交给子代理,子代理在自己的上下文里做完,只把结论带回来。
| 导入 | from deepagents import SubAgentMiddleware |
| 注入 | task |
| 后端 | 需要 |
python
from deepagents import FilesystemMiddleware, create_deep_agent
from langchain_python.tools import web_search
agent = create_deep_agent(
model=model,
subagents=[
{
"name": "researcher",
"description": "查资料、读网页,适合需要外部信息的问题",
"system_prompt": "你只负责检索和整理资料,最后输出要点清单。",
"tools": [web_search], # 不填就继承主 Agent 的工具
},
{
"name": "coder",
"description": "写代码、跑代码,适合需要计算或验证的任务",
"system_prompt": "你只负责写代码并验证它能跑通。",
# 单独指定模型,可以换个便宜的
"model": "anthropic:claude-haiku-4-5",
# 子代理自己的审批规则;不写就继承主 Agent 的 interrupt_on
"interrupt_on": {"write_file": True},
# 子代理自己的技能目录
"skills": ["/home/user/coding-skills/"],
# 子代理自己的中间件,这里限制它只能用这几个文件工具
"middleware": [FilesystemMiddleware(tools=["ls", "read_file"])],
},
],
)
SubAgent 就是个字典:
| 字段 | 必填 | 含义 |
|---|---|---|
name |
✅ | 子代理名,主 Agent 用这个名字点名 |
description |
✅ | 一句话说明它擅长什么,主 Agent 靠这句决定派不派 |
system_prompt |
✅ | 子代理自己的系统提示词 |
tools |
只给它的工具,不填就继承主 Agent 的工具 | |
model |
单独指定模型,可以换个便宜的 | |
middleware |
给它挂自己的中间件 | |
interrupt_on |
子代理自己的审批规则 | |
skills |
子代理自己的技能目录 |
手写:
python
from deepagents import SubAgentMiddleware
SubAgentMiddleware(
backend=backend,
# 每个子代理都要给 name / description / system_prompt
subagents=[{"name": "researcher", "description": "...", "system_prompt": "..."}],
# task 工具本身的描述,不填用内置的;自定义时可以留 {available_agents} 占位符
task_description="把子任务交给合适的子代理去做,它会独立完成后把结论带回来。",
)
两点要知道:不传 subagents= 也会有一个默认的 general-purpose 子代理 ,task 工具照样存在;子代理的中间过程不会出现在主对话里,这是省上下文的关键,代价是主 Agent 也只能看到它交回来的结论。
六、容易踩的坑
- 以为
FilesystemMiddleware挂上就能执行命令。execute只在实现了沙箱协议的后端上有,普通后端调用它只会拿到一段错误信息。 - 以为
tools可以随便减。 里面必须带read_file,漏了直接报错:ValueError: read_file must be included in tools; it is required by FilesystemMiddleware。 - 以为
CodeInterpreterMiddleware跑的是 Python。 它跑的是 QuickJS 里的 JavaScript,工具名默认eval。 - 以为
MemoryMiddleware的sources传进来就自动能记住。 读是它做的,写是模型自己调edit_file做的,后端不可写就存不下来。 - 以为
TodoListMiddleware什么任务都能加速。 内置提示词明确要求简单任务别用清单,硬套多花 token。 - 以为
RubricMiddleware会给你一个「没通过」的最终回答。 它不改最后那条AIMessage,你看到的是没过审的那版草稿;要判断状态得看_rubric_status或用on_evaluation。 - 以为
RubricMiddleware挂在 deepagent 上就自动生效。 它默认不在栈里,而且必须有rubric传进来才工作。 - 以为压缩之后历史就没了。 deepagent 版会把旧消息卸载到
/conversation_history/{session_id}.md,模型还能读回来。 - 以为
interrupt_on是个开关,能管住所有工具。 它是白名单:没写进去的工具一律不中断。 - 以为子代理会继承主 Agent 的全部配置。 只有
tools不填才继承,model、skills、middleware、interrupt_on都是各算各的。
七、小结
- 中间件只做三件事:注入工具、注入提示词、改消息或拦调用。
- 按用途分四类:计划与流程控制、文件与执行、上下文管理、能力扩展,四类可以任意组合。
- 只要动到文件,就得给后端:Filesystem、Skills、Memory、Summarization(deepagent 版)、SubAgent 都依赖它。
execute和技能脚本执行需要沙箱后端,普通后端下这两个能力是空的。- deepagent 的便利参数 :
skills=、memory=、subagents=、interrupt_on=分别对应四个内置中间件,不用手写实例。 - 不内置的三个要自己挂 :
TodoListMiddleware、RubricMiddleware、CodeInterpreterMiddleware。 - 注入给模型的工具名 :TodoList →
write_todos,SubAgent →task,CodeInterpreter →eval,Filesystem →ls/read_file/write_file/edit_file/delete/glob/grep/execute。