RSS

第 9 章 / 共 11 章

隔:子代理与它的代价

约 18 分钟 更新于

9.1 一次清单调查吃掉了整个预算

你要搞清楚 shopfront 里到底有多少处调用了 v1 结算接口。

这听起来是个五分钟的问题。你打字:「找出所有调用 v1 结算接口的地方,列个清单给我。」

agent 开始干活。它先 grep v1,命中三百多行,里面混着注释、变更日志、docs/ 里的历史记录。它再 grep checkout/v1,好一点,还是一百多行。它觉得光看行不够,开始读文件——src/api/client.ts 整个读进来,src/checkout/ 下十几个文件整个读进来,tests/checkout/ 又是十几个。中间它发现有一层 src/checkout/legacy/ 的适配器在包装调用,于是又追进去读了四个文件。

四十个文件之后,它给了你一份清单。清单是对的,十七处调用,分布在九个文件里。

但你抬头看窗口占用:已经过半。主对话里现在躺着三百行 grep 输出、四十个文件的完整内容、以及 agent 自言自语的十几轮推理。你还一行代码都没改。

真正的迁移工作——改十七处调用、补测试、跑一遍——才刚要开始,而它要在一个已经被调查垃圾填了一半的窗口里进行。接下来会发生的事你在第 1 章见过:越跑越钝,读过的文件被重复读,改过的地方被重复改。

9.2 问题不是它翻了四十个文件

这里要说清楚一件事,因为很多人诊断错了方向。

翻四十个文件没有错。要给出一份可信的调用清单,就是得翻这么多。你让它少翻,清单就会漏。这不是效率问题,是任务本身的信息量。

错的是这四十个文件的内容,和最终那份十七行的清单,共用了同一个上下文窗口

你想要的产物是清单。清单大概一千五百 token。为了得到它,付出了几万 token 的中间产物。问题在于中间产物没有随着任务结束而消失——它们留在了对话历史里,和后面几十轮的迁移工作抢注意力。

按第 3 章那四个问题过一遍:这些 grep 输出该不该在主窗口里?不该。它们是一次性的,用完就没价值了。该在但现在不该?也不是——它们从来就不该在主线上。太大了?确实大,但压小了还是脏的。

第四个问题命中了:它会污染主线

这就是隔离(isolate)。调查工作放进一个独立的上下文窗口,在那个窗口里随便翻、随便脏,翻完只把结论带回主线。子代理用几万 token 探索,回传一份约 1000–2000 token 的提炼摘要。主对话看到的只有那份摘要。

同一件事换个说法:你不是在给 agent 找帮手,你是在给一次性的脏活找一个用完就扔的窗口。

9.3 一份能直接用的调查型子代理

Claude Code 的子代理配置是一个带 YAML frontmatter 的 Markdown 文件,放在 .claude/agents/(项目级,跟着仓库走)或 ~/.claude/agents/(用户级,跨项目)。

下面这个是 shopfront 里那次调查该有的样子。存成 .claude/agents/v1-usage-scanner.md

---
name: v1-usage-scanner
description: 扫描仓库中对内部 v1 结算 API 的调用点,回传结构化清单。用于迁移前的盘点,不做任何修改。当需要"找出所有调用 X 的地方"这类只读全仓调查时使用。
tools: Read, Grep, Glob
disallowedTools: Write, Edit
model: sonnet
---

你是 shopfront 仓库的只读调查员。你的唯一产物是一份清单,不是分析报告,不是修改建议。

## 调查范围

- 包含:`src/``tests/`
- 排除:`node_modules/``dist/``docs/``*.md`、任何变更日志
- 注意 `src/checkout/legacy/` 下有一层适配器会包装 v1 调用,间接调用也要计入,并标注为「间接」

## 判定标准

一处「v1 调用」指以下任意一种:
1. 直接调用 `client.checkoutV1.*` 上的任意方法
2. 通过 `legacy/v1-adapter.ts` 导出的函数间接调用
3. 硬编码了 `/api/v1/checkout` 路径字符串的 fetch 或 axios 请求

