一.DeepAgents是什么?
1.概念
LangChain 家族有三个产品:LangChain 、LangGraph 、还有 DeepAgents 。
- LangChain 是给你一堆 AI 开发积木,
- LangGraph 是搭建复杂工作流的底层蓝图,
- DeepAgents 就是提前搭好主体结构的半成品房子。
LangChain 开源的一个"开箱即用"的智能体内核(agent harness) ------你给它一个目标,它会自己拆任务、搜资料、写文件、派小弟干活,最后给你完整交付物,而不是一问一答就结束。
2.DeepAgents解决的痛点
它解决的核心痛点 :普通 Agent 就是"调个工具、读结果、再调下一个"的浅层循环,任务一超过几十步就丢线、上下文爆掉。而 Claude Code、Manus、OpenAI/Google 的 Deep Research 之所以能"往深处钻",LangChain 研究发现它们都收敛到同一套四件套:任务规划 + 文件系统 + 子 Agent 委派 + 详细提示词。Deep Agents 就是把这四件套打包好、开源出来。

典型适用场景:深度调研/竞品分析出报告、复杂代码任务(写代码+跑测试)、多步骤数据处理、需要自主决策的长流程自动化。
3.使用方式
安装资源包
js
npm init -y
npm install @langchain/core @langchain/openai @langchain/langgraph deepagents tavily
tavily是专门为 AI Agent 打造的搜索 API------它不是给人用的搜索引擎,而是给"程序"用的,目标是让 Agent 能像人一样联网查资料。
tavily的核心能力

tavily就是一个搜索网页的中间件,以前我们用博查的apikey做过一个检索网站的tool,现在我们用tavily。
看一下两者之间的区别:

普通搜索 API 返回的是一堆网页链接,Agent 拿到后还得自己再爬页面、清洗正文、截取片段,链路长还容易撞上反爬和版权墙。
Tavily 直接返回结构化、干净的可用内容:标题、正文、链接、甚至多媒体素材,还能按你的需求做过滤和聚合。开发者描述它是"把 Google + 爬虫 + 内容清洗"三件事合成一个 API 调用。
tavily最大的优势就是把网页搜索tool的脏活累活全不干了,但是他对中文网站不友好,国内用的不多。
本质区别:Tavily 是"专为 AI 设计的搜索中间件",博查(Bocha)搜索 API 是"通用搜索能力接口"。 两者都能让 Agent 联网查资料,但定位、返回内容、生态适配差得挺远。
核心差异对照
| 维度 | Tavily | 博查 Bocha 搜索 API |
|---|---|---|
| 定位 | 专为 AI Agent / LLM 设计 | 通用 Web 搜索能力,面向开发者 |
| 返回内容 | 结构化、已清洗的正文片段,可直接喂 LLM | 标准搜索结果(标题、链接、摘要、站点等) |
| 内容处理 | 内置爬取、正文提取、智能过滤、相关性排序 | 通常只返回搜索结果列表,正文提取需自行处理 |
| 面向 AI 的优化 | 有 context 接口,直接输出 LLM-ready 上下文块 |
以"搜索结果呈现"为主,AI 适配需自己包装 |
| LangChain / Agent 生态 | 官方有 TavilySearchResults 等工具封装,零成本接入 |
需自行封装成 LangChain / Deep Agents 工具 |
| 市场与社区 | 海外 AI Agent 圈主流,文档和案例丰富 | 国内为主,中文搜索场景有优势 |
| 网络环境 | 海外服务,国内直连可能有延迟/不稳定 | 国内服务,中文网络访问稳定 |
| 数据侧重点 | 英文 / 全球内容覆盖强 | 中文内容、国内站点覆盖更优 |
| 定价与合规 | 海外,按量付费,国内合规/数据出境需注意 | 国内,数据合规和发票更方便 |
用写 Tool 封装博查,到底差在哪
你说"写个 Tool 利用博查的 API Key 搜索网页",技术上完全可行,Deep Agents 的工具签名只要满足 name + description + schema + func 就能接入。差别在于你得多干多少活:
博查只给"搜索结果列表",你还得自己做这些:
- 正文提取:拿到 URL 后自己爬页面、处理反爬、解析 HTML、提取正文------这一步博查不管
- 内容清洗:去掉广告、导航、脚本、无关区块,只留正文
- 相关性过滤:博查的排序是按通用搜索相关性,未必符合 LLM 的需求,可能要二次筛选
- 结构化输出:把清洗后的内容组织成 LLM 友好的格式(Token 控制、截断、去重)
- 错误处理:超时、限流、被反爬、编码问题,全部自己兜底
- 生态适配:LangChain 没有现成的博查封装,工具描述、Schema、返回格式都得手写
而 Tavily 把这些全包了 :一个 search 调用,返回的就是可直接喂给模型的结构化内容,省掉上面 1-5 步。
选Tavily还是博查?
选 Tavily,如果你:
- 做英文 / 全球内容调研
- 想快速跑通 Agent,不想在"抓取+清洗"上花时间
- 项目在海外或可接受调用海外服务
- 预算允许为"省下的开发时间"付费
选博查(自己封装 Tool),如果你:
- 核心是中文搜索场景,需要国内站点、中文内容的高质量覆盖
- 服务部署在国内,要求低延迟、数据不出境
- 已有博查 API Key 和配额,想复用现有资源
- 团队有能力自己写正文提取和清洗逻辑
Tavily = "开箱即用的 AI 搜索能力",博查 = "需要你自己组装的搜索零件"。
使用DeepAgents
js
import { TavilyClient } from "tavily";
import { createDeepAgent } from "deepagents";
import { initChatModel } from "@langchain/core";
// 1. 初始化模型(对应 Python 版的 model 参数)
const model = await initChatModel("openai:gpt-4o");
// 2. 定义工具:联网搜索(对应 Python 版的 internet_search)
const tavily = new TavilyClient({
apiKey: process.env.TAVILY_API_KEY!,
});
const internetSearch = {
name: "internet_search",
description: "在互联网上搜索信息,返回结构化结果。",
schema: {
type: "object",
properties: {
query: { type: "string", description: "搜索查询" },
max_results: { type: "number", description: "返回条数", default: 5 },
},
required: ["query"],
},
func: async ({ query, max_results = 5 }: { query: string; max_results?: number }) => {
return tavily.search(query, { maxResults: max_results });
},
};
// 3. 创建 Agent(对应 Python 版的 create_deep_agent)
const agent = createDeepAgent({
model,
tools: [internetSearch],
systemPrompt: "你是一名研究员,请深入调研并撰写一份专业报告。",
});
// 4. 跑任务(对应 Python 版的 agent.invoke)
const result = await agent.invoke({
messages: [
{
role: "user",
content: "LangGraph 是什么,它解决了什么问题?",
},
],
});
console.log(result.messages.at(-1)?.content);
二.langchain的中间件
1.中间件是什么?
中间件(
Middleware)就是一种"插队"机制:在主流程跑起来之前或之后,偷偷塞进一段你自己的逻辑。
langchain的中间件有以下6个。
js
createMiddleware({
beforeAgent: (state, runtime) => ..., // Agent 启动前
beforeModel: (state, runtime) => ..., // 每次调模型前
wrapModelCall: async (request, handler) => ..., // 包裹模型调用
afterModel: (state, runtime) => ..., // 每次模型返回后
wrapToolCall: async (request, handler) => ..., // 包裹工具调用
afterAgent: (state, runtime) => ..., // Agent 结束时
})
2.介绍beforeAgent、beforeModel,afterModel、afterAgent
agent运行期间,中间件的调用时机如下:

