DeepAgents 中间件一次性加上技能、记忆、文件与子代理
LangChain/DeepAgents 的 middleware 机制——钩子原理、自定义中间件,以及文件系统、长期记忆、上下文摘要、技能、子代理 5 个开箱即用的官方中间件
Leo
2026.09.04 · 更新于 2026.09.05
别再裸写 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 自己往里写 |
| 对话太长、要控 token | createSummarizationMiddleware | 超阈值就自动摘要历史并存档 |
| 让 Agent 会执行某项专业流程 | createSkillsMiddleware | 按需加载 SKILL.md 技能说明照着做 |
| 任务可拆给专门小 Agent | createSubAgentMiddleware | task 工具委派,上下文隔离 |
三种"让 Agent 记住东西"别搞混
| 想记住的东西 | 选哪个 | 长什么样 |
|---|---|---|
| 这次对话聊了什么、结论是什么(过程史) | Summarization | 按会话压缩成摘要文件 |
| 项目的长期约定 / 用户的长期偏好(事实库) | Memory | AGENTS.md / preferences.md |
| 任意的草稿、报告、代码(中间产物) | Filesystem | 你自己组织的目录文件 |
顺口溜
落地靠 Files,常记用 Memory,太长上 Summarize,专活找 Skills,人多拆 SubAgent;自定义特殊逻辑,
createMiddleware自己造。
还不满意?
上面的官方中间件组合起来,就是一套相当完整的"带电池的 Agent"(电池:规划 + 文件 + 子任务 + 上下文管理)。如果你想要"打开即全套默认配置",DeepAgents 还提供了 createDeepAgent() 一键装配:它内部就是"官方提示词 + 上面这些中间件"的预打包组合,仍然基于 LangGraph,返回的是一个可 stream、可持久化的编译图。先用 createDeepAgent() 跑通,再按需用中间件替换/增删,是绝大多数项目最省事的上手路线。
最后收个尾:middleware 的价值不在"会写钩子",而在把 Agent 的工程化需求变成可插拔、可复用、可替换的积木。遇到"想在模型/工具周围做点什么"的需求,先别急着改提示词或工具函数——问一句"能不能用一个 middleware 解决",往往更干净。
读者留言
COMMENTS · 0发表留言