✦ 大道至简 · 时光是画在卷上的河流 · 行到水穷处,坐看云起时

langchaindeepagents2026.09.04 · 47 分钟阅读

DeepAgents 中间件一次性加上技能、记忆、文件与子代理

LangChain/DeepAgents 的 middleware 机制——钩子原理、自定义中间件,以及文件系统、长期记忆、上下文摘要、技能、子代理 5 个开箱即用的官方中间件

L

Leo

2026.09.04 · 更新于 2026.09.05

3 次浏览
DeepAgents 中间件一次性加上技能、记忆、文件与子代理

别再裸写 Agent 了:用 DeepAgents 中间件一次性加上技能、记忆、文件与子代理

为什么要学 middleware?

写过 Agent 的人大概都体会过:把一个"会聊天的模型"变成"能干活的产品",真正费劲的不是问答本身,而是围绕主循环的一堆横切需求:

  • 想知道这轮调了几次模型?→ 打日志、加计数器;
  • 不想每次把规则写死在系统提示里?→ 想动态注入上下文;
  • 想拦掉敏感请求?→ 要能提前中断;
  • 想让它访问文件、记住你的偏好、执行某个专业技能、把活拆给专门的小 Agent?→ 每一样都得往 Agent 里塞东西。

如果每次都把这些逻辑手写进提示词、工具、图形编排里,代码会越来越脏,还无法复用。

middleware(中间件)就是解决这个问题的机制:它在 Agent 的运行循环里留好了几个固定"插槽",让你把任意横切逻辑做成可插拔的一层层包装,装上去就能生效,拆掉也不影响核心逻辑。LangChain 的 createAgent 原生支持这个机制,而 DeepAgents 则直接帮你把最常用的几类能力做成了开箱即用的官方中间件,装一个顶十行自定义代码。

一句话版本:middleware = 在模型调用前后、工具调用前后"插一脚"的插件系统,可以读/改 state、改请求参数、注册新工具。


Agent 循环长什么样,middleware 插在哪

一个 Agent 跑起来其实是一个循环:模型调用 → 若它想用工具就去执行 → 结果回填后再调模型 → ……直到它认为可以作答。DeepAgents 的官方中间件(技能、记忆、摘要、文件、子代理)本质都是同一套机制的产物。

下面这张图是整个机制的"地图",贯穿全文,先混个眼熟:

补充两点"插一脚"的高级玩法,图上没画出来:

  • 短路:在 beforeModel 里可以返回 jumpTo 直接跳到指定节点,比如命中敏感词就 jumpTo: "end",压根不让模型回答;
  • 扩工具:middleware 可以直接声明 tools,等于在创建 Agent 后还能动态加工具(文件系统、子代理能力就是这么"塞"进去的)。

第一个知识点:自己写一个中间件(钩子与 state)

它是什么

createMiddleware(config) 用来定义一个中间件实例。它的 config 里能挂上各种钩子(hook)——也就是前面地图上的那些插槽。最常见的钩子见下表:

钩子触发时机典型用途
beforeAgent / afterAgent一次 Agent 运行开始 / 结束(只各一次)开场白日志、统计汇总
beforeModel每次模型调用前读 state 判断、拦截(可短路)
afterModel每次模型调用后读最新回复、把结果写回 state
wrapModelCall整个"调用模型"包起来改请求参数(如拼 system)后再放行
wrapToolCall每次执行工具包起来改工具参数、包装结果、加权限/日志
tools(不是钩子,是声明)由该中间件注册额外工具
stateSchema(不是钩子,是声明)声明这个中间件自己的状态字段及其类型/默认值

一个容易混的点:钩子(before/after)是"在某刻顺便看一眼、改一下 state",而包装(wrap)是"把整次调用接管过来"。wrapModelCall / wrapToolCall 会拿到一个 handler,你改完请求参数再 handler(request) 把控制权还回去;你也可以选择不调用 handler,从而改变、拦截、替换整次调用的结果。

最小可运行示例:一个"日志 + 计数"中间件