他们四个就是在agent前后,model前后调用。它们可以是函数,也可以是对象。以beforeModel为例,查看他的值。

state是agent里面完整的状态,包含他自己的messages和中间件的stateSchema。runtime是只读的运行期上下文,canJumpTo是一个白名单数组,容许程序跳过某些节点
案例代码:
js
beforeModel: {
canJumpTo: ["end", "model", "tools"], // 声明:本钩子允许跳到哪些节点
hook: (state, runtime) => {
if (...) {
return { messages: [...], jumpTo: "end" };
}
// 不返回,或返回 undefined → 正常继续
},
}
3.介绍wrapModelCall和wrapToolCall
3.1 wrapModelCall
wrapModelCall是模型的包裹层,它能同时干"调之前"和"调之后"两件事,而且能决定要不要调 。他和beforeModel、aftermodel之间的关系如下:
js
beforeModel ──────────┐
│
wrapModelCall 开始 ──┐│
│││ ← 在这里改 request(追加 system 指令)
handler() ──┐ │││
↓ │││
【真正调模型】 │││ ← 大模型 API 在这一行被调用
↑ │││
拿到结果 ←─┘ │││ ← 在这里改 result(改写模型输出)
wrapModelCall 结束 ──┘│
│
afterModel ───────────┘
他们的维度对比如下:

从上图可以看出,如果你想要修改model里面的request只能在wrapModelCall里面修改,不能在beforeModel里面修改。这个是最容易出错的地方。
使用时机:

3.2 wrapToolCall
wrapToolCall 是 TOOL 的包裹层。它把模型调用工具,产出 tool_calls的过程包裹起来。模型调用了几次工具,wrapToolCall就会被调用几次。
如果你想修改Tool 的request,那么就在这个时候去做。此时的requst里面还有request.toolCall.args参数供你操作。
js
wrapToolCall: async (request, handler) => {
// ① 改参数:比如给所有数据库查询强制加 limit
const args = { ...request.toolCall.args, limit: 10 };
const modifiedCall = { ...request.toolCall, args };
try {
// ② 真正执行工具
const result = await handler({ ...request, toolCall: modifiedCall });
return result;
} catch (e) {
// ③ 失败兜底:工具挂了返回一个默认值,而不是报错打断流程
return { content: "工具暂时不可用,返回缓存数据" };
}
}
4总结
beforeModel是门口的保安(只能看、只能拦),afterModel是出门的登记处(只能看、只能记账),wrapModelCall是贴着模型本体的那层壳(能改请求、能改响应、能决定调不调、能重试)。工具侧完全同理,wrapToolCall 多出来的能力是"改工具参数"和"工具失败兜底"。
如果有多个中间件,包含多个beforeAgent,他们的执行顺序和添加中间件的顺序一样。
中间件的执行流程如下:

5.中间件的调用案例
js
import "dotenv/config";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import {
createAgent,
createMiddleware,
HumanMessage,
AIMessage,
} from "langchain";
// --- 自定义 Middleware ---
/** 日志 + 模型调用次数统计 */
const loggingMiddleware = createMiddleware({
name: "LoggingMiddleware",
stateSchema: z.object({
modelCallCount: z.number().default(0),
}),
beforeAgent: (state) => {
console.log("\n[Logging] agent 开始,消息数:", state.messages.length);
},
beforeModel: (state) => {
console.log(
`[Logging] 即将调用模型,当前消息数: ${state.messages.length},已调用: ${state.modelCallCount} 次`
);
},
afterModel: (state) => {
const last = state.messages.at(-1);
const preview =
typeof last?.content === "string"
? last.content.slice(0, 80)
: JSON.stringify(last?.content)?.slice(0, 80);
console.log(`[Logging] 模型返回: ${preview}...`);
return { modelCallCount: state.modelCallCount + 1 };
},
afterAgent: (state) => {
console.log(
`[Logging] agent 结束,累计模型调用: ${state.modelCallCount} 次\n`
);
},
});
/** 在每次模型调用前追加 system 上下文 */
const addContextMiddleware = createMiddleware({
name: "AddContextMiddleware",
//修改request就用wrapModelCall
wrapModelCall: async (request, handler) => {
console.log("[AddContext] 注入额外 system 上下文");
return handler({
...request,
systemMessage: request.systemMessage.concat(
"\n\n 请用一句话简洁回答。"
),
});
},
});
/** 拦截敏感词,直接结束 agent */
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] 检测到 BLOCKED,短路结束");
return {
messages: [new AIMessage("该请求已被 middleware 拦截,无法处理。")],
jumpTo: "end",
};
}
},
},
});
// --- Agent ---
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 agent = createAgent({
model,
tools: [],
systemPrompt: "你是一个助手。",
middleware: [
loggingMiddleware,
addContextMiddleware,
blockedContentMiddleware,
],
});
for (const text of [
"用中文说:middleware 是什么?",
"这句话包含 BLOCKED 关键词",
]) {
console.log("\n用户:", text);
const { messages, modelCallCount } = await agent.invoke({
messages: [new HumanMessage(text)],
});
console.log("回复:", messages.at(-1)?.content);
console.log("modelCallCount:", modelCallCount);
}
6.中间件调用案例
wrapToolCall 的案例:
js
import "dotenv/config";
import { Command } from "@langchain/langgraph";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import {
createAgent,
createMiddleware,
HumanMessage,
ToolMessage,
tool,
} from "langchain";
const getCurrentTime = tool(() => new Date().toISOString(), {
name: "get_current_time",
description: "返回当前 UTC 时间的 ISO 8601 字符串",
schema: z.object({}),
});
/** 通过 middleware 注册工具,并用 wrapToolCall 包装执行 */
const extendedToolsMiddleware = createMiddleware({
name: "ExtendedToolsMiddleware",
stateSchema: z.object({
toolInvocationCount: z.number().default(0),
}),
tools: [getCurrentTime],
wrapToolCall: async (request, handler) => {
const toolName = request.tool?.name ?? request.toolCall.name;
console.log(
`[Tools] 即将执行: ${toolName}`,
"args:",
request.toolCall.args ?? {}
);
const result = await handler(request);
if (!ToolMessage.isInstance(result)) return result;
const wrapped = new ToolMessage({
content: `${result.content}\n[wrapToolCall] 已由 ExtendedToolsMiddleware 包装`,
tool_call_id: result.tool_call_id,
name: result.name,
});
console.log(
`[Tools] 执行完成: ${toolName}`,
typeof wrapped.content === "string"
? wrapped.content.slice(0, 120)
: wrapped
);
return new Command({
update: {
toolInvocationCount: request.state.toolInvocationCount + 1,
messages: [wrapped],
},
});
},
afterAgent: (state) => {
console.log(
`[Tools] agent 结束,middleware 统计工具调用: ${state.toolInvocationCount} 次`
);
},
});
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 agent = createAgent({
model,
tools: [],
systemPrompt:
"你是一个助手。",
middleware: [extendedToolsMiddleware],
});
for (const text of [
"给我当前时间",
]) {
console.log("\n用户:", text);
const { messages, toolInvocationCount } = await agent.invoke({
messages: [new HumanMessage(text)],
});
console.log("回复:", messages.at(-1)?.content);
console.log("toolInvocationCount:", toolInvocationCount);
}
三.DeepAgent的中间件
1.概念
DeepAgent的中间件是对langchain中间件的封装,它本质是"Agent+ 一组预制中间件 + 一套预定义工具"的组合
你写的createDeepAgent({...}),底层就是createAgent({ middleware: [那堆预制的中间件], tools: [...], ...})。
js
const agent = createDeepAgent({
model,
tools: [internetSearch],
systemPrompt: "...",
// 关键:可以往 Deep Agents 的预制中间件前后再插你自己的
middleware: [myCustomMiddleware],
});
Deep Agents的内置中间件 = 任务规划(TodoList)+ 文件系统(Filesystem)+ 子 Agent(SubAgent)+ 摘要(Summarization)+ 工具调用修补(PatchToolCalls)+ 缓存(Anthropic/Bedrock)+ 按需的Memory/Skills/HITL/AsyncSubAgent。它们按固定顺序装配,你的自定义中间件插在第 6 位、权限闸门之前。
2.中间件汇总

