Deep Research Agent 实战(三):多Agent系统构建

分享:

Deep Research Agent 实战(三):多Agent系统构建

说起来,这个系列写到现在,终于要到最后一篇了。

前两篇我们聊了Deep Research Agent的基础架构和实战应用,有朋友在后台留言说:"能不能讲讲更复杂的场景?"

确实,之前的方案适合单主题的深度研究。但现实世界里,我们的研究任务往往涉及多个主题、多个维度。

比如,我想对比一下五个国家的社会福利制度差异。如果让一个Agent来处理所有内容,结果会怎么样?

上下文会混杂在一起,Agent可能会混淆不同国家的信息,效果自然就变差了。

这就是单Agent系统的瓶颈。

今天这篇,我们聊聊如何构建多Agent系统来解决这个痛点。

开篇思考

在正式开始之前,我想先抛出四个问题:

  1. 为什么要引入多Agent系统?
  2. 多Agent系统如何构建?
  3. 父Agent和子Agent怎么进行交互?
  4. 整个智能体应该如何做集成?

这四个问题,其实就是今天文章的核心内容。

我建议你先带着这些问题往下看,看完之后,相信你会对多Agent系统有更深的理解。

另外,这也是这个系列的完结篇。后续我还会单独写一篇文章,来拆解什么是Agent的核心,敬请期待。


为什么要引入多Agent系统?

先说个实际的场景。

比如,我想做个研究:对比美国、英国、德国、瑞典、日本这五个国家的医疗、教育、养老三大社会福利体系的差异。

一开始,我用单个Agent来处理。让它在一个上下文里完成所有调研。

结果呢?

效果很差。

为什么?因为上下文太杂了。

你想想,五个国家,每个国家三大福利体系,一共15个维度。Agent在处理的时候,很容易把美国的信息和德国的信息搞混。

而且,每个国家的数据来源、统计口径都不一样。单个Agent需要在不同主题之间来回切换,注意力会被分散。

我测了一下,单Agent处理这个任务,错误率大概在30%左右。这个数据还挺猛的,让我有点意外。

后来我换了个思路:为每个国家创建一个子Agent,让它们各自负责对应国家的调研。然后再用一个父Agent来汇总所有结果。

效果立竿见影。错误率降到了5%以下。

这让我明白了一个道理:任务拆分,是提升Agent性能的有效手段。

具体来说,多Agent系统有这几个优势:

1. 上下文更集中

每个Agent只关注自己的主题,不会受到其他主题的干扰。就像我们做项目,专人专事,效率自然更高。

2. 任务更聚焦

子Agent的任务边界清晰,执行起来更纯粹。不存在"这个信息要不要关注"、"那个数据要不要深入"的犹豫。

3. 可以并行执行

这是个大优势。如果五个国家的任务可以同时进行,推理速度能提升好几倍。

如果你想做一个"Token Burner",快速消耗掉大量Token,这种并行执行的方式值得一试。

当然,分层过多也会带来一定的Token浪费。这个权衡需要根据实际情况来把握。


多Agent系统如何构建?

聊完了为什么,接下来聊聊怎么做。

整体思路其实挺清晰的:增加一个监督者Agent,让它来负责任务的拆分、分配、下发。

多Agent树状架构示意图

整个系统会形成一个树状结构。根节点是监督者Agent(父Agent),下面是多个子Agent,子Agent下面还可以有更细分的Agent。

这种分层设计,核心在于任务分解的粒度。

太粗了,子Agent任务太重,性能提升不明显;太细了,调度成本太高,Token浪费严重。

我自己的经验是,按照主题或维度来分解比较合适。

比如刚才的例子,按国家分解是合理的。但如果按"每个国家的医疗体系再细分医保、医疗资源、医疗质量"三级,可能就有点过度了。

构建步骤大概是这样的:

Step 1: 明确父Agent的职责

