DeepAgents 实战:用多 Agent 架构搭一个深度调研助手

前面几篇我们陆续看了 DeepAgents 里的各种能力:Todo List 可以让 Agent 先规划再执行,Summarization 可以在上下文变长时自动压缩,Tool Selector 可以在工具很多时帮模型做筛选,FileSystem 又能把中间结果沉淀到文件里。

这些能力单独看都不难理解,但真正做一个能落地的 Agent 项目时,很快就会遇到一个问题:如果每个能力都靠自己手动拼 middleware、写 prompt、接工具、管文件,代码会变得很散,主 Agent 也很容易什么都亲自干,最后既不可控,也不好调试。

那有没有一个更完整的 API,把常用的多 Agent、skill、memory、filesystem、todo、上下文管理这些能力都整合起来?

有,就是 DeepAgents 提供的 createDeepAgent

本期我们基于 DeepAgents 实现一个多 Agent 架构的"深度调研助手"。用户只需要输入一个调研主题,主 Agent 会自动规划任务、写 todo 列表、委派不同子 Agent 执行联网搜索和数据计算,最后生成一份中文调研报告。

sequenceDiagram participant User as 用户 participant Main as 主 Agent participant Researcher as researcher participant Analyst as analyst participant Editor as editor participant FS as workspace 文件系统 User->>Main: 提交调研主题 Main->>FS: 写入 question.txt 和 research_plan.md Main->>Researcher: 委派子主题调研 Researcher->>FS: 写入 findings_*.md Main->>Analyst: 委派数值分析 Analyst->>FS: 写入 analysis_*.md Main->>FS: 读取 findings 与 analysis Main->>FS: 写入 draft_*.md Main->>Editor: 委派审阅草稿 Editor-->>Main: 返回修改建议 Main->>FS: 修订并写入 report_*.md Main-->>User: 返回报告路径、核心发现、局限

一、为什么需要多 Agent 深度调研助手?

大家平时让大模型做"深度调研"时,通常会遇到几个明显的问题。

第一个问题是任务太长。比如让 Agent 调研一个行业、一个框架,或者一组经济数据,它不能只搜索一次就下结论,而是要先拆解问题,再分别查资料、交叉验证、整理来源,最后形成结构化报告。

第二个问题是能力太杂。调研需要联网搜索,数据分析需要计算,报告输出需要写作,定稿前还需要审阅。如果全部交给一个 Agent 在同一个上下文里完成,它既要搜索,又要算数,又要写报告,还要自己审自己,很容易出现任务漂移。

第三个问题是中间结果不应该只留在对话历史里。复杂任务往往会生成调研计划、搜索结果、分析结果、报告草稿、最终报告。如果这些内容只堆在 messages 里,后续步骤会越来越难读,也更容易被上下文压缩影响。

所以,一个更稳的做法是:让主 Agent 只负责规划和编排,把专业工作交给不同子 Agent,并通过文件系统传递中间结果。

这个项目里,我们把调研助手拆成 4 个角色:

  • 主 Agent:整个系统的编排中心,负责把用户输入拆成可执行流程,并按"规划 → 调研 → 分析 → 起草 → 审阅 → 定稿"推进任务。
  • 调研员子 Agent(researcher) :每次只负责一个聚焦子主题,使用联网搜索收集资料,并把结果写入 findings_*.md
  • 分析师子 Agent(analyst):当任务涉及数字计算、排名、增长率、占比时启用,通过 QuickJS 沙箱执行 JavaScript,避免模型凭感觉算数。
  • 编辑子 Agent(editor):报告草稿完成后介入,只做审阅和反馈,不直接改写报告,保证"写作"和"审稿"职责分离。

这种架构的核心价值,是把复杂任务拆成多个职责清晰的小任务。主 Agent 不需要亲自完成所有细节,而是像项目经理一样协调资源;子 Agent 也不需要理解全局,只要把自己负责的部分做好,并把结果写入约定文件。

二、整体架构:主 Agent 编排,子 Agent 分工

项目目录如下:

text 复制代码
deep-research-assistant
├── AGENTS.md
├── package.json
├── skills
│   ├── report-writer
│   │   └── SKILL.md
│   └── web-research
│       └── SKILL.md
├── src
│   ├── agent.mjs
│   ├── cli.mjs
│   ├── max-input-tokens-test.mjs
│   ├── todo-middleware-test.mjs
│   └── tools
│       └── search.mjs
└── workspace
    ├── reports
    └── sources

这里最重要的是 4 类文件。

src/agent.mjs 是 Agent 的核心定义,主 Agent、子 Agent、模型、文件系统、skills、memory 都在这里组装。

src/cli.mjs 是命令行入口,负责读取用户输入、启动 Agent stream,并把关键步骤打印出来,方便我们观察主 Agent 和子 Agent 的运行过程。

