模块 05 · 第 6 课

多个智能体一起干活

让一个主管智能体拆分任务,交给三个上下文独立的工人智能体并行完成,再汇总结果。和单个智能体比,词元少了三分之一,主管的上下文只有 472 个词元。讲清楚多智能体真正的价值和代价。

  • 约 40 分钟
  • 难度:进阶
  • 实测:2026-09-14 deepseek-flash

一个人干不完的活,可以分给几个人一起干。智能体也一样:让一个"主管"把大任务拆成几个小任务,分给几个"工人"智能体,各自完成后再汇总。这叫多智能体(multi-agent)系统。

听起来很自然,也很有吸引力,很多框架专门为此设计。但在把一个任务交给一群智能体之前,要先搞清楚:多智能体到底解决了什么问题?代价是什么?这一课用第 4 课的那个任务做一次对比。

几种常见的组织方式

  • 主管和工人:主管负责拆分任务、分派、汇总,工人各自完成一个子任务。子任务之间互相独立时最合适。
  • 流水线:第一个智能体的输出是第二个的输入,比如"研究员查资料 → 写手写初稿 → 审稿人检查"。这其实更像工作流,只是每一步由一个智能体完成。
  • 辩论:几个智能体对同一个问题各自给出答案,互相质疑,最后投票或者由裁判决定。

这一课做第一种,因为它最能说明多智能体的核心好处。

实验:主管加三个工人

任务还是第 4 课那个:"对比 httpx 的 Client 和 AsyncClient 在三件事上的配置方式是否一样:超时、代理、HTTP/2。列成一张表,每一格都注明文档出处。"

这个任务天然可以拆开:超时、代理、HTTP/2 三件事互不相关,可以分别去查。

主管的工作分三步:

def supervisor():
    # 1. 主管拆任务:只输出 JSON,不调用工具
    plan, usage = model([{"role": "user", "content":
                          TASK + '\n\n把这个任务拆成互相独立、可以同时进行的子任务,每个子任务只查一件事。'
                                 '输出 json:{"subtasks": ["子任务1", ...]}'}], None)
    subtasks = json.loads(plan.content[plan.content.find("{"):plan.content.rfind("}") + 1])["subtasks"]

    # 2. 工人并行执行,每个工人都从空白的上下文开始,只看得到自己的子任务
    def work(subtask):
        return run_agent(model, subtask + "\n只回答这一件事,写清楚结论和文档出处(文件名和行号),不超过 150 字。",
                         max_steps=8, verbose=False)

    with ThreadPoolExecutor(len(subtasks)) as pool:
        results = list(pool.map(work, subtasks))

    # 3. 主管汇总:只看工人交回的简短结论,看不到工人查过的原文
    reports = "\n\n".join(f"子任务:{s}\n结论:{a}" for s, (a, _) in zip(subtasks, results))
    final, usage = model([{"role": "user", "content": TASK + "\n\n下面是各子任务的调查结论,据此完成任务:\n\n" + reports}], None)
    ……

工人就是第 2 课写的 run_agent,没有任何改动。每个工人从一个空白的消息列表开始,只知道自己的子任务。要求它们"不超过 150 字",是为了让交回给主管的内容足够精炼。

对照组是单个智能体直接做整个任务。完整代码在 code/05-agents/multi_agent.py

结果

单个智能体:

===== 单个智能体
[第 1 步] grep_docs({"keyword": "AsyncClient"}) → ……
[第 1 步] grep_docs({"keyword": "timeout"}) → ……
[第 1 步] grep_docs({"keyword": "proxy"}) → ……
[第 1 步] grep_docs({"keyword": "http2"}) → ……
[第 2 步] read_doc({"path": "advanced/timeouts.md", "start": 1, "end": 70}) → ……
[第 2 步] read_doc({"path": "advanced/proxies.md", "start": 1, "end": 50}) → ……
[第 2 步] read_doc({"path": "http2.md", "start": 20, "end": 75}) → ……
[第 3 步] read_doc({"path": "advanced/timeouts.md", "start": 70, "end": 120}) → ……
[第 3 步] grep_docs({"keyword": "AsyncClient(proxy"}) → 没有找到 AsyncClient(proxy
[第 3 步] grep_docs({"keyword": "AsyncClient(timeout"}) → 没有找到 AsyncClient(timeout
[第 3 步] read_doc({"path": "advanced/clients.md", "start": 1, "end": 40}) → ……
[第 4 步] read_doc({"path": "async.md", "start": 1, "end": 50}) → ……
[第 4 步] read_doc({"path": "api.md", "start": 30, "end": 50}) → ……
  共 5 次模型调用,21047 词元,9 秒

主管加三个工人:

===== 主管 + 工人
  主管拆出了 3 个子任务:
    - 调查 httpx.Client 和 AsyncClient 在超时(timeout)配置上的方式是否一样,并记录文档文件名和行号
    - 调查 httpx.Client 和 AsyncClient 在代理(proxy)配置上的方式是否一样,并记录文档文件名和行号
    - 调查 httpx.Client 和 AsyncClient 在 HTTP/2 配置上的方式是否一样,并记录文档文件名和行号
  工人「调查 httpx.Client 和 As…」:3 次调用,3943 词元,交回 222 字
  工人「调查 httpx.Client 和 As…」:3 次调用,4506 词元,交回 208 字
  工人「调查 httpx.Client 和 As…」:3 次调用,4094 词元,交回 253 字
  主管汇总时的输入:472 词元
  共 11 次模型调用,13720 词元,8 秒

两份最终回答的结论基本一致:HTTP/2 在文档里有明确依据(http2.md 第 50 行"HTTP/2 support is available on both Client and AsyncClient"),超时和代理的示例只写了 Client,AsyncClient 用同样的参数是推断。

读结果

词元少了三分之一。多智能体用了 11 次模型调用,比单个智能体的 5 次多了一倍多,可总词元反而更少:13720 对 21047。

原因在于上下文的增长方式。单个智能体做到第 4 步时,它的消息列表里已经装着超时、代理、HTTP/2 三个主题的所有搜索结果和文档内容,每一步都要带着这一大堆东西调用模型。而每个工人只装着自己那一个主题的内容,上下文一直很小。三个小上下文加起来,比一个不断膨胀的大上下文便宜。

主管的上下文很干净。汇总时,主管的输入只有 472 个词元:三份各两百多字的结论。它看不到工人读过的那些文档原文。这是多智能体最核心的好处:上下文隔离。每个智能体只看到它需要看到的东西,主管不会被细节淹没。在更长、更复杂的任务里,这个好处会更明显。

时间差不多。三个工人并行执行,所以虽然总调用次数多,总耗时(8 秒)和单个智能体(9 秒)差不多。如果串行执行,就会慢得多。

单个智能体的回答又一次以英文开头。它最终回答的第一句是 "I have enough evidence. Let me summarize.",然后才是中文。上下文里堆满英文文档时,模型的语言容易跑偏,第 4 课也出现过一次。多智能体的主管上下文里几乎只有中文结论,就没有这个问题。

代价

  • 细节会丢。工人只交回 150 字的结论,主管拿不到原文。如果某个工人的结论写得含糊,或者漏了关键的出处,主管没办法发现,更没法纠正。要求工人交回的格式越明确(结论、依据、出处、不确定的地方),这个问题越小。
  • 拆分本身可能出错。这次的任务很容易拆,三个主题互不相关。如果子任务之间有依赖,比如"先找到出错的函数,再看它的调用者",并行拆分就行不通了。
  • 调用次数更多,系统更复杂。每多一个智能体,就多一份提示词要写、多一条执行轨迹要看。出了问题,要先判断是拆分的问题、某个工人的问题,还是汇总的问题。

什么时候用

  • 子任务互相独立,而且每个都要读大量材料。上下文隔离能省钱,也能让每个智能体更专注。
  • 想让一个长任务的主线保持清爽。Claude Code 这类编程智能体会把"去代码库里搜索某样东西"这种子任务交给子智能体,子智能体翻遍几十个文件,只把找到的结果交回来,主对话不会被塞满搜索的中间过程。
  • 不同的子任务需要不同的工具或者不同的提示词。给每个工人只配它需要的工具,比给一个智能体配一大堆工具更不容易出错(第 3 课讲过工具多了的问题)。

大多数任务,一个智能体就够了。先用一个,发现上下文膨胀得太厉害、或者任务天然可以并行拆分时,再考虑拆。

练习

  1. 把工人的"不超过 150 字"改成"不超过 50 字",重新运行。最终回答的出处还完整吗?
  2. 让工人按固定格式交回结论:结论:…… 依据原文:…… 出处:…… 不确定的地方:……。汇总后的回答质量有变化吗?
  3. 设计一个子任务之间有依赖的问题(比如"找出 httpx 里抛出 ReadTimeout 的函数,再说明它会被哪些公开 API 调用"),让主管去拆,看看它拆得对不对。

自测

1. 多智能体的调用次数比单个智能体多,为什么总词元数反而更少?

单个智能体的上下文会不断累积所有子主题的搜索结果和文档,后面每一步都要带着这些内容调用模型。多智能体里,每个工人只装着自己子任务的内容,上下文一直很小,主管也只看到简短的结论。几个小上下文加起来,比一个持续膨胀的大上下文便宜。

2. 多智能体里"上下文隔离"指的是什么?有什么好处和坏处?

每个智能体只看到它需要的信息:工人只看自己的子任务和查到的材料,主管只看工人交回的结论。好处是每个上下文都小而专注,省钱,主管不会被细节淹没。坏处是信息在交接时会丢失,主管无法核对工人没有交回的细节。

3. 什么样的任务不适合拆给多个工人并行完成?

子任务之间有依赖的任务。比如后一步要基于前一步找到的结果,就没法并行,强行拆开,后面的工人会缺少前面的信息。另外,任务本身很简单、一个智能体几步就能完成时,拆分只会增加调用次数和复杂度。

提问与讨论

这一课没看懂的地方,在这里问。看到别人的问题,也欢迎你来回答。

提问 +3 积分,回答别人 +6 积分。内容经审核后公开。

正在加载讨论…