以上是一个中间件,并不是全部都安装,无脑安装的只有5个,TodoListMiddleware、FilesystemMiddleware、SubAgentMiddleware、SummarizationMiddleware、PatchToolCallsMiddleware,这五个中间件,只要调 createDeepAgent 就一定有的。
还有四个是有了条件才会有的。比如传了 memory 参数才有MemoryMiddleware,传了 skills 参数才有SkillsMiddleware,传了 interruptOn 参数才有HumanInTheLoopMiddleware,配了异步子 Agent 才有AsyncSubAgentMiddleware
自动判断的有2个,用 Anthropic 模型才挂载AnthropicPromptCachingMiddleware,用 AWS Bedrock 才挂载BedrockPromptCachingMiddleware。这 2 个框架自己看情况决定,互斥------用 Anthropic 就挂第一个,用 Bedrock 就挂第二个,两个不会同时出现。

3.使用方式--createDeepAgent创建agent
js
createDeepAgent({
model,
todoList: { ... }, // 改 TodoListMiddleware
filesystem: { ... }, // 改 FilesystemMiddleware
subagents: { ... }, // 改 SubAgentMiddleware
summarization: { ... }, // 改 SummarizationMiddleware
// PatchToolCallsMiddleware ------ 无配置项,全自动
})
标准写法
js
const agent = createDeepAgent({
model,
// ① 统一配置项:改五个中间件的行为
filesystem: { backend: myBackend },
summarization: { trigger: { tokens: 500 } },
subagents: { defaultModel, subagents: [...] },
// ② 统一钩子:用六个钩子观察/控制一切
middleware: [
createMiddleware({
wrapToolCall: (req, handler) => {
// 前三个中间件的工具,统一在这里拦截
if (["write_todos","write_file","task"].includes(req.toolCall.name)) {
console.log(`[${req.toolCall.name}] 被调用`);
}
return handler(req);
},
afterModel: (state) => {
// Summarization 的压缩时机,你在这里能感知到
},
}),
],
// ③ 统一 prompt:指挥 Agent 用前三个中间件的能力
systemPrompt: `...先拆任务,再派子 Agent,文件写入 /report.md...`,
});
4.使用方式--createAgent 创建agent
createAgent 本身来自 langchain 包,它的预构建中间件分两个来源:

langchain 包自带的 13 个,deepagents 包提供的 5 个

案例
js
import { createAgent } from "langchain";
import {
createFilesystemMiddleware,
CompositeBackend, StateBackend, StoreBackend,
} from "deepagents";
import { createAgent,
summarizationMiddleware, // ① 长对话自动摘要
contextEditingMiddleware, // ② 裁剪/清空旧工具结果
piiRedactionMiddleware, // ③ 脱敏(旧名 piiMiddleware)
} from "langchain";
const agent = createAgent({
model: "claude-sonnet-4-6",
middleware: [
piiRedactionMiddleware({ patterns: ["email", "phone", "ssn"] }),
summarizationMiddleware({
model: "claude-sonnet-4-6",
trigger: { tokens: 500 },
}),
contextEditingMiddleware({ /* 裁剪策略 */ }),
createFilesystemMiddleware({
backend: new CompositeBackend(
new StateBackend(), // / → 临时
{ "/memories/": new StoreBackend() } // /memories/ → 持久
),
// 可选:自定义工具描述
customToolDescriptions: {
ls: "用 ls 列出文件",
read_file: "用 read_file 读取文件",
},
// 可选:只暴露部分工具
tools: ["read_file", "ls", "glob", "grep"],
}),
],
});
案例:利用createFilesystemMiddleware对文件进行读写操作。你会发现我们之前的文件读写Tool写了很多代码,现在只用createFilesystemMiddleware的两行代码和一些配置,就实现了读写操作。
只要加上
deepagents这个FileSystem中间件,agent就有了一个文件系统,并且有了读写搜索文件的各种tool,还做了权限控制。
js
import "dotenv/config";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ChatOpenAI } from "@langchain/openai";
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" },
];
fs.rmSync(workspaceDir, { recursive: true, force: true });
fs.mkdirSync(workspaceDir);
fs.writeFileSync(path.join(workspaceDir, "secret.txt"), "机密:不得读取", "utf8");
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 agent = createAgent({
model,
tools: [],
systemPrompt:
"工作区根路径为 /。用 ls、read_file、write_file、edit_file 操作文件,路径以 / 开头。中文回答。",
middleware: [
createFilesystemMiddleware({
backend: new FilesystemBackend({ rootDir: workspaceDir, virtualMode: true }),
permissions,
}),
],
});
console.log("工作区:", workspaceDir);
console.log("权限:", JSON.stringify(permissions, null, 2));
async function run(label, prompt) {
console.log(`\n=== ${label} ===\n`, prompt, "\n");
const { messages } = await agent.invoke(
{ messages: [new HumanMessage(prompt)] },
{ recursionLimit: 20 }
);
for (const m of messages) {
for (const t of m.tool_calls ?? []) console.log("→", t.name);
}
console.log("回复:", messages.at(-1)?.content);
}
async function expectDenied(label, prompt) {
console.log(`\n=== ${label}(预期拒绝)===\n`, prompt, "\n");
try {
await agent.invoke({ messages: [new HumanMessage(prompt)] }, { recursionLimit: 5 });
console.log("未触发拒绝(异常)");
} catch (e) {
const msg = e.cause?.message ?? e.message;
console.log("✗", msg);
}
}
await run(
"允许的操作",
"write_file 创建 /todo.md(三条待办),edit_file 把第一条标为完成,ls /,一句话总结。"
);
await expectDenied("禁止读", "只调用 read_file,路径 /secret.txt。");
await expectDenied("禁止写", "只调用 write_file,路径 /hack.txt,内容 test。");
5.DeepAgent的bug排查路线