仅出现在注释、字符串字面量的日志文案、或测试 fixture 数据里的 `v1` 字样,不计入。

## 回传格式(严格遵守,不要输出格式之外的任何内容)

先输出一行总计:`共 N 处,分布在 M 个文件`

然后输出一张 markdown 表格,每行一处调用,列依次为:

| 文件路径 | 行号 | 调用的方法或路径 | 直接/间接 | 一句话说明这处在做什么 |

表格之后,输出最多 5 条「需要人判断的疑点」,每条一行,格式为
`疑点:<文件路径>:<行号> — <为什么拿不准>`。没有疑点就写「无疑点」。

## 硬性约束

- 不要粘贴任何文件的源码片段。一行都不要。
- 不要给出迁移建议、风险评估、工作量估计。
- 不要输出你的检索过程、试过哪些 grep、读了哪些文件。
- 单个「一句话说明」不超过 30 字。
- 如果调查结果超过 60 处,不要全部列出,改为按目录聚合计数并说明「结果过多,建议缩小范围」。

请你留意这份配置里字数最多的部分:回传格式

这不是巧合。规定回传格式是写子代理最关键的一步,比选工具、比调模型都关键。

原因很实在。子代理跑完之后,它的整个上下文窗口——那几万 token 的 grep 输出和文件内容——会被丢弃,只有它最后那条消息回到主线。如果你没规定这条消息长什么样,模型的默认倾向是「把我看到的证据都给你」。它会贴代码片段来支撑结论,会复述检索过程来证明自己找全了,会附上一段风险分析显得专业。于是那条回传消息变成八千 token,你隔离了个寂寞——你只是把污染源从「四十个文件」换成了「一份啰嗦的报告」。

所以那三条硬性约束是必须写的:不要粘代码、不要讲过程、不要给建议。它们各自堵住一种膨胀方式。

另外注意 disallowedTools: Write, Edit。调查型子代理不该有写权限——不是因为它会捣乱,是因为你希望这次调用的结果只有信息,没有副作用。副作用不受你控制,也不进主线的视野,出了问题你在主对话里根本看不见发生过什么。收窄权限本身就是隔离的一部分。

9.4 隔离到底隔掉了什么

「独立的上下文窗口」这个说法容易让人以为子代理是一张白纸。它不是。按官方文档,非 fork 型的子代理不继承主对话历史,但它会加载 CLAUDE.md、当前的 git 状态、以及预载的技能。

这个边界很重要,实际影响是两条:

第一,团队约定会自动到场。shopfrontCLAUDE.md 里写着「src/checkout/legacy/ 不参与本次迁移」,子代理能看到这条。所以你不必在每个子代理配置里重抄一遍项目约定。反过来说,CLAUDE.md 越臃肿,每个子代理的起步成本也越高——第 4 章那笔账在这里要再付一次,而且是按子代理数量乘出去的。

**第二,你和主 agent 的讨论不会到场。**你在第 12 轮口头补充的「amount 字段单位是分」,子代理不知道。你和主 agent 争论了三轮才敲定的迁移顺序,子代理不知道。它只知道两样东西:它的 description 和你这次派活时的任务描述。

这一条是子代理最常见的失败来源,而且它不会报错。子代理会按它以为的样子把活干完,回传一份看起来很齐整的清单,清单里少了一整类你在对话里提过、但没写进任务描述的情况。你要到很久以后才发现。

对应的操作是:派活时把口头结论显式塞进任务描述。不要写「扫一下 v1 调用」,要写「扫一下 v1 调用,注意我们之前确认过 legacy/v1-adapter.ts 里的包装也算,docs/ 下的不算」。或者,把这类结论固化进第 6 章那个笔记文件,然后在子代理正文里让它先读那个文件。