skills/ 目录放的是技能说明。它不是子 Agent,而是主 Agent 在执行某类任务时要遵循的流程指南。比如 web-research 告诉主 Agent 怎么做联网调研,report-writer 告诉主 Agent 怎么把 findings 写成报告。

workspace/ 是任务工作区。调研计划、搜索结果、分析结果、草稿、终稿都写到这里。这样每个 Agent 之间不是靠"记住对话"协作,而是靠文件协作。

三、快速上手:创建项目并安装依赖

先创建项目:

bash 复制代码
mkdir deep-research-assistant
cd deep-research-assistant
npm init -y

安装依赖:

bash 复制代码
pnpm install @langchain/core @langchain/langgraph @langchain/openai @langchain/quickjs dedent deepagents dotenv langchain zod

这个项目里用到的关键依赖主要有这些:

  • deepagents:提供 createDeepAgentFilesystemBackend 等核心能力。
  • @langchain/openai:用 OpenAI 兼容接口接入大模型。
  • @langchain/quickjs:给分析师子 Agent 提供 eval 代码执行工具。
  • langchain:提供 tool、createAgent、todoListMiddleware 等基础能力。
  • zod:定义工具入参 schema。
  • dedent:让多行 prompt 在代码里保持可读缩进,同时去掉多余空白。

然后配置 .env

env 复制代码
OPENAI_API_KEY=sk-xx
OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
OPENAI_MODEL="qwen-plus"

# 用于身份验证,实现链路上报
LANGCHAIN_API_KEY=xxx
# 指定 LangSmith 中的项目,追踪结果会归类到该项目下
LANGCHAIN_PROJECT=deep-research-assistant
# 开启 LangSmith 追踪功能
LANGCHAIN_TRACING_V2=true

BOCHA_API_KEY=sk-xx

这里使用的是 OpenAI 兼容接口,所以只要服务商兼容 OpenAI SDK,就可以通过 OPENAI_BASE_URLOPENAI_MODEL 切换模型。

联网搜索使用 Bocha API,因此还需要配置 BOCHA_API_KEY。LangSmith 不是必须的,但强烈建议开启,因为多 Agent 调用时,如果没有 trace,很难看清模型到底调用了哪些工具、哪个子 Agent 写了哪些文件。

四、联网搜索工具:把搜索能力包装成 tool

先看 src/tools/search.mjs

js 复制代码
import { tool } from "langchain"
import { z } from "zod"

const BOCHA_API_URL = "https://api.bochaai.com/v1/web-search"

function formatWebPages(webpages) {
  return webpages
    .map(
      (page, idx) =>
        `引用: ${idx + 1}
标题: ${page.name ?? ""}
URL: ${page.url ?? ""}
摘要: ${page.summary ?? ""}
网站名称: ${page.siteName ?? ""}
网站图标: ${page.siteIcon ?? ""}
发布时间: ${page.dateLastCrawled ?? ""}`
    )
    .join("\n\n")
}

这里先把 Bocha 返回的网页结果格式化成适合模型阅读的文本。每条结果都保留标题、URL、摘要、网站名称和发布时间。对于调研类 Agent 来说,URL 很重要,因为最后报告里必须能追溯来源。

真正调用 API 的函数如下:

js 复制代码
async function bochaWebSearch(query, count) {
  const apiKey = process.env.BOCHA_API_KEY?.trim()
  if (!apiKey) {
    return "Bocha 联网搜索的 API Key 未配置(环境变量 BOCHA_API_KEY),请先在 .env 中配置后再重试。"
  }

  const response = await fetch(BOCHA_API_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      query,
      freshness: "noLimit",
      summary: true,
      count,
    }),
  })

  if (!response.ok) {
    const errorText = await response.text()
    return `搜索 API 请求失败,状态码: ${response.status},错误信息: ${errorText}`
  }

  let json
  try {
    json = await response.json()
  } catch (e) {
    return `搜索 API 请求失败,原因是:搜索结果解析失败 ${e.message}`
  }

  try {
    if (json.code !== 200 || !json.data) {
      return `搜索 API 请求失败,原因是: ${json.msg ?? "未知错误"}`
    }

    const webpages = json.data.webPages?.value ?? []
    if (!webpages.length) {
      return `未找到与「${query}」相关的结果。`
    }

    return formatWebPages(webpages)
  } catch (e) {
    return `搜索 API 请求失败,原因是:搜索结果解析失败 ${e.message}`
  }
}

这段代码没有直接抛错,而是把错误转换成中文文本返回给模型。这样做的好处是:工具失败时,Agent 仍然能读懂失败原因,并决定是换关键词、减少结果数量,还是直接把失败原因反馈出来。

最后用 tool 包装成 LangChain 工具:

