做了大半年多Agent系统,我越来越觉得这个方向最难的其实不是让单个Agent变聪明,而是让一群Agent像一支团队那样协作干活、还不互相踩脚。这个系列前几篇聊了Harness Engineering的基础框架、工具调用、上下文管理,今天这篇专门聊多Agent系统与Agent蜂群——也就是怎么把多个模型实例编排成一个作战单元,而不是一群各自为战的散兵游勇。
先给个结论:多Agent不是银弹,不能为了包装而硬上。但在任务复杂度超过单Agent能力边界、或者业务链路天然适合并行处理时,蜂群模式能用可控的token消耗换到数量级上的效率提升。这篇文章适合已经在做Agent应用、踩过单Agent上下文溢出和错误累积这类坑的开发者,我会把设计思路、通信机制、容错方案和实战参数都摊开来讲。
1. 多Agent系统到底解决了什么问题
1.1 单Agent的边界不是聪明程度,而是上下文和串行
很多第一次接触多Agent的开发者有个误解:以为单Agent不够聪明,所以需要多个Agent群策群力。实际上主流大模型本身的推理能力已经够强,真正的瓶颈在别处。
第一个瓶颈是上下文窗口。单Agent干活的时候,所有中间结果、工具返回、思考过程都挤在一个上下文里。任务链路一长,早先的关键信息就会被挤到注意力边缘。我做过一个实验:让一个Agent去调研某行业的五个竞品并输出对比报告,任务拆成二十多步之后,它经常把第一家公司的数据记混到第三家公司身上。不是模型变笨了,是信息太多太挤了。
第二个瓶颈是串行效率。单Agent再强也只能一条链跑到底。整理一份包含五十个数据点的周报,必须逐个查数据源,串行跑可能二十分钟,用多个Agent并行查,五分钟就能收齐。
第三个瓶颈是错误累积。单Agent链路里,每一步输出都可能引入小误差,误差顺着链路滚下去,到了最后一步经常面目全非。多Agent系统把链路拆成多个可独立验证的环节,某个环节出错可以单独重跑,不用整个链路推倒重来。
1.2 蜂群模式的三种典型形态
我把实践中见到的多Agent编排形态归成三类:编排式、协商式、流水线式。它们不是互斥的,真实系统里经常混合使用。
编排式是最常用的形态,也叫主管-工人模式。一个主管Agent负责任务理解、拆解和派发,多个工人Agent只负责执行被分配的子任务。好处是逻辑清晰、容易调试、每个子Agent的职责边界明确,出问题能快速定位。坏处是主管Agent可能成为瓶颈,而且如果主管拆解能力不行,整个系统的上限被它卡死。
协商式是让多个Agent对同一问题独立给出结论,再由一个决策Agent或投票机制综合输出。典型场景是代码评审、方案选型这类没有标准答案的任务。好处是能显著降低单次判断的偏差率,代价是token消耗成倍增加——N个Agent讨论同一个问题,成本就是N倍起步。
流水线式是把任务按阶段串成一条链路,每个Agent负责一道工序,前一道的输出是后一道的输入。适合流程极其固定的场景,比如数据处理链路:采集、清洗、分析、可视化,各环节分别由不同Agent承担。好处是结构简单,坏处是灵活性差,改需求等于该流水线。
注意:判断该不该上多Agent,有一个简单标准——如果单Agent把任务做砸的原因是可以归纳为具体某一步的,先修那一步,别急着上蜂群。如果原因分散且并发需求明显,再考虑多Agent。
2. 核心设计:任务拆解、分配与结果聚合
2.1 任务分解策略:静态、动态与递归
任务拆解是蜂群设计的第一个分水岭。拆得好,每个子Agent只干一件明确的事,输出质量自然稳定。拆得烂,子任务边界模糊,Agent之间互相重复劳动,聚合的时候一地鸡毛。
静态拆解是预案式写法。开发者在代码里写死任务流程,比如"调研任务固定拆成五个部分,每个部分一个Agent负责"。优点是完全可控,运行期零决策成本,适合业务规则不会频繁变化的场景。缺点是遇到没见过的任务形态就傻眼。
动态拆解是让一个规划Agent在运行期根据任务内容决定怎么拆。灵活度高,能应对多种任务形态,但你要额外承担规划Agent本身的输出不确定性。规划错了,整个蜂群在错误方向上狂奔,哪怕每个子Agent都干得很完美,结果也是错的。
递归拆解是动态拆解的进阶版:主管Agent拆出子任务后,子Agent发现自己干不过来,可以再往下拆一层,形成树状结构。适合真正复杂的任务,但必须给深度加硬限制。我在代码里默认限制拆解深度不超过3层——超过3层,规划开销和通信复杂度会指数级膨胀,得不偿失。
实操参数上,我建议控制子任务的粒度在一个子任务对应一到三次Agent调用。如果某个子任务需要Agent调用十次以上才能完成,说明拆得不够细;如果子任务里嵌套了三个并列目标,说明拆得太粗。
2.2 扇出与扇入:蜂群并发的基本姿势
扇出(Fan-out)和扇入(Fan-in)是蜂群并发的基本功。描述一下:主管Agent收到任务后,识别出可以并行的子任务,一次性派发给多个Worker同时执行,这叫扇出;所有Worker返回结果后统一收回,由汇总逻辑处理,这叫扇入。
实践中扇出最常栽在并发度上。我一个同事(某开发者)做的某跨平台项目里,最初设置30个并发Worker同时调研30个数据源,结果模型接口直接限流,整批请求打回去重试,实际完成时间反而比15并发更慢。接口能承受的并发不是越多越好,你还要算上重试的概率。我现在的习惯是并发度取接口限流阈值的三分之一到二分之一,宁可慢一点,保证不触发批量重试。
扇入的设计容易被忽略。多个Worker返回结果后,你是不是简单地拼个长文本就丢给下一个Agent?这样做十有八九会把汇总Agent的上下文撑爆。我通常让Worker在返回结果时附带结构化摘要字段,扇入阶段先只看摘要做归类,需要详细内容时再按需拉取完整结果。
顺带说一句同步机制。蜂群里的并行子任务,不需要像多线程那样考虑锁和竞态——每个Worker只对自己那份子任务负责,天然隔离。但扇入阶段要注意乱序到达的问题,尤其是子任务耗时差异明显时。解决方案是给每个子任务一个全局唯一的task_id,扇入时按task_id归位,而不是按返回顺序处理。
2.3 结果聚合与冲突处理
扇入之后,棘手的问题来了:多个Worker返回的结果之间出现冲突怎么办。
比如三个Worker分别调研同一家公司的产品定价,一个返回99元,一个返回109元,一个返回"价格未公开"。你不能简单取多数,因为三个Worker的信息源可能来自不同的页面,时效性不同。
我常用的聚合策略有四种:投票、加权、拼接、二次加工。投票适合结果可离散分类的场景;加权适合各Worker可靠性可以量化评估的场景,比如历史准确率高的Worker权重更大;拼接适合维度互补的调研结果;二次加工是让一个汇总Agent通读所有结果生成最终结论——这是最通用的,也是成本最高的。
冲突处理要提前设计,不要等冲突发生了才想怎么办。我的做法是给每条聚合结果带上来源标记和时间戳。来源标记记录它是哪个Worker、来自哪个信息源,时间戳用于判断信息的时效优先级。这样即使冲突不可调和,至少能追踪到根源,不至于让错误信息默默进入最终产出。
注意:所有Worker的输出结构最好在派发任务时就用JSON Schema约束死。模型输出解析失败是蜂群系统排名前三的故障来源,与其靠运气,不如在任务描述里明确规定输出格式,并在聚合前设置一道轻量校验。
3. 通信与状态同步:蜂群设计的灵魂
3.1 消息协议:先定规则再放Agent跑
多Agent系统里Agent之间的通信,本质上是消息传递。别看每个Agent都是大模型,它们之间没有心灵感应,全靠消息文本互相理解。消息协议设计得差,整个蜂群看起来就像一支语言不通的杂牌军。
我建议至少定义一个统一的消息信封结构,包含id(唯一消息编号)、task_id(所属任务)、agent_id(发送方)、parent_id(父任务)、payload(具体内容)、status(状态)。这样不管什么类型的消息,都能统一追踪和管理。
信封结构具体到代码里长这样:
{ "message_id": "msg_20250101_001", "task_id": "task_a1", "agent_id": "worker_research_03", "parent_id": "orchestrator_main", "status": "completed", "payload": { "result_type": "competitor_analysis", "content": "...", "summary": "..." } }这个设计的好处是:日志系统可以根据message_id串起整条链路,排查问题时一行命令搜出所有相关消息;聚合阶段按task_id归位,不会因为并发返回顺序乱套。
协议还有一个细节容易被忽略:给每条消息加schema版本号。蜂群系统迭代速度快,payload结构说变就变。没有版本号,旧消息和新逻辑混在一起,解析报错时你根本分不清是哪个版本的格式问题。我踩过这个坑之后,所有消息协议默认带version字段。
3.2 状态共享:能隔离就隔离,能只读就只读
多Agent系统里状态同步是个两难的问题。完全共享,Agent之间会互相污染数据;完全隔离,又没法协作。我的平衡方案是:共享上下文只读,写操作走主管。
具体来说,全局共享区放的是任务目标、约束条件、统一的领域术语表这些不变量信息。所有Worker启动时读取一份,干活期间不修改。Worker自己的中间结果和私有笔记放在自己的工作区,只有最终结论才回传给主管。这样做能有效避免一个Worker乱改共享信息导致其他Worker输出全偏的问题。
这里可以类比现实中的团队协作:项目组成员共用一个需求文档,但每个人自己的草稿和工作笔记是私有的,交作业时只提交最终版本。如果大家同时在一个文档里改来改去,到截止日期文档早就面目全非了。
还有个细节:全局KV或数据库作为共享状态载体时,并发读没问题,并发写一定要加锁或使用原子操作。虽然大模型Agent不像传统程序那样高频写状态,但一旦发生并发写冲突,错误极其隐蔽,调试成本很高。我见过一个系统,两个Worker同时往共享配置里写入不同的温度参数,导致下游Agent一会儿按低温策略干活一会儿按高温策略干活,输出结果忽高忽低,排查了整整两天。
3.3 上下文压缩:蜂群能跑多远,全看这步
多Agent系统最大的隐性成本是上下文膨胀。每个Worker返回的结果都是大段文本,几十个Worker的结果汇聚到主管或汇总Agent那里,分分钟几万token打底。如果不做压缩,上下文窗口再大也不够用。
我常用的上下文压缩有三种手段。第一种是模型摘要,让一个轻量Agent把长篇结果压缩成几百字的要点,适合语义密集的内容。第二种是结构化抽取,用正则或指令从结果中提取关键字段填入预设表格,适合格式固定的结果。第三种是混合策略,先结构化抽取关键字段,再对剩余的重要段落做语义摘要,最后拼接成紧凑的中间表示。
实操参数上,我给自己定了一条线:单个Worker返回结果超过2000 token时,必须触发压缩再进入上游。压缩后的摘要控制在原始长度的十分之一到五分之一。如果摘要本身还想再精简,就截断并附上"完整版见工作区xx号",需要时按需取用。
这里要强调压缩时序。我见过很多系统是在所有Worker全部返回后才开始压缩,结果就是聚合阶段一次性扛下所有原始结果的内存和计算开销。正确做法是每返回一个压缩一个,把压缩放到扇入管道的每个入口上,而不是攒到最后一把梭。
4. 容错、限流与并发控制
4.1 子Agent失败:重试、降级还是跳过
蜂群里子Agent会失败,这没什么好丢人的。模型接口超时、解析格式出错、输出内容不完整,原因五花八门。设计容错策略的核心问题是:子Agent失败之后,对整个系统的影响有多大。
我按影响程度分三级处理。第一级是可重试错误,比如接口超时、临时网络抖动,采用指数退避重试,第一次等1秒,第二次等2秒,第三次等4秒,最多重试三次。第二级是内容质量不合格,重试大概率也是同样结果,直接降级——比如让另一个思路不同的Worker重新生成一次,用不同的提示词模板。第三级是子任务本身不可完成,或者连续重试两次都失败,跳过该子任务,在最终结果里标记"该部分数据缺失,原因xx"。
关键原则是:不能让一个子任务失败拖垮整个蜂群。我早期犯过一个错误:某一个Worker连续失败导致整个链路抛异常,其他十几个Worker辛辛苦苦算出来的结果全部作废。后来改成局部失败容忍——子任务失败只影响它自己对应的环节,整条链路继续跑,最终结果里如实标注缺失项。用户拿到报告时看到"某数据源不可用"比看到"系统崩溃,请重试"要宽容得多。
4.2 限流与成本控制:先算账再开跑
多Agent系统的成本控制比单Agent重要得多,因为每次调度都是几十个Worker一起跑,一不小心就是十万token级别的消耗。我见过有人跑一次蜂群任务烧掉上百万token,结果产出内容质量还一般。
成本控制第一件事是算账。做任务规划时,先把预计的Agent调用次数乘上单次调用的平均token数,算出一个总预算。比如50个Worker,平均每个输入3000 token、输出1000 token,加上主管、汇总等环节的消耗,一次任务就是二十多万token。如果这个数字超过你的心理预期,要么减少Worker数量,要么降低任务拆解深度。
第二件事是设定硬上限。代码里给每个任务配置一个最大token预算,累积消耗超过预算后蜂群自动熔断,不再派发新任务。宁可产出不完整,也不能让成本失控。
第三件事是并发限流。除了模型接口本身的限制,还要考虑下游被调用的工具和数据源。比如蜂群要抓取某个网站的数据,下游站点扛不住高频请求,你这边并发开再高也只是徒增失败率。我现在的做法是给每类下游工具配置独立的并发配额,例如外部站点抓取并发不超过3,模型接口并发不超过20,这样整个系统的负载曲线是平滑可控的。
4.3 可观测性:没有trace的蜂群就是一团乱麻
单Agent调试已经够麻烦了,多Agent系统的调试难度是指数级上升的。几十个Agent同时运行,互相之间还有消息往来,出了问题根本不知道从哪查起。所以可观测性在蜂群系统里不是加分项,是必需品。
我的做法是强制要求每次Agent调用都带上全局唯一的trace_id,从任务启动的那一刻开始,贯穿所有子Agent、工具调用和消息传递。每一份日志都记录trace_id、agent_id、当前状态、输入摘要、输出摘要和耗时。排查问题时,输入一个trace_id就能捞出整条链路的所有记录。
日志记录的核心字段我整理了一下:
| 字段 | 含义 | 示例 |
|---|---|---|
| trace_id | 全局追踪标识 | tra_20250101_abc123 |
| agent_id | 当前Agent标识 | orchestrator_main |
| parent_id | 上级Agent标识 | task_runner |
| event | 事件类型 | task_dispatched / tool_called / result_received |
| input_summary | 输入摘要 | 任务:调研竞品A定价,长度:3200token |
| output_summary | 输出摘要 | 返回:竞品A定价区间99-129元 |
| latency_ms | 耗时 | 4500 |
| total_cost_tokens | 累计token消耗 | 18320 |
还有一个小技巧:把任务拆解结构打成日志。主管Agent拆出了几个子任务、每个子任务分给了谁、相互之间的依赖关系是什么。这条日志在排查问题时价值极大——很多时候问题根本不在某个Worker干得不好,而在任务拆解本身就拆错了。有了拆解结构,你能一眼看出某个子任务是不是被重复执行了、是不是有节点被漏掉了。
5. 实操案例:某跨平台系统的Agent蜂群全流程
5.1 需求场景与角色定义
用之前同事实际做过的一个案例来讲流程。某跨平台系统需要一个"竞品分析报告自动生成"模块:给定一个目标产品,系统自动调研若干竞品,从功能、定价、用户评价、近期动态四个维度做分析,最终产出一份结构化对比报告。任务天然适合并行:每个竞品、每个维度之间相互独立,串行做太慢,必须上蜂群。
系统定义了六类Agent角色:规划Agent负责把调研目标拆解成任务树;调研Agent负责采集原始信息,每个竞品配一个;分析Agent负责把调研结果按维度整理成结论,每个维度配一个;汇总Agent负责把多维度结论整合成完整报告;质控Agent负责抽查结果并标注可信度;主管Agent负责整体调度和异常处理。
角色拆到这一步,每个Agent的职责已经足够清晰。我建议在代码里把这个角色配置表用常量定义出来,标注每个角色的系统提示词、输入格式、输出格式、使用的工具集,方便后续调试时单独抽某个角色出来做单元测试。
5.2 调度流程与并发参数
整个流程分五步走。第一步,主管Agent接收任务,配合规划Agent拆解任务树,预计生成12到15个叶子任务。第二步,调研Agent并行执行,并发度设置为4(不是15,因为外部数据源扛不住高频访问)。每个调研Agent限定最多调用20次工具,防止个别Agent陷入无意义深挖。第三步,分析Agent逐个处理调研结果,按四个维度生成结构化分析,输出JSON。第四步,汇总Agent读取所有分析结果,生成完整报告框架并填充内容。第五步,质控Agent随机抽检30%的内容,对明显矛盾或数据缺失的部分标注警告。
预算分配上,整个任务预估消耗token约18万,其中调研阶段占大头约60%,分析阶段占20%,汇总和质控各占10%。遇到异常情况时,无论任务是否完成,总消耗超过25万token一律熔断。这里有个经验:预算不能完全按预估来,要留出20%左右的余量给重试和意外情况。
5.3 实测结果与关键观察
这个系统上线后,单个竞品分析任务从原来单Agent串行的四十多分钟压缩到七分钟左右,提速接近六倍。质量上,由于每个Agent只处理自己负责的局部任务,各环节输出的准确率反而比单Agent长链路高出不少——因为上下文短了,模型注意力更集中。
中间观察到一个有趣的现象:限流阈值从4并发提6并发的尝试中,在数据源没限流的前提下,总耗时反而轻微上升。原因是几个棘手的竞品需要较长的分析时间,多出来的两个并发Worker处理的是简单任务,早早就跑完了,对整体进度没有帮助。这个问题暴露了蜂群的一个本质特性——整体吞吐量由最慢的那个Worker决定,单纯加并发不能解决长尾问题。后续优化方向是把慢Worker的任务再拆细,拆分后并发度保持不变。
5.4 这个方案的可持续优化空间
从这套系统继续往下走,还有几个明确可优化的方向。
第一个是动态任务再分配。现在任务拆完就固定派发了,有的Worker早早干完闲置,有的Worker卡在长任务上。可以做一层轻量调度:某个Worker空闲时,把长任务中尚未完成的部分转移给它。这个优化预计能再压缩10%到15%的耗时。
第二个是沉淀共享知识库。每轮任务里,调研Agent如果发现新的数据源或新的市场信息,可以写回到一个知识库中,下一轮任务直接复用。这相当于给蜂群装了一个长期记忆。
第三个是引入反馈闭环。质控Agent标注的问题可以回传成提示词改进建议,下一轮任务自动优化各角色的系统提示词。这需要跑几轮数据后才能见效,但回报非常直接——我统计过,系统提示词优化两轮之后,同一类型任务的返工率降低了近三成。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
实操中遇到的最典型问题,按出现频率排了个序:
| 现象 | 根本原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| Worker输出JSON解析报错 | 提示词没有约束格式,模型放飞自我 | 查看某Worker的完整输出原文 | 系统提示词加入JSON Schema示例,解析失败自动触发一次格式修复重试 |
| 多个Worker输出互相矛盾 | 信息源不一致或时效不同 | 检查每个结果的数据来源标记 | 汇总阶段引入投票或按时间戳加权 |
| 总耗时没有随并发提升而下降 | 长尾Worker拖慢整体进度 | 分析各Worker耗时分布 | 拆分慢任务,配合动态任务再分配 |
| 系统上下文溢出 | 扇入前没有做压缩 | 检查每个Worker返回消息大小 | 在扇入管道入口强制压缩,超阈值即触发摘要 |
| 十几分钟后熔断,结果不完整 | 预算设置过低或有Worker陷入死循环调用工具 | 检查工具的调用次数分布 | 给Worker设置单任务工具调用次数上限 |
| 同一任务重跑结果差异很大 | 模型输出固有随机性 | 对比两轮各Worker的中间结果 | 对重要任务增加投票Agent或对输出做温度参数调低 |
6.2 几个容易踩的深坑
第一个坑是任务拆解时给Worker派发了模糊任务。比如"调研竞品A的情况",这个任务太大了,Worker不知道要调研到多细才算完。正确拆法是把任务明确到动作级别:"调研竞品A的功能列表,输出包含功能名称、简介、上线时间三个字段的JSON数组"。任务描述越具体,Worker输出越稳定。
第二个坑是忽视了Worker之间的信息不对称。两个Worker分别调研不同竞品时,一个可能已经发现某条通用市场信息,另一个不知道,导致重复劳动。轻量解决办法是在任务描述里给一个"领域背景"字段,所有Worker启动时共享一份简短背景说明,降低信息不对称带来的重复成本。
第三个坑是没有给蜂群设置"退出条件"。有的Worker会因为任务目标太开放而一直迭代优化,永远不返回。给每个Worker设置最大步骤数和时间上限很重要。我在代码里默认的最长执行时间是90秒,超过直接标记为超时并返回已有中间结果,至少留点东西给聚合阶段用,总比毛都没有强。
第四个坑,也是我个人体会最深的:子Agent的失败信息一定不要吞掉。系统刚上线时,我在日志里只记录"task failed"这类笼统信息,结果排查问题时完全没法定位根因。后来改成失败信息必须携带失败阶段、上游消息id、错误原文摘要,排查效率提升了不止一倍。
6.3 排查时推荐的工作方式
如果正在调试一个蜂群任务,我建议先用日志把链路画出来:任务是怎么拆解的、每个Worker的耗时和结果状态、哪一步开始出现异常。绝大部分问题都能在这条链路上看出端倪。
具体操作上,我给日志系统加了几个查询快捷命令。输入trace_id查全链路;输入task_id查询某个子任务的所有相关消息;输入agent_id看某个Worker在某个时间段内干的所有事情。这套检索逻辑在排查多Agent问题时的效率,比肉眼翻原始日志高好几个量级。
关于Agent调试还有一个心得:本地复现时,把模型输出全部落盘存成文本文件,不要只存在内存或数据库里。模型输出的可读性和可检索性,决定了排查问题的速度。某个模型输出出了格式问题,直接翻出原始文件看它究竟输出了什么,比在数据库里捞字段轻松得多。
最后分享一个让我获益最大的设计——给蜂群配一个"死信区"。所有约定了输出格式但解析失败的Worker结果,不直接丢弃,统一丢进一个叫dead_letter的目录。这个设计看似只是给失败结果找了个地方放,实际作用远超预期:定期翻死信区,你能快速发现是哪类任务、哪个Agent、哪种提示词最容易产出畸形结果,然后针对性修复。蜂群跑的轮次越多,死信区沉淀的改进线索越厚实。我现在的习惯是每跑五十轮任务就翻一次死信区,每次都能改掉一两个顽固问题,这个习惯比我花大价钱调提示词模板划算得多。