配置里还有几个可选字段值得知道存在:permissionMode(这次调用用什么权限模式)、skills(预载哪些技能)、mcpServers(给不给它 MCP 连接)、memorymaxTurns(最多跑几轮,防它无限翻)、isolation。这些字段名和默认值会随版本变化,用之前请对着你当前版本的官方文档核一遍,别照抄本书。

还有三个官方给的数字,直接影响你的设计:

  • **所有子代理的 description 合计建议控制在 15000 token 以内。**因为 description 是常驻主上下文的——主 agent 得知道有哪些帮手可以派,才能在合适的时候派出去。这意味着子代理不是白开的,你每定义一个,主窗口的常驻预算就少一点。定义二十个各司其职的子代理,听起来很有条理,实际是在给自己的主窗口交年费。
  • **默认最多嵌套 3 层。**子代理还能再开子代理。听起来很强,但每多一层,信息就多经过一次摘要,也就多丢一次。
  • **默认并发上限 20。**能并行不等于该并行,下一节说这个。

9.5 15 倍:这一章必须讲的那部分

到这里为止子代理听起来全是好处。现在说代价,而且要说透,因为这是最容易被跳过的一节。

Anthropic 自己的多 agent 研究系统靠并行子代理跑赢了单 agent 架构。这是真的,效果差距明显。同一份报告里还有另一个数字:它的 token 用量约为单 agent 的 15 倍

这个数字不是某次异常,它是并行架构的结构性成本。每个子代理都要重新加载 CLAUDE.md、重新加载工具定义、重新读一遍它需要的文件——那些主 agent 可能已经读过的文件。共享上下文换来的效率,在隔离架构里被系统性地放弃了。你换到的是干净的主窗口和并行度,付出的是重复劳动。

所以「开子代理」不是一个免费的好习惯,它是一笔交易。值不值取决于两件事:调查产生的中间垃圾有多大,以及这些垃圾如果留在主线会造成多少后续损失。扫全仓 v1 调用,几万 token 的垃圾换一份清单,这笔交易划算得很。让子代理去读一个你已经知道路径的文件,那是纯亏——启动成本比你直接 Read 一次还高。

第二笔代价是信息损耗,前面提过一次,这里给它一个准确的说法:摘要本身会丢信息,而且丢的是你事先不知道会需要的那部分

子代理翻文件的时候,它看到了一些你没让它汇报的东西:src/checkout/refund.ts 里那个函数写得特别绕,tests/checkout/ 里有两个测试其实是重复的,legacy/v1-adapter.ts 顶上有一行注释写着「2023 年临时方案,等 v2 上线删掉」。这些都不在你规定的回传格式里,所以它们跟着子代理的窗口一起消失了。三小时后你在迁移某个文件时卡住,而答案本来就在那行注释里。

这个代价没法消除,只能管理。管理办法是在回传格式里留一个口子——上面那份配置里的「需要人判断的疑点」就是这个口子。它是子代理唯一被允许说「我看到一些不在格式里但你可能想知道的事」的地方,限 5 条,防止它借机把整个窗口倒回来。

第三笔代价是你看不见过程。主对话里只有一份清单,没有它怎么找到的。清单错了你很难 debug,因为证据已经不存在了。所以只读调查适合子代理,而需要你随时纠偏的工作不适合。

9.6 子代理适用性判断表

把上面几节压成一张表。派活之前对一眼。

情况为什么
适合只读调查:找出所有调用点、梳理某个模块的依赖、盘点测试覆盖了哪些分支中间产物大、产物形态清晰、没有副作用,隔离收益最高
适合大量日志或测试输出的处理:跑一遍 tests/checkout/,只回传失败用例和失败原因原始输出可能上万 token,你真正需要的是十几行
适合可并行的独立分支:src/checkout/src/api/tests/checkout/ 三块各扫各的彼此不依赖,并行能真正省时间,代价是 token 乘上分支数
适合需要收窄权限的操作:让某次调查绝对不能写文件disallowedTools 给你一个比嘴上叮嘱更硬的边界
不适合需要连续编辑同一批文件:把十七处调用逐个改成 v2编辑要延续状态,隔离会让每个子代理重新建立理解,还容易互相覆盖
不适合需要看到完整讨论上下文:你和 agent 磨了半小时才敲定的方案,现在要落地非 fork 子代理不继承对话历史,这半小时的共识传不过去
不适合任务本身很短:读一个你已知路径的文件、跑一条命令启动成本高于收益,纯亏
不适合几个子任务之间强耦合:改完 client.ts 才知道 cart.ts 要怎么改并行会让它们基于过期假设各干各的,合起来是一团乱