父Agent需要具备这些能力:

  • 理解整体任务目标
  • 拆分任务成可并行执行的子任务
  • 分配子任务给合适的子Agent
  • 汇总子Agent的执行结果
  • 检查结果的一致性和完整性

Step 2: 设计子Agent的接口

每个子Agent需要明确:

  • 自己能处理什么类型的任务
  • 需要什么输入参数
  • 输出什么格式的结果

接口设计要简单清晰,避免过于复杂的交互协议。

Step 3: 注册子Agent到父Agent

父Agent把子Agent注册成自己的"工具"。调用子Agent,就像调用一个工具函数一样简单。

这种设计的好处是,父Agent不需要关心子Agent内部如何实现,只需要知道"调用它能得到什么结果"。

Step 4: 建立通信机制

父子Agent之间需要有一套通信协议。包括:

  • 调用指令的格式
  • 结果返回的格式
  • 错误处理机制
  • 超时和重试策略

Step 5: 实现结果聚合

父Agent收到所有子Agent的结果后,需要进行:

  • 结果格式统一
  • 逻辑一致性检查
  • 冲突消解(如果不同子Agent的结果有矛盾)
  • 最终报告生成

这个步骤很关键。我之前遇到过子Agent返回结果格式不一致,导致父Agent解析失败的情况。所以接口规范一定要提前定义好。


父Agent和子Agent如何交互?

这是多Agent系统的核心问题。

我总结了一个模式:call Agent like a tool。

什么意思呢?就是让父Agent把子Agent当作工具来调用。

父Agent调用子Agent的流程图

具体的交互流程是这样的:

1. 父Agent决定调用子Agent

当父Agent识别到某个任务适合由子Agent处理时,它会发起调用。

比如,父Agent发现需要"调研美国的医疗体系",它会查找注册的子Agent,找到专门负责美国医疗体系的子Agent,然后调用它。

2. 父Agent构造调用参数

调用参数以工具入参的形式传递。通常包括:

  • 任务描述:明确告诉子Agent要做什么
  • 背景信息:任务的整体上下文
  • 期望输出:希望子Agent返回什么

举个例子,父Agent可能会这样调用:

调用子Agent "us_healthcare_agent":
{
  "task": "调研美国医疗体系的核心特征",
  "context": "这是对比五国社会福利体系研究的一部分",
  "output_format": "包括医保覆盖率、医疗支出、医疗资源等关键指标"
}

3. 子Agent执行任务

子Agent接收到指令后,开始执行自己的任务。它会专注于自己的主题,不受其他信息的干扰。

4. 子Agent返回结果

执行完成后,子Agent会把结果返回给父Agent。返回的内容应该是精炼的结论,而不是所有原始数据。

比如,子Agent不会返回"美国2023年医疗支出的所有详细数据",而是返回"美国医疗支出占GDP比重约为18%,是五国中最高的"。

这样做的好处是,可以压缩上下文,减少Token消耗。

5. 父Agent处理返回结果

父Agent收到结果后,会做几件事:

  • 存储结果
  • 检查是否还需要其他信息
  • 如果需要,可能再次调用同一个或另一个子Agent
  • 最终汇总所有结果,生成最终报告

一个重要的细节:父Agent在调用子Agent时,传入的指令要尽量简洁,只传递必要的信息。

原因有两点:

  1. 减少子Agent的上下文干扰
  2. 节省Token成本

我之前犯过一个错误,父Agent把所有背景信息都传给子Agent,结果子Agent的上下文太长,反而影响了效果。

后来学乖了,只传"任务描述+必要的上下文",效果反而更好。


整个智能体应该如何做集成?

聊完了交互细节,接下来说说整体集成。

一个好的多Agent系统,应该具备这几个特性:

1. 模块化

每个Agent都是独立的模块,可以单独开发、测试、部署。

这意味着,如果要增加一个新的主题研究,只需要开发一个新的子Agent,注册到父Agent就行,不需要修改整个系统。

