1. 故障排查的分工
任务要求排查一次线上故障:接口返回 502。
主 Agent 把它拆成三件:查日志、改配置、跑验证。三件分别交给三个子级,各自带着一份任务描述独立执行,结果回到主 Agent 汇总。
拆开之后,三件事变了。
上下文不再共享。 查日志的子级读了十几个日志文件、几万行内容,这些内容没有进主上下文——主上下文只收到一句"上游超时导致,源头在订单服务"。这是节省,也是代价:主 Agent 无法回看它到底读了什么。
权限可以分开。 查日志只需要读权限,改配置需要写权限,跑验证需要执行权限。三个子级拿三套工具,互不干扰。
失败可以分开归因。 如果最后没修好,可以分别看三段的轨迹:是日志没看对、配置改错了、还是验证标准本身有问题。
拆开也带来三处会出事的地方。
互相等。 改配置的等验证确认,验证的等改配置通知,主 Agent 在等三方都完成。三方都在等,任务停住。
状态断。 阶段、待办、已改文件都只在各自的会话历史里。查日志的会话超时被压缩,它就不记得查过哪些文件了;换一个子级接手,前面做过的要从头再来。
权限跟错人。 主 Agent 有写权限,子 Agent 也顺手拿到了。它本来只该读日志。
三个问题都不是模型不够强,是归属没定清:控制流归谁、状态归谁、权限归谁。
2. 多 Agent 什么时候真的更好
多 Agent 常被描述成「多个模型一起想,答案更好」。这个说法在工程上不成立:同一个模型跑三遍再投票只能降低采样方差,不能提升能力上限。真正带来收益的是三处分离。
上下文分离。 一次代码库探索可能读几十个文件、几万行内容。这些内容对「改一个函数」这个动作没有用,但只要进了主上下文,就永久占用窗口、破坏缓存。把探索放进子上下文、主上下文只收结论,是实打实的节省。
权限分离。 一个只做代码审查的 Agent 不需要写文件权限。把它限制成只读,即使它被诱导,也改不了代码。效果不止是「更安全一点」:它把一类事故变成不可能。
失败归因。 单 Agent 出错时,你只能看到一段长轨迹和错误结果,很难定位是检索错了、工具选错了还是判断错了。职责分开后,每个子任务的输入输出都被显式传递,出错的那一段可以被单独回放。
三处收益各有对价,而且代价常常被低估。
| 收益 | 直接对价 | 延迟对价 |
|---|---|---|
| 上下文分离 | 子级看不到主历史,任务必须自包含 | 交接需要一次序列化与一次重建 |
| 权限分离 | 每个角色要单独定义工具集,配置成本上升 | 权限不足时的失败要在事后发现 |
| 失败归因 | 需要跨 Agent 的轨迹关联 | 观测系统要支持跨会话拼接 |
第二列是立刻能感觉到的成本,第三列不是。延迟对价的意思是:它在开发时不出现,在运行一段时间后以「怎么总是差一点」的形式出现。上下文分离的矛盾通常在任务写到一半才发现描述不全,权限分离的问题通常在子级静默失败时才发现。
如果这三样收益都用不上,多 Agent 只会增加开销。这就是「先单 Agent,压不住了再拆」的来源——它说的不是保守,是说这三处分离必须有明确的对象。
3. 分离靠什么机制:控制流的四种归属
三处分离里,上下文与权限靠配置就能分开。最难的是第三样:控制流。它回答「谁决定下一步做什么」,而这一样一旦定错,另外两样都救不回来。
控制流可以归四种角色持有。
按「控制流归谁持有」划分,有四种形态。这个划分比「有几个 Agent」更有用,因为控制流归属直接决定了三件事的答案:谁判断完成、谁承担失败、谁有权终止。
中心编排。 调度者持有控制流:它拆任务、派任务、收结果、判断是否继续。子任务之间不直接通信,一切经过调度者。
优点是终止条件明确,调度者说结束就结束。缺点是它成为单点:判断错了,错误会放大到所有子任务,而且它必须理解每个子任务的输出才能正确决策。
对等移交。 当前执行者把控制流整个交给下一个执行者,自己退场。接收方成为新的持有者,直到它再交出去。
优点是链路短、每次只有一方在工作,不需要协调。缺点是「来回踢球」:A 交给 B,B 认为该 A 处理,交回 A,形成循环。必须有规则约束「什么情况下不许交回」。
共享黑板。 没有单一持有者。所有参与者读写同一份共享状态,靠写入竞争推进。协调发生在状态上,不在消息上。
优点是参与者可以并行,不需要两两通信。缺点是写入冲突与活锁:两个参与者反复改写同一字段,或者互相等对方写入某个值。
独立复核。 执行方与判定方分离。判定方不参与执行,只负责否决或放行。
优点是它把「自己检查自己」的偏差消掉——执行者倾向于认为自己的结果是对的。缺点是判定方的信息可能不足:只看到结果,看不到执行过程中的权衡。
四种形态在七个维度上的差别:
| 维度 | 中心编排 | 对等移交 | 共享黑板 | 独立复核 |
|---|---|---|---|---|
| 控制流持有者 | 调度者 | 当前执行者 | 无 | 复核者(仅否决权) |
| 状态归属 | 调度者 | 随控制流转移 | 黑板 | 执行者 |
| 终止条件 | 调度者判定完成 | 无人可移交或目标达成 | 黑板进入收敛态 | 判定给出结论 |
| 失败可见性 | 集中在调度者 | 落在最后一环 | 落在最后一次写入 | 落在判定者 |
| 通信路径 | 星形 | 链形 | 全连接(经由状态) | 单向 |
| 并行度 | 高(子任务并行) | 低(一次一方) | 高但需冲突处理 | 低(判定串行) |
| 恢复方式 | 重派子任务 | 从头重放链路 | 从黑板快照恢复 | 重跑判定 |
这张表解释了一类常见争论:多 Agent 到底该用哪种形态。答案取决于任务里最贵的东西是「时间」「一致性」还是「可信度」。时间贵选中心编排(子任务并行),一致性贵选对等移交(一次一方,没有中间态分叉),可信度贵选独立复核。
4. 控制流归属靠三件事强制
上一节的四种形态是设计意图。设计意图要变成实际约束,靠的是三件事:调用是否阻塞、子级能否反向调用、超时由谁设定。
调用是否阻塞。 父级发出委派后,本轮是等待结果还是继续干别的。阻塞式让控制流天然留在父级:父级不结束,整个链路就不结束。非阻塞式把控制流分出去了:父级要继续,就必须自己维护「哪个子级对应哪个任务」的映射。
子级能否反向调用。 如果子级可以请求父级做事,控制流就有了回边,对等移交的「谁都可以交出去」才成立。如果子级只能返回结果、不能发起请求,就是严格的主从结构,形态被限定在中心编排。
超时由谁设定。 超时是终止权的具体形式。父级设超时,意味着父级有权宣布「不等了」;子级自己设超时,意味着它可以自主放弃。
四家在这三件事上的选择:
| 实现 | 调用是否阻塞 | 子级能否反向调用 | 超时归属 |
|---|---|---|---|
| Claude Code | 阻塞(本轮等待) | 团队模式下可发消息 | 父级设定 |
| Codex | 非阻塞(可先干别的) | 可发消息 | 父级设定,默认 30 秒、可配 10 秒到 1 小时 |
| OpenCode | 非阻塞(作为独立任务) | 不可 | 调用方设定 |
| DeepSeek Harness | 由后端决定 | 由后端决定 | 由后端决定 |
Codex 的三个数值值得记:默认并发上限 4 个线程,等待超时默认 30 秒、允许配置范围 10 秒到 1 小时。这三个数共同定义了一种形态——可以同时跑四个子级,每次等待最多半小时,等待超时不算失败,只是「还没好」。
Claude Code 的阻塞式选择带来一个直接后果:并行子级必须由父级在一次消息里同时发出多个调用,分三轮发就变成串行了。这个约束不是缺陷,是阻塞式的必然结果。
DeepSeek Harness 把这三件事全部交给后端决定,于是同一个框架可以同时支持阻塞的主从结构和非阻塞的并行结构。代价是形态不再是框架的属性,而是部署配置的属性——换一个后端,协作形态就变了。
5. 责任分配依赖状态能否外部化
控制流归属回答了「谁决定下一步」,但没有回答「谁记住已经做了什么」。
这两件事在单 Agent 里是同一件事——执行者既是决策者也是状态的载体。一旦拆开,就必须回答状态存在哪。
状态留在会话历史里。 阶段、待办、已改文件都靠消息承载。谁执行,谁就拥有这份状态。换执行者就断,压缩还可能把它摘掉。
状态外置为任务对象。 阶段、待办、授权、产物登记在独立对象里,执行者只是可替换的资源。换执行者只需重新绑定,崩溃只需重新拉起。
第二种形态带来的直接后果是「三维归属」成立:
| 维度 | 归属 | 换执行者时的影响 |
|---|---|---|
| 上下文 | 执行者 | 断(这是设计应有的行为) |
| 状态 | 任务 | 不受影响 |
| 权限 | 来源 | 不受影响 |
第三行最容易被违反。「父 Agent 有权限,所以子 Agent 也应该有」在直觉上很自然,但这条直觉会带来一个具体后果:父 Agent 被诱导执行了越权动作,子 Agent 会继承这个越权。正确做法是权限随任务走、不随上下文走——任务声明「允许写 src/ 下的文件」,执行者无论是谁、无论看到了什么,都受这条约束。
5.1 状态对象该带什么字段
状态外置不是「把会话历史存到数据库」,需要区分两类内容。
| 内容 | 属于状态 | 理由 |
|---|---|---|
| 目标与范围 | 是 | 换执行者后仍然成立 |
| 当前阶段 | 是 | 是判断下一步的依据 |
| 已完成项 | 是 | 避免重复执行 |
| 待办项 | 是 | 是交接的输入 |
| 授权范围 | 是 | 与执行者无关 |
| 产物路径 | 是 | 换执行者后仍可访问 |
| 失败记录 | 是 | 避免重复踩坑 |
| 工具调用全文 | 否 | 属于轨迹,用于审计 |
| 中间推理 | 否 | 属于上下文,不跨执行者 |
分界线是「换一个执行者之后是否仍然成立」。目标、阶段、授权、产物都成立;工具调用全文和中间推理不成立——它们是某一次执行的产物,留给轨迹即可。
这个区分有个实用后果:状态对象的体积有上界(几十个字段),而轨迹的体量随任务时长线性增长。把两者混在一起,状态对象会变成第二个轨迹,读写成本随之上升。
5.2 迁移只做一半比不做更糟
从「状态在会话」迁移到「状态在对象」,需要三件事同时完成:把会话里已被归纳的结论抽出来、给它定字段、让执行者每轮从对象读取,不从历史推断。
少做第三件会留下一个隐蔽故障:状态写进了对象,但执行者仍从会话历史推断当前阶段。两份状态会分叉,而且没人知道哪份是真的。表现出来是「它有时记得改过,有时重改一遍」。
这个故障的诊断成本很高,因为两份状态都在正确的地方——只是没有被谁指定为准。修法只有一条:指定唯一来源,另一份要么删掉,要么降级为只读参考。
6. 交接成本的结构
状态外置之后,每次交接都要显式传递信息。设一次交接的总成本为 \(h\):
其中 \(s\) 是序列化成本(把要传的东西写成子级能读的形式),\(r\) 是恢复成本(接收方读到这些内容并重建理解),\(l\) 是损耗(传递过程中丢失的信息造成的返工)。
三项里 \(l\) 最难察觉也最贵。一个典型例子:主 Agent 把「修复登录失败」交给子级,省略了「上次试过改 session 超时,没用」。子级重新试了一遍改超时,浪费一轮。这一轮的成本不包括在任何一次调用的账上,它体现在整体吞吐上。
要压低 \(l\),需要判断「子任务需要多少它自己没有的信息」。设子任务实际需要的外部状态为 \(e\),已传递的状态为 \(p\):
\(\rho\) 的判定分三档,而且判据不是数值本身,是「子级能否自己获取缺失部分」:
| 区间 | 含义 | 处理 |
|---|---|---|
| \(\rho \le 1\) | 自包含 | 直接交接 |
| \(1 < \rho \le 3\) | 缺的部分可由工具补齐 | 交接时说明「去哪查」 |
| \(\rho > 3\) | 缺的是判断依据,工具补不到 | 必须补齐再交接,否则必然返工 |
三个算例。
探索子任务。 任务是「找出支付回调的处理函数,列出定义与调用点」。它需要的外部状态是仓库本身,按探索次数估算 \(e \approx 5\);已传递的是「目标、范围、输出格式」,\(p \approx 1\)。\(\rho = 5\),落在第三档。但这里可以接受:探索子任务有检索工具,缺失的是「查哪」,它可以自己查。所以真正的判据是 \(\rho\) 与「可查性」的乘积——\(e\) 里能被工具补齐的部分不计入。
修正后的判据是:只把「工具补不到的部分」计入 \(e\)。探索子任务里工具补不到的部分接近零,因此实际 \(\rho \approx 1\)。
修改子任务。 任务是「按这份方案改三个文件」。如果只传文件列表,\(e\) 包含方案意图、改动范围、验收方式,工具补不到(这三样不能从代码里查出来),\(p\) 只有文件列表,\(\rho\) 明显超过 3,返工概率很高。补上方案与验收标准后降到 1 附近。
复核子任务。 任务是「检查这次改动有没有引入回归」。\(e\) 包含改动前的状态、改动的意图、判断标准。如果子级继承了父级前缀,\(p\) 已经包含前两项,只需补判断标准;如果全新建,三项都要传,且前两项很难完整传递——这是复核型路线通常选「继承前缀」的原因。
三个算例的结论:\(\rho\) 的大小要看子级能自己获取什么。能自己查的(代码、文档、历史日志),\(\rho\) 大可以接受;不能自己查的(决策意图、历史教训、用户偏好、判断标准),必须压到 1 以下。
7. 不结束的三种形态
控制流归属不清、状态分叉、损耗过高,三者的共同表现都是「不结束」。
互相等待。 A 等 B 的信号,B 等 A 的通知。成因是没有任何一方被指定为「判断何时结束」的持有者。判据:任何等待关系里,是否存在至少一方有超时或轮次上限。
重复委派。 A 把任务交给 B,B 认为该 A 处理,交回 A,A 再交给 B。判据:交接路径上是否有「同一任务不得两次经过同一 Agent」的约束。
无限复核。 复核者不断发现问题,执行者不断修改,没有收敛判据。判据:复核轮次是否有上限,以及超出上限后由谁裁决。
三种死锁的修复方式都是显式指定终止权:
| 形态 | 终止权归属 | 附加约束 |
|---|---|---|
| 互相等待 | 指定一方为持有者,其余为被通知方 | 持有者设轮次上限 |
| 重复委派 | 链上记录已访问的 Agent | 重复即拒绝,并回报原因 |
| 无限复核 | 预设轮次上限 | 超限转人工,或按最后一版放行并记录 |
第三条的「按最后一版放行并记录」值得解释:当复核无法收敛时,与其无限循环,不如放行并记录「这是第 N 版,复核未通过但已放行」。这比卡住更诚实,也让后续可以追查。放行不等于放弃——它把判断从「继续修」转成「带着已知问题交付」,后者是可审计的。
8. 失败模式与责任归属
四类失败,每一类都能追溯到归属没定清。
责任稀释。 症状:出了错,每个 Agent 都能说「这不是我负责的」。成因:控制流持有者不明确,或者持有者没有权限约束下游。修的位置在形态选型——中心编排把责任放在调度者,对等移交放在最后的持有者。
权限漂移。 症状:子级做了父级不该做或不会做的事。成因:权限随上下文传播,没有随任务声明。修的位置在权限模型——把权限做成任务的属性,不继承。
汇总失真。 症状:多个子任务的结果都正确,合并后结论错误。成因:汇总时丢掉了限定条件(「在测试环境」「只对某类订单」)。修的位置在回报结构——要求回报必须带条件与来源,不只给结论。
终止缺失。 症状:任务长时间不结束,或反复在两个 Agent 之间往返。成因:没有指定终止权持有者。
四类里,前两类是设计问题,后两类是实现问题。它们的共同表现是:系统在动,但没人知道该停下来或该改哪里。
8.1 四类失败的检测方式
每一类都有便宜的检测手段,不必等出事。
| 类型 | 检测方式 | 成本 |
|---|---|---|
| 责任稀释 | 抽一条失败任务,问「这该由谁负责」 | 一次询问 |
| 权限漂移 | 对比子级的实际动作与任务声明的范围 | 一次日志比对 |
| 汇总失真 | 检查回报里有没有限定条件字段 | 一次字段检查 |
| 终止缺失 | 统计任务的轮次分布,看长尾 | 一次统计 |
第一条的检测很像管理动作,但它有技术含义:如果一个失败任务找不到明确的责任方,说明控制流归属在实现里已经模糊。这不是组织问题,是设计问题的外显。
第二条可以自动化:任务声明了「只写某目录」,就检查它的所有写动作是否都在该目录内。越界即漂移。
第三条的判据是回报结构。如果回报里只有结论没有条件,汇总失真必然发生——不是可能,是必然,因为汇总时没有任何依据判断「这条结论在什么条件下成立」。
第四条看轮次分布的长尾。正常任务的轮次分布应当有一个明确的峰;出现长尾说明部分任务在反复往返。长尾任务的占比超过 5% 就该查。
8.2 责任归属与可观测性
四类失败中,「责任稀释」与「终止缺失」都指向同一件事:观测不足。
一个具体的例子:任务在两个 Agent 之间往返了七次,最后超时。事后能看到的只有「七次往返」与「超时」两个事实,看不到每次往返时双方的判断依据。这种情况下,责任归属无从谈起。
所以下一节的观测三项不是可选的附加功能,它们是责任归属的前提。没有轨迹拼接就没有「谁在什么时候做了什么」;没有交接留痕就没有「信息是否传到位」;没有首错定位就没有「从哪里开始错」。
三者合起来才能回答一个具体问题:这次失败该改派发方、改执行方,还是改交接协议。
9. 跨 Agent 的观测
失败归因要求「能定位到出错的那一段」。多 Agent 让这件事变难:一条任务被切成若干段,每段有自己的轨迹,而判断失败需要的证据往往跨两段以上。
要支持归因,观测必须满足三个条件。
轨迹可拼接。 每个子级的轨迹带父级标识与派发时的任务描述。少了前者,孤儿轨迹无法归属;少了后者,事后无法判断当时任务的范围到哪。
交接点留痕。 每次派发与每次返回都要记录一条事件,内容含:传递了哪些字段、各字段多大、耗时多久。这条记录是自包含度的唯一实测来源——事后想知道「这次交接是不是漏了东西」,只能看这条。
首错可定位。 一条轨迹内要能标出「第一个不可接受的动作」及其证据位置。这条与交接损耗直接相关:如果交接时漏了关键背景,首错往往落在子级的第一或第二次工具调用上。
一个具体的观测缺口示例:
父级轨迹
├─ 12:00:01 派发 → 子级 A(任务:修复登录失败)
│ 传递字段:目标 1 项、范围 1 项、验收 0 项
├─ 12:00:45 子级 A 返回(结论:已修复,改了 session 超时)
└─ 12:03:10 父级发现未修复,重派 → 子级 B
子级 A 轨迹
├─ 12:00:05 read config/session.ts
├─ 12:00:12 edit config/session.ts(首次不可接受动作)
└─ 12:00:40 返回结论从这段记录能读出三件事:验收字段为空(自包含度偏低)、首错落在子级的第二次调用、根因在派发时的信息缺失,子级的判断本身没有问题。缺少「传递字段」这项记录,第三条就读不出来——只能看到子级改错了地方。
这三项观测能力的成本不高,但它们决定了多 Agent 系统能不能被排障。没有它们,多 Agent 的失败会退化成「整体表现下降」这种无法行动的结论。
10. 退回单 Agent 的判据
三条判据,任何一条成立就该考虑退回。
子任务之间需要频繁共享上下文。 如果三个子任务每做一步都要看另外两个的中间结果,拆开只会增加传递成本。共享黑板能缓解,但它的协调复杂度通常不低于单 Agent 的长上下文。
任务短于交接开销。 一个子任务如果本身只需要两次工具调用,而交接需要一次上下文重建,拆开就是负收益。粗算:交接成本约等于一次完整的短任务(序列化 + 恢复 + 一次可能的返工),子任务短于这个量级就不值得拆。
权限没有实质差别。 如果所有子角色都需要同一套工具、同一份权限,权限分离的收益为零,剩下的只有上下文分离——而上下文分离用单 Agent 加子任务摘要也能做到,不必引入多 Agent 的编排复杂度。
第三条常被忽略。很多多 Agent 设计里,所有角色的工具集完全一样,只是提示词不同。这种设计等价于用多个提示词跑同一个 Agent:收益仅来自上下文隔离,却要付出全部编排成本。
11. 选错形态之后的迁移
形态能不能中途改,取决于状态存在哪。
| 起点 → 终点 | 可行性 | 主要代价 |
|---|---|---|
| 中心编排 → 对等移交 | 可行 | 调度者要把判断权交出,且要保证链上每环都能判断「该交给谁」 |
| 对等移交 → 中心编排 | 可行 | 需要引入一个能读全链状态的调度者,状态必须先外置 |
| 中心编排 → 共享黑板 | 代价高 | 子任务要改成无状态、可并发,等于重写 |
| 任一 → 独立复核 | 可行 | 只需增加一个只读角色,不动原有结构 |
这张表给出一个实用结论:独立复核是最容易后期加入的形态。它不改动控制流,只增加一个只读判定方。这也是实践中常见的做法——先做单 Agent 或中心编排,发现问题后加一个复核角色,不必一开始就设计多 Agent 编排。
迁移的另一个方向是往回收:从多 Agent 退回单 Agent。这个方向在状态已外置时同样可行,因为状态不在会话里,收回时不会丢。
11.1 迁移的三步
不管往哪个方向,迁移都遵循同一个三步。
第一步,把状态从会话里抽出来。 这是所有迁移的前置条件。状态在会话里时,任何形态变化都会丢状态,因此迁移无从谈起。
抽出状态的成本取决于原来的实现:如果状态本来就是靠消息承载的,需要人工定义字段(第 5 节那张表);如果已经有部分结构化(例如待办列表工具),可以复用。
第二步,改变控制流归属。 这一步只改「谁判断下一步」,不改状态与权限。
具体做法是把「判断何时结束」这个逻辑从一个位置挪到另一个位置。中心编排 → 对等移交是把这个逻辑从调度者挪到当前执行者;对等移交 → 中心编排是反过来。
第三步,调整权限与回报。 这一步常常被省略,但它决定了新形态能不能真正工作。中心编排下调度者需要读全部子任务的输出,因此它需要一份能理解各子任务结果的上下文;对等移交下每环只需要知道「交给谁」,不需要读全部。
三步的顺序不能换。先改控制流而不抽状态,会出现「谁都不记得做过什么」;先调权限而不改控制流,会出现「有权做但不该由它决定」。
11.2 迁移的实例
从「单 Agent 长会话」迁到「中心编排加只读探索子级」。
第一步,抽出状态。原来状态在会话历史里,现在要定字段:目标、已完成项、待办项、产物路径。这一步的产物是一个任务对象,它必须能被单独读写。
第二步,改控制流。调度者(原主会话)保留判断权,新增的是「把探索委派出去」这个动作。探索子级只做检索、只回结论,不参与判断。
第三步,调权限与回报。探索子级只有只读工具;回报要求给绝对路径与行号,不做归纳(归纳由调度者做,因为它有全局视图)。
这次迁移的收益是上下文节省:原来探索读的几十个文件全在主上下文里,现在只有结论进来。
代价是新增了一次交接。如果一次任务只有一两次探索,交接成本可能超过节省的上下文;如果探索很重(读几十个文件),收益明显。
这个判断与第 10 节「任务短于交接开销就不拆」是同一条。