Skip to main content

概述

本指南演示如何使用 Deep Agents 从头开始构建一个多步骤网络研究代理。该代理将研究问题分解为聚焦的任务,将其委派给专门的子代理,并将发现综合成一份全面的报告。 您构建的代理将:
  1. 使用待办事项列表规划研究
  2. 将聚焦的研究任务委派给具有隔离上下文的子代理
  3. 在收集信息时评估搜索结果并规划后续步骤
  4. 将发现综合成带有适当引用的最终报告
生成的子代理将使用 Tavily 进行网络搜索,获取完整的网页内容进行分析。

关键概念

本教程涵盖:

先决条件

需要以下 API 密钥:
  • Anthropic (Claude) 或 Google (Gemini)
  • Tavily 用于网络搜索(可选 - 免费套餐足够)
  • LangSmith 用于跟踪(可选)

设置

1

创建项目目录

mkdir deep-research-agent
cd deep-research-agent
2

安装依赖项

npm
npm install deepagents @langchain/anthropic @langchain/core
3

设置 API 密钥

export ANTHROPIC_API_KEY="your_anthropic_api_key"
export TAVILY_API_KEY="your_tavily_api_key"
export LANGSMITH_API_KEY="your_langsmith_api_key"   # 可选

构建代理

在您的项目目录中创建 agent.ts
1

添加工具

添加自定义搜索工具。tavily_search 工具使用 Tavily 发现 URL,然后获取完整的网页内容,以便代理可以分析完整的来源,而不是摘要。
import { tool } from "langchain";
import { z } from "zod";

async function fetchWebpageContent(
  url: string,
  timeout = 10_000,
): Promise<string> {
  try {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);
    const response = await fetch(url, {
      headers: {
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
      },
      signal: controller.signal,
    });
    clearTimeout(id);
    if (!response.ok) {
      return `Error fetching ${url}: HTTP ${response.status}`;
    }
    return await response.text();
  } catch (e) {
    return `Error fetching ${url}: ${e}`;
  }
}

const tavilySearch = tool(
  async ({
    query,
    maxResults = 1,
    topic = "general",
  }: {
    query: string;
    maxResults?: number;
    topic?: "general" | "news" | "finance";
  }) => {
    const response = await fetch("https://api.tavily.com/search", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.TAVILY_API_KEY}`,
      },
      body: JSON.stringify({ query, max_results: maxResults, topic }),
    });
    const data = (await response.json()) as {
      results: Array<{ url: string; title: string }>;
    };
    const results = data.results ?? [];
    const resultTexts: string[] = [];
    for (const result of results) {
      const content = await fetchWebpageContent(result.url);
      resultTexts.push(
        `## ${result.title}\n**URL:** ${result.url}\n\n${content}\n---`,
      );
    }
    return (
      `Found ${resultTexts.length} result(s) for '${query}':\n\n` +
      resultTexts.join("\n")
    );
  },
  {
    name: "tavily_search",
    description:
      "Search the web for information on a given query. Uses Tavily to discover relevant URLs, then fetches and returns full webpage content.",
    schema: z.object({
      query: z.string().describe("Search query to execute"),
      maxResults: z
        .number()
        .optional()
        .default(1)
        .describe("Maximum number of results to return (default: 1)"),
      topic: z
        .enum(["general", "news", "finance"])
        .optional()
        .default("general")
        .describe(
          "Topic filter - 'general', 'news', or 'finance' (default: 'general')",
        ),
    }),
  },
);
2

添加提示