有一条经验法则可以覆盖大半场景:如果这个活的产物能用一段结构化文本描述清楚,而过程会产生大量你不想看的中间物,就开子代理;否则不开。

第9章:主线与子代理的上下文边界

图 9.1:中间是主对话的上下文窗口,三个子代理各自挂着一个独立窗口。注意看进出的箭头粗细——进子代理的是一句任务描述加 CLAUDE.md,子代理内部膨胀成几万 token,回主线的只剩一根细线。也注意每个子代理的 description 都有一小块常驻在主窗口里,那是你为「有帮手可派」付的固定费用。

9.7 沙箱:另一种隔离

隔离不只有子代理这一种形态。还有一种更朴素的:把大体积对象留在执行环境里,只把结果拿回来

shopfront 的迁移里会遇到这类东西。你让 agent 分析结算接口的一份线上响应样本,那个 JSON 有八千行。你让它对比迁移前后的两张页面截图。你让它统计 tests/checkout/ 跑完的覆盖率报告。

这些对象的共同点是:体积巨大,而你要的答案很小。八千行 JSON,你要的是「v2 响应里 amount 是不是分」。两张截图,你要的是「结算按钮位置有没有变」。覆盖率报告几百行,你要的是「哪三个文件覆盖率低于 60%」。

朴素的做法是把整个对象读进上下文,然后让模型在里面找。这等于让模型用注意力预算去做一件 jq 能做的事。

隔离的做法是让它写一小段代码在执行环境里处理,只把结果打印出来。八千行 JSON 用 jq '.data.amount, .data.currency' 变成两行。覆盖率报告用一条命令过滤成三行。图片压根不进上下文,让它在环境里比对完只报结论。

这跟子代理是同一个心智模型的两种规格:大的东西不必进窗口,进窗口的应该是关于它的结论。区别只在隔离边界画在哪——子代理隔的是一整段推理过程,沙箱隔的是一个数据对象。

顺带一提,第 6 章那个笔记文件其实也在做同一件事,只不过它隔离的是时间维度上的状态。写、选、压、隔在具体操作上从来不是干净分开的,这在第 3 章末尾已经说过。

如果你想动手实现子代理和技能,站内《给 AI 写规矩》系列有完整的实现过程,比本章的配置片段细。这一章只管你什么时候该派、派完付多少钱。

9.8 常见的三种误用

**误用一:把子代理当成「更强的 agent」。**子代理不比主 agent 聪明,它只是视野不同——通常还更窄,因为它没有你们的对话历史。派活时如果你心里想的是「这个太难了主 agent 搞不定,交给专家」,那多半会失望。子代理解决的是上下文卫生问题,不是能力问题。

**误用二:定义了一堆子代理,然后从来不用。**你写了八个子代理配置,test-runnerdoc-checkerdep-analyzer……大部分从来没被派出去过。但它们的 description 全都常驻在主上下文里,每一轮都在收费。这是第 5 章工具集臃肿的翻版:没被用过的能力定义是纯成本。定期删掉三个月没派过的子代理。

**误用三:并发拉满。**默认上限 20,于是有人真的一次开十几个。这里有两个问题:token 用量按并发数线性涨,而 15 倍那个数字本来就是并行架构的成本;更麻烦的是,如果这些子任务其实有耦合,你会得到十几份基于不同假设的结论,合并它们的工作量比一开始串行做还大。并发的前提是任务真正独立,这件事需要你判断,不是模型判断。

9.9 给一次真实调查开一个子代理