在编码 Agent 工程里,"子 Agent(subagent)"不是把一个大模型换成几个小模型那么简单。它本质上是一种上下文与算力的隔离手段:每个子 Agent 拥有独立的上下文窗口、独立的工具结果流,只把"结论"回传给主循环。理解这一点,才能把子 Agent 用在刀刃上,而不是为了并行而并行。

子 Agent 到底解决什么问题

主循环(main loop)的上下文是稀缺资源。一旦它被几十个文件的原始内容、grep 的噪声输出、失败的尝试塞满,模型的注意力就会被稀释,KV 缓存也会因为频繁变动而失效。子 Agent 的核心价值,是让这些"脏活"在别处发生,主循环只接收提炼后的答案。

具体有三类典型用途:

  • 用便宜模型跑子任务:主循环保持单一模型(保护其上下文缓存的稳定性),把"读 20 个文件找出哪个实现了某接口"这种体力活交给一个 haiku 级别的子 Agent。主循环模型不变,缓存命中率高,整体既快又省。
  • 并行 fan-out:面对多文件、多候选、多假设时,同时派出多个子 Agent 各跑一份,最后汇总。
  • orchestrator-worker 编排:主 Agent 作为编排者只负责拆解任务、分派、合并结果,真正干活的是 worker。

fan-out:把可并行的部分真正并行

fan-out 的适用前提是任务之间没有共享可变状态、没有顺序依赖。比如"在三个模块里分别定位同名函数的实现",或者"对同一个 bug 生成三种修复候选再择优"。这类任务串行做是线性时间,fan-out 后接近常数时间(受限于最慢的那个 worker)。

一段伪代码说明编排者怎么发散再收敛:

1
2
3
4
5
6
7
8
9
10
11
12
# orchestrator 主循环里
candidates = ["src/auth", "src/billing", "src/gateway"]

# fan-out:同一批 worker 并行派发,各自独立上下文
results = parallel_map(candidates, lambda mod: spawn_subagent(
model="haiku", # 便宜模型跑搜索
task=f"在 {mod} 下找出实现 RateLimiter 接口的类,"
f"只回传 文件路径:行号 和一句话职责,不要贴整段代码",
))

# fan-in:编排者只拿到三段结论,上下文干净
summary = synthesize(results) # 由主循环模型整合

关键纪律有两条。第一,worker 的产出契约要窄——明确要求它只回传结论(路径、行号、一句话),不要把读过的源码原样吐回来,否则 fan-in 时主循环又被噪声淹没,并行省下的上下文又赔了进去。第二,派完就别自己再做一遍。一个常见错误是主循环派出搜索子 Agent 后,自己也忍不住 grep 一遍,既浪费 token 又制造了状态分叉;正确做法是派发后等待结果。

orchestrator-worker:分层而非扁平

当任务有天然层级时——例如"实现一个新特性,涉及数据层、服务层、接口层"——orchestrator-worker 比单纯 fan-out 更合适。编排者维护全局计划与依赖关系,worker 只看到自己那一块的局部上下文。

1
2
3
4
5
6
orchestrator
├── 规划:拆出 3 个子任务 + 它们的依赖
├── worker A(数据层) ← 独立上下文,只给相关 schema
├── worker B(服务层) ← 依赖 A 的产出契约
└── worker C(接口层) ← 依赖 B
└── 合并 + 一致性检查

注意这里 worker 之间有依赖,所以不是纯并行:A 必须先产出"数据层对外的接口契约",B 才能开工。编排者的职责正是管理这种依赖,而不是把所有东西一股脑塞进一个上下文让模型自己理顺。分层带来的副作用是每层上下文都小而聚焦,模型在每一层都不容易跑偏。

一条反直觉但好用的经验:用独立的"fresh-context 验证者"

让一个 Agent 自我批判(“再检查一遍你刚写的代码有没有问题”)效果往往不好——它和刚才写代码的是同一段上下文,带着同样的思维定式和同样的盲点,很容易自我背书。

更可靠的做法是派出一个全新上下文的验证者子 Agent:它不知道之前的纠结过程,只拿到规约(spec)和产出物,被要求独立判断"这个实现是否满足规约"。fresh context 意味着没有沉没成本、没有先入为主,它更可能发现原作者视而不见的问题。

1
2
3
4
5
6
7
8
9
10
# 不要:同一上下文自我审查
self.reflect("再检查一遍上面的实现") # 容易自我背书

# 推荐:fresh-context 验证者
verdict = spawn_subagent(
model="sonnet",
task="只看 spec.md 和 diff,判断实现是否满足每条验收标准;"
"逐条给出 通过/不通过 + 证据,不要假设作者意图",
context=[spec, diff], # 干净上下文,无历史包袱
)

这其实是把"代码 review 要换个人看"的工程常识,迁移到了 Agent 协作上。

什么时候不该用子 Agent

子 Agent 不是免费的:每次派发都有启动开销、序列化开销和合并开销。如果任务很小、上下文也不脏(比如改一个已知文件的一行配置),直接在主循环做更快。判断标准是:这个子任务会不会污染主上下文,或者能不能真正并行——两者皆否,就别拆。同理,能用便宜模型独立完成的体力活才值得外派;需要全局视野的决策应该留在主循环。

还有一点容易被忽略:子 Agent 的产出契约要尽量"自包含、可校验"。如果 worker 回传的是"我觉得在这附近"这种模糊结论,编排者无法核对,只能照单全收,错误就被悄悄带进了汇总。好的契约长这样——“文件路径 + 行号 + 一句话职责”,编排者甚至可以再派一个廉价子 Agent 去抽查几条是否属实。把契约设计得可机器校验,并行才不会以牺牲可靠性为代价。这与上一节的验证者思路一脉相承:隔离上下文的同时,别把可核对性也一起隔离掉。

把子 Agent 当成上下文与算力的"隔离舱",而非简单的多线程:该并行的 fan-out 出去、该分层的用 orchestrator-worker 编排、该验证的交给 fresh-context 的独立验证者——隔离用对了,省的是 token,得到的是更干净的判断。