四.skill是什么?
1.概念
首先,
Skill不是工具。Skill是"说明书",工具是"手"。 Skill本身不能执行任何动作,它只是告诉Agent"该用哪些工具、按什么顺序做"。

2.存在的意义
说白了,skill的存在形态就是一个 SKILL.md文件。他不是函数,啥都做不了。那他存在的意义是什么?
Skill 靠引用工具来完成任务。 没有工具,Skill 只是一纸空文;没有 Skill,工具也能用,但 Agent 得自己现想怎么做------慢、还容易错。
所以说,Skill 存在的意义,就是给那些"工具太通用、模型不够懂"的场景补知识。
3.使用场景
如果你能一句话说清"让 Agent 用 某 工具去做 某件事",就不需要 Skill
如果"怎么做"里有一堆讲究(顺序、参数、坑、规范),就该写 Skill。
skill就是一套长期操作的行为规范,比如发布项目。你先要做什么,传什么参数等等。
| 使用场景 | 一句话定义 | 典型例子 | 为什么需要 Skill |
|---|---|---|---|
| 项目特有规范 | 只有你这个项目/团队才有的硬性规则 | 函数必须有 JSDoc、禁止用 var、异步函数以 handle 开头、提交前必跑 lint | 工具给不了------这是团队独有的约定,只能写成文档告诉 Agent |
| 复杂流程 SOP | 多步骤、有顺序、错了会出事的流程 | 部署:跑测试 → 打 tag → 推镜像 → 改 k8s 配置 → 灰度发布 | 步骤顺序关键,写成 Skill 照章执行,比让模型每次现推理稳得多 |
| 领域经验 | 模型能调工具,但不知道你偏好的套路 | 数据分析用 pandas 处理缺失值、PDF 扫描件先 OCR、SQL 避免全表扫描 | "怎么做"里的讲究和坑,是经验不是能力,工具本身不包含 |
| 命令和脚本模板 | 团队常用的固定命令与参数 | npm run dev -- --port 3001、npm run test -- --coverage |
把常用命令沉淀下来,Agent 不用每次现猜参数 |
| 带依赖的完整方案 | 一整套能力,含脚本、模板、示例 | 生成周报:SKILL.md + fetch_commits.py + render.py | Skill 可打包脚本和模板,形成可复用的完整方案 |
4.skill的接入方式
createSkillsMiddleware 是skill官方推荐的接入方式。是 DeepAgent的一个工具函数。
skill的接入方式有2种,1是用官方推荐方式,2是用手动文件读取方式。
项目目录:

在.agents/skills/coffee/目录下面有一个SKILL.md文件,内容如下:
js
---
name: coffee
description: 按标准流程冲一杯咖啡
---
# 冲咖啡技能
## 何时使用
用户说"帮我冲杯咖啡"时触发。
## 执行步骤
1. 烧水(水温 92°C)
2. 取 15g 咖啡粉放入滤杯
3. 缓慢注水 30ml 闷蒸 30 秒
4. 分三次注水至总量 225ml
5. 完成,提醒用户趁热喝
4.1.官方推荐--createSkillsMiddleware
我们使用createSkillsMiddleware调用SKILL.md文件。
js
import { createAgent, HumanMessage } from "langchain";
import { createSkillsMiddleware } from "deepagents";
const agent = createAgent({
model: "gpt-4o-mini",
tools: [], // 不放任何工具
systemPrompt: "需要时用 use_skill 加载技能。",
middleware: [
createSkillsMiddleware({
sources: ["./.agents/skills/"], // 指向技能目录
}),
],
});
await agent.invoke({
messages: [new HumanMessage("帮我冲杯咖啡")],
});
运行skill中间件以后,systemPrompt会变成这样:
js
[系统消息] 你是助手。需要时用 use_skill 加载技能。
技能目录:
- coffee:按标准流程冲一杯咖啡
- pdf:提取 PDF 文本、合并拆分
- data-analysis:用 pandas 做数据清洗
- deploy:部署流程 SOP
[用户消息] 帮我冲杯咖啡 ← 模型看到这句才做匹配
4.2.手动读取skill.md
如果脱离createSkillsMiddleware你也可以实现,具体如下:
js
import fs from "node:fs";
import { ChatOpenAI } from "@langchain/openai";
const skillContent = fs.readFileSync(
"./.agents/skills/coffee/SKILL.md",
"utf8"
);
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
await model.invoke([
{ role: "system", content: `技能说明:\n${skillContent}` }, // ← 直接塞进 prompt
{ role: "user", content: "帮我冲杯咖啡" },
]);
先要大模型读取具体的文件,然后再回答问题。
4.3.比较两个接入方式
手动读取skill.md其实和createSkillsMiddleware的实现思路是一样的。但是createSkillsMiddleware内部做了很多优化点,具体如下:

4.4 skill的组成部分
一个标准的
SKILL.md文件由两部分 构成:YAML 元数据头 (机器读)和 Markdown 正文(模型读)。 元数据指的是name,description,keywords 这三个字段。除此之外都是正文。

