ai agent --- DeepAgents 中间件
英文标题:ai agent --- DeepAgents 中间件
一.DeepAgents是什么? 1.概念 LangChain 家族有三个产品:LangChain 、LangGraph 、还有 DeepAgents 。 LangChain 是给你一堆 AI 开发积
30.1 KB
33.2 KB
52.4 KB
12.2 KB
18.2 KB
34.5 KB
20.9 KB
30.4 KB
77.2 KB
26.3 KB
10.3 KB
127.8 KB
19.6 KB
16.6 KB
5.6 KB
19.1 KB
12.3 KB
31.3 KB
中文
一.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.使用方式
安装资源包
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
import { TavilyClient } from "tavily";
import { createDeepAgent } from "deepagents";
import { initChatModel } from "@langchain/core";
const model = await initChatModel("openai:gpt-4o");
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 });
},
};
const agent = createDeepAgent({
model,
tools: [internetSearch],
systemPrompt: "你是一名研究员,请深入调研并撰写一份专业报告。",
});
const result = await agent.invoke({
messages: [
{
role: "user",
content: "LangGraph 是什么,它解决了什么问题?",
},
],
});
console.log(result.messages.at(-1)?.content);
二.langchain的中间件
1.中间件是什么?
中间件(
Middleware)就是一种"插队"机制:在主流程跑起来之前或之后,偷偷塞进一段你自己的逻辑。
langchain的中间件有以下6个。
createMiddleware({
beforeAgent: (state, runtime) => ...,
beforeModel: (state, runtime) => ...,
wrapModelCall: async (request, handler) => ...,
afterModel: (state, runtime) => ...,
wrapToolCall: async (request, handler) => ...,
afterAgent: (state, runtime) => ...,
})
2.介绍beforeAgent、beforeModel,afterModel、afterAgent
agent运行期间,中间件的调用时机如下:
他们四个就是在agent前后,model前后调用。它们可以是函数,也可以是对象。以beforeModel为例,查看他的值。
state是agent里面完整的状态,包含他自己的messages和中间件的stateSchema。runtime是只读的运行期上下文,canJumpTo是一个白名单数组,容许程序跳过某些节点
案例代码:
beforeModel: {
canJumpTo: ["end", "model", "tools"],
hook: (state, runtime) => {
if (...) {
return { messages: [...], jumpTo: "end" };
}
},
}
3.介绍wrapModelCall和wrapToolCall
3.1 wrapModelCall
wrapModelCall是模型的包裹层,它能同时干"调之前"和"调之后"两件事,而且能决定要不要调 。他和beforeModel、aftermodel之间的关系如下:
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参数供你操作。
wrapToolCall: async (request, handler) => {
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.中间件的调用案例
import "dotenv/config";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import {
createAgent,
createMiddleware,
HumanMessage,
AIMessage,
} from "langchain";
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`
);
},
});
const addContextMiddleware = createMiddleware({
name: "AddContextMiddleware",
wrapModelCall: async (request, handler) => {
console.log("[AddContext] 注入额外 system 上下文");
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")) {
console.log("[Blocked] 检测到 BLOCKED,短路结束");
return {
messages: [new AIMessage("该请求已被 middleware 拦截,无法处理。")],
jumpTo: "end",
};
}
},
},
});
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 的案例:
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({}),
});
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: [...], ...})。
const agent = createDeepAgent({
model,
tools: [internetSearch],
systemPrompt: "...",
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
createDeepAgent({
model,
todoList: { ... },
filesystem: { ... },
subagents: { ... },
summarization: { ... },
})
标准写法
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) => {
},
}),
],
systemPrompt: `...先拆任务,再派子 Agent,文件写入 /report.md...`,
});
4.使用方式--createAgent 创建agent
createAgent 本身来自 langchain 包,它的预构建中间件分两个来源:
langchain 包自带的 13 个,deepagents 包提供的 5 个
案例
import { createAgent } from "langchain";
import {
createFilesystemMiddleware,
CompositeBackend, StateBackend, StoreBackend,
} from "deepagents";
import { createAgent,
summarizationMiddleware,
contextEditingMiddleware,
piiRedactionMiddleware,
} 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() }
),
customToolDescriptions: {
ls: "用 ls 列出文件",
read_file: "用 read_file 读取文件",
},
tools: ["read_file", "ls", "glob", "grep"],
}),
],
});
案例:利用createFilesystemMiddleware对文件进行读写操作。你会发现我们之前的文件读写Tool写了很多代码,现在只用createFilesystemMiddleware的两行代码和一些配置,就实现了读写操作。
只要加上
deepagents这个FileSystem中间件,agent就有了一个文件系统,并且有了读写搜索文件的各种tool,还做了权限控制。
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文件,内容如下:
---
name: coffee
description: 按标准流程冲一杯咖啡
---
# 冲咖啡技能
## 何时使用
用户说"帮我冲杯咖啡"时触发。
## 执行步骤
1. 烧水(水温 92°C)
2. 取 15g 咖啡粉放入滤杯
3. 缓慢注水 30ml 闷蒸 30 秒
4. 分三次注水至总量 225ml
5. 完成,提醒用户趁热喝
4.1.官方推荐--createSkillsMiddleware
我们使用createSkillsMiddleware调用SKILL.md文件。
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会变成这样:
[系统消息] 你是助手。需要时用 use_skill 加载技能。
技能目录:
- coffee:按标准流程冲一杯咖啡
- pdf:提取 PDF 文本、合并拆分
- data-analysis:用 pandas 做数据清洗
- deploy:部署流程 SOP
[用户消息] 帮我冲杯咖啡 ← 模型看到这句才做匹配
4.2.手动读取skill.md
如果脱离createSkillsMiddleware你也可以实现,具体如下:
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}` },
{ role: "user", content: "帮我冲杯咖啡" },
]);
先要大模型读取具体的文件,然后再回答问题。
4.3.比较两个接入方式
手动读取skill.md其实和createSkillsMiddleware的实现思路是一样的。但是createSkillsMiddleware内部做了很多优化点,具体如下:
4.4 skill的组成部分
一个标准的
SKILL.md文件由两部分 构成:YAML 元数据头 (机器读)和 Markdown 正文 (模型读)。 元数据指的是name,description,keywords 这三个字段。除此之外都是正文。
一个标准的skill.md文件必然包含下面这些内容
---
name: skill-name
description: 一句话说明这个技能做什么
keywords: [...]
---
# 标题
## 何时使用
## 执行步骤
## 命令 / 代码模板
## 示例
## 注意事项
4.5 skill的运行流程
在项目里面skill一般会这样放置
.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国内网络不同导致下载不了,还有一个原因是这个命令有时间限制,超时就会报错。
解决办法:
执行下面的命令。
git clone https:
你会在你的项目下面看到一个文件夹:awesome-copilot,它里面的skills文件夹下面就装着很多skill。
找到awesome-copilot\skills\excalidraw-diagram-generator,复制粘贴到你项目的.agents\skills目录下面
此时你就可以测试这个skill了,这个skill的目的是要大模型生成一个图标,保存到当前目录下面的src/deepagents/output/deepagents-skills-flow.excalidraw里面
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) {
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实现,没有问题。
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "langchain";
import { createAgent } from "langchain";
import { createSubAgentMiddleware } from "deepagents";
import { z } from "zod";
const model = "openai:qwen3.7-plus";
const getWeather = tool(
async ({ city }) => `天气:${city} 晴,25°C,适合出行。`,
{
name: "get_weather",
description: "查询指定城市的天气。输入城市名,返回天气和气温。",
schema: z.object({ city: z.string().describe("城市名") }),
}
);
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"]),
}),
}
);
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() }),
}
);
const searchWeb = tool(
async ({ query }) => `关于「${query}」的搜索摘要:这是一段示例内容...`,
{
name: "search_web",
description: "搜索网页信息,返回相关摘要。",
schema: z.object({ query: z.string() }),
}
);
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],
};
const agent = createAgent({
model,
tools: [],
systemPrompt: [
"你是主管 agent。",
"遇到具体任务时,用 task 工具委派给合适的子 agent:",
"- weather:查天气",
"- calculator:做计算",
"- file_worker:读写文件",
"- web_researcher:搜索资料",
"拿到子 agent 的结果后,用自己的话总结给用户。",
].join("\n"),
middleware: [
createSubAgentMiddleware({
defaultModel: model,
defaultTools: [],
subagents: [weatherAgent, mathAgent, fileAgent, webAgent],
generalPurposeAgent: true,
}),
],
});
async function ask(question) {
console.log(`\n>>> ${question}\n`);
const result = await agent.invoke(
{ messages: [{ role: "user", content: question }] },
{ 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 || "[无文本输出]");
}
await ask("北京今天天气怎么样?顺便帮我计算下 56+12 是多少?再帮我搜索下 LangChain 最新版本的信息。 ");
process.exit(0);
测试如下:
把ask里面的invoke改成stream
async function ask(question) {
const stream = await agent.stream(
{ messages: [{ role: "user", content: question }] },
{ recursionLimit: 50, streamMode: "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的时候将中间件注入进去就好了。
import "dotenv/config";
import { tool } from "langchain";
import { createAgent } from "langchain";
import {
createMemoryMiddleware,
FilesystemBackend,
} from "deepagents";
import { z } from "zod";
import { mkdirSync, writeFileSync } from "node:fs";
mkdirSync(".deepagents", { recursive: true });
writeFileSync(
".deepagents/AGENTS.md",
[
"# 项目记忆",
"",
"## 用户偏好",
"- 用户姓名:张三",
"- 语言偏好:回复用中文",
"- 代码风格:变量用驼峰命名",
"",
"## 项目背景",
"- 这是一个天气查询机器人 demo",
"- 技术栈:Node.js + LangChain + deepagents",
].join("\n")
);
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 },
});
const getWeather = tool(
async ({ city }) => `${city}:晴,25°C`,
{
name: "get_weather",
description: "查询指定城市的天气",
schema: z.object({ city: z.string() }),
}
);
const memoryMiddleware = createMemoryMiddleware({
backend: new FilesystemBackend({ rootDir: process.cwd() }),
sources: ["./.deepagents/AGENTS.md"],
});
const agent = createAgent({
model,
tools: [getWeather],
systemPrompt: "你是助手。你拥有持久记忆,请充分利用它来回答。",
middleware: [memoryMiddleware],
});
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,你只需要通过提示词告诉大模型需要干嘛,他自己就会调用中间件去存取数据。
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文件才新建,然后在将系统提示词加上追加信息就好了。
具体实现代码
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 || "[无文本]"));
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 消耗、避免上下文溢出,同时保证核心对话语义不丢失。
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"]) }),
}
);
const summarizer = createSummarizationMiddleware({
model,
backend: new FilesystemBackend({ rootDir: process.cwd() }),
trigger: { type: "messages", value: 8 },
keep: { type: "messages", value: 4 },
historyPathPrefix: ".conversation_history",
});
const agent = createAgent({
model,
tools: [facts],
systemPrompt: "你是数据助手。被问到资料时务必调用 fetch_dataset 工具。",
middleware: [summarizer],
});
const questions = [
"北京的城市数据有哪些?",
"再查一次北京的数据。",
"换个主题,历史事件的资料给我。",
"北京数据再补充一些。",
"历史事件还有别的吗?",
"北京数据第6到12条是什么?",
];
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的顶层有个类型用来定义他的方法:
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处理。
const backend: BackendProtocol = ...;
backend = new FilesystemBackend({ rootDir: "/data" });
backend = new InMemoryBackend();
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 并配相对路径,否则文件会写到系统根目录。
English / 原文
一.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.使用方式
安装资源包
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
import { TavilyClient } from "tavily";
import { createDeepAgent } from "deepagents";
import { initChatModel } from "@langchain/core";
const model = await initChatModel("openai:gpt-4o");
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 });
},
};
const agent = createDeepAgent({
model,
tools: [internetSearch],
systemPrompt: "你是一名研究员,请深入调研并撰写一份专业报告。",
});
const result = await agent.invoke({
messages: [
{
role: "user",
content: "LangGraph 是什么,它解决了什么问题?",
},
],
});
console.log(result.messages.at(-1)?.content);
二.langchain的中间件
1.中间件是什么?
中间件(
Middleware)就是一种"插队"机制:在主流程跑起来之前或之后,偷偷塞进一段你自己的逻辑。
langchain的中间件有以下6个。
createMiddleware({
beforeAgent: (state, runtime) => ...,
beforeModel: (state, runtime) => ...,
wrapModelCall: async (request, handler) => ...,
afterModel: (state, runtime) => ...,
wrapToolCall: async (request, handler) => ...,
afterAgent: (state, runtime) => ...,
})
2.介绍beforeAgent、beforeModel,afterModel、afterAgent
agent运行期间,中间件的调用时机如下:
他们四个就是在agent前后,model前后调用。它们可以是函数,也可以是对象。以beforeModel为例,查看他的值。
state是agent里面完整的状态,包含他自己的messages和中间件的stateSchema。runtime是只读的运行期上下文,canJumpTo是一个白名单数组,容许程序跳过某些节点
案例代码:
beforeModel: {
canJumpTo: ["end", "model", "tools"],
hook: (state, runtime) => {
if (...) {
return { messages: [...], jumpTo: "end" };
}
},
}
3.介绍wrapModelCall和wrapToolCall
3.1 wrapModelCall
wrapModelCall是模型的包裹层,它能同时干"调之前"和"调之后"两件事,而且能决定要不要调 。他和beforeModel、aftermodel之间的关系如下:
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参数供你操作。
wrapToolCall: async (request, handler) => {
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.中间件的调用案例
import "dotenv/config";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import {
createAgent,
createMiddleware,
HumanMessage,
AIMessage,
} from "langchain";
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`
);
},
});
const addContextMiddleware = createMiddleware({
name: "AddContextMiddleware",
wrapModelCall: async (request, handler) => {
console.log("[AddContext] 注入额外 system 上下文");
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")) {
console.log("[Blocked] 检测到 BLOCKED,短路结束");
return {
messages: [new AIMessage("该请求已被 middleware 拦截,无法处理。")],
jumpTo: "end",
};
}
},
},
});
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 的案例:
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({}),
});
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: [...], ...})。
const agent = createDeepAgent({
model,
tools: [internetSearch],
systemPrompt: "...",
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
createDeepAgent({
model,
todoList: { ... },
filesystem: { ... },
subagents: { ... },
summarization: { ... },
})
标准写法
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) => {
},
}),
],
systemPrompt: `...先拆任务,再派子 Agent,文件写入 /report.md...`,
});
4.使用方式--createAgent 创建agent
createAgent 本身来自 langchain 包,它的预构建中间件分两个来源:
langchain 包自带的 13 个,deepagents 包提供的 5 个
案例
import { createAgent } from "langchain";
import {
createFilesystemMiddleware,
CompositeBackend, StateBackend, StoreBackend,
} from "deepagents";
import { createAgent,
summarizationMiddleware,
contextEditingMiddleware,
piiRedactionMiddleware,
} 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() }
),
customToolDescriptions: {
ls: "用 ls 列出文件",
read_file: "用 read_file 读取文件",
},
tools: ["read_file", "ls", "glob", "grep"],
}),
],
});
案例:利用createFilesystemMiddleware对文件进行读写操作。你会发现我们之前的文件读写Tool写了很多代码,现在只用createFilesystemMiddleware的两行代码和一些配置,就实现了读写操作。
只要加上
deepagents这个FileSystem中间件,agent就有了一个文件系统,并且有了读写搜索文件的各种tool,还做了权限控制。
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文件,内容如下:
---
name: coffee
description: 按标准流程冲一杯咖啡
---
# 冲咖啡技能
## 何时使用
用户说"帮我冲杯咖啡"时触发。
## 执行步骤
1. 烧水(水温 92°C)
2. 取 15g 咖啡粉放入滤杯
3. 缓慢注水 30ml 闷蒸 30 秒
4. 分三次注水至总量 225ml
5. 完成,提醒用户趁热喝
4.1.官方推荐--createSkillsMiddleware
我们使用createSkillsMiddleware调用SKILL.md文件。
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会变成这样:
[系统消息] 你是助手。需要时用 use_skill 加载技能。
技能目录:
- coffee:按标准流程冲一杯咖啡
- pdf:提取 PDF 文本、合并拆分
- data-analysis:用 pandas 做数据清洗
- deploy:部署流程 SOP
[用户消息] 帮我冲杯咖啡 ← 模型看到这句才做匹配
4.2.手动读取skill.md
如果脱离createSkillsMiddleware你也可以实现,具体如下:
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}` },
{ role: "user", content: "帮我冲杯咖啡" },
]);
先要大模型读取具体的文件,然后再回答问题。
4.3.比较两个接入方式
手动读取skill.md其实和createSkillsMiddleware的实现思路是一样的。但是createSkillsMiddleware内部做了很多优化点,具体如下:
4.4 skill的组成部分
一个标准的
SKILL.md文件由两部分 构成:YAML 元数据头 (机器读)和 Markdown 正文 (模型读)。 元数据指的是name,description,keywords 这三个字段。除此之外都是正文。
一个标准的skill.md文件必然包含下面这些内容
---
name: skill-name
description: 一句话说明这个技能做什么
keywords: [...]
---
# 标题
## 何时使用
## 执行步骤
## 命令 / 代码模板
## 示例
## 注意事项
4.5 skill的运行流程
在项目里面skill一般会这样放置
.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国内网络不同导致下载不了,还有一个原因是这个命令有时间限制,超时就会报错。
解决办法:
执行下面的命令。
git clone https:
你会在你的项目下面看到一个文件夹:awesome-copilot,它里面的skills文件夹下面就装着很多skill。
找到awesome-copilot\skills\excalidraw-diagram-generator,复制粘贴到你项目的.agents\skills目录下面
此时你就可以测试这个skill了,这个skill的目的是要大模型生成一个图标,保存到当前目录下面的src/deepagents/output/deepagents-skills-flow.excalidraw里面
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) {
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实现,没有问题。
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "langchain";
import { createAgent } from "langchain";
import { createSubAgentMiddleware } from "deepagents";
import { z } from "zod";
const model = "openai:qwen3.7-plus";
const getWeather = tool(
async ({ city }) => `天气:${city} 晴,25°C,适合出行。`,
{
name: "get_weather",
description: "查询指定城市的天气。输入城市名,返回天气和气温。",
schema: z.object({ city: z.string().describe("城市名") }),
}
);
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"]),
}),
}
);
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() }),
}
);
const searchWeb = tool(
async ({ query }) => `关于「${query}」的搜索摘要:这是一段示例内容...`,
{
name: "search_web",
description: "搜索网页信息,返回相关摘要。",
schema: z.object({ query: z.string() }),
}
);
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],
};
const agent = createAgent({
model,
tools: [],
systemPrompt: [
"你是主管 agent。",
"遇到具体任务时,用 task 工具委派给合适的子 agent:",
"- weather:查天气",
"- calculator:做计算",
"- file_worker:读写文件",
"- web_researcher:搜索资料",
"拿到子 agent 的结果后,用自己的话总结给用户。",
].join("\n"),
middleware: [
createSubAgentMiddleware({
defaultModel: model,
defaultTools: [],
subagents: [weatherAgent, mathAgent, fileAgent, webAgent],
generalPurposeAgent: true,
}),
],
});
async function ask(question) {
console.log(`\n>>> ${question}\n`);
const result = await agent.invoke(
{ messages: [{ role: "user", content: question }] },
{ 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 || "[无文本输出]");
}
await ask("北京今天天气怎么样?顺便帮我计算下 56+12 是多少?再帮我搜索下 LangChain 最新版本的信息。 ");
process.exit(0);
测试如下:
把ask里面的invoke改成stream
async function ask(question) {
const stream = await agent.stream(
{ messages: [{ role: "user", content: question }] },
{ recursionLimit: 50, streamMode: "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的时候将中间件注入进去就好了。
import "dotenv/config";
import { tool } from "langchain";
import { createAgent } from "langchain";
import {
createMemoryMiddleware,
FilesystemBackend,
} from "deepagents";
import { z } from "zod";
import { mkdirSync, writeFileSync } from "node:fs";
mkdirSync(".deepagents", { recursive: true });
writeFileSync(
".deepagents/AGENTS.md",
[
"# 项目记忆",
"",
"## 用户偏好",
"- 用户姓名:张三",
"- 语言偏好:回复用中文",
"- 代码风格:变量用驼峰命名",
"",
"## 项目背景",
"- 这是一个天气查询机器人 demo",
"- 技术栈:Node.js + LangChain + deepagents",
].join("\n")
);
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 },
});
const getWeather = tool(
async ({ city }) => `${city}:晴,25°C`,
{
name: "get_weather",
description: "查询指定城市的天气",
schema: z.object({ city: z.string() }),
}
);
const memoryMiddleware = createMemoryMiddleware({
backend: new FilesystemBackend({ rootDir: process.cwd() }),
sources: ["./.deepagents/AGENTS.md"],
});
const agent = createAgent({
model,
tools: [getWeather],
systemPrompt: "你是助手。你拥有持久记忆,请充分利用它来回答。",
middleware: [memoryMiddleware],
});
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,你只需要通过提示词告诉大模型需要干嘛,他自己就会调用中间件去存取数据。
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文件才新建,然后在将系统提示词加上追加信息就好了。
具体实现代码
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 || "[无文本]"));
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 消耗、避免上下文溢出,同时保证核心对话语义不丢失。
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"]) }),
}
);
const summarizer = createSummarizationMiddleware({
model,
backend: new FilesystemBackend({ rootDir: process.cwd() }),
trigger: { type: "messages", value: 8 },
keep: { type: "messages", value: 4 },
historyPathPrefix: ".conversation_history",
});
const agent = createAgent({
model,
tools: [facts],
systemPrompt: "你是数据助手。被问到资料时务必调用 fetch_dataset 工具。",
middleware: [summarizer],
});
const questions = [
"北京的城市数据有哪些?",
"再查一次北京的数据。",
"换个主题,历史事件的资料给我。",
"北京数据再补充一些。",
"历史事件还有别的吗?",
"北京数据第6到12条是什么?",
];
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的顶层有个类型用来定义他的方法:
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处理。
const backend: BackendProtocol = ...;
backend = new FilesystemBackend({ rootDir: "/data" });
backend = new InMemoryBackend();
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 并配相对路径,否则文件会写到系统根目录。