我们写一个每次调用模型都打印日志、并统计调用次数的中间件。跑之前需要 .env 里配好你的 OPENAI_API_KEY / OPENAI_BASE_URL(按你的模型服务商来)。

import "dotenv/config";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, createMiddleware, HumanMessage } from "langchain";

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  temperature: 0,
});

/** 日志 + 统计模型调用次数 */
const loggingMiddleware = createMiddleware({
  name: "LoggingMiddleware",
  // 声明中间件自己的状态:一个默认 0 的计数器
  stateSchema: z.object({ modelCallCount: z.number().default(0) }),
  beforeAgent: (state) => {
    console.log(`[Logging] agent 开始,消息数: ${state.messages.length}`);
  },
  beforeModel: (state) => {
    console.log(`[Logging] 即将调用模型,已调用: ${state.modelCallCount} 次`);
  },
  afterModel: (state) => {
    const last = state.messages.at(-1);
    const preview =
      typeof last?.content === "string"
        ? last.content.slice(0, 60)
        : String(last?.content ?? "").slice(0, 60);
    console.log(`[Logging] 模型返回: ${preview}...`);
    // 返回一个"部分 state 更新",计数 +1(会被自动 merge 回 state)
    return { modelCallCount: state.modelCallCount + 1 };
  },
  afterAgent: (state) => {
    console.log(`[Logging] agent 结束,累计调用: ${state.modelCallCount} 次`);
  },
});

const agent = createAgent({
  model,
  tools: [],
  systemPrompt: "你是一个助手。",
  middleware: [loggingMiddleware],
});

const { messages, modelCallCount } = await agent.invoke({
  messages: [new HumanMessage("用一句话介绍你自己")],
});
console.log("回复:", messages.at(-1)?.content);
console.log("统计到的调用次数:", modelCallCount);

运行后终端会依次出现 agent 开始 → 即将调用模型 → 模型返回 → agent 结束,并且 modelCallCount 也会出现在 invoke 的返回值里——因为它是 middleware 自己声明的 state 字段。

要点:

  • stateSchema 声明的是"只属于这个中间件的状态",用 zod 描述,还能给默认值;它在 state 里各占一格,互不干扰。
  • 钩子返回值 = 部分 state 更新,框架自动帮你 merge,不用手动改整个 state(上面 afterModel 返回 { modelCallCount: ... } 即可)。
  • afterModel 读取最新一条消息用的是 state.messages.at(-1)——模型回复会被自动追加进 messages,这是最常用的取结果姿势。
  • before* / after* 的差别:Agent 跑一轮主循环可能调好几次模型(每次工具结果回填都会再调一次),所以 beforeModel/afterModel 会触发多次,而 beforeAgent/afterAgent 一次运行只各触发一次。

第二个知识点:三个"插一脚"的高级姿势

日志只是开胃菜。middleware 真正的威力来自下面三种姿势,它们各自对应真实场景。为节省篇幅,下面都省略了前面那段相同的模型初始化代码(假设已有 model)。

姿势一:wrapModelCall —— 改请求参数

在真正把请求发给模型前动手脚,比如给每个请求统一追加一段 system 指令。

import { createMiddleware } from "langchain";

const addContextMiddleware = createMiddleware({
  name: "AddContextMiddleware",
  wrapModelCall: async (request, handler) => {
    console.log("[AddContext] 注入额外 system 上下文");
    // 改完参数再放行:真正的模型调用在 handler 里发生
    return handler({
      ...request,
      systemMessage: request.systemMessage.concat(
        "\n\n 请用一句话简洁回答。"
      ),
    });
  },
});

姿势二:短路 —— 命中敏感词直接结束

beforeModel 里可以配置 canJumpTo 并返回 jumpTo,实现"不等模型开口就结束 Agent"。

import { AIMessage, createMiddleware } from "langchain";

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")) {
        console.log("[Blocked] 检测到敏感词,短路结束");
        return {
          messages: [new AIMessage("该请求已被中间件拦截,无法处理。")],
          jumpTo: "end",
        };
      }
    },
  },
});