js 复制代码
export const webSearch = tool(
  async input => {
    const count = input.count ?? 10
    console.log(`  🔎 搜索: ${input.query}(${count} 条)`)
    return bochaWebSearch(input.query, count)
  },
  {
    name: "web_search",
    description:
      "使用 Bocha 联网搜索 API 检索互联网网页。输入中文或中英结合的搜索关键词,可选 count 指定结果数量。返回标题、URL、摘要、网站名称、图标和发布时间。",
    schema: z.object({
      query: z
        .string()
        .min(1)
        .describe(
          "搜索关键词,优先使用中文,例如:2026年 AI Agent 框架对比、LangGraph 最新动态"
        ),
      count: z
        .number()
        .int()
        .min(1)
        .max(20)
        .optional()
        .describe("返回的搜索结果数量,默认 10 条"),
    }),
  }
)

工具描述要写得具体,因为模型会根据 namedescriptionschema 判断什么时候调用工具、传什么参数。这里还特别强调"优先使用中文关键词",因为我们的调研目标主要是中文资料源。

五、子 Agent 设计:每个 Agent 只做一类事

接下来进入 src/agent.mjs。先准备项目路径和依赖:

js 复制代码
import path from "node:path"
import { fileURLToPath } from "node:url"
import dedent from "dedent"
import { ChatOpenAI } from "@langchain/openai"
import { createCodeInterpreterMiddleware } from "@langchain/quickjs"
import { createDeepAgent, FilesystemBackend } from "deepagents"

import { webSearch } from "./tools/search.mjs"

const projectDir = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  ".."
)

这里 projectDir 指向项目根目录,后面 FilesystemBackend 会用它作为实际文件系统根目录。

5.1 researcher:只调研一个子主题

调研员子 Agent 的定义如下:

js 复制代码
const researcherSubAgent = {
  name: "researcher",
  description:
    "通过联网搜索调研单一子主题。每次只分配一个子主题;多个独立子主题可并行启动多个调研员。",
  systemPrompt: dedent`
    你是一名专业调研员,负责调研**一个**分配给你的子主题,并写入**一份**调研结果文件。

    ## 工作流程(严格遵守,禁止空转循环)

    1. **可选**:用 write_todos 列出最多 3 条中文执行步骤(例如「搜索官方文档」「搜索社区评价」「整理并写入 findings」),然后按步骤执行
    2. 最多调用 3 次 web_search(硬性上限,绝不超过)
    3. 将搜索结果整理为结构化摘要,包含关键事实与来源 URL
    4. 调用 write_file **一次**,保存到任务指定的路径(必须在 /workspace/sources/findings_*.md 下,禁止写到其他目录)
    5. 用一句话确认已完成,然后**立即停止**,不要再搜索、写文件或更新 todo

    ## write_todos 使用规则(若使用)

    - 最多 3 条,每条 content 必须用中文
    - 仅用于拆解本子的调研步骤,不要重复主 Agent 已完成的总体规划
    - 最后一条 todo 必须是「写入 findings 文件」;该步骤完成后将所有 todo 标为 completed 并结束

    ## 其他规则

    - 不要重复相同的搜索关键词
    - write_file 完成后禁止再次搜索------你的任务已结束
    - 其他人只能看到你写入的文件,内容必须完整、自洽
    - **所有输出必须使用中文**(专有名词如 LangGraph 可保留英文)
    - 搜索关键词优先使用中文;若主题本身是英文专有名词,可中英结合
  `,
  tools: [webSearch],
}

这里有几个细节很关键。

第一,调研员每次只做一个子主题。这样主 Agent 可以把大问题拆成多个互不重叠的小问题,并行启动多个 researcher。

第二,搜索次数要限制。Agent 如果没有边界,很容易"再搜一下""再确认一下",最后陷入空转循环。所以这里明确最多调用 3 次 web_search

第三,调研员必须把结果写入 /workspace/sources/findings_*.md。这意味着主 Agent 后续写报告时,不需要从子 Agent 的对话历史里找信息,只要读取文件即可。

5.2 analyst:所有计算都交给代码执行

分析师子 Agent 的定义如下:

js 复制代码
const analystSubAgent = {
  name: "analyst",
  description:
    "使用 eval REPL 进行数值计算与结构化数据分析。适用于计算、排名、同比对比或 JSON/CSV 分析。",
  systemPrompt: dedent`
    你是一名数据分析师,所有计算必须通过 eval REPL 完成------**禁止**猜测数字。

    ## 工作流程

    1. 从 /workspace/sources/ 读取数据文件(或从调研结果中提取数字)
    2. 在 REPL 中编写并运行 JavaScript,计算总和、均值、排名、增长率等
    3. 将分析结果保存到 /workspace/sources/analysis_*.md,包含计算逻辑与结论

    必须展示计算过程,结论可从 REPL 输出复现。所有输出使用中文。
  `,
  middleware: [createCodeInterpreterMiddleware()],
}

大模型最不适合直接做精确计算。比如 GDP 占比、同比增速排名、总和、平均值,看起来都很简单,但只要数字多一点,模型就可能算错。

