Skip to content

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\):

\[h = s + r + l \]

其中 \(s\) 是序列化成本(把要传的东西写成子级能读的形式),\(r\) 是恢复成本(接收方读到这些内容并重建理解),\(l\) 是损耗(传递过程中丢失的信息造成的返工)。

三项里 \(l\) 最难察觉也最贵。一个典型例子:主 Agent 把「修复登录失败」交给子级,省略了「上次试过改 session 超时,没用」。子级重新试了一遍改超时,浪费一轮。这一轮的成本不包括在任何一次调用的账上,它体现在整体吞吐上。

要压低 \(l\),需要判断「子任务需要多少它自己没有的信息」。设子任务实际需要的外部状态为 \(e\),已传递的状态为 \(p\):

\[\rho = \frac{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 让这件事变难:一条任务被切成若干段,每段有自己的轨迹,而判断失败需要的证据往往跨两段以上。

要支持归因,观测必须满足三个条件。

轨迹可拼接。 每个子级的轨迹带父级标识与派发时的任务描述。少了前者,孤儿轨迹无法归属;少了后者,事后无法判断当时任务的范围到哪。

交接点留痕。 每次派发与每次返回都要记录一条事件,内容含:传递了哪些字段、各字段多大、耗时多久。这条记录是自包含度的唯一实测来源——事后想知道「这次交接是不是漏了东西」,只能看这条。

首错可定位。 一条轨迹内要能标出「第一个不可接受的动作」及其证据位置。这条与交接损耗直接相关:如果交接时漏了关键背景,首错往往落在子级的第一或第二次工具调用上。

一个具体的观测缺口示例:

text
父级轨迹
  ├─ 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 节「任务短于交接开销就不拆」是同一条。