一个标准的skill.md文件必然包含下面这些内容
js
---
name: skill-name
description: 一句话说明这个技能做什么
keywords: [...]
---
# 标题
## 何时使用
## 执行步骤
## 命令 / 代码模板
## 示例
## 注意事项
4.5 skill的运行流程
在项目里面skill一般会这样放置
js
.agents/skills/
├── coffee/SKILL.md ← 读元数据 ✓
├── pdf/SKILL.md ← 读元数据 ✓
├── data-analysis/SKILL.md ← 读元数据 ✓
└── deploy/SKILL.md ← 读元数据 ✓
如果一个项目里面有多个skill文件,什么时候加入文件,什么时候读取哪个文件?
启动时,
createSkillsMiddleware读取所有SKILL.md的元数据,追加到你原有systemPrompt后面形成技能目录;执行invoke拿到用户问题后,模型把问题和每条技能的description做语义匹配,选出最相关的那个,再调use_skill去读取对应SKILL.md的正文。
拼接的 prompt 如图所示:

SkillsMiddleware的运行流程图

使用这套机制的好处是:
| 好处 | 属于谁 | 理由 |
|---|---|---|
| ① 省 Token(按需加载) | 机制 | 是"先给目录、按需取正文"这个加载策略省的钱,和 SKILL.md 内容无关 |
| ② 防工具过载 | 机制 | 防工具过载------模型先粗筛目录再细读正文,选得准。 |
| ③ 能力可无限扩展 | 机制 | 新增技能的边际成本趋近于零,这是 Skill 能做成生态的前提; |
| ④ 技能解耦、好维护 | 机制 | 各自独立文件,好维护、好复用。本质上就是把"选"和"做"拆成两层,模型每一步都只看该看的东西。 |
如果把所有的skill.md都放到systemPrompt里面去,大模型用的token就比较多。
4.6 使用skill的好处是:
Skill 的本质价值是"把项目知识文档化、标准化、资产化"------不用每次重新教 Agent,保证每次输出一致,加能力只需写 Markdown 不用改代码,还能跟着 Git 走、跨项目跨团队分享;再配合按需加载,能力越多单次开销几乎不涨。它是让 Agent 从"通用助手"变成"懂你项目的专属助手"的关键。
4.7 skill包
介绍常用的skill 包,地址:www.skills.sh
skills.sh 是由 Vercel(vercel-labs)运营的"开放 Agent Skills 目录与排行榜",定位是 AI 技能界的 npm------用来发现、安装、发布各种 AI Agent 可复用的能力包。
如何使用他里面的skill?
4.7.1.进入网页,搜索


4.7.2.点进去,拿到安装命令

4.7.3.在项目里面运行命令

运行这个命令的时候,你会发现他会报错,原因有2个,一个是github国内网络不同导致下载不了,还有一个原因是这个命令有时间限制,超时就会报错。

解决办法:
执行下面的命令。
js
git clone https://github.com/github/awesome-copilot.git
你会在你的项目下面看到一个文件夹:awesome-copilot,它里面的skills文件夹下面就装着很多skill。 
找到awesome-copilot\skills\excalidraw-diagram-generator,复制粘贴到你项目的.agents\skills目录下面

此时你就可以测试这个skill了,这个skill的目的是要大模型生成一个图标,保存到当前目录下面的src/deepagents/output/deepagents-skills-flow.excalidraw里面
js
import "dotenv/config";
import { existsSync, mkdirSync } from "node:fs";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, HumanMessage } from "langchain";
import {
LocalShellBackend,
createFilesystemMiddleware,
createSkillsMiddleware,
} from "deepagents";
const skills = "/.agents/skills/";
const output = "src/deepagents/output/deepagents-skills-flow.excalidraw";
if (!existsSync(".agents/skills/excalidraw-diagram-generator/SKILL.md")) {
throw new Error(
"未找到 excalidraw-diagram-generator,请先: npx skills add github/awesome-copilot --skill excalidraw-diagram-generator -y"
);
}
mkdirSync("src/deepagents/output", { recursive: true });
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 = 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 }),
],
});
const prompt = [
"画一张流程图,描述本项目的 skills-agent 工作流:",
"用户 Prompt → createAgent → createSkillsMiddleware → createFilesystemMiddleware → 模型回复。",
`保存为 ${output}。要求:`,
"- 顶部大标题 + 副标题",
"- 每个主节点 numbered(①②...)且框内 2~3 行中文说明",
"- 右侧一列「说明:...」补充细节",
"- 箭头上标注阶段名(如 invoke、wrapModelCall)",
"- 底部图例(颜色含义 + 如何运行 demo)",
].join("\n");
const stream = await agent.stream(
{ messages: [new HumanMessage(prompt)] },
{ recursionLimit: 100 }
);
let skillsMetadata;
console.log("\n--- 流式输出 ---\n");
try {
for await (const chunk of stream) {
// chunk 是 AIMessageChunk,content 可能是 string 或数组
const text = Array.isArray(chunk.content)
? chunk.content.map((p) => (typeof p === "string" ? p : p?.text ?? "")).join("")
: (chunk.content ?? "");
if (text) process.stdout.write(text);
}
} catch (e) {
console.error("\n\n[错误]", e.cause?.message ?? e.message);
throw e;
}
if (existsSync(output)) {
console.log("图表:", output);
console.log("打开: https://excalidraw.com → Open → 选择该文件");
} else {
console.log("未生成:", output);
}
await backend.close();
测试

我们进入打开: excalidraw.com → Open → 选择src\deepagents\output\deepagents-skills-flow.excalidraw文件