所以这里给 analyst 加了 createCodeInterpreterMiddleware()。这个 middleware 会提供 eval 工具,让模型生成 JavaScript 代码,并在 QuickJS 沙箱里执行。这样数字结论不是模型"想出来"的,而是代码算出来的。

这个设计在数据调研里非常有用。主 Agent 只需要判断"这个任务涉及计算",然后委派 analyst;analyst 负责读取数据、执行代码、写分析结果。

5.3 editor:只审阅,不改写

编辑子 Agent 的定义如下:

js 复制代码
const editorSubAgent = {
  name: "editor",
  description:
    "审阅报告草稿的准确性、结构与完整性。在 /workspace/reports/draft_*.md 写好后使用。",
  systemPrompt: dedent`
    你是一名资深情报编辑,负责**审阅**报告草稿------**不要**亲自改写报告。

    ## 阅读材料

    - 原始问题:/workspace/sources/question.txt
    - 待审草稿:任务中指定的路径
    - 支撑材料:/workspace/sources/ 下的调研文件(如需要)

    ## 审阅要点

    - 报告是否直接回答了原始问题?
    - 章节结构是否清晰,段落是否充实(而非只有 bullet 列表)?
    - 是否引用了来源,并在「参考资料」章节列出?
    - 是否有遗漏、无依据的断言或缺失的视角?
    - 语言是否为中文,表述是否专业?

    ## 输出

    返回简洁的审阅意见和具体、可操作的修改建议。
    **不要**写入报告文件,只提供反馈。所有输出使用中文。
  `,
}

这里故意让 editor "不要亲自改写报告"。原因很简单:报告的整体结构和最终表达应该由主 Agent 统一控制,编辑只负责提出问题和修改建议。

这相当于把"写"和"审"分开。主 Agent 写草稿,editor 负责挑问题,主 Agent 根据反馈修订一次并保存终稿。这样流程更接近真实写作协作,也更容易调试。

六、主 Agent:把流程写进 orchestratorPrompt

子 Agent 负责专业任务,主 Agent 负责流程编排。这个流程主要写在 orchestratorPrompt 里。

js 复制代码
const orchestratorPrompt = dedent`
  你是「深度调研助手」的主 Agent,负责协调调研、分析与编辑,产出高质量调研简报。

  ## 语言要求

  - **所有输出必须使用中文**:对话回复、write_todos 任务列表、文件内容、搜索关键词
  - write_todos 中每条 todo 的 content 必须用中文描述,例如「撰写调研计划」「委派调研员调研 LangGraph」
  - 搜索时优先使用中文关键词;英文专有名词(如 LangGraph、AutoGen)可保留
  - 报告、调研笔记、计划文件全部用中文撰写

  ## 你的职责

  协调调研员、分析师和编辑完成报告。不要亲自完成所有调研------将专业工作委派给子 Agent。

  ## 标准流程

  1. **规划** --- 用 write_todos 拆解任务(中文)。将用户问题保存到 /workspace/sources/question.txt
  2. **调研** --- 按 web-research 技能:写 research_plan.md,委派调研员子 Agent(可并行)
  3. **分析** --- 若涉及数字对比或数据表,委派分析师子 Agent
  4. **起草** --- **由你亲自**按 report-writer 技能撰写,用 write_file 写入 /workspace/reports/draft_[主题].md
  5. **审阅** --- 委派编辑子 Agent 审稿,根据反馈修订一次
  6. **定稿** --- 保存最终报告到 /workspace/reports/report_[主题]_[日期].md
`

这个 prompt 不是简单告诉模型"你要做调研",而是把工作流拆成明确阶段。复杂 Agent 的 prompt 一定要写流程,否则模型很容易想到哪做到哪。

后面还要明确哪些是子 Agent,哪些是 skill:

js 复制代码
  ## task 工具(子 Agent 委派)

  **仅**以下 subagent_type 合法:researcher、analyst、editor、general-purpose。

  - web-research、report-writer 是**技能**(写作指南),**不是**子 Agent,禁止作为 subagent_type 调用
  - 报告起草、修订、定稿由**主 Agent 自己**用 write_file / edit_file 完成,不要委派 task

这一段非常重要。

DeepAgents 里既有 subagents,也有 skills。它们都能影响 Agent 行为,但不是一回事。

subagents 是可以通过 task 工具委派的执行角色,比如 researcheranalysteditor

skills 是能力说明文档,主 Agent 会读取它们来知道"某类任务应该怎么做",但不能把 skill 当成 subagent_type

如果不在 prompt 里说清楚,模型可能会尝试调用一个不存在的 web-research 子 Agent,导致任务失败。

再看委派规则:

js 复制代码
  ## 委派规则

  - 每个调研员只负责一个聚焦的子主题
  - **每份报告最多 3 个调研员**------只选最相关的子主题
  - 框架对比类任务:优先调研用户明确点名的框架;否则选最重要的 3 个
  - 最多并行启动 3 个调研员,已有 3 份 findings 文件后不再新增调研员
  - 仅在确实需要数值计算时使用分析师
  - 每份报告只调用编辑一次(草稿完成后)
  - 调研完成后直接进入起草 → 审阅 → 定稿,不要额外开调研轮次