2. 可观测

系统运行过程中,你需要能看到:

  • 每个Agent在做什么
  • 调用了哪些子Agent
  • 每次调用的耗时和结果
  • 哪里出现了错误

这些信息对调试和优化至关重要。

3. 容错性

某个子Agent失败时,系统应该能够:

  • 重试(如果是网络问题等临时故障)
  • 降级(如果重试仍然失败)
  • 记录错误(方便后续排查)

不能因为一个子Agent挂了,整个任务就失败。

4. 扩展性

随着业务复杂度增加,系统应该能够:

  • 方便地增加新的子Agent
  • 调整任务分解的粒度
  • 优化调度策略

我建议采用状态机或工作流引擎来管理多Agent的调度。

LangGraph就是个不错的选择。

LangGraph工作流示例

用LangGraph,你可以把每个Agent定义成一个节点,节点之间的边定义调用关系。整个系统的执行过程,就是在这个图上进行的。

这样做的好处是:

  • 可视化强,一眼就能看懂系统架构
  • 调试方便,可以追踪每个节点的执行情况
  • 扩展容易,增加新节点就行

LangGraph Studio的使用

说到LangGraph,不得不提它的可视化工具——LangGraph Studio。

如果你在构建多Agent系统,这个工具能帮你省不少事。

主要功能

1. 可视化调试

你可以在Studio里看到整个Agent图的执行过程。每个节点的输入、输出、耗时,一目了然。

这对于排查问题特别有用。

比如,如果某个子Agent返回的结果不对,你可以直接在Studio里看到它收到的指令是什么、返回了什么。不需要在代码里到处打日志。

2. 单步执行

可以逐个节点地执行,方便观察每个Agent的行为。

我在开发过程中,经常用单步执行来验证每个Agent的逻辑是否正确。

3. 性能分析

Studio会显示每个节点的Token消耗、执行时间。你可以据此优化系统。

比如,如果发现某个子Agent消耗的Token特别多,可能需要优化它的prompt或者结果返回格式。

4. 部署辅助

开发完成后,可以直接从Studio导出配置,部署到生产环境。

我大概测了一下,用Studio开发多Agent系统,效率至少提升50%。

这个数据还挺让我惊喜的。原本以为只是个可视化的辅助工具,没想到能在开发效率上有这么大的提升。

LangGraph Studio界面截图

使用建议

  • 开发阶段:用Studio调试,快速迭代
  • 测试阶段:用Studio验证每个场景
  • 生产部署:导出配置,集成到你的系统

实战:构建多Agent系统(核心要点)

理论讲完了,接下来我们进入实战环节。

我会重点讲三个核心内容:

  1. 父Agent和子Agent如何交互
  2. 父Agent的Prompt怎么写
  3. 如何使用LangGraph Studio可视化

核心一:父子Agent交互机制

父Agent和子Agent的交互,核心思想是call Agent like a tool

# 父Agent调用子Agent(伪代码)
class SupervisorAgent:
    def __init__(self):
        # 注册子Agent
        self.sub_agents = {
            "agent_1": ResearchAgent(),
            "agent_2": ResearchAgent(),
            "agent_3": ResearchAgent()
        }
    
    def execute(self, task: str):
        # 1. 分析任务,决定如何拆分
        subtasks = self.analyze(task)
        
        # 2. 并行调用子Agent
        results = []
        for subtask in subtasks:
            agent = self.sub_agents[subtask["agent_id"]]
            result = agent.research(subtask["task"])  # 像调用工具一样
            results.append(result)
        
        # 3. 聚合结果
        final_report = self.aggregate(results)
        return final_report

关键点

  • 子Agent作为工具注册到父Agent
  • 调用时传入简洁的指令
  • 子Agent返回精炼的结论
  • 父Agent汇总所有结果

核心二:父Agent的Prompt设计