总结:
在上面的代码里面我们用了2个中间件:
createSkillsMiddleware({ backend, sources: [skills] }),使用skill库里面的图标生成。createFilesystemMiddleware({ backend }),它里面默认装了很多文件处理的tool,你只需要在提示词里面说了,他就会自动用这些Tool,处理文件。
process.stdout.write(text);和console.log(text)他们都能在控制台上输出对应的文本,console.log 是"打一行日志",stdout.write 是"往字节流里塞东西",所以他可以实现流式输出。
五.DeepAgent的其他中间件
1.SubAgentMiddleware
在代码里面实现:查询天气的,做加减法的,读写文件的,查询网页信息这四个子agent。
这个案例你当然也可以用langgraph实现,没有问题。
js
// sub-agent-demo.mjs
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "langchain";
import { createAgent } from "langchain";
import { createSubAgentMiddleware } from "deepagents";
import { z } from "zod";
/* ==================== 模型 ====================
* 关键:字符串格式必须是 "provider:model"
* 你之前报 "Unable to infer model provider" 就是因为缺了 provider 前缀
*/
const model = "openai:qwen3.7-plus";
// 如果你更习惯用实例,就这样写(二选一,注释掉上面那行)
// const model = new ChatOpenAI({
// model: process.env.MODEL_NAME || "qwen3.7-plus",
// apiKey: process.env.OPENAI_API_KEY,
// temperature: 0,
// configuration: { baseURL: process.env.OPENAI_BASE_URL },
// });
/* ==================== 工具 ==================== */
// 1) 天气
const getWeather = tool(
async ({ city }) => `天气:${city} 晴,25°C,适合出行。`,
{
name: "get_weather",
description: "查询指定城市的天气。输入城市名,返回天气和气温。",
schema: z.object({ city: z.string().describe("城市名") }),
}
);
// 2) 计算器
const calculator = tool(
async ({ a, b, op }) => {
const r = op === "add" ? a + b : op === "sub" ? a - b : op === "mul" ? a * b : a / b;
return `${a} ${op} ${b} = ${r}`;
},
{
name: "calculator",
description: "做加减乘除。op 可选 add/sub/mul/div。",
schema: z.object({
a: z.number(),
b: z.number(),
op: z.enum(["add", "sub", "mul", "div"]),
}),
}
);
// 3) 文件读写
import { readFile, writeFile } from "node:fs/promises";
const readMyFile = tool(
async ({ path }) => (await readFile(path, "utf8")),
{
name: "read_file",
description: "读取本地文件内容。",
schema: z.object({ path: z.string() }),
}
);
const writeMyFile = tool(
async ({ path, content }) => {
await writeFile(path, content, "utf8");
return `已写入 ${path}`;
},
{
name: "write_file",
description: "把内容写入本地文件。",
schema: z.object({ path: z.string(), content: z.string() }),
}
);
// 4) 网页查询
const searchWeb = tool(
async ({ query }) => `关于「${query}」的搜索摘要:这是一段示例内容...`,
{
name: "search_web",
description: "搜索网页信息,返回相关摘要。",
schema: z.object({ query: z.string() }),
}
);
/* ==================== 子 agent 定义 ====================
* 每个子 agent 必须有 name + description + tools + systemPrompt
* model 不填就用 defaultModel
*/
const weatherAgent = {
name: "weather",
description: "查询城市天气。当用户问到天气、气温时使用。",
systemPrompt: "你是天气查询助手,必须用 get_weather 工具查询后回答。",
tools: [getWeather],
};
const mathAgent = {
name: "calculator",
description: "做加减乘除运算。用户问计算、算数时使用。",
systemPrompt: "你是计算助手,必须用 calculator 工具计算后回答。",
tools: [calculator],
};
const fileAgent = {
name: "file_worker",
description: "读写本地文件。用户要创建、读取、修改文件时使用。",
systemPrompt: "你是文件助手,用 read_file / write_file 操作文件。",
tools: [readMyFile, writeMyFile],
};
const webAgent = {
name: "web_researcher",
description: "搜索网页信息。用户要查资料、搜新闻时使用。",
systemPrompt: "你是搜索助手,必须用 search_web 获取信息后回答。",
tools: [searchWeb],
};
/* ==================== 主 agent ==================== */
const agent = createAgent({
model, // 字符串 "provider:model" 或模型实例
tools: [], // 主 agent 自身不挂工具,全靠委派
systemPrompt: [
"你是主管 agent。",
"遇到具体任务时,用 task 工具委派给合适的子 agent:",
"- weather:查天气",
"- calculator:做计算",
"- file_worker:读写文件",
"- web_researcher:搜索资料",
"拿到子 agent 的结果后,用自己的话总结给用户。",
].join("\n"),
middleware: [
createSubAgentMiddleware({
defaultModel: model, // 子 agent 默认模型,和主 agent 一致
defaultTools: [], // 子 agent 默认不继承任何工具
subagents: [weatherAgent, mathAgent, fileAgent, webAgent],
generalPurposeAgent: true, // 保留通用子 agent(可选)
}),
],
});
/* ==================== 调用 ==================== */
async function ask(question) {
console.log(`\n>>> ${question}\n`);
const result = await agent.invoke(
{ messages: [{ role: "user", content: question }] },
{ recursionLimit: 50 }
);
// result.messages 最后一条是最终 AI 回复
const last = result.messages[result.messages.length - 1];
const text = typeof last.content === "string"
? last.content
: Array.isArray(last.content)
? last.content.map(p => p?.text ?? "").join("")
: "";
console.log(text || "[无文本输出]");
}
// 分别测试四个子 agent
await ask("北京今天天气怎么样?顺便帮我计算下 56+12 是多少?再帮我搜索下 LangChain 最新版本的信息。 ");
// await ask("123 乘以 456 等于多少?");
// await ask("帮我创建一个 notes.txt,内容是 hello world");
// await ask("搜索一下 LangChain 最新版本的信息");
process.exit(0);
测试如下:

把ask里面的invoke改成stream
js
async function ask(question) {
// console.log(`\n>>> ${question}\n`);
// const result = await agent.invoke(
// { messages: [{ role: "user", content: question }] },
// { recursionLimit: 50 }
// );
// // result.messages 最后一条是最终 AI 回复
// const last = result.messages[result.messages.length - 1];
// const text = typeof last.content === "string"
// ? last.content
// : Array.isArray(last.content)
// ? last.content.map(p => p?.text ?? "").join("")
// : "";
// console.log(text || "[无文本输出]");
const stream = await agent.stream(
{ messages: [{ role: "user", content: question }] }, // ← 对象,不是数组
{ recursionLimit: 50, streamMode: "messages" } // ← messages 模式才能逐条拿消息
);
for await (const chunk of stream) {
const [msg, meta] = chunk;
const c = msg.content;
const text = Array.isArray(c)
? c.filter(p => p?.type === "text").map(p => p.text).join("")
: (c ?? "");
if (text) process.stdout.write(text);
}
}
2.MemoryMiddleware-createMemoryMiddleware
长期记忆也是 Agent 必备的功能,deepagents 提供了 MemoryMiddleware
可以把记忆存储在 markdown 文件里,可以读取、更新,持久化存储。
2.1 只读取记忆的案例
下面这个案例就是在文件夹里面建立一个文件.deepagents/AGENTS.md,然后写入用户的信息和爱好。
之后利用createMemoryMiddleware读取文件里面的存储信息。在createAgent的时候将中间件注入进去就好了。