这里的限制不是为了"少做事",而是为了让 Agent 可控。多 Agent 系统最怕的就是无限派生任务,尤其是调研类任务,如果不限制轮次和子 Agent 数量,很容易搜索很多轮却迟迟不写报告。

七、createDeepAgent:把模型、文件系统、memory、skills、subagents 串起来

最后看核心创建函数:

js 复制代码
export function createIntelligenceDeskAgent() {
  const apiKey = process.env.OPENAI_API_KEY?.trim()
  if (!apiKey) {
    throw new Error("未设置 OPENAI_API_KEY 环境变量")
  }

  const model =
    process.env.MODEL_NAME?.trim() ||
    process.env.OPENAI_MODEL?.trim() ||
    "gpt-4o"
  const baseURL = process.env.OPENAI_BASE_URL?.trim() || undefined

  const backend = new FilesystemBackend({
    rootDir: projectDir,
    virtualMode: true,
  })

  const chatModel = new ChatOpenAI({
    model,
    temperature: 0,
    apiKey,
    ...(baseURL
      ? {
          configuration: {
            baseURL,
          },
        }
      : {}),
  })

  return createDeepAgent({
    model: chatModel,
    systemPrompt: orchestratorPrompt,
    backend,
    memory: [path.join(projectDir, "AGENTS.md")],
    skills: ["/skills/"],
    subagents: [researcherSubAgent, editorSubAgent, analystSubAgent],
  })
}

这段代码就是整个项目的核心。

model 负责模型调用。这里通过 ChatOpenAI 接 OpenAI 兼容接口,temperature 设置为 0,是为了让调研和报告生成尽量稳定。

backend 负责文件系统。FilesystemBackendrootDir 指向项目根目录,virtualMode: true 表示 Agent 看到的是虚拟路径,比如 /workspace/sources/question.txt,但实际文件会落到项目目录下。

memory 加载长期记忆文件。这里加载的是 AGENTS.md,里面写了报告偏好、调研标准和工作区目录。

skills 指向 /skills/,DeepAgents 会读取技能目录下的 SKILL.md,让主 Agent 在需要联网调研和写报告时遵循对应流程。

subagents 注册 3 个子 Agent。后续主 Agent 可以通过 task 工具委派 researcheranalysteditor

相比自己手动创建 LangGraph 节点、边、中间件和状态,createDeepAgent 的好处就是:它把复杂 Agent 常见的能力都包装好了,我们只需要把模型、prompt、文件系统、skills 和子 Agent 配进去。

八、skills:把流程指南沉淀成可复用能力

这个项目里有两个 skill。

第一个是 skills/web-research/SKILL.md

md 复制代码
---
name: web-research
description: 结构化多来源联网调研,支持并行委派调研员子 Agent
---

# 联网调研技能

当用户要求调研、调查、对比或深度分析某个主题时使用本技能。

> **注意**:本技能是主 Agent 的流程指南,不是子 Agent。联网搜索请委派 `researcher` 子 Agent,**不要**将 `web-research` 作为 subagent_type 调用。

它规定了联网调研流程:先写 question.txt,再写 research_plan.md,然后最多并行委派 3 个 researcher,最后读取 findings 做综合。

第二个是 skills/report-writer/SKILL.md

md 复制代码
---
name: report-writer
description: 将调研结果整理为结构清晰、专业的中文情报报告
---

# 报告撰写技能

将调研 findings 综合为最终交付物时使用本技能。

> **注意**:本技能是主 Agent 的写作指南,不是子 Agent。请主 Agent 亲自用 `write_file` 撰写报告,**不要**通过 `task` 工具委派 `report-writer`。

它规定了报告结构:标题、执行摘要、背景、核心发现、分析、结论、参考资料。

这就是 skill 的价值:把某类任务的执行规范从主 prompt 里拆出去。主 prompt 只保留总体编排,具体任务规范放到 skill。以后如果想调整报告格式,只需要改 report-writer skill,不用重写主 Agent。

九、AGENTS.md:给 Agent 加长期记忆

项目根目录还有一个 AGENTS.md

md 复制代码
# 深度调研助手 --- 用户偏好

本文件在 Agent 启动时作为长期记忆加载。

## 报告偏好

- **所有报告、笔记、任务列表均使用中文**
- 报告顶部包含「执行摘要」(3--5 条要点)
- Markdown 标题层级:`#` 标题、`##` 章节、`###` 小节
- 每份报告末尾必须有「参考资料」章节,列出所有引用 URL
- 正文以段落叙述为主;仅在摘要或对比表时使用 bullet 列表

## 调研标准

- 重要结论尽量用两个以上独立来源交叉验证
- 明确标注信息冲突,不要掩盖分歧
- 区分事实与推测/预测
- 时效性信息需标注日期