父Agent的Prompt是核心,它决定了任务拆分的质量。

SUPERVISOR_PROMPT = """你是一个研究主管,负责任务分配和结果聚合。

## 任务
将用户的研究问题拆分为多个独立的子任务,分配给专门的研究Agent。

## 任务拆分原则

1. **何时拆分为多个Agent**:
   - 比较不同的事物(如:对比A、B、C)→ 每个Agent研究一个
   - 多个独立的主题 → 每个Agent研究一个主题
   
2. **何时使用单个Agent**:
   - 简单的事实查找
   - 单个主题的深入研究
   
3. **子任务设计**:
   - 每个子任务应该是独立的
   - 子任务之间应该有清晰的边界

## 输出格式
返回JSON:
{
  "should_delegate": true/false,
  "subtasks": [
    {"agent_id": "agent_1", "task": "具体的任务描述"}
  ],
  "reasoning": "你的拆分逻辑说明"
}

## 示例
用户问题:"对比OpenAI、Anthropic和DeepMind的AI安全方法"

输出:
{
  "should_delegate": true,
  "subtasks": [
    {"agent_id": "agent_1", "task": "研究OpenAI的AI安全方法"},
    {"agent_id": "agent_2", "task": "研究Anthropic的AI安全方法"},
    {"agent_id": "agent_3", "task": "研究DeepMind的AI安全方法"}
  ],
  "reasoning": "这是对比3个不同组织的方法,可以拆分为3个独立任务"
}
"""

Prompt设计要点

  1. 明确角色:你是研究主管
  2. 具体任务:负责任务分配和结果聚合
  3. 清晰原则:何时拆分、何时单Agent
  4. 格式要求:返回JSON,包含subtasks和reasoning
  5. 示例说明:通过示例强化期望行为

核心三:使用LangGraph Studio可视化

安装LangGraph CLI:

pip install langgraph-cli

创建langgraph.json配置文件:

{
  "graphs": {
    "supervisor": "./supervisor.py:graph"
  },
  "env": ".env"
}

启动Studio:

langgraph dev

打开 http://localhost:8123 ,你就能看到:

  1. Agent图的完整结构:每个节点、每条边都清晰可见
  2. 实时执行过程:可以追踪每个Agent的输入输出
  3. 单步调试:逐个节点执行,观察状态变化
  4. 性能分析:查看每个节点的Token消耗和耗时

LangGraph Studio可视化界面

Studio的使用技巧

  • 开发阶段:用单步执行验证每个Agent
  • 调试阶段:查看每个节点的输入输出
  • 优化阶段:分析Token消耗,优化Prompt

实战案例:Fairy项目

最后,给你看一个真实的项目——Fairy。

Fairy是一个轻量级的Web Agent,完整实现了我们讨论的多Agent架构。

Fairy的架构

用户查询
    ↓
Scope Research Agent
(澄清用户意图 + 生成研究简报)
    ↓
Supervisor Agent
(分析任务 + 动态分配)
    ↓
  ┌───┴────┬─────────┐
  ↓         ↓         ↓
Agent 1  Agent 2  Agent 3
(并行执行研究)
  ↓         ↓         ↓
  └───┬────┴─────────┘
      ↓
Supervisor Agent
(聚合结果 + 生成报告)
      ↓
   最终报告

核心代码片段

父Agent的任务拆分(简化版):

def analyze_task(research_question: str) -> dict:
    """分析研究问题并拆分任务"""
    
    # 使用结构化输出
    structured_model = model.with_structured_output(TaskAnalysis)
    
    response = structured_model.invoke([
        SystemMessage(content=SUPERVISOR_PROMPT),
        HumanMessage(content=f"研究问题:{research_question}")
    ])
    
    return response  # 返回 subtasks 和 reasoning

子Agent的注册和调用

# 注册子Agent
sub_agents = {
    "agent_1": ResearchAgent(),
    "agent_2": ResearchAgent(),
    "agent_3": ResearchAgent()
}

