很多团队第一次做 Agent,关注点通常会落在 Prompt、模型和工具调用上:怎么写系统提示词,怎么注册 tool,怎么让模型多轮调用工具,怎么把最终答案组织得更像人。但真正把 Agent 放进工程场景之后,麻烦往往不在这些表层能力,而在运行时基建:
- 任务执行过程中,Agent 需要一个可控的工作区,而不是随意读写宿主机。
- 长任务需要保存中间文件、检索文件、修改文件,还要能限制权限。
- 团队希望复用某些能力,比如画图、写报告、查代码、生成测试,而不是每个 Agent 都塞一堆 Prompt。
- 复杂任务不适合一个上下文窗口硬扛,应该拆给不同子 Agent。
- 对话越来越长时,需要压缩历史,又不能把关键事实丢掉。
- 用户偏好、项目约定、仓库事实需要长期记忆,而不是每次都重新喂给模型。
这篇文章的核心结论是:
DeepAgents 的价值不只是"开箱即用的 Agent",而是把复杂 Agent 常见的运行时基建收敛成一组可组合 middleware。它让你不用从零搭文件系统、Skill、子 Agent、长期记忆和上下文压缩,但前提是你必须清楚每个 middleware 的职责边界和安全代价。
先把问题说清楚:Agent 难的不是调用一次模型
最简单的 Agent demo 通常长这样:
- 用户输入一个问题。
- 模型判断是否需要调用工具。
- 工具返回结果。
- 模型基于结果回答。
这个流程可以证明"模型会用工具",但它离真实业务还很远。真实任务更像下面这样:
用户希望 Agent 分析一个项目,读 README,搜索源码,写一份迁移方案,生成一张架构图,记住项目约定,第二天还能接着聊。如果上下文太长,要自动压缩;如果需要某项专业能力,要加载 Skill;如果某一步是独立分析,应该交给子 Agent;如果要写文件,只能写到工作区,不能碰用户隐私文件。
这时你会发现,Agent 不是单个 LLM 调用,而是一套运行系统。它至少包含这些层:
如果你自己基于 LangGraph 从零做,这些能力都能实现,但成本并不低。你要设计状态结构、工具协议、路由循环、权限规则、记忆读写策略、摘要触发策略,还要处理工具输出太大、历史消息太长、子任务上下文污染等问题。DeepAgents 的定位,就是在 LangGraph 和业务 Agent 之间提供一层更高阶的 harness,也就是"带运行时设施的 Agent 外壳"。
LangChain 给你模型、消息、工具等基础积木;LangGraph 给你状态图、循环控制、持久化和可观测执行;DeepAgents 则进一步把长任务 Agent 经常重复写的运行时能力封装出来。它不是替代 LangGraph,而是把一部分高频 Agent 架构固化成默认组合。
DeepAgents 与 LangChain middleware 的关系
要理解 DeepAgents,先要理解 middleware 在 Agent 链路里的位置。
在 LangChain 的 createAgent 中,middleware 不是"请求拦截器"的简单翻版。它可以做几类事:
- 在 Agent 运行前后读写状态。
- 在模型调用前后注入上下文、统计调用次数、短路流程。
- 包装模型调用,比如动态改写
systemMessage。 - 扩展工具列表,或者包装工具调用。
- 通过
stateSchema给 Agent 状态增加字段。 - 在特定 hook 中用
jumpTo控制流程跳转,比如直接结束。
当前 demo 里的 src/middleware-test.mjs 就展示了这个机制。下面是经过整理后的关键片段:
js
import { z } from "zod"
import { createMiddleware, AIMessage } from "langchain"
const loggingMiddleware = createMiddleware({
name: "LoggingMiddleware",
stateSchema: z.object({
modelCallCount: z.number().default(0),
}),
beforeModel: state => {
console.log(
`[Logging] messages=${state.messages.length}, calls=${state.modelCallCount}`
)
},
afterModel: state => {
return { modelCallCount: state.modelCallCount + 1 }
},
})
const conciseAnswerMiddleware = createMiddleware({
name: "ConciseAnswerMiddleware",
wrapModelCall: async (request, handler) => {
return handler({
...request,
systemMessage: request.systemMessage.concat("\n\n请用一句话简洁回答。"),
})
},
})
const blockedContentMiddleware = createMiddleware({
name: "BlockedContentMiddleware",
beforeModel: {
canJumpTo: ["end"],
hook: state => {
const last = state.messages.at(-1)
const text =
typeof last?.content === "string"
? last.content
: String(last?.content ?? "")
if (text.includes("BLOCKED")) {
return {
messages: [new AIMessage("该请求已被 middleware 拦截。")],
jumpTo: "end",
}
}
},
},
})
这段代码并不复杂,但它说明了 middleware 的几个关键点。
第一,stateSchema 是 Agent 状态扩展点。modelCallCount 不是消息本身,而是运行过程中的附加状态。很多生产能力都会依赖这类状态,例如预算控制、工具调用计数、用户权限上下文、trace 标记等。
第二,wrapModelCall 不是修改用户消息,而是修改模型请求。这里把回答风格追加到 systemMessage 中,比在每个用户 Prompt 后面拼接一句"请简洁回答"更稳定,也更容易集中管理。
第三,beforeModel 可以短路流程。这个能力在工程里很实用,比如命中风控规则、缺少权限、预算耗尽、输入不合法时,不必再调用模型。
所以,DeepAgents 提供的 createFilesystemMiddleware、createSkillsMiddleware、createSubAgentMiddleware、createMemoryMiddleware、createSummarizationMiddleware 本质上不是"几个工具函数",而是一组已经写好的 Agent 运行时 middleware。
从默认选型看:什么时候该用 DeepAgents
在做 Agent 架构选型时,我通常会按任务复杂度分三层。
第一层是普通 createAgent。如果你的需求只是接几个工具、回答几个固定业务问题,普通 Agent 加少量自定义 middleware 就够了。它更轻,心智成本低,适合客服 FAQ、简单数据查询、单轮工具调用。
第二层是 DeepAgents。只要任务开始出现"长上下文、多步骤、可写文件、可复用技能、子任务拆分、记忆或摘要"中的两三项,DeepAgents 就更适合作为默认方案。你不需要一开始就写完整 LangGraph,但也不是把一切都塞进 Prompt。
第三层是直接写 LangGraph。当你的流程需要强确定性状态机、复杂人工审批、多分支回滚、严格审计、跨服务编排,或者每个节点都有清晰业务语义时,直接使用 LangGraph 更合适。DeepAgents 是高阶封装,封装意味着省事,也意味着一部分运行策略已经替你做了默认选择。
可以简单对比一下:
| 方案 | 默认适用场景 | 优点 | 代价 |
|---|---|---|---|
createAgent + 自定义 middleware |
简单助手、少量工具、轻量状态扩展 | 简洁、可控、依赖少 | 长任务基建要自己补 |
createDeepAgent 或 DeepAgents middleware |
研发助手、文档助手、调研 Agent、多步骤任务 | 文件系统、子 Agent、Skill、记忆、摘要有现成组合 | 需要理解默认行为和安全边界 |
| 原生 LangGraph | 复杂业务流程、强状态机、生产编排 | 控制力最强,可审计性最好 | 开发成本高,样板代码多 |
我的默认建议是:**从 createAgent 开始验证工具和 Prompt,从 DeepAgents 开始做长任务能力,从 LangGraph 接管强流程编排。**不要为了"架构完整"一开始就上最重的方案,也不要在长任务 Agent 中继续用一个 Prompt 硬扛所有问题。
DeepAgents 的整体运行模型
DeepAgents 官方 README 把它称为 "batteries-included agent harness",也就是带电池的 Agent 运行外壳。它默认包含规划、文件系统、子 Agent、上下文管理等能力。本文重点看 demo 中直接使用的 middleware,因为它们更能体现工程可组合性。
这里有两个概念必须分清。
**middleware 负责把能力接入 Agent 运行链路。**例如文件系统 middleware 会注册 ls、read_file、write_file、edit_file 等工具,并把文件状态接入 Agent;Skill middleware 会读取 Skill 元数据并注入系统上下文;摘要 middleware 会在模型调用前判断是否需要压缩历史。
**backend 负责能力背后的存储或执行环境。**同样是文件系统能力,backend 可以是本地目录、状态存储、LangSmith Hub、组合后端,或者能执行 shell 的本地后端。middleware 是"怎么接入 Agent",backend 是"能力落在哪里"。
这也是 DeepAgents 工程使用的关键判断点:你不能只问"我要不要加 FilesystemMiddleware",还要问"这个文件系统背后到底连着哪里,权限边界是什么"。
实战场景:做一个可控的研发助手 Agent
为了避免 demo 过于教学化,我们把示例改造成一个更真实的场景:一个"研发助手 Agent",它要能做这些事:
- 读写项目工作区里的任务文件。
- 加载 Skill,比如画图、写报告、分析代码。
- 把数学计算、代码检索、文档撰写等子任务委派给不同子 Agent。
- 从
AGENTS.md和用户偏好文件中读取长期记忆。 - 对长对话自动摘要,避免上下文爆掉。
数据流大致是:
这条链路的关键,不是把所有功能都打开,而是给每个能力安排清楚的职责:文件系统只做工作区,Skill 只做能力复用,Memory 只做长期约定,Summarization 只做上下文预算管理,SubAgent 只做任务隔离。
下面逐个拆。
文件系统 middleware:给 Agent 一个可控工作区
Agent 一旦具备写文件能力,就必须有边界。很多人会把"让 Agent 能写文件"理解成把本机目录暴露给它,这在个人实验里能跑,但在工程里风险很高。
当前 demo 的 filesystem-agent.mjs 使用了 FilesystemBackend,并开启 virtualMode: true:
js
import { createAgent, HumanMessage } from "langchain"
import { createFilesystemMiddleware, FilesystemBackend } from "deepagents"
const workspaceDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"workspace"
)
const permissions = [
{ operations: ["read"], paths: ["/secret.txt"], mode: "deny" },
{ operations: ["write"], paths: ["/todo.md"], mode: "allow" },
{ operations: ["write"], paths: ["/**"], mode: "deny" },
]
const agent = createAgent({
model,
tools: [],
systemPrompt:
"工作区根路径为 /。用 ls、read_file、write_file、edit_file 操作文件,路径以 / 开头。中文回答。",
middleware: [
createFilesystemMiddleware({
backend: new FilesystemBackend({
rootDir: workspaceDir,
virtualMode: true,
}),
permissions,
}),
],
})
这里有三个值得认真看的点。
第一,rootDir 是物理目录,virtualMode: true 把它映射成虚拟根目录 /。模型看到的是 /todo.md、/secret.txt,而不是宿主机上的真实绝对路径。这样可以减少路径泄露,也能防止模型绕到工作区外。
第二,permissions 是按顺序匹配的,先命中先生效;没有命中任何规则时默认允许。demo 中先拒绝读 /secret.txt,再允许写 /todo.md,最后拒绝写所有路径。顺序如果写反,语义就会变。
第三,权限规则只覆盖文件系统工具的读写类操作。DeepAgents 的类型声明里明确提示,execute 这类 shell 执行能力不能靠路径权限兜住,因为 shell 命令可以访问任何路径。如果后端支持执行命令,就不能只靠 permissions 产生安全感。
工程里我更建议把文件系统分成三类:
| 区域 | 建议 backend | 是否允许写 | 典型用途 |
|---|---|---|---|
| 临时工作区 | FilesystemBackend 或状态型 backend |
允许 | 中间产物、报告、任务清单 |
| 项目只读区 | 只读权限规则 | 默认只读 | README、源码、设计文档 |
| 执行环境 | sandbox backend,而不是普通本地 shell | 谨慎 | 跑测试、生成构建产物 |
如果只是让 Agent 写一份报告,用虚拟工作区即可。如果要让 Agent 跑命令,必须额外考虑隔离、超时、环境变量、密钥、输出截断和审计。
Skill middleware:把"会做某事"从 Prompt 里拆出来
很多 Agent 项目做着做着,会把系统 Prompt 写成一本小册子:如何画图、如何写测试、如何生成报告、如何做代码评审、如何提交 PR。短期看方便,长期看会出三个问题。
第一,Prompt 越来越长,每次调用都消耗上下文。第二,能力之间互相干扰,模型很难判断当前任务到底该用哪一段说明。第三,能力复用困难,另一个 Agent 想用"画图能力",只能复制一大段 Prompt。
Skill middleware 解决的是这个问题:把能力说明放在独立的 SKILL.md 中,Agent 初始只看到 Skill 的名称和描述,真正需要时再读取完整 Skill 文件。这就是 progressive disclosure,也就是渐进式披露。
当前 demo 的 skills-agent.mjs 使用本地 .agents/skills/excalidraw-diagram-generator/SKILL.md 来生成 Excalidraw 流程图:
js
import {
LocalShellBackend,
createFilesystemMiddleware,
createSkillsMiddleware,
} from "deepagents"
const skills = "/.agents/skills/"
const output = "src/deepagents/output/deepagents-skills-flow.excalidraw"
const backend = await LocalShellBackend.create({
rootDir: ".",
virtualMode: true,
inheritEnv: true,
})
const agent = createAgent({
model,
tools: [],
systemPrompt: "按 skills 库完成任务,需要时 read_file 对应 SKILL.md。中文回答。",
middleware: [
createSkillsMiddleware({ backend, sources: [skills] }),
createFilesystemMiddleware({ backend }),
],
})
这里 createSkillsMiddleware 只负责加载 Skill 元数据和暴露 Skill 目录;真正读写文件仍然依赖 createFilesystemMiddleware。这就是 middleware 组合的典型方式:Skill 告诉 Agent "有哪些专业手册",Filesystem 让 Agent "能读取手册、能写输出文件"。
不过,demo 使用的是 LocalShellBackend,并且 inheritEnv: true。本地类型声明中对 LocalShellBackend 的安全警告很明确:它能直接在宿主机执行 shell,没做进程隔离,也没有沙箱边界。它适合本地开发 CLI、个人可信环境,不适合生产 API 或多租户服务。
所以,Skill middleware 的工程建议是:
- Skill 目录可以开放给 Agent 读取,但不要把整个项目写权限也顺手开放。
- Skill 描述要短而准,描述"什么时候用",不要写成营销文。
- Skill 内容可以长,但应该分层,先给工作流,再给细节,不要把所有参考资料堆进去。
- 需要执行脚本的 Skill 必须配合沙箱或受限执行环境。
- 同名 Skill 多来源时要有覆盖策略,避免项目 Skill 悄悄覆盖全局 Skill 导致行为变化。
Skill 的本质不是"更长的 Prompt",而是可发现、可复用、可按需加载的能力包。
SubAgent middleware:用任务隔离代替上下文硬塞
很多复杂任务失败,不是模型不聪明,而是一个上下文窗口里塞了太多角色。主 Agent 既要搜资料,又要算数,又要写报告,还要检查格式。最后它会在各个目标之间摇摆,工具调用也越来越混乱。
SubAgent middleware 的核心价值是:把独立任务委派给拥有独立系统提示词、工具和上下文窗口的子 Agent。主 Agent 不必亲自做所有事,而是通过 task 工具把任务发出去,再拿回结果。
当前 demo 的 subagent-agent.mjs 选择了一个小学数学辅导场景,虽然业务很轻,但结构很清楚:
js
import { createSubAgentMiddleware } from "deepagents"
const subagents = [
{
name: "math-solver",
description:
"解小学应用题:用 calc、divide_evenly 列式计算,给出最终答案与算式。有具体数字时先用此 Agent。",
systemPrompt: [
"你是解题子 Agent。",
"必须用 calc、divide_evenly 完成计算,不要心算。",
"输出:题目理解、分步算式、最终答案。",
].join("\n"),
tools: [calc, divideEvenly],
},
{
name: "kid-tutor",
description:
"把 math-solver 的解法讲给家长听,方便辅导孩子。",
systemPrompt: "你是辅导讲解子 Agent,用短句和分步提问解释。",
tools: [],
},
{
name: "practice-maker",
description: "出 2 道同类练习题。",
systemPrompt: "你是出题子 Agent,调用 make_similar_problem 至少 2 次。",
tools: [makeSimilarProblem],
},
]
const agent = createAgent({
model,
tools: [],
systemPrompt: [
"你是小学数学辅导主 Agent,通过 task 委派子 Agent。",
"按顺序:math-solver、kid-tutor、practice-maker。",
"最后向家长汇总答案、辅导要点、练习题。",
].join("\n"),
middleware: [
createSubAgentMiddleware({
defaultModel: model,
subagents,
generalPurposeAgent: false,
}),
],
})
把这个场景换成研发助手,结构可以类比成:
code-searcher:只负责搜索源码和定位文件。design-reviewer:只负责架构风险分析。doc-writer:只负责整理成技术文档。test-planner:只负责补测试策略。
子 Agent 的 description 非常关键,因为它是主 Agent 选择子 Agent 的路由提示。很多人写子 Agent 时只认真写 systemPrompt,忽略 description,结果主 Agent 不知道什么时候该调用谁。我的经验是,description 要写清楚三件事:擅长任务、输入条件、输出形式。
generalPurposeAgent: false 也值得注意。DeepAgents 默认可以提供一个通用子 Agent,但 demo 关掉了它,让主 Agent 只能调用显式声明的三个子 Agent。这在教学和受控业务里更好,因为你能确保任务只会落到你定义的角色上。对于开放式研究任务,通用子 Agent 很有用;对于强业务流程,显式子 Agent 更可控。
SubAgent 不是越多越好。每增加一个子 Agent,模型就多一个路由选择,也多一次上下文传递。工程上建议先按"工具边界"和"输出责任"拆,而不是按组织结构拆。一个子 Agent 如果没有独立工具、独立 Prompt、独立输出责任,大概率没有必要单独存在。
Memory middleware:长期记忆不是聊天历史
Agent 的记忆经常被混用。很多项目把所有历史消息都当记忆,最后上下文越来越长,旧信息真假难辨。更合理的做法是区分三类东西:
- 聊天历史:当前会话里的完整消息。
- 压缩摘要:为了继续对话而保留的历史概要。
- 长期记忆:跨会话稳定有效的项目事实、用户偏好和工作约定。
createMemoryMiddleware 处理的是第三类。当前 demo 的 memory-agent.mjs 把记忆放在两个 Markdown 文件里:
js
import {
createFilesystemMiddleware,
createMemoryMiddleware,
FilesystemBackend,
} from "deepagents"
const projectMemoryPath = "/AGENTS.md"
const preferencesMemoryPath = "/memory/preferences.md"
const backend = new FilesystemBackend({
rootDir: workspaceDir,
virtualMode: true,
})
const agent = createAgent({
model,
tools: [],
systemPrompt: [
"你是项目助手。工作区根路径为 /,可用 ls、read_file、write_file、edit_file。",
"根据 <agent_memory> 回答;用户要求记住时,必须立刻 edit_file,且按类型写入对应文件:",
`- ${projectMemoryPath}:项目说明、技术栈、架构、仓库约定等`,
`- ${preferencesMemoryPath}:用户个人偏好(语言、包管理器、回答风格等)`,
"不要混写:项目事实不要写入 preferences,个人偏好不要写入 AGENTS.md。",
].join("\n"),
middleware: [
createFilesystemMiddleware({ backend }),
createMemoryMiddleware({
backend,
sources: [projectMemoryPath, preferencesMemoryPath],
}),
],
})
这段代码有一个很好的工程意识:它没有把所有记忆塞到一个文件里,而是拆成项目记忆和用户偏好。项目事实写到 AGENTS.md,个人偏好写到 preferences.md。这样做的好处是后续更容易治理,例如项目迁移时只带项目记忆,换用户时不带用户偏好。
createMemoryMiddleware 会从 sources 指定的路径加载内容,并注入到 Agent 上下文。它本身解决"读记忆和注入记忆"的问题,但"何时写、写到哪里、写成什么格式"仍然要靠系统 Prompt、文件系统工具和权限策略共同约束。
这也是长期记忆最容易踩坑的地方:不要把模型生成的每一句"我记住了"都当成可靠持久化。真正可靠的做法是:
- 用户明确要求记住,或者系统规则明确要求沉淀时,才写长期记忆。
- 写入前区分事实、偏好、决策、临时上下文。
- 写入要可审计,最好是 Markdown 条目、时间、来源、适用范围。
- 记忆需要定期清理,否则旧偏好和旧项目事实会污染新任务。
- 敏感信息不应该进入普通记忆文件。
长期记忆不是"把聊天记录永久保存",而是把稳定、可复用、后续任务会依赖的信息结构化沉淀。
Summarization middleware:上下文压缩是预算管理,不是无损存档
长对话的常见做法是把所有历史消息一直带着,直到模型上下文撑不住。这个做法有两个问题:成本越来越高,且旧消息里的噪声会影响后续判断。
createSummarizationMiddleware 的思路是:当上下文超过阈值时,把较早的消息写入后端存储,再生成一条摘要消息替代它们,同时保留最近几条原始消息。当前 demo 的 summarization-agent.mjs 用了很低的阈值,方便触发:
js
import { createSummarizationMiddleware, FilesystemBackend } from "deepagents"
const historyPathPrefix = "/conversation_history"
const summaryPrompt = `你是对话摘要助手。请用中文总结以下对话,包含:
1. 讨论的主要话题
2. 达成的关键结论或决定
3. 继续对话所需的重要上下文
保持简洁,不要罗列无关细节。
待摘要的对话:
{conversation}
摘要:`
const agent = createAgent({
model,
tools: [],
systemPrompt:
"你是会话助手。记住用户提到的关键事实,中文简短回答。若看到「此前对话摘要」,请据此继续对话。",
middleware: [
createSummarizationMiddleware({
model,
backend,
historyPathPrefix,
summaryPrompt,
trigger: { type: "messages", value: 8 },
keep: { type: "messages", value: 4 },
}),
],
})
trigger 决定什么时候触发摘要,keep 决定摘要后保留多少最近上下文。DeepAgents 支持按 messages、tokens、fraction 这几种方式描述上下文大小。demo 用消息条数是为了容易演示;生产环境更推荐按 token 或模型 profile 推断,因为消息条数不能反映真实上下文长度。
上下文压缩有明显收益:
- 控制 token 成本。
- 降低超出模型上下文窗口的风险。
- 把旧历史从"逐字消息"变成"任务相关摘要"。
- 长任务可以继续推进,不必频繁重开会话。
但它不是无损存档。摘要一定会丢细节,尤其是代码 diff、数字、日志、用户原话、合同条款这类精确信息。所以摘要 middleware 不能替代文件存档、数据库、审计日志。我的经验是:
- 需要精确复用的信息写入文件或结构化存储,不要只依赖摘要。
- 摘要 Prompt 要明确保留决策、约束、待办、文件路径、错误信息。
- 对代码任务,摘要中必须保留已改文件、未解决问题、测试结果。
- 对法律、财务、医疗等高风险场景,不要把摘要当成事实依据。
keep不宜过小,否则模型会失去最近交互的语气和细节。
如果把上下文比作工作台,Summarization middleware 做的是"把旧材料归档成标签清晰的摘要卡片",而不是"复制一份完整档案"。
参数和配置:不要只知道 API 名字
DeepAgents 的 middleware 很容易上手,但要稳定落地,关键参数必须理解背后的取舍。
| 参数 | 出现场景 | 推荐理解 |
|---|---|---|
rootDir |
FilesystemBackend、LocalShellBackend |
Agent 可见工作区背后的物理根目录。生产中不要指向用户主目录或仓库根目录的全权限空间。 |
virtualMode |
文件 backend | 开启后用虚拟绝对路径映射到 rootDir,能减少路径穿透和真实路径泄露。 |
permissions |
Filesystem middleware | 文件读写规则,按顺序匹配,默认允许。敏感路径要显式 deny,写权限最好默认 deny 再 allow 白名单。 |
sources |
Skills、Memory middleware | Skill 或记忆来源。Skill 里后面的 source 可以覆盖前面的同名 Skill,记忆 source 则会组合注入。 |
generalPurposeAgent |
SubAgent middleware | 是否开启默认通用子 Agent。开放任务可开,强流程任务建议关掉或显式约束。 |
defaultModel |
SubAgent middleware | 子 Agent 默认模型。成本敏感时可以给不同子 Agent 配不同模型。 |
trigger |
Summarization middleware | 摘要触发阈值。demo 可用消息数,生产更建议 token 或 fraction。 |
keep |
Summarization middleware | 摘要后保留的最近上下文。过小会丢近期细节,过大会削弱压缩效果。 |
historyPathPrefix |
Summarization middleware | 旧消息归档位置。要与工作区权限、清理策略、隐私策略一起设计。 |
summaryPrompt |
Summarization middleware | 摘要质量的核心控制点。必须说明保留哪些信息,而不是泛泛说"总结一下"。 |
recursionLimit |
Agent invoke/streamEvents 配置 | 限制 Agent 循环步数,防止工具调用失控。复杂任务要调高,但不能无限。 |
streaming |
ChatOpenAI |
用于长任务交互体验和事件观察。与 streamEvents 配合可以看到模型输出和工具开始事件。 |
temperature |
模型配置 | 工具调用和工程任务通常设为 0 或较低,减少不必要发散。 |
这些参数背后的共同原则是:**越接近执行环境和持久化边界,越不能只用默认值。**Prompt 写错了最多回答不好;权限、后端、记忆和执行环境配置错了,可能会造成数据泄露、误写文件或不可追踪的行为。
组合顺序:middleware 不是随便堆
middleware 可以组合,但组合顺序和依赖关系要清楚。以 demo 中的 Skill 为例:
js
middleware: [
createSkillsMiddleware({ backend, sources: [skills] }),
createFilesystemMiddleware({ backend }),
]
Skill middleware 让 Agent 知道有哪些 Skill;Filesystem middleware 让 Agent 能读取 Skill 文件、写生成结果。两者用同一个 backend,保证 Skill 路径和输出路径在同一套虚拟文件系统语义下。
Memory middleware 也常常要和 Filesystem middleware 一起用。前者负责把记忆注入上下文,后者负责让 Agent 在需要"记住"时能修改记忆文件。如果只有 Memory,没有 Filesystem,Agent 可以读记忆,但不能按你的规则更新记忆。如果只有 Filesystem,没有 Memory,Agent 能读写文件,但不会自动把记忆内容放进系统上下文。
Summarization middleware 与 Memory 的职责也不同。Memory 是跨会话稳定事实,Summarization 是当前会话的历史压缩。不要把摘要写进长期记忆,也不要把长期记忆当成摘要来不断覆盖。
我在工程里常用的组合是:
js
const middleware = [
createFilesystemMiddleware({ backend, permissions }),
createMemoryMiddleware({ backend, sources: memorySources }),
createSkillsMiddleware({ backend, sources: skillSources }),
createSubAgentMiddleware({
defaultModel: model,
subagents,
generalPurposeAgent: false,
}),
createSummarizationMiddleware({
model,
backend,
trigger: { type: "tokens", value: 24000 },
keep: { type: "messages", value: 12 },
summaryPrompt,
}),
]
这不是唯一顺序,但表达了一个原则:先提供基础工作区,再注入长期上下文和能力目录,再提供任务委派,最后处理长上下文预算。实际项目中还会加入自定义 logging、budget、guardrail、trace middleware。
为什么不是直接用 RAG 解决上下文问题
很多人看到 Memory 和 Summarization,会自然想到 RAG。RAG 当然重要,但它解决的问题不完全一样。
RAG 更适合从大规模外部知识库中检索相关片段,比如产品文档、代码库、规章制度、历史工单。它强调 Loader、Splitter、Embedding、Retriever、重排和引用来源。
DeepAgents 的 Memory middleware 更像 Agent 自身的工作记忆和长期约定,规模通常较小,内容更稳定,直接注入系统上下文的价值更高。
Summarization middleware 则处理当前会话的历史消息压缩,它不是从知识库召回,而是把"已经发生过的对话"变成继续执行所需的摘要。
三者可以共存:
如果要做企业知识库问答,RAG 是核心;如果要做长任务 Agent,Memory 和 Summarization 更像运行时基础设施;如果要做研发助手,三者往往都需要。
常见误区和易错点
误区一:把 DeepAgents 当成万能 Agent 框架。
DeepAgents 提供的是默认运行时能力,不代表业务流程自动正确。业务规则、权限边界、数据来源可信度、输出验收标准仍然要自己设计。
误区二:只靠 Prompt 限制 Agent 行为。
系统 Prompt 可以告诉模型"不要读 secret.txt",但真正可靠的是 backend、permissions 和 sandbox。安全边界必须落在工具和执行环境上。
误区三:权限规则忘了默认允许。
Filesystem permission 没命中时默认允许。如果你想做白名单,应该先写 allow,再用 deny 覆盖其他路径,或者更明确地设计默认拒绝策略。
误区四:把 LocalShellBackend 用进生产 API。
它会在宿主机直接执行命令,适合本地可信开发,不适合处理不可信用户输入。生产环境要用沙箱、容器、远程执行环境或更严格的工具封装。
误区五:Skill 写得像资料仓库。
Skill 不是把所有文档塞进去,而是告诉 Agent 在某类任务下按什么流程做、读哪些补充文件、调用哪些脚本。好的 Skill 应该可执行、可判断、可复用。
误区六:子 Agent 越多越专业。
过多子 Agent 会增加路由成本和上下文传递损耗。优先按工具边界和输出责任拆,不要为了"多智能体"而多智能体。
误区七:摘要等于记忆。
摘要是压缩后的会话上下文,可能丢失细节;记忆是跨会话稳定知识,应该有结构和治理。两者不能混用。
误区八:忽略当前源码与文档差异。
当前 demo 目录中 src/middleware-test2.mjs 是空文件,而原文里展示了一个 middleware 扩展工具的示例。写教程或接手项目时应以当前源码为准,文档只能作为参考。这个差异也提醒我们:Agent demo 很容易在文章、代码、依赖版本之间漂移,工程验收不能只看文档。
工程落地建议:从能跑到可控
如果把 DeepAgents 用在真实项目里,我建议按下面的顺序推进。
第一步,只做最小 Agent。接入模型、一个或两个确定性工具、基础 logging middleware,验证模型是否能稳定调用工具。此时不要急着加 Memory、Skill、SubAgent。
第二步,引入文件工作区。用 FilesystemBackend 和 virtualMode,给 Agent 一个干净目录,配置读写权限。先让它只写报告、待办、草稿,不要直接改源码。
第三步,把重复 Prompt 沉淀成 Skill。凡是可以复用的工作流,比如画架构图、写复盘、做代码审查、生成测试计划,都可以做成 Skill。Skill 描述要帮助模型判断"何时使用",完整内容再按需读取。
第四步,再拆子 Agent。只有当某个任务有独立工具、独立 Prompt 或独立输出责任时,才拆成子 Agent。每个子 Agent 要有清晰 description,不要只写内部系统提示词。
第五步,加入长期记忆。先从少量 Markdown 记忆文件开始,例如 AGENTS.md、preferences.md。建立写入规则,不允许模型把临时聊天内容随便写入长期记忆。
第六步,加入上下文压缩。先在低风险场景下观察摘要质量,再决定 trigger、keep 和 summaryPrompt。关键事实要写文件,不要只留在摘要里。
第七步,补可观测和回放。长任务 Agent 一定要记录模型调用、工具调用、文件变更、子 Agent 委派、摘要触发等事件。没有 trace 的 Agent 很难排查问题。
第八步,最后才考虑更复杂的 LangGraph 编排。当你已经知道哪些步骤需要强控制、哪些步骤可以交给模型自由规划,再把关键路径下沉到 LangGraph 节点里。
这套顺序背后的原则是:先让能力稳定,再扩大自主性;先收紧边界,再开放工具;先沉淀事实,再做长期记忆。
一个更完整的组合示例
下面给出一个贴近研发助手的组合代码。它不是为了覆盖所有 API,而是展示每个 middleware 在链路中的位置:
js
import "dotenv/config"
import path from "node:path"
import { ChatOpenAI } from "@langchain/openai"
import { createAgent } from "langchain"
import {
FilesystemBackend,
createFilesystemMiddleware,
createMemoryMiddleware,
createSkillsMiddleware,
createSubAgentMiddleware,
createSummarizationMiddleware,
} from "deepagents"
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
temperature: 0,
streaming: true,
})
const backend = new FilesystemBackend({
rootDir: path.resolve("agent-workspace"),
virtualMode: true,
})
const permissions = [
{ operations: ["read"], paths: ["/private/**"], mode: "deny" },
{ operations: ["write"], paths: ["/reports/**"], mode: "allow" },
{ operations: ["write"], paths: ["/memory/**"], mode: "allow" },
{ operations: ["write"], paths: ["/**"], mode: "deny" },
]
const summaryPrompt = `请把待压缩对话总结成中文执行摘要,必须保留:
1. 用户目标和当前进度
2. 已读写的文件路径
3. 已确认的约束、决策和待办
4. 未解决的问题和下一步
待摘要内容:
{conversation}
摘要:`
const agent = createAgent({
model,
tools: [],
systemPrompt: [
"你是研发助手,优先基于工作区文件、项目记忆和可用 Skill 完成任务。",
"写文件只能写入 /reports 或 /memory。",
"需要专业分析时通过 task 委派子 Agent,不要在主上下文里硬塞所有推理。",
].join("\n"),
middleware: [
createFilesystemMiddleware({ backend, permissions }),
createMemoryMiddleware({
backend,
sources: ["/AGENTS.md", "/memory/preferences.md"],
}),
createSkillsMiddleware({
backend,
sources: ["/.agents/skills/"],
}),
createSubAgentMiddleware({
defaultModel: model,
generalPurposeAgent: false,
subagents: [
{
name: "code-reader",
description: "阅读源码并定位实现细节,输出文件路径、关键函数和风险点。",
systemPrompt: "你是代码阅读子 Agent,只做源码分析,不写报告。",
},
{
name: "doc-writer",
description: "把已有分析整理成结构化中文技术文档。",
systemPrompt: "你是技术写作子 Agent,重视结构、边界和工程建议。",
},
],
}),
createSummarizationMiddleware({
model,
backend,
historyPathPrefix: "/conversation_history",
trigger: { type: "tokens", value: 24000 },
keep: { type: "messages", value: 12 },
summaryPrompt,
}),
],
})
这段代码的重点不是"把所有 middleware 都加上",而是把职责边界表达清楚。
Filesystem 负责工作区和权限。Memory 负责长期项目上下文。Skills 负责能力发现。SubAgent 负责任务拆分。Summarization 负责上下文预算。主 Agent 的系统 Prompt 只写总原则,不把每个能力的细节都塞进去。
如果项目还没有 Skill,就先删掉 createSkillsMiddleware。如果任务不需要长对话,就先不加 Summarization。DeepAgents 的好处是可组合,不是要求你一次性打开全部能力。
什么时候不适合用 DeepAgents middleware
DeepAgents 很适合长任务 Agent,但并不适合所有场景。
如果你的业务流程强确定,比如"先查库存、再锁单、再支付、再发消息",每一步都必须严格按事务状态走,直接使用 LangGraph 或业务工作流引擎更合适。DeepAgents 可以作为某个节点里的智能助手,但不应该替代核心交易状态机。
如果你的系统面对不可信用户输入,并且需要执行代码或命令,不要直接使用 LocalShellBackend。你需要沙箱、容器、资源限制、网络隔离、密钥隔离和完整审计。
如果你的任务只是简单问答,不需要文件、Skill、子 Agent、记忆和摘要,DeepAgents 会显得偏重。普通 createAgent 更容易维护。
如果你的知识规模很大,比如上万篇文档或百万级代码片段,Memory middleware 不是 RAG 的替代品。你仍然需要 Loader、Splitter、Embedding、向量数据库、Retriever 和重排模型。
如果你对输出有强格式和强正确性要求,比如财务报表、合同条款、医疗建议,middleware 只能提供上下文和工具,不能替代业务校验。最终结果仍然要经过确定性规则、人工审核或领域系统验证。
总结:把 Agent 当成运行系统,而不是一段 Prompt
DeepAgents 最值得学习的地方,不是某个 API 名字,而是它对复杂 Agent 的拆分方式:把运行时能力做成 middleware,把存储和执行环境抽象成 backend,把长任务中的文件、Skill、子 Agent、记忆和摘要放在各自该在的位置。
这也是本文的主结论:DeepAgents 适合用来搭建长任务 Agent 的运行时底座,但它不是自动正确的黑盒。你应该把它当成可组合的工程设施,清楚配置每个 middleware 的边界、权限和代价。
如果你只是跑通 demo,DeepAgents 看起来像"帮我省了几段代码"。但如果你在做真正的 AI 工程,它更大的价值是帮你少造几类重复但难做好的基础设施:可控文件系统、渐进式 Skill、任务隔离、长期记忆、上下文压缩。
复杂 Agent 的默认方向,不应该是把 Prompt 越写越长,而应该是把能力拆成清晰的运行时模块。DeepAgents 的 middleware 正是这条路线上比较务实的一种实现。