LangChain 中间件

一、中间件是什么

中间件是一段挂在 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=。

按用途分成四类,后文就按这个顺序讲:

flowchart TB M[中间件] --> P[计划与流程控制] M --> F[文件与执行] M --> C[上下文管理] M --> E[能力扩展] P --> P1[TodoList<br/>先列清单再动手] P --> P2[HumanInTheLoop<br/>工具执行前问人] P --> P3[Rubric<br/>答完按标准自评] F --> F1[Filesystem<br/>读写文件] F --> F2[CodeInterpreter<br/>写JS处理数据] C --> C1[Summarization<br/>旧消息压成摘要] C --> C2[Memory<br/>跨会话的记忆] E --> E1[Skills<br/>按需加载技能包] E --> E2[SubAgent<br/>丢给子代理去做]

要读出来的是:这张图只是分类,不是依赖关系 ,没有「得先挂哪一类」的说法,同一个中间件也能按不同参数挂多份。真正要盯的是「需要后端」那一列,标 ✅ 的几个中间件共用同一个 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 也只能看到它交回来的结论。


六、容易踩的坑

  1. 以为 FilesystemMiddleware 挂上就能执行命令。 execute 只在实现了沙箱协议的后端上有,普通后端调用它只会拿到一段错误信息。
  2. 以为 tools 可以随便减。 里面必须带 read_file,漏了直接报错:ValueError: read_file must be included in tools; it is required by FilesystemMiddleware。
  3. 以为 CodeInterpreterMiddleware 跑的是 Python。 它跑的是 QuickJS 里的 JavaScript,工具名默认 eval。
  4. 以为 MemoryMiddleware 的 sources 传进来就自动能记住。 读是它做的,写是模型自己调 edit_file 做的,后端不可写就存不下来。
  5. 以为 TodoListMiddleware 什么任务都能加速。 内置提示词明确要求简单任务别用清单,硬套多花 token。
  6. 以为 RubricMiddleware 会给你一个「没通过」的最终回答。 它不改最后那条 AIMessage,你看到的是没过审的那版草稿;要判断状态得看 _rubric_status 或用 on_evaluation。
  7. 以为 RubricMiddleware 挂在 deepagent 上就自动生效。 它默认不在栈里,而且必须有 rubric 传进来才工作。
  8. 以为压缩之后历史就没了。 deepagent 版会把旧消息卸载到 /conversation_history/{session_id}.md,模型还能读回来。
  9. 以为 interrupt_on 是个开关,能管住所有工具。 它是白名单:没写进去的工具一律不中断。
  10. 以为子代理会继承主 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。
相关推荐
杨超越luckly3 小时前
一线加新一线占61.7%,506家店,西西弗书店的“贵地生存法则”
数据分析·agent·可视化·西西弗书店·生意经
半糖程序员4 小时前
从零构建 Agent(11):压缩过长的上下文
typescript·agent
析数塔4 小时前
rea 逆向工具体验:把 Electron 应用、Native 模块、.NET 程序都交给 Agent 追问
开源·agent
浪子明X4 小时前
LangChain 接入 Embedding 之前:先画清文本、向量与检索器的契约
人工智能·langchain·embedding
北京地铁1号线4 小时前
为大模型创建一个简单的skill:在12306网站查询车次(仅作查询,不提供购票功能,仅作学习使用)
自然语言处理·大模型·agent·技能·skill
架构师那点事儿4 小时前
Agent Skill: 视频/PPT 内容提取 Skill —— 从 0 到 1 诞生记 + 使用指南
llm·agent·ai编程
大连好光景4 小时前
LangChain是干啥的?有哪些核心组件?
langchain·ai应用
sarasuki4 小时前
MCP 为什么是一个协议而不是一个框架呢?
设计模式·agent·mcp
全栈Agent 小李5 小时前
【无标题】
前端·后端·agent·ai编程·全栈·cursor·mcp