ai agent --- DeepAgents 中间件

0 阅读20分钟

一.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 就是把这四件套打包好、开源出来。

image.png

典型适用场景:深度调研/竞品分析出报告、复杂代码任务(写代码+跑测试)、多步骤数据处理、需要自主决策的长流程自动化。

3.使用方式

安装资源包

npm init -y
npm install @langchain/core @langchain/openai @langchain/langgraph deepagents tavily

tavily是专门为 AI Agent 打造的搜索 API——它不是给人用的搜索引擎,而是给"程序"用的,目标是让 Agent 能像人一样联网查资料。

tavily的核心能力

image.png

tavily就是一个搜索网页的中间件,以前我们用博查的apikey做过一个检索网站的tool,现在我们用tavily。

看一下两者之间的区别:

image.png

普通搜索 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 就能接入。差别在于你得多干多少活:

博查只给"搜索结果列表",你还得自己做这些:

  1. 正文提取:拿到 URL 后自己爬页面、处理反爬、解析 HTML、提取正文——这一步博查不管
  2. 内容清洗:去掉广告、导航、脚本、无关区块,只留正文
  3. 相关性过滤:博查的排序是按通用搜索相关性,未必符合 LLM 的需求,可能要二次筛选
  4. 结构化输出:把清洗后的内容组织成 LLM 友好的格式(Token 控制、截断、去重)
  5. 错误处理:超时、限流、被反爬、编码问题,全部自己兜底
  6. 生态适配: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";

// 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个。

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运行期间,中间件的调用时机如下:

image.png

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

image.png

  • state是agent里面完整的状态,包含他自己的messages和中间件的stateSchema。
  • runtime是只读的运行期上下文,
  • canJumpTo 是一个白名单数组,容许程序跳过某些节点

案例代码:

beforeModel: {
  canJumpTo: ["end", "model", "tools"],   // 声明:本钩子允许跳到哪些节点
  hook: (state, runtime) => {
    if (...) {
      return { messages: [...], jumpTo: "end" };
    }
    // 不返回,或返回 undefined → 正常继续
  },
}

3.介绍wrapModelCall和wrapToolCall

3.1 wrapModelCall

wrapModelCall是模型的包裹层,它能同时干"调之前"和"调之后"两件事,而且能决定要不要调。他和beforeModel、aftermodel之间的关系如下:

beforeModel  ──────────┐
                       │
  wrapModelCall 开始 ──┐│
                     │││   ← 在这里改 request(追加 system 指令)
    handler() ──┐    │││
                ↓    │││
          【真正调模型】 │││   ← 大模型 API 在这一行被调用
                ↑    │││
    拿到结果 ←─┘    │││   ← 在这里改 result(改写模型输出)
  wrapModelCall 结束 ──┘│
                       │
afterModel  ───────────┘

他们的维度对比如下:

image.png

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

使用时机:

image.png

3.2 wrapToolCall

wrapToolCall 是 TOOL 的包裹层。它把模型调用工具,产出 tool_calls的过程包裹起来。模型调用了几次工具,wrapToolCall就会被调用几次。

如果你想修改Tool 的request,那么就在这个时候去做。此时的requst里面还有request.toolCall.args参数供你操作。

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,他们的执行顺序和添加中间件的顺序一样。

中间件的执行流程如下:

image.png

5.中间件的调用案例

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 的案例:

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: [...], ...})。

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.中间件汇总

Sheet_20260926 (1).png

以上是一个中间件,并不是全部都安装,无脑安装的只有5个,TodoListMiddleware、FilesystemMiddleware、SubAgentMiddleware、SummarizationMiddleware、PatchToolCallsMiddleware,这五个中间件,只要调 createDeepAgent 就一定有的。

还有四个是有了条件才会有的。比如传了 memory 参数才有MemoryMiddleware,传了 skills 参数才有SkillsMiddleware,传了 interruptOn 参数才有HumanInTheLoopMiddleware,配了异步子 Agent 才有AsyncSubAgentMiddleware

自动判断的有2个,用 Anthropic 模型才挂载AnthropicPromptCachingMiddleware,用 AWS Bedrock 才挂载BedrockPromptCachingMiddleware。这 2 个框架自己看情况决定,互斥——用 Anthropic 就挂第一个,用 Bedrock 就挂第二个,两个不会同时出现。

image.png

3.使用方式--createDeepAgent创建agent

createDeepAgent({
  model,
  todoList:       { ... },   // 改 TodoListMiddleware
  filesystem:     { ... },   // 改 FilesystemMiddleware
  subagents:      { ... },   // 改 SubAgentMiddleware
  summarization:  { ... },   // 改 SummarizationMiddleware
  // PatchToolCallsMiddleware —— 无配置项,全自动
})

标准写法

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 包,它的预构建中间件分两个来源:

image.png

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

image.png

案例

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,还做了权限控制。

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排查路线

image.png

四.skill是什么?

1.概念

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

image.png

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.pySkill 可打包脚本和模板,形成可复用的完整方案

4.skill的接入方式

createSkillsMiddleware 是skill官方推荐的接入方式。是 DeepAgent的一个工具函数。

skill的接入方式有2种,1是用官方推荐方式,2是用手动文件读取方式。

项目目录:

image.png

在.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}` },  // ← 直接塞进 prompt
  { role: "user", content: "帮我冲杯咖啡" },
]);

先要大模型读取具体的文件,然后再回答问题。

4.3.比较两个接入方式

手动读取skill.md其实和createSkillsMiddleware的实现思路是一样的。但是createSkillsMiddleware内部做了很多优化点,具体如下:

image.png

4.4 skill的组成部分

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

image.png

一个标准的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 如图所示:

image.png

SkillsMiddleware的运行流程图

image.png

使用这套机制的好处是:

好处属于谁理由
① 省 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.进入网页,搜索

image.png

image.png

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

image.png

4.7.3.在项目里面运行命令

image.png

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

image.png

解决办法:

执行下面的命令。

 git clone https://github.com/github/awesome-copilot.git

你会在你的项目下面看到一个文件夹:awesome-copilot,它里面的skills文件夹下面就装着很多skill。 image.png

找到awesome-copilot\skills\excalidraw-diagram-generator,复制粘贴到你项目的.agents\skills目录下面

image.png

此时你就可以测试这个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) {
    // 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();

测试

image.png

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

image.png

总结:

在上面的代码里面我们用了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实现,没有问题。

// 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);

测试如下:

image.png

把ask里面的invoke改成stream

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的时候将中间件注入进去就好了。

image.png

// 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);

测试

image.png

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);

image.png

读写保存历史记忆

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

image.png

image.png

具体实现代码

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);

测试

红框里面是老的,后面是追加的。

image.png

3.SummarizationMiddleware

我们用做了一个读取超大文本的tool,还用createSummarizationMiddleware中间件。

SummarizationMiddleware 中间件的作用是:如果当前对话上下文长度超过预设阈值,就自动对历史对话进行摘要压缩,剔除冗余信息,只保留关键上下文摘要,再传入大模型进行后续续写 / 问答。

这样做的好处是:可以控制 Token 消耗、避免上下文溢出,同时保证核心对话语义不丢失。

// 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);

测试

image.png

4.FilesystemBackend读写文件

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

image.png

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的对比

image.png

FilesystemBackend 内部就是用 fs 实现的这个接口。你完全可以把它理解成:FilesystemBackend = 一个用 fs 包装出来的、符合 BackendProtocol 规范的适配器。

不管我的数据是从内存来的,还是磁盘来的,甚至是云端来的,都可以用createFilesystemMiddleware处理。

// 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 并配相对路径,否则文件会写到系统根目录。