先给你一个稍微反直觉的结论:多智能体系统能不能扛住复杂任务,很多时候不取决于底层模型有多聪明,而取决于你的 agents 之间有没有真正的 agency,也就是是不是有一个具备统筹权的“总代理”,能像运营团队一样把任务分下去、收回来、对结果负责。我这几轮多智能体改造里反复印证过,凡是把一堆 agents 直接丢进同一条链上、各自为战的设计,最终正确率反而会随着 agent 数量增加而下降。真正能稳定出活的形态,是让一个中心 coordinator 把目标拆清楚、把任务路由给合适的子代理,再通过监控和验证把结果收拢回来。这套组织方式在实践里被叫过很多名字,我这里统一叫它 agency-agents。
如果你刚接触这个方向,可以先把它理解成“代理的代理”:它不是某一个具体的模型,也不是一套封装好的 SDK,而是一种多智能体系统的组织架构。适合正在被“单代理上下文爆炸”“任务拆解不了”“多个 agent 互相推诿”这些问题折磨的开发者读。下面我把从架构设计、通信协议到真实故障排查的完整经验讲透,最后给一个可以直接改着用的小骨架。
1. 为什么单个代理干不了复杂活儿:四种典型失效模式
很多人一开始都觉得,模型能力已经这么强了,让一个 agent 从头干到尾不就行了?真到复杂业务里你会发现,单代理的失效不是偶发,而是必然,只是时间早晚和表现形式不同。
1.1 上下文窗口不是容量,而是预算
单代理最大的隐患是上下文窗口看似很大,实际上是一个会被不断消耗的预算。你有没有遇到过这种场景:任务第一步是读取一份 5 万字的资料,第二步是总结要点,第三步是调用搜索补信息,第四步是根据全部内容写报告,第五步还要检查格式。每一步都会把前面的中间结果重新塞回上下文中,到后面几步时,最初的业务要求早就被挤到“注意力末尾”了,模型开始丢三落四,甚至用幻觉填充细节。
我在一个模拟项目X里做过对比实验:同一个复杂调研任务,单代理在第十五轮工具调用之后,提取关键约束的准确率掉了接近四成;而把任务拆给三个专职代理后,准确率反而稳定在较高水平。原因很简单:每个子代理只需要在足够小的上下文窗口里完成一件事,不会被前序步骤污染。上下文窗口真正合理的定位是“阶段性工作台”,而 agency-agents 的核心工作,就是不断帮子代理清空和重建这个工作台。
1.2 “全栈代理”是一种诱人的幻觉
现在很多产品宣传都在讲“一个 agent 搞定一切”:既能读文件,又能写代码,还能调外部 API。听起来很美好,做起来很痛苦。因为你一旦把所有能力都塞进一个 agent 的 system prompt 和工具列表里,提示词就会变得极其臃肿,模型在每个推理步骤里都要从几百个候选工具里选一个,选错的概率会随着任务复杂度直线上升。
这不是模型不够聪明,而是决策面太宽,注意力被稀释了。做一个专职写文案的代理,它只需要知道几种文案风格和输出格式;做一个专职测试的代理,它只需要知道怎么跑用例、怎么比对预期。把大而全的职责拆分给能力边界更清晰的子代理,每个子代理的决策空间变小、工具列表变短、行为更可控,这才是多智能体架构真正的收益来源。
1.3 没有反馈闭环,再长的链条也只是一条脚本
单代理还有一个天然缺陷:它很少能脱离自身视角完成“自我验收”。让它写完一篇代码,它通常会基于自己的理解说“写完了,应该没问题”,但“应该没问题”不是通过测试得出来的结论,而是模型根据文本分布预测出来的自信表达。你以为自动化已经闭环了,实际上只是“执行”这一半闭环了,验证和纠错完全缺位。
agency-agents 的价值之一,就是可以把“执行者”和“检查者”拆成两个角色。一个代理负责产出,另一个代理用独立的判断标准去挑问题。这种拆法和软件开发里的 code review 是一个逻辑:作者不能既是运动员又是裁判,至少不能只靠同一个模型瞬间完成这两件事。独立验证角色的加入,会让整个系统在出现低级错误时及时刹车。
1.4 agency 说的是“动作空间”,不是“堆人头”
最后一个失效模式,也是最难被察觉的:把多个 agent 串起来之后,系统并不天然就更聪明。如果 agent A 的输出直接丢给 agent B,B 又丢给 C,中间没有任何人对任务目标负责,那这就是一条更长的链条,错误会被逐级放大。
agency-agents 强调的是“代理拥有施动能力”:总代理要想清楚目标是什么、资源边界在哪、每个子代理用什么标准产出自认为合格的结果。它不是在参与某个步骤的执行,而是在管理整条链路的推进状态。就像一家公司,不会因为员工数量多就自动高效,必须有人排优先级、管进度、做验收,这个角色就是我们要重点设计的中心代理。
2. 核心骨架:调度器、注册表、共享状态怎么串起来
搞清楚为什么需要 agency-agents 之后,下一步就是设计系统的骨架。我见过很多团队一上来就画复杂的流程图,结果代码里根本没有对应的结构。你先记住五个必备组件就够了:调度器、代理注册表、任务队列、共享存储、可观测日志。
2.1 五个组件各司其职,缺一个就会出事
这五个组件的职责,一定要在项目早期就分清楚。我用一张表带你过一遍。
| 组件 | 核心职责 | 常见误区 |
|---|---|---|
| 调度器 | 负责接收用户任务、拆解计划、路由委派、聚合结果 | 把调度器当成“万能的执行器”,什么都自己干 |
| 代理注册表 | 登记所有子代理的能力、入参、输出格式、并发限制 | 注册表只存名称,没有能力描述,路由没法选 |
| 任务队列 | 承接待执行子任务,控制并发和优先级 | 直接同步调用子代理,失败时全链路阻塞 |
| 共享存储 | 保存长文本、中间产物、共识状态,供多个代理协作 | 让所有子代理共享同一份超大上下文 |
| 可观测日志 | 记录每个任务从产生到完成的全链路 trace | 只在出问题时才加日志,排错成本高到吓人 |
调度器是大脑,注册表是人力档案,任务队列是工作流系统,共享存储是公司共享盘,可观测日志是审计系统。这个类比很粗糙,但对理解架构很有用。只要有一个组件缺失,你就只能在系统出错以后临时补丁式修复,最后整个代码库越改越乱。
2.2 代理注册表要用“自描述清单”,而不是硬编码跳转
我见过很多团队把路由写成疯狂的 if-else:如果任务是写总结就走代理A,如果任务是画图就走代理B。这种写法在子代理只有两个的时候还能接受,一旦超过五个,任何一个新增能力都要改动调度器代码,扩展性几乎为零。
更可靠的方式是把代理描述做成一份自描述清单,让调度器根据“能力语义”动态匹配。你可以定义这样一份注册表 JSON:
{ "agents": [ { "name": "text_writer", "description": "擅长根据结构化大纲生成通俗易懂的中文文案", "capabilities": ["文案创作", "报告撰写", "内容改写"], "input_schema": { "type": "object", "properties": { "outline": {"type": "string"}, "tone": {"type": "string"} } }, "max_concurrency": 2 }, { "name": "image_designer", "description": "擅长根据文案主题生成配图方案并输出绘图参数", "capabilities": ["配图设计", "视觉方案"], "input_schema": { "type": "object", "properties": { "theme": {"type": "string"}, "text": {"type": "string"} } }, "max_concurrency": 1 } ] }这套描述的价值是什么?它把“路由决策”从代码搬到了数据上。调度器读取每个代理的 capabilities,再结合用户当前任务的意图去匹配,匹配不到就走兜底策略。以后要加一个代码审查子代理,只需要在注册表里新增一项,调度器代码一行都不用改。
2.3 调度器自己也要是一个代理,但它不是全知的神
调度器本身通常也是由一个大模型驱动的代理,负责规划任务、选择子代理、处理异常。但你要特别注意,它不该事必躬亲地去执行所有子任务。它是个项目经理,不是低头写代码的程序员。
实操里我会把调度器拆成两步:第一步是“生成计划”,第二步是“执行计划”。生成计划时,它面对的是原始用户需求;执行计划时,它面对的是任务列表和注册表。这两步之间的状态转换如果做清晰,调度器就不会在执行计划的过程中又把目标带偏。
调度器还需要具备“下钻”的能力:当某个子代理返回的结果质量不达标时,它可以决定是让原代理重试一次,还是换一个代理来处理,或者修改子任务描述后再试。这个决策逻辑不是简单的轮询,而是带优先级判断的异常处理分支。
3. 从任务到结果回收:一次委派到底要拆几步
很多人以为“把任务发给子代理”只是一次 API 调用,其实一次完整的委派生命周期至少要经历五个阶段:任务归一化、计划拆解、选择代理、执行与监控、结果回收与合并。每一步都有值得深挖的细节。
3.1 任务接单之后先做归一化
用户输入往往很粗糙,比如“帮我做一份关于新品的发布方案”。这种描述绝对不能直接丢给子代理,因为“发布方案”到底包含什么内容、给谁看、篇幅多长、用什么语气,全是未定义状态。调度器要做一次任务归一化:把模糊需求转成结构化的任务说明书。
任务说明书至少要包含四块:目标、约束、可交付物、验收标准。你可以让调度器在接到原始任务后先输出这样一段结构化规划:
目标:生成新品发布方案 约束:面向电商渠道,预算上限 20 万,周期两周 可交付物:一份发布方案文档,包含目标用户、渠道、预算分配、关键节点 验收标准:方案必须包含具体时间点、渠道清单、预算表格,且无前后矛盾这一步看着多余,实际上决定了后面所有子代理的工作质量。子代理拿到清晰的验收标准,就不会在产出过程中自行脑补需求。归一化做得不好,后面每一个环节都会传染上模糊性。
3.2 计划拆解的三条边界:粒度、依赖、可验证性
归一化之后,调度器要把任务拆成若干个子任务。拆解不是拍脑袋切块,而是要满足三条硬边界。
第一是粒度。子任务不能太碎,否则每个任务都要走一遍调度、序列化、结果回收的流程,系统开销会吃掉收益;也不能太大,否则子代理等于又回去做全栈任务。我习惯上会估算每个子任务预计消耗的 token 和工具调用次数,把它控制在一个子代理能“单次专注完成”的量级。
第二是依赖关系。有些子任务之间有天然顺序,比如先做市场调研,才能写文案;先写文案,才能做配图。调度器生成计划时要识别出这种前置依赖,并用一个轻量级的 DAG 结构去管理,而不是简单地把任务塞进队列。依赖错了,整个执行顺序就乱了。
第三是可验证性。每个子任务都要有一个能被外部判断是否完成的标准。不要用“写一段好文案”这种标准,要用“输出文案必须包含三个卖点、两个用户场景、一个行动号召”这种可检查的标准。这样验收环节才不会变成纯粹的模型自嗨。
3.3 路由与并发:不是所有子任务都要顺序执行
计划拆好之后,调度器根据注册表里的能力描述来路由。这里我强烈建议你给路由过程加一个“副手思路”:先按能力匹配候选代理,再按当前负载、历史成功率、预算消耗做排序,而不是盲选第一个匹配项。
并发同样要控制。agency-agents 系统里,多个子代理同时工作会显著提升速度,但也带来资源竞争和状态混乱。我在一个跨部门协作模拟系统里用过两层并发控制:调度器只负责给每个任务设定最大并发数,真正的并发执行交给任务队列来管理。任务队列会用标准的生产者消费者模式去消费子任务,这样即使某个子代理阻塞,也不会拖垮整个调度器。
子任务发起后,调度器要给每个任务设置超时时间。我一般取该任务预估耗时的 1.5 到 2 倍,超时后自动标记为失败,而不是无限期等待。没有超时机制,一个卡死的子代理会把整条链路拖到彻底不可用。
3.4 结果回收:合并不是简单拼接
子代理陆续返回结果后,调度器要做一次“收拢”。收拢不是把结果拼在一起就完了,而是要解决三个问题:冲突、重复、质量。
冲突是指多个子代理对同一事实给出不同结论。比如调研代理说目标用户是 18 到 25 岁,另一份数据代理给出的结论是 25 到 35 岁,调度器就要根据数据可信度、信息来源优先级去裁决。这里我一般会让调度器调用一个独立评审代理,而不是自己拍板。
重复是指多个子代理产出了功能重叠的内容。合并时要按内容哈希或主题相似度去重,避免最终报告里出现两段几乎一样的论述。
质量问题则要求调度器对每个子任务结果做一次“格式校验和验收标准比对”,不合格的直接走重试分支。很多团队把这一步省掉了,结果总代理拿到一堆漂亮的中间产物,最后合并出来的却是错误百出的成品。
4. 多个代理之间怎么通信:消息信封、记忆和上下文压缩
当系统里的子代理超过三四个,通信协议就变成一个必须严肃对待的设计点。没有统一协议,每个代理的输入输出格式都靠现场商量,系统很快就变成一团乱麻。
4.1 统一的消息信封:task_id、parent_id、trace_id
所有任务在下发和回传时,都应该用一个统一的消息信封包住。信封里至少要有三个字段:
{ "task_id": "task_10001", "parent_id": "task_10000", "trace_id": "trace_8888", "capability": "文案创作", "payload": { "outline": "...", "tone": "professional" }, "status": "pending" }task_id 用来标识当前任务,parent_id 用来记录它是从哪个任务拆出来的,trace_id 是整条链路的总编号。这三个 ID 串起来,你才能在日志里完整回放“这个结果到底是怎么一步步生成的”。
没有 trace_id 的 multi-agent 系统是最难排查的。线上出问题时,你只能看到一个最终结果,却不知道是哪个子代理在哪个环节产生了错误。只要通信带上 trace_id,配合日志系统,任何一个问题的排查都能从“大海捞针”变成“按图索骥”。
4.2 上下文压缩:子代理只回传摘要,关键证据进共享存储
多个代理协作时,最忌讳的是把完整上下文层层回传。A 完成了十万字摘要,传给 B 时如果把这十万字全带上,B 的工作台立刻饱和。我采用的策略是“摘要上卷 + 证据外置”。
子代理完成任务后,除了返回结果本体,还必须生成一个简短的执行说明,包括它读到了什么关键信息、做了什么决策、哪些地方存在不确定性。这份说明会被调度器汇总给下一层,但原始材料、中间表格、完整代码这类内容则放进共享存储,只在需要时按 ID 取用。
这有点像公司的汇报机制:部门负责人向上汇报只需要讲结论和风险,具体材料留在资料库,谁需要谁自己查。这样每一层的上下文占用都保持在一个很低的水平,整个链路也能跑得更长。
4.3 共享状态要隔离,不要给所有子代理一个公共上下文
共享存储解决了“长文本放哪儿”的问题,但也带来了新的陷阱:子代理之间不能共享同一个上下文。如果所有子代理都能读到彼此的原始上下文,它们会互相干扰,还可能基于过时信息做决策。
更合理的做法是给每个子任务划分独立的读写区间。一个子代理只能读它任务所需的数据片段,写也只能写到自己的结果区。如果多个子代理需要更新同一份状态,就通过版本号或锁机制来确保不会互相覆盖。没有这个隔离设计,共享存储迟早会变成脏数据的垃圾桶。
4.4 独立验证代理:防止“模特判断模特”
我特别建议在这个架构里单独安排一个验证角色,它不是执行者,而是只对结果做检验。执行子代理说“写完了”,验证代理要独立检查内容是否符合验收标准、有没有事实错误、有没有格式问题。
很多团队会觉得这太浪费 token,但事实上,让两个角色各司其职比让一个模型又做运动员又做裁判更省心。因为验证代理的任务定义是全新的,它会带着“挑刺”的预设去看结果,而不是像执行代理那样带着“完成目标”的预设去自我肯定。
我通常用一套硬编码可检查规则给验证代理打底,比如长度范围、关键字段是否存在、数字是否一致、格式是否合法。硬规则过了以后,再让模型验证代理做语义层面的检查。这种“硬规则 + 软规则”的双层校验,能把大多数荒谬错误挡在进入总结果之前。
5. 最容易翻车的三类故障:递归失控、空转与幻觉式成功
真实跑起来之后,问题从来不会按教科书出现。我在多轮迭代中踩过不少坑,下面这几类故障的频率最高,也最容易被误判为“模型能力不够”。
5.1 递归失控:子任务拆到永远停不下来
调度器拆任务,子代理也可能拆任务,拆出来的子代理再拆任务,系统会进入一种可怕的递归:每个代理都觉得自己手里的活儿还应该再拆一层,结果任务深度越来越深,却始终没有真正的执行发生。
我见过一次特别典型的崩溃:一个写报告的任务被拆成了“写引言”“写背景”“写方法论”“写结论”,其中“写背景”的子代理又把这部分拆成了“行业背景”“公司背景”“竞品背景”,每个背景再拆成“数据收集”“信息整理”“观点提炼”。拆到第五层时,某个子代理已经不产生任何实际内容了,只是在不停地生成计划。
解决办法是在调度器里强制设置“任务深度上限”和“每层任务数量上限”。我常用的经验值是深度不超过四层,每个节点的直接子任务不超过六个。如果拆解结果超出上限,调度器必须合并部分任务,而不是继续膨胀。这个限制看起来粗暴,但能保住系统的最基本稳定。
5.2 代理空转与重复劳动
另一种高发故障是同一个子任务被执行了很多次,但结果完全一样,系统还在傻乎乎地重试。这往往是因为失败重试逻辑设计得过于简单,只要子代理返回了任何错误,就把它原封不动地再塞进队列。
避免空转的核心是“幂等”:每个任务都带着唯一 task_id,任务队列在执行前先检查该 task_id 是否已经执行过、结果是否已经落库。如果是,就直接取缓存结果,不再重新执行。同时,重试次数要有限制,最多两到三次,而不是无限重试。
我还会在日志里给每个子任务打上“执行次数”标记。如果某个子任务的执行次数超过三次但结果仍然失败,调度器就该换一个策略,而不是继续换汤不换药地重跑。
5.3 幻觉式成功:子代理宣称完成了,实际上什么都没完成
这类故障最隐蔽:子代理没有报错,返回了一堆看起来正常的结果,但仔细一查,它并没有真的执行关键动作。比如它本应去查一个外部数据源,结果模型直接根据训练记忆编了一段数据;本应调用一个工具,结果它说“已调用”却没触发任何副作用。
对付幻觉式成功,靠提示词“请如实报告是否调用成功”几乎没有作用。真正有用的做法是让子代理必须回传可核查的执行证据:它读了哪些文件、写了哪些记录、调用了哪个外部接口、返回状态码是什么。这些证据再通过独立验证代理做抽查,能抹掉大多数“表演式完成”的问题。
我把这类故障单独列成一张排查表,方便在实际系统里定位问题。
| 故障现象 | 典型根因 | 有效手段 |
|---|---|---|
| 子任务拆解无限加深 | 调度器没有深度限制 | 设置 max_depth、max_children |
| 同一任务反复重跑 | 缺少幂等和缓存 | 用 task_id 查重、限制重试次数 |
| 子代理自称完成但证据缺失 | 未强制回传执行轨迹 | 强制记录工具调用和外部返回 |
| 两个代理互相改稿停不下来 | 没有收敛条件 | 设置修改轮数上限或差异阈值 |
| 一个代理失败后全链路崩溃 | 同步调用无超时 | 任务队列异步化并配置超时 |
5.4 共振回路:两个代理互相甩锅
有时候你会看到代理 A 修改了产出交给代理 B,代理 B 觉得不行又退回给 A,A 再修改再给 B,两边来回拉扯十几轮,谁也没有真正解决问题。我把这称为“共振回路”,它极消耗上下文和成本,却几乎没有进展。
解决共振回路的方法很直接:给交互设定收敛条件。比如允许同一条反馈最多往返三轮,三轮之后如果两边判定仍然不一致或者差异不再缩小,就提交给更高层级的仲裁代理做终审。仲裁代理的任务不是继续改,而是必须做出最终取舍并说明理由。这相当于在工作流里设置了一个强制结束按钮,让问题流到决策层。
6. 可直接复制改造的最小骨架与验证步骤
讲完原理和坑,最后给你一套足够简单、又保留了 agency-agents 核心思想的骨架。它不依赖任何重型框架,用标准库就能读明白,跑通之后你可以按照实际业务去扩展。
6.1 用 Python 定义注册表和消息信封
先建立最基础的数据模型。我这里用 dataclass 来表达,目的就是让你看清楚每个字段承担什么职责。
from dataclasses import dataclass, field from typing import Optional @dataclass class AgentManifest: name: str description: str capabilities: list[str] max_concurrency: int = 1 call_count: int = 0 @dataclass class TaskMessage: task_id: str parent_id: Optional[str] capability: str payload: dict status: str = "pending" REGISTRY = { "text_writer": AgentManifest( name="text_writer", description="根据大纲生成通俗中文内容", capabilities=["文案创作", "报告撰写"], ), "image_designer": AgentManifest( name="image_designer", description="将文案主题转化为配图参数", capabilities=["配图设计"], ), }这个设计已经奠定了整个架构的基础:REGISTRY 是唯一的能力来源,TaskMessage 是所有任务流转的统一格式。后面不管加多少子代理,都不用改这个基础结构。
6.2 调度器只负责“选人、派活、回收”
调度器的核心逻辑可以浓缩成一个 dispatch 方法。注意,这个方法里没有写任何具体业务的 if-else,它只做路由决策。
import uuid class Dispatcher: def __init__(self): self.tasks = {} def create_subtask(self, parent_id, capability, payload): task = TaskMessage( task_id=f"task_{uuid.uuid4().hex[:8]}", parent_id=parent_id, capability=capability, payload=payload, ) self.tasks[task.task_id] = task return task def route(self, task: TaskMessage) -> AgentManifest: candidates = [ manifest for manifest in REGISTRY.values() if task.capability in manifest.capabilities ] if not candidates: raise RuntimeError(f"no agent for capability: {task.capability}") # 这里可以继续按负载、成功率做排序 return candidates[0] async def dispatch(self, task: TaskMessage): manifest = self.route(task) result = await self.execute_agent(manifest, task) task.status = "succeeded" return task.task_id, result async def execute_agent(self, manifest: AgentManifest, task: TaskMessage): # 这里接真正的子代理调用,或者调用外部模型 raise NotImplementedError这段骨架最大的价值,是把“路由”和“执行”隔离开。你在 execute_agent 里可以去接任何子代理实现,也可以先写死返回一串假数据做链路测试,调度器本身不用动。
另外,你可以在 execute_agent 里顺手做几件重要的事:记录调用开始时间、检查 manifest.call_count 是否超过 max_concurrency、给 TaskMessage 打上 meta 信息。这些细节会让系统在扩展时少踩很多坑。
6.3 先用五组模拟场景做验证,再接入真实模型
不少团队拿到骨架以后第一件事就是接大模型 API,我劝你先别急。先用五组模拟场景把链路里的调度逻辑测稳,再上真实模型。
第一组:两个互不依赖的子任务,派给两个不同 agent,看它们是否按并发限制执行,结果是否能正确合并回父任务。
第二组:注册表里查不到对应能力,调度器是否抛出明确异常,而不是放任任务丢失。
第三组:一个子任务执行时间超过超时阈值,调度器是否把它标记为失败,并且不阻塞后面的任务。
第四组:同一个 task_id 被重复提交,系统是否直接返回缓存结果,而不是重跑。
第五组:把 task_id、parent_id、trace_id 打点日志整理出来,检查整条链路是否能完整回放。
这五组场景跑通以后,你再去接真实模型,之后遇到的绝大多数问题都能快速定位是模型问题还是架构问题,而不是一团乱麻查无可查。
最后再分享两个让我少走非常多弯路的小细节:第一,所有子代理回传的结果里,永远要带上“置信度”或“不确定点”,这比单纯给结论有用得多;第二,日志里不要只记成功和失败,还要记录“为什么失败”和“失败前最后一步做了什么”,这两个信息在多智能体排障时比任何指标都值钱。