js
// memory-demo.mjs
import "dotenv/config";
import { tool } from "langchain";
import { createAgent } from "langchain";
import {
createMemoryMiddleware,
FilesystemBackend,
} from "deepagents";
import { z } from "zod";
/* ========== 1. 准备记忆文件 ==========
* middleware 会读这个文件,把内容拼进 system prompt。
* 如果文件不存在,middleware 会 console.debug 一条失败信息,但不报错。
*/
import { mkdirSync, writeFileSync } from "node:fs";
mkdirSync(".deepagents", { recursive: true });
writeFileSync(
".deepagents/AGENTS.md",
[
"# 项目记忆",
"",
"## 用户偏好",
"- 用户姓名:张三",
"- 语言偏好:回复用中文",
"- 代码风格:变量用驼峰命名",
"",
"## 项目背景",
"- 这是一个天气查询机器人 demo",
"- 技术栈:Node.js + LangChain + deepagents",
].join("\n")
);
/* ========== 2. 模型 ==========
* 注意:"openai:xxx" 格式下 baseURL 没法传,走兼容端点必须用实例写法。
*/
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: process.env.MODEL_NAME || "qwen3.7-plus",
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
});
/* ========== 3. 一个简单工具 ========== */
const getWeather = tool(
async ({ city }) => `${city}:晴,25°C`,
{
name: "get_weather",
description: "查询指定城市的天气",
schema: z.object({ city: z.string() }),
}
);
/* ========== 4. Memory 中间件 ==========*/
const memoryMiddleware = createMemoryMiddleware({
backend: new FilesystemBackend({ rootDir: process.cwd() }),
sources: ["./.deepagents/AGENTS.md"],
});
/* ========== 5. Agent ========== */
const agent = createAgent({
model,
tools: [getWeather],
systemPrompt: "你是助手。你拥有持久记忆,请充分利用它来回答。",
middleware: [memoryMiddleware],
});
/* ========== 6. 调用 ========== */
const result = await agent.invoke(
{ messages: [{ role: "user", content: "我叫什么名字?我偏好什么代码风格?" }] },
{ recursionLimit: 50 }
);
const last = result.messages[result.messages.length - 1];
const text =
typeof last.content === "string"
? last.content
: Array.isArray(last.content)
? last.content.map((p) => p?.text ?? "").join("")
: "";
console.log("\n=== 回复 ===\n" + (text || "[无文本]"));
process.exit(0);
测试

2.2 读写文件都有
代码里面用了2个中间件, createMemoryMiddleware,createFilesystemMiddleware,你只需要通过提示词告诉大模型需要干嘛,他自己就会调用中间件去存取数据。
js
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent } from "langchain";
import {
createMemoryMiddleware,
createFilesystemMiddleware,
FilesystemBackend,
} from "deepagents";
import { z } from "zod";
import { mkdirSync, writeFileSync } from "node:fs";
mkdirSync(".deepagents", { recursive: true });
writeFileSync(".deepagents/AGENTS.md", "# 项目记忆\n");
const model = new ChatOpenAI({
model: process.env.MODEL_NAME || "qwen3.7-plus",
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
});
const backend = new FilesystemBackend({ rootDir: process.cwd() });
const memoryMiddleware = createMemoryMiddleware({
backend,
sources: ["./.deepagents/AGENTS.md"],
});
const fsMiddleware = createFilesystemMiddleware({ backend });
const agent = createAgent({
model,
tools: [],
systemPrompt: "你是助手。善用 edit_file 把学到的重要信息写回记忆文件。",
middleware: [memoryMiddleware, fsMiddleware],
});
const result = await agent.invoke(
{
messages: [
{
role: "user",
content:
"请记住:我喜欢敲代码,也喜欢画画,给孩子教语文。记住后告诉我你记住了什么。",
},
],
},
{ recursionLimit: 50 }
);
const last = result.messages[result.messages.length - 1];
const text =
typeof last.content === "string"
? last.content
: Array.isArray(last.content)
? last.content.map((p) => p?.text ?? "").join("")
: "";
console.log("\n=== 回复 ===\n" + (text || "[无文本]"));
// 验证记忆是否真的写进去了
const updated = await backend.read(`${process.cwd()}/.deepagents/AGENTS.md`);
console.log("\n=== AGENTS.md 现在的内容 ===\n" + updated.content);
process.exit(0);

读写保存历史记忆
和上面那个每次都存新数据的案例之间的差别就是,不存在这个md文件才新建,然后在将系统提示词加上追加信息就好了。


具体实现代码
js
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent } from "langchain";
import {
createMemoryMiddleware,
createFilesystemMiddleware,
FilesystemBackend,
} from "deepagents";
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
const MEMORY_FILE = ".deepagents/AGENTS.md";
// 关键改动:文件不存在才初始化,存在就保留 ------ 这就是"追加"的前提
mkdirSync(".deepagents", { recursive: true });
if (!existsSync(MEMORY_FILE)) {
writeFileSync(
MEMORY_FILE,
[
"# 项目记忆",
"",
"## 用户偏好",
"",
"## 重要事实",
"",
].join("\n")
);
}
const model = new ChatOpenAI({
model: process.env.MODEL_NAME || "qwen3.7-plus",
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
});
const backend = new FilesystemBackend({ rootDir: process.cwd() });
const memoryMiddleware = createMemoryMiddleware({
backend,
sources: [`./${MEMORY_FILE}`],
});
const fsMiddleware = createFilesystemMiddleware({ backend });
const agent = createAgent({
model,
tools: [],
systemPrompt: [
"你是助手,拥有持久记忆,记忆文件是 .deepagents/AGENTS.md。",
"当对话中出现值得长期保存的信息(用户偏好、个人情况、工作习惯等),",
"你必须在回复前先用 edit_file 工具把它追加到记忆文件的「## 重要事实」段落之后。",
"只追加新增内容,不要覆盖已有条目。",
"如果用户说的信息是临时/一次性的(如\"我今晚要出门\"),不要保存。",
].join("\n"),
middleware: [memoryMiddleware, fsMiddleware],
});
// 跑前:读一次,确认旧记忆还在
const before = readFileSync(MEMORY_FILE, "utf8");
console.log("=== 写入前 AGENTS.md ===\n" + before);
const result = await agent.invoke(
{
messages: [
{
role: "user",
content:
"请记住:早晨我喜欢喝牛奶吃面包,中午我喜欢吃臊子面,下午我喜欢喝稀饭。记住后告诉我你记住了什么。",
},
],
},
{ recursionLimit: 50 }
);
const last = result.messages[result.messages.length - 1];
const text =
typeof last.content === "string"
? last.content
: Array.isArray(last.content)
? last.content.map((p) => p?.text ?? "").join("")
: "";
console.log("\n=== 回复 ===\n" + (text || "[无文本]"));
// 关键:验证要读 backend 最终持有的版本,而不是文件系统上的旧缓存
const after = await backend.read(`${process.cwd()}/${MEMORY_FILE}`);
console.log("\n=== 写入后 AGENTS.md ===\n" + after.content);
process.exit(0);
测试
红框里面是老的,后面是追加的。