这个文件通过 memory: [path.join(projectDir, "AGENTS.md")] 注入到 Agent。它适合放稳定偏好,比如报告语言、标题层级、引用规范、工作区目录。

长期记忆和 skill 的区别在于:memory 更像全局偏好,skill 更像某类任务的操作手册。

十、CLI:观察多 Agent 执行过程

src/cli.mjs 负责启动 Agent,并把关键过程打印出来。核心逻辑是:

js 复制代码
const agent = createIntelligenceDeskAgent()

for await (const [namespace, chunk] of await agent.stream(
  { messages: [new HumanMessage(query)] },
  { streamMode: "updates", subgraphs: true, recursionLimit }
)) {
  for (const [node, data] of Object.entries(chunk)) {
    if (node === "model_request") {
      trackFileCalls(data, pending)
      trackEvalCalls(data, pendingEval)
      console.log(stepLabel(namespace, node))
    } else if (node === "tools") {
      logToolResults(data, pending, pendingEval)
    } else if (node === "todoListMiddleware.after_model") {
      console.log(stepLabel(namespace, node))
    }
  }
}

这里用了 streamMode: "updates"subgraphs: true

updates 可以看到每个节点的更新,subgraphs: true 可以看到子 Agent 内部的执行过程。对于多 Agent 项目来说,这个配置很有用,因为你可以区分当前是主 Agent 在调用工具,还是某个子 Agent 在执行自己的子图。

另外,CLI 里还专门追踪了文件工具和 eval 工具:

js 复制代码
const FILE_TOOLS = new Set([
  "write_file",
  "edit_file",
  "read_file",
  "ls",
  "glob",
  "grep",
])

const EVAL_TOOL = "eval"

这样运行时可以看到类似:

text 复制代码
[主 Agent] model_request
  write_file: workspace/sources/question.txt
  write_file: workspace/sources/research_plan.md
[subagent:researcher] model_request
  web_search ...
  write_file: workspace/sources/findings_nbs_official.md
[subagent:analyst] model_request
  🧮 eval: ...
  write_file: workspace/sources/analysis_results.md

这类日志比直接打印完整 messages 更容易读,也更适合调试多 Agent 流程。

十一、跑一个真实任务:调研 2023 年省级 GDP 前 6 名

我们用这个命令启动:

bash 复制代码
node src/cli.mjs "调研国家统计局公开的2023年省级地区生产总值(GDP)数据:提取GDP总量前6名省份的具体数值及同比增速,计算六省GDP总和、各省占全国GDP的比重,并按增速从高到低排名"

这是一个很适合测试的任务,因为它同时包含:

  • 官方数据调研
  • 多来源验证
  • GDP 总量提取
  • 占全国 GDP 比重计算
  • 增速排序
  • 最终报告撰写

主 Agent 首先会写入原始问题:

text 复制代码
workspace/sources/question.txt

内容就是用户输入的调研目标。

然后写调研计划:

md 复制代码
# 国家统计局2023年省级GDP数据调研计划

## 主调研问题
获取国家统计局官方发布的2023年各省级行政区GDP数据,提取GDP总量前6名省份的具体数值及同比增速,计算六省GDP总和、各省占全国GDP的比重,并按增速从高到低排名。

## 子主题划分
1. **国家统计局官方数据源**:查找国家统计局官网发布的2023年国民经济和社会发展统计公报中省级GDP数据
2. **权威媒体汇总报道**:查找新华社、人民日报等权威媒体对2023年省级GDP数据的汇总报道和解读
3. **专业经济数据库**:查找中国统计年鉴2024或专业经济数据库(如CEIC、Wind)中的2023年省级GDP数据

这个计划体现了主 Agent 的职责:先拆分问题,而不是直接搜索。

后续 researcher 会生成多个 findings 文件,例如:

text 复制代码
workspace/sources/findings_nbs_official.md
workspace/sources/findings_economic_databases.md

如果涉及计算,主 Agent 会委派 analyst。分析结果写入:

text 复制代码
workspace/sources/analysis_results.md

其中包含了基础数据和计算结果:

md 复制代码
## 基础数据
- 全国GDP总量:1260583亿元(126.06万亿元)
- GDP前6名省份及数据:
  - 广东:135673.2亿元,增速4.8%
  - 江苏:128204.7亿元,增速5.8%
  - 山东:92069.0亿元,增速5.2%
  - 浙江:82553.2亿元,增速5.2%
  - 河南:61345.1亿元,增速4.1%
  - 四川:60132.9亿元,增速6.0%

## 计算结果

### 六省GDP总和
559978.1亿元

### 各省占全国GDP比重
- 广东:10.76%
- 江苏:10.17%
- 山东:7.30%
- 浙江:6.55%
- 河南:4.87%
- 四川:4.77%

最后主 Agent 会生成报告:

text 复制代码
workspace/reports/draft_gdp_2023.md
workspace/reports/report_gdp_2023_20240527.md

