DeepAgents middleware 工程实战:把复杂 Agent 的运行时基建交给可组合中间件

很多团队第一次做 Agent,关注点通常会落在 Prompt、模型和工具调用上:怎么写系统提示词,怎么注册 tool,怎么让模型多轮调用工具,怎么把最终答案组织得更像人。但真正把 Agent 放进工程场景之后,麻烦往往不在这些表层能力,而在运行时基建:

  • 任务执行过程中,Agent 需要一个可控的工作区,而不是随意读写宿主机。
  • 长任务需要保存中间文件、检索文件、修改文件,还要能限制权限。
  • 团队希望复用某些能力,比如画图、写报告、查代码、生成测试,而不是每个 Agent 都塞一堆 Prompt。
  • 复杂任务不适合一个上下文窗口硬扛,应该拆给不同子 Agent。
  • 对话越来越长时,需要压缩历史,又不能把关键事实丢掉。
  • 用户偏好、项目约定、仓库事实需要长期记忆,而不是每次都重新喂给模型。

这篇文章的核心结论是:

DeepAgents 的价值不只是"开箱即用的 Agent",而是把复杂 Agent 常见的运行时基建收敛成一组可组合 middleware。它让你不用从零搭文件系统、Skill、子 Agent、长期记忆和上下文压缩,但前提是你必须清楚每个 middleware 的职责边界和安全代价。

先把问题说清楚:Agent 难的不是调用一次模型

最简单的 Agent demo 通常长这样:

  1. 用户输入一个问题。
  2. 模型判断是否需要调用工具。
  3. 工具返回结果。
  4. 模型基于结果回答。

这个流程可以证明"模型会用工具",但它离真实业务还很远。真实任务更像下面这样:

用户希望 Agent 分析一个项目,读 README,搜索源码,写一份迁移方案,生成一张架构图,记住项目约定,第二天还能接着聊。如果上下文太长,要自动压缩;如果需要某项专业能力,要加载 Skill;如果某一步是独立分析,应该交给子 Agent;如果要写文件,只能写到工作区,不能碰用户隐私文件。

这时你会发现,Agent 不是单个 LLM 调用,而是一套运行系统。它至少包含这些层:

flowchart TD U["用户请求"] --> A["Agent 运行入口"] A --> M["middleware 栈"] M --> FS["文件系统工作区"] M --> SK["Skills 能力库"] M --> SA["SubAgent 委派"] M --> MEM["长期记忆"] M --> SUM["上下文压缩"] FS --> T["工具调用"] SK --> T SA --> T MEM --> P["系统上下文"] SUM --> P P --> LLM["Chat Model"] T --> LLM LLM --> R["最终回复或中间动作"]

如果你自己基于 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 提供的 createFilesystemMiddlewarecreateSkillsMiddlewarecreateSubAgentMiddlewarecreateMemoryMiddlewarecreateSummarizationMiddleware 本质上不是"几个工具函数",而是一组已经写好的 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,因为它们更能体现工程可组合性。

flowchart LR REQ["HumanMessage"] --> AG["createAgent"] AG --> MW1["自定义 middleware"] MW1 --> MW2["FilesystemMiddleware"] MW2 --> MW3["SkillsMiddleware"] MW3 --> MW4["SubAgentMiddleware"] MW4 --> MW5["MemoryMiddleware"] MW5 --> MW6["SummarizationMiddleware"] MW6 --> MODEL["ChatOpenAI"] MODEL --> TOOLS["tool calls"] TOOLS --> BACKEND["Backend"] BACKEND --> STATE["Agent state"] STATE --> AG

这里有两个概念必须分清。

**middleware 负责把能力接入 Agent 运行链路。**例如文件系统 middleware 会注册 lsread_filewrite_fileedit_file 等工具,并把文件状态接入 Agent;Skill middleware 会读取 Skill 元数据并注入系统上下文;摘要 middleware 会在模型调用前判断是否需要压缩历史。

**backend 负责能力背后的存储或执行环境。**同样是文件系统能力,backend 可以是本地目录、状态存储、LangSmith Hub、组合后端,或者能执行 shell 的本地后端。middleware 是"怎么接入 Agent",backend 是"能力落在哪里"。

这也是 DeepAgents 工程使用的关键判断点:你不能只问"我要不要加 FilesystemMiddleware",还要问"这个文件系统背后到底连着哪里,权限边界是什么"。

实战场景:做一个可控的研发助手 Agent

为了避免 demo 过于教学化,我们把示例改造成一个更真实的场景:一个"研发助手 Agent",它要能做这些事:

  1. 读写项目工作区里的任务文件。
  2. 加载 Skill,比如画图、写报告、分析代码。
  3. 把数学计算、代码检索、文档撰写等子任务委派给不同子 Agent。
  4. AGENTS.md 和用户偏好文件中读取长期记忆。
  5. 对长对话自动摘要,避免上下文爆掉。

数据流大致是:

sequenceDiagram participant User as 用户 participant Agent as 主 Agent participant Memory as MemoryMiddleware participant Summary as SummarizationMiddleware participant Skills as SkillsMiddleware participant FS as FilesystemMiddleware participant Sub as SubAgentMiddleware participant LLM as Chat Model User->>Agent: 提出研发任务 Agent->>Memory: 读取项目约定和偏好 Agent->>Summary: 判断历史是否需要压缩 Agent->>Skills: 注入可用 Skill 元数据 Agent->>LLM: 带系统上下文发起模型调用 LLM-->>Agent: 请求工具或 task Agent->>FS: 读写工作区文件 Agent->>Sub: 委派独立子任务 Sub-->>Agent: 返回子任务结论 Agent->>LLM: 汇总工具结果 LLM-->>User: 输出最终答案