3.SummarizationMiddleware
我们用做了一个读取超大文本的tool,还用createSummarizationMiddleware中间件。
SummarizationMiddleware 中间件的作用是:如果当前对话上下文长度超过预设阈值,就自动对历史对话进行摘要压缩,剔除冗余信息,只保留关键上下文摘要,再传入大模型进行后续续写 / 问答。
这样做的好处是:可以控制 Token 消耗、避免上下文溢出,同时保证核心对话语义不丢失。
js
// summarization-demo.mjs
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "langchain";
import { createAgent } from "langchain";
import {
createSummarizationMiddleware,
FilesystemBackend,
} from "deepagents";
import { z } from "zod";
/* ========== 模型 ========== */
const model = new ChatOpenAI({
model: process.env.MODEL_NAME || "qwen3.7-plus",
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
});
/* ========== 一个能造出长对话的工具 ==========
* 用固定条目列表,让每轮都能产出可观的文本,方便观察摘要触发
*/
const facts = tool(
async ({ topic }) => {
const db = {
beijing: Array.from({ length: 12 }, (_, i) => `北京第${i + 1}条城市数据:人口约2189万,GDP第${i + 1}位`).join("\n"),
history: Array.from({ length: 15 }, (_, i) => `历史事件${i + 1}: 发生于公元${1000 + i}年的重要转折`).join("\n"),
};
return db[topic] || "未找到该主题资料";
},
{
name: "fetch_dataset",
description: "按主题拉取一段较长的数据集文本",
schema: z.object({ topic: z.enum(["beijing", "history"]) }),
}
);
/* ========== Summarization 中间件 ==========*/
const summarizer = createSummarizationMiddleware({
model, // 用同一个模型做摘要
backend: new FilesystemBackend({ rootDir: process.cwd() }),
// 关键:显式设小阈值。用 tokens 维度对本地模型不稳,改用 messages 条数
trigger: { type: "messages", value: 8 }, // 累计 8 条消息就触发摘要
keep: { type: "messages", value: 4 }, // 摘要后只保留最近 4 条
historyPathPrefix: ".conversation_history",
});
const agent = createAgent({
model,
tools: [facts],
systemPrompt: "你是数据助手。被问到资料时务必调用 fetch_dataset 工具。",
middleware: [summarizer],
});
/* ========== 跑多轮,观察摘要累积 ========== */
const questions = [
"北京的城市数据有哪些?",
"再查一次北京的数据。",
"换个主题,历史事件的资料给我。",
"北京数据再补充一些。",
"历史事件还有别的吗?",
"北京数据第6到12条是什么?", // 到这里 messages 够多了,应该触发摘要
];
let messages = [];
for (const q of questions) {
console.log(`\n>>> 用户: ${q}`);
// 把上一轮的回复带进去,模拟多轮
const result = await agent.invoke(
{ messages: [...messages, { role: "user", content: q }] },
{ recursionLimit: 50 }
);
const last = result.messages[result.messages.length - 1];
const text =
typeof last.content === "string"
? last.content
: Array.isArray(last.content)
? last.content.map((p) => p?.text ?? "").join("")
: "";
console.log(`<<< 助手: ${(text || "[无文本]").slice(0, 120)}...`);
// 更新消息历史(把完整轮次带回下一轮)
messages = result.messages;
// 检查是否产生了摘要消息
const summaryMsg = result.messages.find(
(m) => m?.additional_kwargs?.lc_source === "summarization"
);
if (summaryMsg) {
console.log(
` [摘要已生成] 摘要内容: ${String(summaryMsg.content).slice(0, 150)}...`
);
}
console.log(` [当前消息条数] ${result.messages.length}`);
}
/* ========== 验证归档文件 ========== */
console.log("\n=== 归档的历史对话 ===");
const backend = new FilesystemBackend({ rootDir: process.cwd() });
console.log("拿到了什么?", await backend.ls("/conversation_history"))
try {
const {files} = await backend.ls("/conversation_history");
for (const f of files) {
const c = await backend.read(f.path);
console.log(`\n--- ${f.name} ---`);
console.log(String(c.content).slice(0, 500));
}
} catch (e) {
console.log("读取归档失败(可能尚未触发摘要):", e.message);
}
process.exit(0);
测试

4.FilesystemBackend读写文件
FilesystemBackend是deepagents的读写文件的一个类。

FilesystemBackend的顶层有个类型用来定义他的方法:
js
interface BackendProtocol {
read(path): Promise<ReadResult> // 读文件
write(path, content): Promise<WriteResult> // 写文件
delete(path): Promise<DeleteResult> // 删
ls(path): Promise<ListResult> // 列目录
exists(path): Promise<boolean> // 是否存在
// ...
}
node内置的fs 和 BackendProtocol的对比

FilesystemBackend 内部就是用 fs 实现的这个接口。你完全可以把它理解成:FilesystemBackend = 一个用 fs 包装出来的、符合 BackendProtocol 规范的适配器。
不管我的数据是从内存来的,还是磁盘来的,甚至是云端来的,都可以用createFilesystemMiddleware处理。
js
// middleware 不关心存在哪,它只调接口
const backend: BackendProtocol = ...;
// 可以是本地磁盘
backend = new FilesystemBackend({ rootDir: "/data" });
// 也可以是内存(测试时用)
backend = new InMemoryBackend();
// 也可以是云端 / LangSmith Sandbox
backend = new ContextHubBackend(...);
//中间件使用
const fsMiddleware = createFilesystemMiddleware({ backend });
六:总结
DeepAgents 是 LangChain 家族的开源智能体内核,把"深度干活"收敛成四件套:任务规划、文件系统、子 Agent 委派、详细提示词,让 agent 能自主拆任务、搜资料、写文件并交付完整产物。
LangChain 中间件是"插队"机制,六个钩子分两类:beforeAgent/beforeModel/afterModel/afterAgent 只观察或拦截;wrapModelCall 与 wrapToolCall 才能改请求、改响应、做兜底。
createDeepAgent 内置 TodoList、Filesystem、SubAgent、Summarization、PatchToolCalls 五个固定中间件。
你可以在beforeAgent/beforeModel/afterModel/afterAgent/wrapModelCall /wrapToolCall 六个钩子函数里面写入中间件要做的事情。也可以直接用:TodoList、Filesystem、SubAgent、Summarization、PatchToolCalls 五个固定中间件。对应的函数是:createTodoListMiddleware,createFilesystemMiddleware,createSubAgentMiddleware,createSummarizationMiddleware,patchToolCallsMiddleware,还有一个createSkillsMiddleware中间件。
Skill 是"说明书"而非工具,用 SKILL.md 沉淀领域经验与流程规范,按需加载省 token。
落地时两点最易踩坑:走兼容端点必须用 ChatOpenAI 实例写法;FilesystemBackend 要用 virtualMode 并配相对路径,否则文件会写到系统根目录。