姿势三:wrapToolCall —— 注册工具 + 包装工具执行

middleware 不仅能"看",还能在 tools 字段里直接注册新工具,并用 wrapToolCall 给每次工具执行加日志、统计,甚至包装返回结果。这是后面所有"文件系统、子代理"能力的技术底座。

import { Command } from "@langchain/langgraph";
import { z } from "zod";
import { createMiddleware, tool, ToolMessage } from "langchain";

const getCurrentTime = tool(() => new Date().toISOString(), {
  name: "get_current_time",
  description: "返回当前 UTC 时间的 ISO 8601 字符串",
  schema: z.object({}),
});

const toolsMiddleware = createMiddleware({
  name: "ToolsMiddleware",
  stateSchema: z.object({ toolCallCount: z.number().default(0) }),
  tools: [getCurrentTime], // ① 用 middleware 注册工具
  wrapToolCall: async (request, handler) => {
    const name = request.tool?.name ?? request.toolCall.name;
    console.log(`[Tools] 即将执行: ${name}`);
    const result = await handler(request); // ② 真正执行
    if (!ToolMessage.isInstance(result)) return result;

    const wrapped = new ToolMessage({
      content: `${result.content}\n——已由 middleware 包装`,
      tool_call_id: result.tool_call_id,
      name: result.name,
    });
    // ③ 用 Command 更新 state 里的计数器
    return new Command({
      update: {
        toolCallCount: request.state.toolCallCount + 1,
        messages: [wrapped],
      },
    });
  },
});

要点:

  • wrap* 里 handler(request) 才是"正主":不调它 = 拦截;调它并改参数 = 注入;在它外面再包一层 = 装饰/日志/统计。三种写法自由组合。
  • request.state 让你在包装器里也能读到中间件状态,方便计数、鉴权、限流。
  • 返回值可以是普通结果,也可以是 Command({ update })——用 Command 才能把包装产生的"副作用"(比如计数器)写回 state,或替换回填给模型的消息。

开箱即用:DeepAgents 的 5 个官方中间件

上面那些自定义写法,你大部分时候根本不用写。DeepAgents 把这些高频能力封装成了官方中间件,一行接入。它们清一色挂在 createAgent 的 middleware 数组里,用法完全统一。

引入方式(Node 侧):

import {
  createFilesystemMiddleware,
  createMemoryMiddleware,
  createSummarizationMiddleware,
  createSubAgentMiddleware,
  createSkillsMiddleware,
} from "deepagents";

先记住一个贯穿它们的通用概念 backend(后端):很多中间件要"读写东西",但它们不直接碰磁盘,而是通过一个可替换的 backend 抽象来读写。DeepAgents 提供两类:

  • FilesystemBackend({ rootDir, virtualMode }) —— 内存/虚拟文件系统,适合沙箱演示,写不进真磁盘;
  • LocalShellBackend.create({ rootDir, virtualMode }) —— 真实本地文件系统,真正落盘,用完记得 backend.close()。

下面 5 个小节,每个都按"它解决什么问题 → 最小代码 → 要点"来讲。


知识三:Filesystem 中间件 —— 让 Agent 能读写文件

解决什么问题

长任务里 Agent 往往需要"工作台":把草稿、报告、数据写到文件里,而不是全部堆在上下文(上下文会被烧完、还会越聊越贵)。这个中间件给 Agent 注入一套文件工具(ls、read_file、write_file、edit_file、glob、grep),并支持精细的权限规则,规定哪些路径能读、能写。

最小可运行示例

import fs from "node:fs";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, HumanMessage } from "langchain";
import { createFilesystemMiddleware, FilesystemBackend } from "deepagents";

// 虚拟工作区,目录里先生成一个"机密文件"用于演示权限
const backend = new FilesystemBackend({ rootDir: "./workspace", virtualMode: true });