这条链路的关键,不是把所有功能都打开,而是给每个能力安排清楚的职责:文件系统只做工作区,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、文件系统工具和权限策略共同约束。

这也是长期记忆最容易踩坑的地方:不要把模型生成的每一句"我记住了"都当成可靠持久化。真正可靠的做法是:

  1. 用户明确要求记住,或者系统规则明确要求沉淀时,才写长期记忆。
  2. 写入前区分事实、偏好、决策、临时上下文。
  3. 写入要可审计,最好是 Markdown 条目、时间、来源、适用范围。
  4. 记忆需要定期清理,否则旧偏好和旧项目事实会污染新任务。
  5. 敏感信息不应该进入普通记忆文件。

长期记忆不是"把聊天记录永久保存",而是把稳定、可复用、后续任务会依赖的信息结构化沉淀。

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 支持按 messagestokensfraction 这几种方式描述上下文大小。demo 用消息条数是为了容易演示;生产环境更推荐按 token 或模型 profile 推断,因为消息条数不能反映真实上下文长度。

上下文压缩有明显收益:

  • 控制 token 成本。
  • 降低超出模型上下文窗口的风险。
  • 把旧历史从"逐字消息"变成"任务相关摘要"。
  • 长任务可以继续推进,不必频繁重开会话。

但它不是无损存档。摘要一定会丢细节,尤其是代码 diff、数字、日志、用户原话、合同条款这类精确信息。所以摘要 middleware 不能替代文件存档、数据库、审计日志。我的经验是:

  • 需要精确复用的信息写入文件或结构化存储,不要只依赖摘要。
  • 摘要 Prompt 要明确保留决策、约束、待办、文件路径、错误信息。
  • 对代码任务,摘要中必须保留已改文件、未解决问题、测试结果。
  • 对法律、财务、医疗等高风险场景,不要把摘要当成事实依据。
  • keep 不宜过小,否则模型会失去最近交互的语气和细节。

如果把上下文比作工作台,Summarization middleware 做的是"把旧材料归档成标签清晰的摘要卡片",而不是"复制一份完整档案"。

参数和配置:不要只知道 API 名字

DeepAgents 的 middleware 很容易上手,但要稳定落地,关键参数必须理解背后的取舍。

参数 出现场景 推荐理解
rootDir FilesystemBackendLocalShellBackend 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 则处理当前会话的历史消息压缩,它不是从知识库召回,而是把"已经发生过的对话"变成继续执行所需的摘要。

三者可以共存:

flowchart TD Q["用户当前问题"] --> RAG["RAG 检索外部知识"] Q --> MEM["Memory 读取长期约定"] Q --> HIS["Summarization 压缩会话历史"] RAG --> CTX["模型上下文"] MEM --> CTX HIS --> CTX CTX --> ANS["回答或行动"]

如果要做企业知识库问答,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。

第二步,引入文件工作区。用 FilesystemBackendvirtualMode,给 Agent 一个干净目录,配置读写权限。先让它只写报告、待办、草稿,不要直接改源码。

第三步,把重复 Prompt 沉淀成 Skill。凡是可以复用的工作流,比如画架构图、写复盘、做代码审查、生成测试计划,都可以做成 Skill。Skill 描述要帮助模型判断"何时使用",完整内容再按需读取。

第四步,再拆子 Agent。只有当某个任务有独立工具、独立 Prompt 或独立输出责任时,才拆成子 Agent。每个子 Agent 要有清晰 description,不要只写内部系统提示词。

第五步,加入长期记忆。先从少量 Markdown 记忆文件开始,例如 AGENTS.mdpreferences.md。建立写入规则,不允许模型把临时聊天内容随便写入长期记忆。

第六步,加入上下文压缩。先在低风险场景下观察摘要质量,再决定 triggerkeepsummaryPrompt。关键事实要写文件,不要只留在摘要里。

第七步,补可观测和回放。长任务 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 正是这条路线上比较务实的一种实现。

相关推荐
前端环境观察室1 小时前
别让 Agent 浏览器任务无限重试:失败分类、RetryPolicy 与人工复核
前端
喵个咪1 小时前
Headless 后端实践:基于Go的企业级多栈管理系统脚手架
前端·vue.js·react.js
m0_738120721 小时前
渗透测试基础——黑盒测试下的Web漏洞挖掘与利用解析(一)
服务器·前端·网络·安全·php
小江的记录本2 小时前
【JVM虚拟机】类加载机制:类加载全流程:加载→验证→准备→解析→初始化(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·算法·安全·spring·面试
隐层漫游者3 小时前
深度解密:基于Python的高性能RAG系统设计,从向量检索到缓存穿透防御,这篇就够了
llm
Larcher3 小时前
JS 变量提升:代码没动,为什么执行顺序就变了?
前端·javascript·前端框架
yingyima3 小时前
MySQL 事件调度器速查:核心语法与实战代码
前端
GISer_Jing3 小时前
Claude Code多Agent架构深度剖析
前端·人工智能·架构·自动化
comphub3 小时前
comp-hub:让你的 Vue 业务组件真正"活"起来
前端