将编排器工作流和子代理提示模板添加到 agent.ts
const RESEARCH_WORKFLOW_INSTRUCTIONS = `# 研究工作流

遵循此工作流处理所有研究请求:

1. **规划**:使用 write_todos 创建待办事项列表,将研究分解为聚焦的任务
2. **保存请求**:使用 write_file() 将用户的研究问题保存到 \`/research_request.md\`
3. **研究**:使用 task() 工具将研究任务委派给子代理 - 始终使用子代理进行研究,切勿自行进行研究
4. **综合**:审查所有子代理的发现并整合引用(每个唯一 URL 在所有发现中分配一个编号)
5. **撰写报告**:将全面的最终报告写入 \`/final_report.md\`(参见下面的报告撰写指南)
6. **验证**:阅读 \`/research_request.md\` 并确认您已通过适当的引用和结构处理了所有方面

## 研究规划指南
- 将类似的研究任务批量处理到一个 TODO 中以最小化开销
- 对于简单的事实查找问题,使用 1 个子代理
- 对于比较或多方面主题,委派给多个并行子代理
- 每个子代理应研究一个特定方面并返回发现

## 报告撰写指南

将最终报告写入 \`/final_report.md\` 时,遵循以下结构模式:

**对于比较:**
1. 引言
2. 主题 A 概述
3. 主题 B 概述
4. 详细比较
5. 结论

**对于列表/排名:**
只需列出项目及其详细信息 - 无需引言:
1. 项目 1 及解释
2. 项目 2 及解释
3. 项目 3 及解释

**对于摘要/概述:**
1. 主题概述
2. 关键概念 1
3. 关键概念 2
4. 关键概念 3
5. 结论

**通用指南:**
- 使用清晰的章节标题(## 用于章节,### 用于子章节)
- 默认以段落形式撰写 - 文本密集,不仅仅是项目符号
- 不要使用自我指涉的语言(“我发现...”、“我研究了...”)
- 撰写专业报告,不带元评论
- 每个章节应全面且详细
- 仅在列表比散文更合适时使用项目符号

**引用格式:**
- 使用 [1]、[2]、[3] 格式在行内引用来源
- 为每个唯一 URL 分配一个引用编号,贯穿所有子代理发现
- 报告末尾使用 ### Sources 部分列出每个编号的来源
- 顺序编号来源,无间隔(1,2,3,4...)
- 格式:[1] 来源标题:URL(每行一个,以便正确呈现列表)
- 示例:

 一些重要发现 [1]。另一个关键见解 [2]。

 ### Sources
 [1] AI Research Paper: https://example.com/paper
 [2] Industry Analysis: https://example.com/analysis
`;
const RESEARCHER_INSTRUCTIONS = `您是研究助理,对用户的输入主题进行研究。上下文是,今天的日期是 {date}。

您的工作是使用工具收集有关用户输入主题的信息。
您可以使用 tavily_search 工具查找有助于回答研究问题的资源。
您可以串行或并行调用它,您的研究在工具调用循环中进行。

您有权使用 tavily_search 工具进行网络搜索。

像时间有限的人类研究人员一样思考。遵循以下步骤:

1. **仔细阅读问题** - 用户需要什么具体信息?
2. **从更广泛的搜索开始** - 首先使用广泛、全面的查询
3. **每次搜索后暂停并评估** - 我有足够的信息吗?还缺少什么?
4. **在收集信息时执行更窄的搜索** - 填补空白
5. **当您能自信地回答时停止** - 不要继续追求完美而不断搜索

**工具调用预算**(防止过度搜索):
- **简单查询**:最多使用 2-3 次搜索工具调用
- **复杂查询**:最多使用 5 次搜索工具调用
- **始终停止**:如果 5 次搜索工具调用后仍无法找到正确的来源,则停止

**立即停止的情况**:
- 您能全面回答用户的问题
- 您有 3 个以上与问题相关的示例/来源
- 您最后两次搜索返回了相似的信息

每次搜索后,在继续之前评估结果:我发现了什么关键信息?缺少什么?我有足够的信息回答吗?我应该继续搜索还是提供答案?

当向编排器提供您的发现时:

1. **构建您的响应**:使用清晰的标题和详细解释组织发现
2. **在行内引用来源**:引用搜索信息时使用 [1]、[2]、[3] 格式
3. **包含来源部分**:以 ### Sources 结尾,列出每个编号的来源及其标题和 URL

示例:
## 关键发现
上下文工程是 AI 代理的关键技术 [1]。研究表明,适当的上下文管理可以将性能提高 40% [2]。

### Sources
[1] Context Engineering Guide: https://example.com/context-guide
[2] AI Performance Study: https://example.com/study

编排器将把所有子代理的引用整合到最终报告中。
`;
const SUBAGENT_DELEGATION_INSTRUCTIONS = `# 子代理研究协调

您的角色是通过将任务从您的 TODO 列表委派给专门的研究子代理来协调研究。

## 委派策略

**默认:从 1 个子代理开始** 处理大多数查询:
- "什么是量子计算?" -> 1 个子代理(一般概述)
- "列出旧金山十大咖啡店" -> 1 个子代理
- "总结互联网历史" -> 1 个子代理
- "研究 AI 代理的上下文工程" -> 1 个子代理(涵盖所有方面)

**仅在查询明确要求比较或具有明显独立方面时并行化:**

**明确的比较** -> 每个元素 1 个子代理:
- "比较 OpenAI、Anthropic 和 DeepMind 的 AI 安全方法" -> 3 个并行子代理
- "比较 Python 和 JavaScript 用于 Web 开发" -> 2 个并行子代理

**明显分离的方面** -> 每个方面 1 个子代理(谨慎使用):
- "研究欧洲、亚洲和北美的可再生能源采用情况" -> 3 个并行子代理(地理分离)
- 仅当方面无法通过单个全面搜索有效覆盖时使用此模式

## 关键原则
- **倾向于单个子代理**:一个全面的研究任务比多个狭窄的任务更节省令牌
- **避免过早分解**:不要将“研究 X”分解为“研究 X 概述”、“研究 X 技术”、“研究 X 应用” - 只使用 1 个子代理处理所有 X
- **仅对明确的比较进行并行化**:当比较不同实体或地理分离的数据时使用多个子代理

## 并行执行限制
- 每次迭代最多使用 {maxConcurrentResearchUnits} 个并行子代理
- 在单个响应中进行多次 task() 调用以启用并行执行
- 每个子代理独立返回发现

## 研究限制
- 如果在 {maxResearcherIterations} 次委派轮次后仍未找到足够的来源,则停止
- 当您有足够的信息全面回答时停止
- 倾向于聚焦研究而非详尽探索`;
3