// 权限规则:先匹配先生效;都没命中则默认放行
const permissions = [
  { operations: ["read"], paths: ["/secret.txt"], mode: "deny" }, // 禁止读 secret.txt
  { operations: ["write"], paths: ["/notes/**"], mode: "allow" }, // 允许写 notes 目录
  { operations: ["write"], paths: ["/**"], mode: "deny" },        // 其它一律禁止写
];

const model = new ChatOpenAI({ model: process.env.MODEL_NAME, temperature: 0 });

const agent = createAgent({
  model,
  tools: [],
  systemPrompt: "工作区根路径为 /。用 ls、read_file、write_file、edit_file 操作文件,路径以 / 开头。",
  middleware: [createFilesystemMiddleware({ backend, permissions })],
});

await agent.invoke({
  messages: [new HumanMessage("write_file 在 /notes/ 下创建 todo.md,写三条待办,然后 ls 查看")],
});

Agent 拿到这套工具后会自己决定"怎么用文件",你只要在 system prompt 里交代清楚工作区根路径和可用工具名即可。

要点:

  • 权限规则形如 { operations, paths, mode },mode 是 "allow" | "deny",paths 支持 /secret.txt、/notes/** 这类 glob。
  • 按数组顺序先匹配先生效,全不命中则默认放行——所以"默认拒绝"要用一条兜底 deny(上面把 /** 写权限 deny 了)。
  • FilesystemBackend 配 virtualMode: true 是纯内存沙箱,跑完不留痕;想真实写盘就换 LocalShellBackend.create({ rootDir: ".", inheritEnv: true }),结束后 await backend.close()。
  • 值得单独强调:DeepAgents 的安全哲学是"信任模型,边界交给工具层"——它只保证工具能做什么,不指望模型自我约束,所以权限规则是你的最后一道闸。

知识四:Memory 中间件 —— 长期记忆(可读可改的 Markdown)

解决什么问题

默认的模型是无状态的:新开一轮,它不记得你上轮说"我用 pnpm、住北京"。Memory 中间件让 Agent 拥有跨会话的长期记忆,而且记忆是"显式的 Markdown 文件",人可读、可手改、可审阅,不需要向量库。

原理其实很朴素:Agent 每次开始时,把指定的一批记忆文件(通常两类:项目说明 + 用户偏好)读进来注入上下文;当它决定"要记住某事"时,就用文件工具主动编辑对应文件,下次启动再注入——于是形成了持久记忆。

最小可运行示例

注意:Memory 通常要叠加 Filesystem 中间件一起用,因为"写入记忆"靠的是文件工具。

import { ChatOpenAI } from "@langchain/openai";
import { createAgent, HumanMessage } from "langchain";
import {
  createFilesystemMiddleware,
  createMemoryMiddleware,
  FilesystemBackend,
} from "deepagents";

const backend = new FilesystemBackend({ rootDir: "./workspace", virtualMode: true });

const projectMemoryPath = "/AGENTS.md";                // 项目说明、技术栈、仓库约定
const preferencesMemoryPath = "/memory/preferences.md"; // 用户个人偏好(语言、包管理器…)

const agent = createAgent({
  model: new ChatOpenAI({ model: process.env.MODEL_NAME, temperature: 0 }),
  tools: [],
  systemPrompt: [
    "你是项目助手。根据 <agent_memory> 回答;用户要求记住时,必须立刻 edit_file,并按类型写入对应文件:",
    `- ${projectMemoryPath}:项目说明、技术栈、架构等`,
    `- ${preferencesMemoryPath}:用户个人偏好`,
    "不要混写:项目事实不要写进 preferences,个人偏好不要写进 AGENTS.md。",
  ].join("\n"),
  middleware: [
    createFilesystemMiddleware({ backend }),
    createMemoryMiddleware({ backend, sources: [projectMemoryPath, preferencesMemoryPath] }),
  ],
});

await agent.invoke({
  messages: [new HumanMessage("请记住:我常用的包管理器是 pnpm。")],
});

第一轮让它"记住 pnpm",它会把这句话写进 /memory/preferences.md;下一轮问"我常用什么包管理器",中间件会把该文件内容当作 <agent_memory> 注入,Agent 就能答上来。

要点:

  • sources 列出要注入的记忆文件,多个文件会合并进上下文(块名叫 <agent_memory>)。
  • 写入时机由 Agent 自己判断,所以 system prompt 里要交代清楚"什么该记、记到哪个文件",否则它会不知道该不该动笔。
  • 这是"显式记忆"而非语义检索:没有 embedding、没有相似度,胜在可审计、可手改——哪天记错了,打开 .md 改一行即可。
  • 所以目录规划很重要(项目 vs 偏好分开),Agent 与人都好维护。

知识五:Summarization 中间件 —— 上下文太长就自动"压缩"

解决什么问题

长对话最烦的是上下文无限膨胀:每轮把全部历史塞给模型,又贵又容易超窗口。这个中间件会在历史超过阈值时,用模型把"老对话"自动总结成一页 Markdown 存档,再从上下文里撤下已被总结的部分,只保留最近的若干条——既控住 token,又留了完整档案可回溯。

最小可运行示例

import { ChatOpenAI } from "@langchain/openai";
import { createAgent, HumanMessage } from "langchain";
import { createSummarizationMiddleware, FilesystemBackend } from "deepagents";

const backend = new FilesystemBackend({ rootDir: "./workspace", virtualMode: true });
const model = new ChatOpenAI({ model: process.env.MODEL_NAME, temperature: 0 });

const agent = createAgent({
  model,
  tools: [],
  systemPrompt: "记住用户提到的关键事实,中文简短回答。若看到「此前对话摘要」,请据此继续对话。",
  middleware: [
    createSummarizationMiddleware({
      model,                      // 用来写摘要的模型
      backend,
      historyPathPrefix: "/conversation_history", // 摘要存档目录
      // 下面两个阈值仅用于 demo 演示;生产可省略,由模型 profile 自动推断
      trigger: { type: "messages", value: 8 },    // 超过 8 条历史就触发摘要
      keep: { type: "messages", value: 4 },       // 上下文里只保留最近 4 条
    }),
  ],
});

// 连续多轮对话,让它记住若干事实;消息一多就会自动触发摘要
await agent.invoke({
  messages: [new HumanMessage("请记住:我的猫叫小橘。")],
});

连续聊个五六轮、累计超过 8 条消息后,你会看到存档目录里多出按会话命名的摘要文件,后续对话的 token 用量明显回落。

要点:

  • trigger 决定什么时候压缩(按消息条数或字符数,此处是超过 8 条),keep 决定压完上下文里留几条。
  • 总结是"提炼要点"不是"截断":摘要由模型生成,会把结论与继续对话所需上下文保留下来。
  • 摘要被落盘成文件 → 与 Filesystem 一脉相承,同样可读、可追踪"哪轮压了什么"。
  • 和 Memory 怎么分工:摘要管"这次对话发生了什么"(过程史),Memory 管"长期该记住什么"(事实库),两者正好互补。

知识六:Skills 中间件 —— 让 Agent 学会"执行专业技能"

解决什么问题

想让 Agent 干一件很专业的活(画图、导数据、跑脚本),把每个步骤都写死在系统提示里既笨重又没法复用。Skills 中间件让 Agent 能按需发现并加载"技能包":技能是一份自包含的 SKILL.md(说明它是什么、怎么用、有哪些步骤),常见来源是社区技能库,用一行命令安装,跨项目复用。

最小可运行示例

先安装一个社区技能(技能装进 .agents/skills/):

npx skills add github/awesome-copilot --skill <某个技能名> -y

再让 Agent 挂上技能中间件 + 文件中间件(Agent 读取 SKILL.md 需要文件工具):

import { ChatOpenAI } from "@langchain/openai";
import { createAgent, HumanMessage } from "langchain";
import {
  createSkillsMiddleware,
  createFilesystemMiddleware,
  LocalShellBackend,
} from "deepagents";

const backend = await LocalShellBackend.create({
  rootDir: ".",
  virtualMode: true,
  inheritEnv: true,
});

const agent = createAgent({
  model: new ChatOpenAI({ model: process.env.MODEL_NAME, temperature: 0, streaming: true }),
  tools: [],
  systemPrompt: "按技能库完成任务,需要时 read_file 对应 SKILL.md。",
  middleware: [
    createSkillsMiddleware({ backend, sources: ["/.agents/skills/"] }),
    createFilesystemMiddleware({ backend }),
  ],
});

// 让它用一个"画 Excalidraw 流程图"的技能去产图(假设已安装该技能)
const stream = await agent.streamEvents(
  { messages: [new HumanMessage("用流程图技能,把「用户输入→Agent→回复」画成一张流程图保存")] },
  { recursionLimit: 100 }
);
for await (const event of stream) {
  if (event.event === "on_chat_model_stream") {
    const chunk = event.data?.chunk;
    if (typeof chunk?.content === "string") process.stdout.write(chunk.content);
  }
}

跑起来你会看到:Agent 先"意识到自己会这个技能"→ 打开对应 SKILL.md 读说明 → 一步步照着执行。示例里用 streamEvents 是想看到它"现学现卖"的过程,普通场景直接 invoke 即可。

要点:

  • 技能 = 一份 SKILL.md 自包含说明书:名字、触发时机、步骤、可选示例。DeepAgents 在每次调用前把可用技能清单注入上下文,Agent "知道有得用"。
  • 细节按需加载:它不会把每份技能全文塞进上下文,而是先看清单、需要用哪个再 read_file 哪个——这正是"技能 + 文件系统"要叠着用的原因。
  • 能力来自"会读会照做",模型本身不新增参数;所以技能对模型的指令遵循能力有一定要求,太弱的模型会"读了但做不好"。

知识七:SubAgent 中间件 —— 把活委派给专门的小 Agent

解决什么问题

当任务能拆成几个独立步骤时,最优雅的做法是分工:主 Agent 负责编排,把每个子任务 task 委派给一个"专才"子 Agent。子 Agent 拥有自己的 system prompt 和工具、上下文互相隔离,不会把主 Agent 的窗口塞爆,也不互相污染;它还天然构成"规划 → 执行 → 讲解 → 出题"这类流水线。

官方中间件会为主 Agent 注入一个 task 工具;你在 subagents 里描述好每个子 Agent 的职责,谁来干活由主 Agent 根据 description 判断。

最小可运行示例

以"小学应用题辅导"为例,定义一个会计算的子 Agent、一个会讲解的子 Agent:

import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, HumanMessage, tool } from "langchain";
import { createSubAgentMiddleware } from "deepagents";

const calc = tool(
  ({ a, b, op }) => {
    const ops = { add: a + b, subtract: a - b, multiply: a * b, divide: a / b };
    return JSON.stringify({ expression: `${a} ${op} ${b}`, result: ops[op] });
  },
  {
    name: "calc",
    description: "计算两个数的加减乘除",
    schema: z.object({
      a: z.number(), b: z.number(),
      op: z.enum(["add", "subtract", "multiply", "divide"]),
    }),
  }
);

const model = new ChatOpenAI({ model: process.env.MODEL_NAME, temperature: 0 });

const agent = createAgent({
  model,
  tools: [],
  systemPrompt: "你是小学数学辅导主 Agent,通过 task 委派子 Agent,自己不解题。",
  middleware: [
    createSubAgentMiddleware({
      defaultModel: model,          // 子 Agent 默认复用主模型
      generalPurposeAgent: false,   // 是否额外生成一个"啥都能干"的兜底子 Agent
      subagents: [
        {
          name: "math-solver",
          description: "解小学应用题:用 calc 列式计算,给出最终答案与算式。有具体数字时先用它。",
          systemPrompt: "必须用 calc 完成计算,不要心算。输出:分步算式 + 最终答案。",
          tools: [calc],
        },
        {
          name: "kid-tutor",
          description: "把 math-solver 的解法讲给家长听。description 里会有完整解题过程。",
          systemPrompt: "面向小学生家长,用短句和比喻讲解,不用公式堆砌。不使用工具。",
          tools: [],
        },
      ],
    }),
  ],
});

await agent.invoke({
  messages: [
    new HumanMessage(
      "「小明有 24 块糖,平均分给 6 个同学。每个同学现在有几块?」" +
      "请先 math-solver 解题,再 kid-tutor 教我家长怎么给孩子讲。"
    ),
  ],
});

主 Agent 收到请求后会自行判断:"算数 → 派 math-solver,得到解法后 → 派 kid-tutor,并把解法写进 description 传过去"。两个子 Agent 各干各的,主 Agent 的上下文始终保持干净。

要点:

  • subagents 里每个子 Agent 的最小要素 = name + description + systemPrompt (+tools),其中 description 最重要——主 Agent 完全靠它决定"什么时候派谁",写得越具体,分工越准。
  • 上下文通过 description 传递:子 Agent 看不到主 Agent 的对话,主 Agent 想让它知道什么,就把内容塞进 task 的 description 里(如上例把"完整解题过程"传给讲解子 Agent)。
  • defaultModel 让子 Agent 复用主模型;generalPurposeAgent: false 表示不生成通用兜底子 Agent,强迫它只从你给的名单里挑。
  • 子 Agent 还能各自再挂中间件、叠子 Agent(递归委派),表达能力很强;但要小心别把链条叠太深,模型容易在多层委派里"迷路"。

怎么选:一张决策表 + 一个顺口溜

按需求选中间件

你的需求官方中间件一句话原理
让 Agent 读写文件、落地中间产物createFilesystemMiddleware注入文件工具 + 权限闸门
跨会话记住项目约定 / 个人偏好createMemoryMiddleware把 Markdown 记忆注入上下文,Agent 自己往里写
对话太长、要控 tokencreateSummarizationMiddleware超阈值就自动摘要历史并存档
让 Agent 会执行某项专业流程createSkillsMiddleware按需加载 SKILL.md 技能说明照着做
任务可拆给专门小 AgentcreateSubAgentMiddlewaretask 工具委派,上下文隔离

三种"让 Agent 记住东西"别搞混

想记住的东西选哪个长什么样
这次对话聊了什么、结论是什么(过程史)Summarization按会话压缩成摘要文件
项目的长期约定 / 用户的长期偏好(事实库)MemoryAGENTS.md / preferences.md
任意的草稿、报告、代码(中间产物)Filesystem你自己组织的目录文件

顺口溜

落地靠 Files,常记用 Memory,太长上 Summarize,专活找 Skills,人多拆 SubAgent;自定义特殊逻辑,createMiddleware 自己造。

还不满意?

上面的官方中间件组合起来,就是一套相当完整的"带电池的 Agent"(电池:规划 + 文件 + 子任务 + 上下文管理)。如果你想要"打开即全套默认配置",DeepAgents 还提供了 createDeepAgent() 一键装配:它内部就是"官方提示词 + 上面这些中间件"的预打包组合,仍然基于 LangGraph,返回的是一个可 stream、可持久化的编译图。先用 createDeepAgent() 跑通,再按需用中间件替换/增删,是绝大多数项目最省事的上手路线。

最后收个尾:middleware 的价值不在"会写钩子",而在把 Agent 的工程化需求变成可插拔、可复用、可替换的积木。遇到"想在模型/工具周围做点什么"的需求,先别急着改提示词或工具函数——问一句"能不能用一个 middleware 解决",往往更干净。

标签 / TAGSlangchaindeepagents
L

Leo

博主

独立开发者 / Blogger,原博客「大道至简」维护者。正在把 WordPress 上攒了几年的文章与拾语迁移到 Next.js。

读者留言

COMMENTS · 0

发表留言

评论经审核后展示 · 请友善发言0/100