报告中会把前 6 名省份整理成表格,并给出核心发现:

md 复制代码
| 排名 | 省份 | GDP总量(亿元) | 同比增速 |
|------|------|------------------|----------|
| 1 | 广东 | 135673.2 | 4.8% |
| 2 | 江苏 | 128204.7 | 5.8% |
| 3 | 山东 | 92069.0 | 5.2% |
| 4 | 浙江 | 82553.2 | 5.2% |
| 5 | 河南 | 61345.1 | 4.1% |
| 6 | 四川 | 60132.9 | 6.0% |

从这个例子可以看到,主 Agent 没有把所有事情都塞进一次模型调用里,而是把任务拆成了多个阶段:计划、调研、计算、写草稿、审阅、定稿。每个阶段都有文件产物,后续步骤可以明确读取。

十二、Todo List:复杂任务不能走一步想一步

复杂 Agent 最重要的习惯之一,就是先列 todo,再执行。

DeepAgents 里已经集成了 todo 相关能力。主 Agent 在收到复杂任务后,会生成中文 todo 列表,并随着执行推进更新状态。

todo 的状态通常有 3 种:

  • pending:尚未执行。
  • in_progress:正在执行。
  • completed:已经完成。

子 Agent 也可以使用 todo。比如 researcher 在调研单一子主题时,可以列出最多 3 条步骤:

text 复制代码
1. 搜索官方数据源
2. 整理关键事实和来源 URL
3. 写入 findings 文件

这个机制的意义不是让输出更好看,而是让模型在长任务里保持执行轨迹。没有 todo 时,Agent 很容易在搜索、阅读、写文件之间反复横跳;有 todo 后,它更容易按阶段推进。

如果想单独理解 LangChain 的 todoListMiddleware,可以看项目里的 src/todo-middleware-test.mjs

js 复制代码
import dotenv from "dotenv"
dotenv.config({ override: true })
import { ChatOpenAI } from "@langchain/openai"
import { createAgent, HumanMessage, todoListMiddleware } from "langchain"

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

const agent = createAgent({
  model,
  tools: [],
  systemPrompt:
    "你是生活规划助手。收到需要多步完成的请求时,先用 write_todos 列出中文执行步骤,然后简要说明你的计划。",
  middleware: [todoListMiddleware()],
})

const query =
  "我下周末想带女朋友去珠海玩两天,帮我规划一下:交通怎么选、住哪里方便、必去景点和吃什么,预算控制在人均 1500 元左右。"

const result = await agent.invoke({
  messages: [new HumanMessage(query)],
})

console.log("todos:", JSON.stringify(result.todos, null, 2))
console.log("─".repeat(50))
console.log("回复:", result.messages.at(-1)?.content)

这里没有使用 DeepAgents,而是直接用 createAgent + todoListMiddleware()。运行后可以看到,middleware 会给 Agent 注入 write_todos 工具,并把 todo 写到 graph state 里。

DeepAgents 的好处是,这些常见能力已经被集成到更完整的 Agent 框架里,不需要我们每次从零拼。

十三、QuickJS:让模型生成代码,让沙箱负责计算

前面提到 analyst 使用了 createCodeInterpreterMiddleware()

js 复制代码
middleware: [createCodeInterpreterMiddleware()]

它会给 Agent 提供一个 eval 工具,底层通过 QuickJS 执行 JavaScript。

为什么需要这个?

因为"会写解释"和"会精确计算"是两回事。大模型可以解释 GDP 占比怎么算,但它未必每次都能把一组数字算对。只要涉及总和、占比、排序、增长率,最好都让模型生成代码,再由沙箱执行。

比如 GDP 示例里,六省 GDP 总和、各省占全国 GDP 比重、增速排序,都应该由代码计算。这样报告里的结论可以复现,也方便后续排查。

这个思路可以扩展到很多场景:

  • JSON 数据分析
  • CSV 表格计算
  • 排名和聚合
  • 环比、同比、占比计算
  • 简单模拟和规则校验

如果你希望执行 Python、SQL 或其他语言,也可以换成对应的代码执行工具。关键不是 QuickJS 本身,而是让"计算"从模型输出里分离出来,交给可执行环境。

十四、LangSmith:用 filter 看清多 Agent 链路

多 Agent 系统如果不开 trace,调试体验会比较痛苦。

这个项目建议开启:

env 复制代码
LANGCHAIN_API_KEY=xxx
LANGCHAIN_PROJECT=deep-research-assistant
LANGCHAIN_TRACING_V2=true

开启后,可以在 LangSmith 里查看每次模型调用、每个 tool 调用、每个子 Agent 的输入输出。

这里有个小技巧:在 LangSmith 里通过 filter 过滤所有 tool 调用,可以更快理清流程。比如你可以重点看:

  • task 调用了哪些子 Agent
  • web_search 搜了哪些关键词
  • write_file 写了哪些文件
  • read_file 读取了哪些中间结果
  • eval 执行了什么计算代码