创建代理

将模型初始化和代理创建添加到 agent.ts
import { createDeepAgent } from "deepagents";
import { ChatAnthropic } from "@langchain/anthropic";

const maxConcurrentResearchUnits = 3;
const maxResearcherIterations = 3;

const currentDate = new Date().toISOString().split("T")[0];

const INSTRUCTIONS =
  RESEARCH_WORKFLOW_INSTRUCTIONS +
  "\n\n" +
  "=".repeat(80) +
  "\n\n" +
  SUBAGENT_DELEGATION_INSTRUCTIONS.replace(
    "{maxConcurrentResearchUnits}",
    String(maxConcurrentResearchUnits),
  ).replace("{maxResearcherIterations}", String(maxResearcherIterations));

const researchSubAgent = {
  name: "research-agent",
  description: "Delegate research to the sub-agent. Give one topic at a time.",
  systemPrompt: RESEARCHER_INSTRUCTIONS.replace("{date}", currentDate),
  tools: [tavilySearch],
};

const model = new ChatAnthropic({
  model: "claude-sonnet-4-5-20250929",
  temperature: 0,
});

const agent = createDeepAgent({
  model,
  tools: [tavilySearch],
  systemPrompt: INSTRUCTIONS,
  subagents: [researchSubAgent],
});

运行代理

您可以同步运行代理,这意味着它将等待完整结果然后打印,或者您可以流式传输更新。 将相应选项卡中的代码添加到 agent.ts 底部:
{
  async function main() {
    const result = await agent.invoke({
      messages: [
        {
          role: "user",
          content:
            "What are the main differences between RAG and fine-tuning for LLM applications?",
        },
      ],
    });

    for (const msg of result.messages ?? []) {
      if (msg.content) {
        console.log(msg.content);
      }
    }
  }

  main().catch((err) => {
    console.error(err);
    process.exitCode = 1;
  });
}
从项目根目录运行代理:
npx tsx agent.ts
如果在运行前设置了 LANGSMITH_API_KEY 环境变量,您可以在 LangSmith 中查看代理的跟踪以调试和监控多步骤行为。

完整代码

在 GitHub 上查看完整的 深度研究示例

后续步骤

现在您已经构建了代理,可以通过更改代理文件中的提示常量来自定义它,以调整工作流、委派策略或研究人员行为。 您还可以调整委派限制以允许更多并行子代理或委派轮次。 有关本教程中概念的更多信息,请查看以下资源:
  • 子代理:了解如何配置具有不同工具和提示的子代理
  • 自定义:自定义模型、工具、系统提示和规划行为
  • LangSmith:跟踪研究运行并调试多步骤行为
  • 深度研究课程:关于使用 LangGraph 进行深度研究的完整课程