子Agent、Fork还是协调者——多Agent模式怎么选,关键看还要不要中间输出
原型做到现在,主 Agent 身上的活越堆越多:查知识库、总结会话、提取用户信息、回复用户,全挤在一个上下文里。
结果上下文涨得飞快。查一次知识库,几十段检索结果原样留在对话历史里,后面每一轮都要带着它们。更麻烦的是,总结、提取这类本来不着急的活,也在主流程里同步跑,用户等回复的时间被白白拉长。
是时候把活拆出去了。但拆成几个 agent、怎么拆、拆完怎么协作,又是一个新问题。于是去看了看 Claude Code 是怎么做多 agent 的。
一、为什么要多 Agent
单个 agent 有两个硬伤:上下文窗口有限,而且没法并行。
而很多任务天然适合分而治之。比如调研一个问题,可以拆成几个独立的子问题同时查;比如一个大的改动,可以一边改一边让另一个 agent 去验证。
不过要先泼一盆冷水。Anthropic 在《Building effective agents》里反复强调的是:先用最简单的方案,确实需要了再加复杂度。多 agent 意味着更多的 token、更难排查的问题,不是 agent 越多越好。
Claude Code 的做法也是这样:核心是一个单体的 Agent Loop,多 agent 是按需叠加上去的。它通过同一个 Agent 工具,提供了三种由轻到重的模式。
二、三种模式
先看一张对比表:
| 维度 | 子 Agent | Fork | 协调者 |
|---|---|---|---|
| 上下文 | 全新的对话 | 完整继承父级 | 每个 Worker 各自独立 |
| 系统提示词 | 子 Agent 自己的 | 继承父级 | 协调者专用 |
| 缓存 | 不共享 | 共享父级缓存 | 不共享 |
| 能否再派生 | 可以 | 禁止 | Worker 不能 |
| 适用 | 独立的小任务 | 需要上下文的并行探索 | 复杂的多步骤项目 |
1. 子 Agent:刚走进房间的聪明同事
子 Agent 拿到的是一个全新的对话,除了你给它的那段 prompt,它什么都不知道。
所以 Claude Code 对写 prompt 的要求是:像给一个刚走进房间的聪明同事交代工作一样。他很能干,但他没参加刚才的会,你得把背景、目标、约束都讲清楚。
好处是主上下文保持干净。子 Agent 在自己的上下文里读了二十个文件、跑了十条命令,最后只给主 Agent 交回一份摘要,那些中间过程全都留在它自己那边。
2. Fork:继承上下文,共享缓存
有时候子任务需要知道前面聊过什么,重新交代一遍成本太高。这时候用 Fork:完整继承父级的上下文,相当于把当前的自己复制一份出去干活。
Fork 有一个很巧的设计:几个 fork 同时出去,它们的请求前缀是逐字节相同的,只有最后追加的那一小段指令不一样。这样所有 fork 都能命中父级的 prompt cache,复制上下文几乎不花钱。
为了保证字节一致,fork 的工具列表和父级完全一样,连"派生 agent"这个工具也在。但 fork 里不允许再 fork,否则就无限递归了。怎么办?工具留着,调用时报错;同时在 fork 的入口写一段很直白的话:
STOP. READ THIS FIRST. You are a forked worker … Your system prompt says "default to forking." IGNORE IT.
父级的系统提示词里说"默认用 fork",fork 继承了这段提示词,就会想继续 fork。所以这里承认矛盾指令的存在,然后显式地覆盖它。这比假装那条指令不存在要可靠得多。
3. 协调者:理解这一步不能外包
最重的是协调者模式。主 Agent 自己不写代码,手里只有派活、发消息、停任务这几个工具,像一个项目经理。
它的工作流分四个阶段:
- Research:派几个 Worker 并行去调研;
- Synthesis:协调者自己把调研结果综合起来,形成理解;
- Implementation:再派 Worker 去实现;
- Verification:最后派 Worker 去验证。
关键在第 2 步。Claude Code 里有一句原则:Never delegate understanding,不要把理解外包出去。
协调者给 Worker 派活时,最常见的错误写法是:
根据你的调研结果,把登录的 bug 修一下。
这等于把"理解问题"这一步也甩给了 Worker,协调者自己什么都没想。正确的写法应该是:
src/auth/session.ts 的 refreshToken() 里,过期判断用的是 < 而不是 <=,
导致 token 恰好在过期那一刻被拒绝。把比较符改掉,在 session.test.ts 里
补一个 exp === now 的单元测试,跑 npm test -- session 确认通过。
可以看到,后者把问题在哪、为什么错、改成什么、怎么验证都写清楚了。协调者真正理解了问题,Worker 才能干好活。
三、怎么选
三种模式看下来,选哪个其实只需要问一个问题:
以后我还需要这些中间输出吗?
不需要,就派出去,只拿回结果。调研的中间过程、读过的文件、试错的命令,对主 Agent 来说都是噪音,留在主上下文里只会占地方。需要,就留在自己的上下文里做。
至于派出去之后用哪种模式,看子任务需不需要现有的上下文:不需要就用子 Agent,需要就用 Fork,任务大到要分阶段、要多人协作,再上协调者。
还有一个更细的视角:委派这件事,其实可以在四个旋钮上分别裁剪:
- 模型:子任务简单,就换一个便宜的模型;
- 工具:只给它完成任务所需的工具;
- 回合数:限制它最多跑几轮;
- 上下文:给全量、给一部分,还是一点都不给。
Claude Code 里有一个 /btw 命令就是很好的例子:用户在 agent 干活的时候随口问一个问题,它会派生一个带完整上下文、但不能用任何工具、只跑一轮的查询来回答。上下文开到最大,工具和回合数拧到最小,既能答得准,又不会干扰主任务。
四、验证 Agent:专治"看起来做完了"
多 agent 里有一个角色我觉得特别实用:验证 Agent。
LLM 做验证有两个典型的毛病,Claude Code 直接在提示词里点名了:
- 验证回避:读读代码,描述一下"应该怎么测",就写个通过;
- 被前 80% 迷惑:界面挺好看,测试也过了,就没注意到一半的按钮点了没反应。
所以验证 Agent 的设定是:
- 只读,不能改代码,但可以在临时目录写测试脚本;
- 至少做一次对抗性的探测,比如并发、边界值、重复提交;
- 最后必须给出结论
VERDICT: PASS、FAIL或PARTIAL,其中PARTIAL只能用于"环境不支持验证",不能用来表达"我不确定"。
最后这条很狠,等于把"含糊其辞"这条退路直接堵死了。
不指望模型不犯错,而是让它知道自己容易犯哪些错,这是一种对 LLM 固有弱点的工程补偿。
五、平级协作:共享任务图,而不是聊天
上面三种模式都是"父派生子"的层级关系。如果是多个 agent 平级协作呢?
直觉上是让它们互相发消息商量。但 Claude Code 在这类场景下的思路是:协作的核心不是聊天,而是一张共享的任务图。
每个任务是图上的一个节点,带着状态、负责人和依赖关系。agent 空闲下来就去图上领一个"没人领、依赖都完成了"的任务,领取是原子的,不会两个 agent 抢到同一个。
模型不用在自然语言里推断"现在哪个任务空着、依赖解开了没有",这件事交给代码。调度和推理分开,并行来自共享状态,而不是来自消息协商。这和我们做分布式任务调度的思路其实是一样的。
六、我们的原型怎么拆
回到开头的问题。照着"还要不要中间输出"这个标准,我把原型拆成了这样:
- 主对话 Agent:只负责和用户对话,上下文里只有对话本身;
- 知识库调研:派给子 Agent,它自己去查、去筛,只把整理好的结论交回来;
- 会话总结、用户信息提取:派给后台的子 Agent,异步执行,不阻塞回复。
第 3 条用上了四个旋钮:
@dataclass(frozen=True)
class SubAgentSpec:
model: str # 模型
tools: tuple[str, ...] # 工具
max_turns: int # 回合数
context: Literal["none", "recent", "full"] # 上下文
MEMORY_EXTRACTOR = SubAgentSpec(
model="cheap-model", # 提取信息不需要最强的模型
tools=("write_memory",), # 只能写记忆,不能调其他任何工具
max_turns=3, # 最多跑 3 轮
context="recent", # 只给最近一轮对话,不给全量历史
)
这样主 Agent 的上下文干净了,回复也不用再等后台的活跑完。
总结
- 多 Agent 是按需叠加的复杂度,能用单个 agent 解决的,就别急着拆;
- 子 Agent 拿全新上下文,适合独立任务;Fork 继承上下文并共享缓存,适合需要背景的并行探索;协调者适合分阶段的复杂项目;
- 选模式先问一句:以后还需要这些中间输出吗?不需要就派出去;
- 委派可以在模型、工具、回合数、上下文四个旋钮上分别裁剪,理解这一步永远不能外包;
- 验证 Agent 要只读、做对抗性探测、必须给出明确结论;平级协作靠共享任务图,调度交给代码,推理交给模型。