前面几篇我们陆续看了 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 执行联网搜索和数据计算,最后生成一份中文调研报告。
一、为什么需要多 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:提供createDeepAgent、FilesystemBackend等核心能力。@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_URL 和 OPENAI_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 条"),
}),
}
)
工具描述要写得具体,因为模型会根据 name、description 和 schema 判断什么时候调用工具、传什么参数。这里还特别强调"优先使用中文关键词",因为我们的调研目标主要是中文资料源。
五、子 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 工具委派的执行角色,比如 researcher、analyst、editor。
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 负责文件系统。FilesystemBackend 的 rootDir 指向项目根目录,virtualMode: true 表示 Agent 看到的是虚拟路径,比如 /workspace/sources/question.txt,但实际文件会落到项目目录下。
memory 加载长期记忆文件。这里加载的是 AGENTS.md,里面写了报告偏好、调研标准和工作区目录。
skills 指向 /skills/,DeepAgents 会读取技能目录下的 SKILL.md,让主 Agent 在需要联网调研和写报告时遵循对应流程。
subagents 注册 3 个子 Agent。后续主 Agent 可以通过 task 工具委派 researcher、analyst、editor。
相比自己手动创建 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调用了哪些子 Agentweb_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 "多调用几个工具",而是把复杂任务执行中真正需要的流程能力都打包好了。对于深度调研、技术选型、数据分析、报告生成这类任务,用它来搭第一版系统会非常省力。