第 3 课:并行化:分段与投票
学习目标:
- 分清并行化的两个变体——分段与投票——各自解决什么问题,并守住「输出由程序聚合」这半句定义划出的边界
- 用
Promise.all 和一个自己写的并发池把分段落成代码,让汇合只回传引用、不回传载荷
- 算清扇出的三笔账(结果回灌上下文、真实产品的并发上界、投票的 N 倍 token),并据此判断一个提案该不该并行
前置要求:读完第 1、2 课,手上有第 2 课那个 runAgent() 封装 | 上一课 第 2 课 << | 下一课 第 4 课 >>
12 份文档,同一条链,排了一个小时
第 2 课那条链现在能跑了:出提纲 → 过一道关卡 → 写正文 → 再过一道关卡 → 查术语。本课把三个阶段的提示词换成审阅用的——抽要点、给改写建议、核对术语,链的形状一根手指头没动。挂到一份文档上跑完,大概六七分钟。
然后产品那边丢过来一个目录:12 份文档,每份都要走同一遍审阅。
你写了个 for 循环,跑起来去泡咖啡。一小时后回来,日志停在第 9 份,第 10 份正在抽要点。
这个小时里,机器几乎一直在等。等第 1 份的 API 返回,才开始发第 2 份的请求;等第 2 份跑完四个阶段,才轮到第 3 份。可以问一句很实在的话:第 3 份文档的审阅结论,有没有一个字依赖第 2 份的结果?
没有。它们是 12 份互不相干的文档,各自的审阅报告谁先出来都一样。链式里那种等待是有理由的——下一步的输入就是上一步的输出,不等不行。这里没有这个理由,这 12 次运行只是被 for 循环排成了队。
本系列第 6 门课已经讲过扇出-汇总的协作形状和多视角投票是怎么回事。这一课把它落成代码,顺便把账算清楚:扇出不是免费的,快是真的快,代价也是真的。
定义:可以同时开工,输出由程序聚合
先把原话摆出来。LLM 有时可以同时处理任务、输出由程序聚合;这个工作流叫并行化,有两个关键变体1:
- 分段(Sectioning):把一个任务拆成互相独立的子任务,并行跑1。12 份文档各审一遍,就是分段。
- 投票(Voting):同一个任务跑多次,拿到多样化的输出1。同一段文案让三个视角各评一次,就是投票。
什么时候用它:拆开的子任务能并行、可以换来速度,或者需要多个视角、多次尝试来换更高置信度的结果1。还有一句容易被跳过、但很值钱的补充——考虑点很多的复杂任务,每个考虑点交给单独一次 LLM 调用来处理,注意力更聚焦,通常表现更好1。这句话是说:分段和投票不只是省时间的手段,把「法务、安全、品牌」三件事塞进一个提示词里,和让三次调用各盯一件事,出来的质量本来就不一样。
定义里还有半句得钉住:输出由程序聚合。扇出去的结果回来之后,是你的代码在做判断、做筛选、做汇总——不是再叫一个模型来读完 12 份报告写个总结。让模型来做汇总是另一个模式,第 4 课的编排者干的就是那件事,这一课先把这条线划清楚。现行的 Claude 平台文档在多代理编排里也列了 Parallelization:并行扇出独立的子任务(检索多个来源、分析不同文件),再由协调者汇总结果2——注意那一版里做汇总的是协调者,而这一课要写的是程序聚合那一版1。同一个词,聚合那一端谁来做,是两码事。
分段:把 for 循环换成 Promise.all
串行版本长这样,总耗时是 12 份之和:
分段版本只改一行,总耗时接近最慢的那一份:
runAgent() 站的还是第 2 课那个封装的位置——一次完整的 harness 循环,内部按 stop_reason 转。本课的桩响应都一步到位(end_turn),所以文末脚本里的版本省掉了工具分支,是个简化版;接真 client 或要用工具时,把第 2 课那个带工具分发的版本原样搬回来。并行化不改这个循环本身的任何一行,它只是不再让这些循环排队。
聚合在下一行发生,做聚合的是这段代码:
这三行里没有第二次模型调用。filter、reduce、一个阈值判断,全是确定性的代码——同样的 12 份报告进去,永远出同样的一行结论。这正是把聚合留在程序里的好处:扇出去的那 12 次是不确定的,汇合这一步是确定的,出了问题你知道该怀疑哪一边。
Promise.all 有个脾气要提前知道:任何一个 promise reject,整个 await 就 reject,其他 11 份哪怕已经跑完,结果也拿不到。审 12 份文档,第 7 份撞上一次 500 就全盘作废、其他 11 份白跑,这个代价不合理。要么换 Promise.allSettled,要么像本课练习那样在每个 worker 内部 try/catch 把失败收成一条记录——扇出的每一路都该能单独失败。
为什么并行不只是快
如果并行只是「同一件事更早做完」,那它就是个性能技巧,不值得单开一课。真正的理由在上下文那一侧。
Anthropic 复盘他们的多 Agent 研究系统时说得很直白:检索的本质是压缩——从庞大的语料里提炼洞见。子代理带着各自的上下文窗口并行工作,同时探索问题的不同侧面,再把最重要的 token 压缩后交给主研究代理。每个子代理还提供了关注点分离——不同的工具、不同的提示词、不同的探索轨迹——这减少了路径依赖,让每一路调查都能独立做透3。
把这两句拆开看,扇出至少买到三样东西:
- 窗口容量。他们把这条架构判断写成了结论:把工作分布到拥有独立上下文窗口的多个代理上,是在为并行推理增加容量3。第 1 课讲过,真正到头的不是窗口大小,是「一个循环」这个形状;扇出绕开单窗口上限的办法不是把窗口撑大,是多开几个。
- 关注点分离。三个子代理拿着不同的工具和提示词,天然不会互相污染。
- 减少路径依赖。一个循环里,第 3 步的判断会被第 2 步的措辞带偏;三条独立轨迹不会共享同一个偏差。
现行平台文档也给了同方向的说法:多个代理可以带着各自隔离的上下文并行行动,这有助于提升产出质量,也能改善完成时间2。注意它把质量放在前面。
速度那一侧他们给了个数字,语境要一起抄下来:他们早期的代理是顺序检索,慢得难受;为了提速引入了两级并行——主代理并行起 3-5 个子代理而不是串着起,子代理并行使用 3 个以上的工具。这些改动对复杂查询把研究时间砍掉最多 90%3。
这个数字必须连着它的三个限定条件用:它是延迟数字,不是质量数字;它限定在复杂查询上(简单查询本来也没多少可并行的);它来自他们自家的那套系统。你把 for 换成 Promise.all 之后能省多少,取决于你的子任务有多少真的独立、每一路有多慢、以及并发被卡在哪——这一课后半段全在讲这个。
第一笔账:汇合会把省下的上下文吃回去
扇出的时候大家都盯着「同时跑几路」,翻车通常翻在回来那一步。
Claude Code 的子代理文档把这个成本写在明面上:子代理完成时,结果会回到你的主对话;跑很多子代理、每个都返回详细结果,会消耗掉可观的上下文4。子代理本来是用来保护主对话上下文的——把探索和实现挡在主对话之外4——可一旦回传的东西太重,保护就反过来了。
同一篇复盘给了处置办法,而且给得很具体:与其要求子代理把所有东西都通过主代理来传达,不如实现工件系统——让专门的代理把产出创建成能独立留存的东西。子代理调用工具把工作存进外部系统,然后只把轻量引用传回协调者3。
翻译成写代码时的一句口诀:传引用,不传载荷。
差别有多大,本课练习里的脚本会给你一个真实数字:8 份产出落盘一共 1668 字节,回到汇合侧的只有 643 字节,而且这个比例随文档变长会迅速拉开——报告长十倍,回传那一坨还是一行摘要加一个路径。谁需要看全文,照着路径去读就是了。
这条路顺带买到别的东西。工作流文档提到,运行时会随着运行推进逐步追踪每个代理的结果,这正是一次运行可以在同一会话内恢复的原因;把工作扇给很多小代理的工作流,因此比一条长代理保住了更多进度5。产出落在外面、逐条留痕——留痕这半句跟本系列第 11 门课讲观测是同一个道理;「中途挂了不用从头再来」那半句,是本系列第 9 门课「让长任务经得起中断」的地盘。
第二笔账:并发从来不是无界的
写下 Promise.all(docs.map(...)) 的那一刻,你其实是在说「并发数 = 数组长度」。数组是 12 还好,是 200 就是另一回事了。
看三个真实产品各自的上界:
- Claude Code:默认情况下,一个会话里有 20 个子代理正在跑时,再用 Agent 工具起一个就会失败,报
Concurrent subagent limit reached,而且错误信息会明确告诉 Claude 不要重试4。
- Claude Code 的工作流运行时:最多 16 个并发代理,当 Claude Code 可用的 CPU 更少时(包括在受限的容器里)会更少5;单次运行总共 1000 个代理封顶5。
- Managed Agents:最多支持 25 个并发线程;协调者可以调用同一个代理的多个副本,这些副本各自开线程2。
三个不同团队、三种不同实现,全都设了上界,数字还都不大。这件事本身就是教学材料:无界扇出是事故,不是优化。(第 4 课会讲一个真实的翻车案例——早期的代理会为一个简单查询起 50 个子代理3。到那时你会发现,「谁来决定起几个」这个问题比「上限是多少」更棘手。)
最省事的限流是分批:
能用,但有个木桶效应:每一批都要等本批最慢的那个跑完,才开下一批。三份文档里有一份特别长,另外两条路就干等着。
并发池没有这个问题——固定开 N 条「泳道」,每条泳道跑完手上这个就立刻从共享游标里取下一个,永远有 N 个在飞:
十来行,不需要装任何依赖。cursor++ 在单线程的 JavaScript 里是安全的——两次 await 之间的同步代码不会被打断,不存在两条泳道抢到同一个下标的情况。练习里你会给它补上 try/catch,让单路失败不拖垮整批。
limit 该填几?没有普适答案,它是你的 API 配额、下游服务承受力、单路耗时三者的交集。但填一个具体的数字,跟不填,是两种工程。
第三笔账:投票要付 N 倍 token
投票的定义只有一句:同一个任务跑多次,拿到多样化的输出1。落成代码也很短:
这里的聚合还是程序做的——filter 加一个阈值。阈值定在几票,是产品决策,写死在代码里,随时可以改、可以对账,不该交给模型临场发挥。高风险场景可以把阈值调到「一票否决」,低风险场景可以要求三票全中才拦。
账很直白:投 N 次就是 N 倍的 token。这笔钱要放到第 1 课那个乘数上一起看——按他们的数据,Agent 用掉的 token 大约是聊天交互的 4 倍,多 Agent 系统大约是聊天的 15 倍;正因如此,多 Agent 系统需要任务本身价值够高,高到能覆盖这份性能提升的成本3。三视角投票的价签是在单 Agent 那个 4 倍上再乘 3;注意别拿 15 倍再乘 3——那个 15 倍里已经含了扇出的账。
所以投票不是「多跑几遍求个心安」,它得换到具体的东西。工作流文档说得比较清楚:把计划搬进代码之后,工作流能施加一种可重复的质量模式,而不只是跑更多代理——它可以让独立的代理对抗式地互审彼此的发现,之后才上报;或者从几个角度各起草一份计划,再互相权衡,这样得到的结果比单趟跑一遍更值得信5。
「独立的代理互审」这几个字,跟本系列第 10 门课那条规矩是一回事:干活的不当自己的裁判。同一个上下文里让模型自查,它多半会为刚才的输出辩护;换一路独立的上下文、换一套提示词,它才有可能真的挑出毛病。投票和互审买的不是「多数」,是独立性。
顺带说一句边界:一个模型的三次调用不是三个独立的判断者,它们共享同样的训练偏差。投票能过滤掉的是采样噪声和单次注意力的疏漏,过滤不掉系统性偏见。别把它当成投出来就一定对的机制。
分寸:独立性是前提,不是可选项
这一课所有的收益都建立在一个前提上,前面反复出现,值得单独拎出来:子任务之间必须真的互不依赖。
Claude Code 的文档在讲多个子代理同时调查时特意补了一句:每个子代理独立探索自己那块,然后 Claude 综合这些发现,而这种做法在各条研究路径互不依赖时效果最好4。反过来的条件写在多 Agent 复盘里:有些领域要求所有代理共享同一份上下文,或者代理之间有很多依赖,这类领域今天并不适合多 Agent 系统;比如大多数编码任务里真正可并行的部分比研究少,而且 LLM 代理目前还不太擅长实时协调和向其他代理委派3。
判断一个提案该不该并行,只需要问一句:第二路要不要等第一路的结论才知道该干什么?
- 要等 → 这不是并行化的形状。上一步的输出是下一步的输入,那是第 2 课的链式。
- 不要等 → 分段。
- 同一件事想要几个独立判断 → 投票。
第一种情况最容易被含糊过去:明明有依赖,却因为「大概能凑合」硬扇出去。结果是几路代理各写各的、彼此不知道对方的结论,汇合时你得手工把矛盾抹平——省下的时间全花在抹平上,还多付了 token。
边界:这一课的拆分是预定义的
最后钉一个词,它是第 4 课的入口。
这一课所有的例子里,子任务是谁定的?是你。12 份文档,是你从目录里读出来的;三个视角,是你在数组里写死的。代码跑起来之前,扇出几路、每路干什么,全都定了。这叫预定义的拆分。
它的对面是:由模型来决定拆成几份、每份干什么。原典把这个差别当成两个模式之间的关键分水岭——编排者-工人工作流里,一个中心 LLM 动态地拆解任务、把子任务委派给工人 LLM,再综合它们的结果1;虽然形状上跟并行化很像,但关键差异在于灵活性:子任务不是预定义的,而是由编排者根据具体输入决定的1。
所以两者的分界不在「同时跑几路」,而在「谁写的那个数组」。数组是你写的,就是这一课;数组是模型当场生成的,就是下一课。上一课的路由已经把一个决策交给了模型(走哪条分支),下一课交出去的是更大的一块:分解本身。
💻 练习
小结
- 并行化的定义是「LLM 有时可以同时处理任务、输出由程序聚合」,两个变体是分段(把任务拆成独立子任务并行跑)和投票(同一任务跑多次拿多样化输出)1
- 它的适用条件是子任务可并行提速、或需要多视角与多次尝试来换更高置信度;考虑点多的复杂任务,每个考虑点单独一次调用、注意力更聚焦,通常表现更好1
- 扇出买到的不只是速度:子代理带着各自的上下文窗口并行探索、再把最重要的 token 压缩回来,还带来关注点分离(不同工具、提示词、探索轨迹)与更少的路径依赖3;把工作分布到拥有独立上下文窗口的代理上,是在为并行推理增加容量3
- 提速的数字要连语境一起记:他们的 Research 引入两级并行(主代理并行起 3-5 个子代理、子代理并行用 3 个以上工具)后,对复杂查询把研究时间砍掉最多 90%3——这是他们自家系统上的延迟数字,不是质量数字
- 汇合是第一笔账:子代理完成后结果回到主对话,很多子代理各自返回详细结果会消耗大量上下文4;解法是工件系统——子代理把产出存进外部系统,只把轻量引用传回协调者3
- 并发从来不是无界的:Claude Code 默认 20 个并发子代理、超了就报错并明说不要重试4;工作流运行时最多 16 个并发代理(CPU 少时更少)、单次运行 1000 个代理封顶5;Managed Agents 最多 25 个并发线程2
- 投票的账是 N 倍 token,要放进那个乘数里一起算——按他们的数据,Agent 约为聊天的 4 倍、多 Agent 系统约为聊天的 15 倍,任务价值得高到能覆盖它3;换来的应该是可重复的质量模式,比如独立代理对抗式互审、或从几个角度各起草再互相权衡5
- 独立性是前提:多个子代理同时调查,在各条研究路径互不依赖时效果最好4;要求共享同一份上下文、或代理之间依赖很多的领域今天并不适合3——有依赖就回到链式的形状
- 这一课的拆分全是预定义的(数组是你写的);子任务不预定义、而由编排者根据具体输入决定,是编排者-工人与并行化的关键差异1,也是下一课的主题
>> 第 4 课:编排者-工人:让分解本身动起来