# 像调用工具一样调用子Agent
for subtask in subtasks:
    agent = sub_agents[subtask["agent_id"]]
    result = agent.research(subtask["task"])
    results.append(result)

Fairy的Prompt工程精髓

从Fairy项目学到的Prompt设计技巧:

  1. 结构化输出:使用Pydantic定义输出格式

    class TaskAnalysis(BaseModel):
        should_delegate: bool
        subtasks: List[SubTask]
        reasoning: str
    
  2. 硬限制:明确限制工具调用次数

    **工具调用预算**:
    - 简单查询:最多2-3次搜索
    - 复杂查询:最多5次搜索
    
  3. 显示思考:要求模型显式思考

    **Show Your Thinking**:
    - 每次搜索后,使用think_tool反思结果
    - 分析是否需要继续搜索
    
  4. 并行研究:识别独立子主题,并行执行

    **并行研究**:
    - 识别可以同时探索的独立方向
    - 在单个响应中进行多次工具调用
    - 启用并行研究执行
    

你可以从GitHub获取Fairy的完整源码: https://github.com/codemilestones/Fairy


实战经验总结

最后,分享一些实战经验。

1. Prompt是核心

父Agent的Prompt直接决定了任务拆分的质量。一定要:

  • 明确角色和任务
  • 提供清晰的拆分原则
  • 给出具体示例
  • 定义严格的输出格式

2. 接口要简单

子Agent的接口要简单清晰:

  • 输入:任务描述
  • 输出:精炼的研究发现
  • 不要传递过多的背景信息

3. 用好工具

LangGraph和LangGraph Studio能大大提升开发效率:

  • 可视化调试
  • 性能分析
  • 单步执行

不要试图自己从头造轮子。

4. 从简单开始

先实现一个简单版本:

  • 单个父Agent + 两个子Agent
  • 线性执行(不并行)
  • 手动管理状态

验证可行后,再逐步增加复杂度。

5. 持续优化

多Agent系统不是一蹴而就的:

  • 记录每次调用的耗时和结果
  • 分析哪些子Agent经常失败
  • 优化Prompt和调用策略

结语

这个系列写到这里,Deep Research Agent的实战部分就差不多了。

从最初的单Agent架构,到今天的多Agent系统,我们一起探讨了如何构建一个强大的研究型Agent。

回顾这三篇文章,核心思路其实就一个:通过合理的架构设计,让Agent更好地完成任务。

单Agent适合简单场景,多Agent适合复杂的多主题任务。没有绝对的优劣,只有适不适合。

今天我们重点讨论了:

  • 父子Agent的交互机制
  • 父Agent的Prompt设计
  • LangGraph Studio的使用
  • Fairy项目的实战案例

这些内容都是我在实际项目中的经验总结,希望对你有帮助。

接下来,我会写一篇更基础的文章,来拆解什么是Agent的核心。

很多朋友在问我:"Agent到底是个什么?和普通的AI应用有什么区别?"

这篇文章,我会从第一性原理出发,聊聊Agent的本质。

敬请期待。


系列文章回顾

这个系列一共三篇文章,记录了我构建Deep Research Agent的完整实践过程:

  1. 《Deep Research Agent 实战(一):基础架构》- 从0到1构建研究型Agent
  2. 《Deep Research Agent 实战(二):实战应用》- 在真实场景中的应用
  3. 《Deep Research Agent 实战(三):多Agent系统构建》(本篇)- 应对复杂场景的解决方案

如果你对这个主题感兴趣,建议按顺序阅读,会有更完整的理解。


关于作者

我是代码里程碑,一个AI Native Coder。这个系列文章记录了我构建Deep Research Agent的实践过程。

如果你对Agent系统感兴趣,欢迎在评论区交流。也欢迎关注我的公众号,获取更多AI实战经验。


参考资源