对于多 Agent 项目,调试的关键不是只看最终答案,而是看每一步有没有按预期推进。尤其是当结果不稳定时,LangSmith trace 往往能直接暴露问题:是调研员没有写文件,还是主 Agent 没读 findings,还是 analyst 没有被触发。

十五、上下文压缩:什么时候触发 Summarization?

DeepAgents 还内置了上下文压缩能力。它会根据模型的输入上下文限制判断是否需要总结历史消息。默认逻辑可以理解为:当上下文接近模型输入上限时,触发摘要,只保留一部分最新上下文。

但这里有一个实践问题:不是所有 OpenAI 兼容模型都会正确暴露 profile.maxInputTokens。比如使用某些 Qwen 兼容接口时,模型对象里可能拿不到准确的上下文长度。

项目里用 src/max-input-tokens-test.mjs 演示了如何覆盖这个值:

js 复制代码
import dotenv from "dotenv"
dotenv.config({ override: true })
import { ChatOpenAI } from "@langchain/openai"

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

console.log(model.profile.maxInputTokens)

Object.defineProperty(model, "profile", {
  get: () => ({ maxInputTokens: 1_024 }),
})

console.log(model.profile.maxInputTokens)

真实项目里可以按模型实际上下文长度设置,比如 32K、128K 等。设置得太小,会过早触发摘要;设置得太大,又可能在模型真实上限前没有及时压缩。

上下文压缩不是越早越好。对于调研任务来说,关键事实最好写入文件,而不是依赖对话历史。Summarization 可以作为兜底能力,但稳定的多 Agent 工作流应该优先通过文件系统保存中间结果。

十六、这个架构适合什么场景?

这个深度调研助手适合所有"需要多阶段、多角色、多文件产物"的任务。

比如:

  • 技术框架调研:对比 LangGraph、AutoGen、CrewAI 等框架。
  • 行业研究:调研某个产业链、市场规模、竞争格局。
  • 数据简报:搜索公开数据,计算指标,并生成分析报告。
  • 选型报告:调研多个工具或服务,整理优缺点和推荐结论。
  • 代码仓库研究:读取项目资料,生成架构说明和改造建议。

但它也不是万能的。

如果任务只是简单问答,没必要上多 Agent。多 Agent 会增加调用次数、成本和调试复杂度。只有当任务确实需要规划、搜索、计算、写作、审阅这些阶段时,多 Agent 才能体现价值。

另外,联网搜索结果质量取决于搜索 API 和关键词设计。Agent 可以整理资料,但不能保证所有来源天然可靠。因此重要结论仍然需要来源追踪和交叉验证。

十七、总结

本期我们基于 DeepAgents 的 createDeepAgent 实现了一个多 Agent 架构的深度调研助手。

这个项目里,主 Agent 负责编排流程:先用 todo 拆解任务,再写调研计划,随后委派 researcher 做联网搜索,必要时委派 analyst 用 QuickJS 执行计算,报告草稿完成后再交给 editor 审阅,最后由主 Agent 修订并保存终稿。

相比手动拼各种 middleware,createDeepAgent 把常见的复杂 Agent 能力整合到了一个更高层的 API 里。我们只需要配置模型、主 prompt、filesystem、memory、skills 和 subagents,就能搭出一个支持规划、文件协作、多 Agent 委派、沙箱计算、长期记忆和上下文压缩的完整系统。

这也是 DeepAgents 适合工程实战的地方:它不是只让 Agent "多调用几个工具",而是把复杂任务执行中真正需要的流程能力都打包好了。对于深度调研、技术选型、数据分析、报告生成这类任务,用它来搭第一版系统会非常省力。

相关推荐
雪宫街道2 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
云水一下3 小时前
JavaScript 从零基础到精通系列:前世今生与编程启蒙
前端·javascript
月亮邮递员6163 小时前
Markdown语法总结
开发语言·前端·javascript
XLYcmy3 小时前
全链路验证测试系统:一个针对智能代理(Agent)系统全链路能力的自动化验证脚本
分布式·python·http·网络安全·ai·llm·agent
AI人工智能+电脑小能手4 小时前
【大白话说Java面试题 第84题】【Mysql篇】第14题:为什么用 InnoDB 存储引擎的表建议用整型的自增主键?
java·开发语言·数据库·mysql·面试
小江的记录本4 小时前
【JVM虚拟机】JVM调优:常用JVM参数、调优核心指标、OOM排查、GC日志分析、Arthas工具使用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
丷丩4 小时前
MapLibre GL JS第27课:添加COG栅格源
javascript·map·mapbox·maplibre gl js
不好听6135 小时前
JavaScript 到底是怎么运行的?从编译阶段到执行上下文全面解析
javascript
丷丩6 小时前
MapLibre GL JS第29课:添加Canvas源
javascript·gis·map·mapbox·maplibre gl js