要在整个仓库里找全 parse_date 的调用点,都读进主上下文吗?
本课一问:parse_date 修好之后,要在整个仓库里找全它的调用点、逐个判断还兼不兼容。这些搜索结果,都该读进主 agent 的上下文吗?
这一课先不碰:交接时该把什么交给子 agent(第 2 课);子 agent 还能不能再派、轮数算谁的(第 3 课);谁来判定做完了(第 4 课);几个 agent 同时改代码(岔路 B1)。
从已有出发
A5 第 1 课拆开过一轮请求:模型 API 不记得上一轮,harness 每一轮都把整段历史重发;历史里涨得最快的是工具结果,读回来的文件从那一轮起每轮都跟着重发。A5 的办法是满了再压缩。这一章换一个方向:有些东西一开始就不进主窗口。
贯穿任务还是 test_parse_date。它修好之后自然冒出下一个问题(大纲里任务约束第 5 条):parse_date 改成了「日在前」,仓库里还有谁在用它、它们受不受影响?本课项目把这一步录成了 T4(任务 tasks/callers),任务原文就是给 agent 的这段话:
为什么这里换了例子:修 test_parse_date 本身只要读一两个文件,用不上分工;「找全调用点」要搜整个仓库、读很多文件而最后只需要一张表,正是子 agent 被设计出来要做的那类活。任务仓里有 6 个真实调用点(1 个经别名 to_date 调用),其中 2 个不兼容;验收脚本在 agent 视野之外。
先自己设计:主 agent 自己 grep、自己读
最直接的做法:主 agent 自己 grep parse_date,看结果,需要时再逐个读文件,最后写表。本课项目的「不用子 agent」两次运行就是这么做的。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
t4-nosub-s2 第 1 轮那次 grep parse_date 的 24 行,按任务仓源码逐行分类:
| 这一行是 | 行数 | 例子 |
|---|---|---|
| 真实调用点 | 5 | app/billing.py:33 |
| import | 6 | app/billing.py:8 |
| 注释 / docstring | 3 | app/__init__.py:1 |
| 函数定义本身 | 1 | dateparse.py:21 |
| 测试文件(任务说了不算) | 9 | tests/test_dateparse.py:6 |
第 6 个调用点 app/importers/legacy.py:15 写的是 to_date(...),搜 parse_date 根本搜不到它;两次运行都漏了。
这些行一旦进了主窗口,就和 A5 里的 dateparse.py 一样:之后每一轮请求都带着它们。主窗口里搜索类结果累计:t4-nosub-s1 563 token,t4-nosub-s2 1,120 token(录制模型的 tokenizer 数的内容 token)。
这个任务真正需要的产出,是一张 6 行的表。搜索过程中的那些 import、注释、测试行,看过就可以扔。问题变成:能不能让「看」发生在另一个窗口里,只把结论拿回来?
改进:子 agent,就是一份新的 messages[]
learn-claude-code 的 s06 把子 agent 写到了最小:一个工具 task,调用时在工具内部同步跑一个嵌套的循环,从一份只有 prompt 的新 messages[] 开始,跑完把最后一条回复的文本当作这次工具调用的结果交回去。
本课项目的 delegate 工具是同一个思路(miniagent/subagent.py),另加了三条:子 agent 只有只读工具、权限默认拒绝;预算独立(T4 里 15 轮);父 agent 收到的工具结果是 Helper report: 加子 agent 的最后一段话。下面是同一个 seed 的三次真实运行并排,三列共用一根纵轴。
数据:本课项目 miniagent 的 T4 真实运行(Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server,2026-09-26;src/data/precomputed/a07-subagents.json)。柱高 = 服务端 usage 的 prompt_tokens;绿色段与流带 = 用录制模型 tokenizer 数的内容 token(不含聊天模板的标记,所以比柱高的增量略小)。三列共用同一纵轴。思考截取约 500 字、工具输出约 900 字。
试三件事:① 看上泳道三列的最高柱:不用子 agent 的那列,峰值是哪一轮、绿色(搜索结果)占了多少;② 看中间一列的两条流带:蓝色往下是父 agent 交给子 agent 的任务(19 token),绿色往上是回到父窗口的报告(192 token),子 agent 自己那两轮的 grep 输出一个字都没进上泳道;③ 切到 seed 2,找出唯一一组「用了子 agent、主窗口峰值反而更高」的是哪一列,再点它第 1 轮的柱子看为什么。
读数(全部来自服务端 usage):
| 配置 | seed 1 主窗口峰值 | seed 2 主窗口峰值 | seed 1 总 token(主 + 子) | seed 2 总 token(主 + 子) |
|---|---|---|---|---|
| 不用子 agent | 1,767 | 2,328 | 14,410 | 16,141 |
| 用子 agent · 只给任务文本 | 1,327 | 1,629 | 14,232(-1%) | 11,419(-29%) |
| 用子 agent · 带上父决定 | 2,281 | 1,863 | 21,040(+46%) | 21,961(+36%) |
- 主窗口峰值:4 组对照里 3 组更低。不成立的那一组是 sd1:父 agent 先自己 grep 了一遍(622 token 进了主窗口),才把活派出去,于是「自己搜」和「报告」两份都留在主窗口里。子 agent 不是魔法,主 agent 先自己读了,隔离就白做了。
- 总 token 不是一个方向。「只给任务文本」的两次总数反而更少,「带上父决定」的两次多了四成左右。原因在下一课:只给任务文本的子 agent 两轮就交差,父 agent 拿着报告直接收尾——花得少,是因为做得少。
- 6 次运行 verifier 全部失败:不用子 agent 的两次找到 5/6,用了子 agent 的四次一个都没对上。这一课只证明了「隔离能让主窗口更小」,没有证明「隔离让结果更好」;把它们混在一起,是这一章最容易犯的错。
极小真数字:主窗口到底为这次搜索付了多少
同一个 seed 2,看「搜索结果进主窗口」的那一轮前后,服务端 usage 涨了多少:
它省的是主窗口:主 agent 每一轮要重发的东西变少了,离窗口上限更远。总 token 要把子 agent 自己的每一轮都加上:子 agent 从零开始,自带 system prompt 和工具定义,每轮同样整份重发。本课项目里「带上父决定」的两次,总数比不用子 agent 多了 46% 与 36%;Anthropic 自报它的多 agent 研究系统用的 token 约为普通对话的 15 倍(单 agent 约 4 倍) 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方自报,研究类任务、多个子 agent 并行;不是编码任务,也不可复现。本课项目的数字是单一模型、每种配置 2 个 seed 的实测。 出处Anthropic · How we built our multi-agent research system(2025-06-13) ↗需联网 。
常见误解二:「子 agent 是隔离的,所以它是安全的。」隔离的只是消息。learn-claude-code 的 s06 README 原话:「This is message isolation, not process or filesystem isolation」——父子在同一个进程、同一个工作目录里,子 agent 写的文件父 agent 立刻看得到(s06_subagent/README.md:25)。能不能写、能碰哪些路径,仍然是权限(A3)和沙箱(A4)的事;本课项目给子 agent 只读工具、权限默认拒绝,是另外加的。
各家怎么把结果交回来
同一个动作——开一份新的上下文、跑完只交回一段话——各家的差别在「从什么开始」和「交回之前要不要截」:
| 子 agent 从什么开始 | 交回父 agent 的是什么 | 出处(pin) | |
|---|---|---|---|
| learn-claude-code s06 | 只有 prompt 的新 messages[] | 最后一条回复的文本,不截断 | s06_subagent/code.py:270-307 |
| CoreCoder v0.7.0 | 新的 Agent,工具表去掉 agent,子 agent 上限 20 轮 | [Sub-agent completed] + 结果;超过 5,000 字符截成 4,500 | tools/agent.py:46-67 |
| OpenCode | 新 session,parentID 指向父会话;可用 task_id 续跑同一个子会话 | 最后一段 text,包在 <task … state="completed"> 里;子 agent 出错时以错误回传 | tool/task.ts:156-172,200-225,344 |
| Hermes | 新对话,system prompt 由 goal 与 context 拼成 | summary;按父窗口剩余空间与 max_summary_chars(24,000)限长,超出的部分落盘并给出读取位置 | delegate_tool.py:900-976;config_defaults.py:1753 |
| Pi(扩展,非核心) | 另起一个 pi 进程,--no-session | 最后一条 assistant 文本;并行模式每个任务 50KB 上限 | examples/extensions/subagent/index.ts:170-180,294 |
| Codex v1 | 一个新线程(可选 fork 父历史,第 2 课) | wait_agent 等到结束,「Completed statuses may include the agent’s final message」 | multi_agents_spec.rs:275 |
| Claude Code | 自己的 system prompt、任务、CLAUDE.md 等,看不到父对话 | 只回摘要;到 maxTurns 时标记为 partial | 官方文档 sub-agents(2026-09-26 核对) |
| 本课项目 | 空历史 + 只读工具,权限默认拒绝 | Helper report: + 最后一段话;预算用完时追加一行说明 | miniagent/subagent.py:99-105 |
CoreCoder 的实现只有 35 行,作者自述「Sub-agents only run the plainest synchronous execution」:
🔍 Claude Code 的官方交互时间轴里,子 agent 那一步是怎么画的?
Claude Code 文档《Explore the context window》的可播放时间轴里有一段子 agent:它有自己更短的 system prompt、自己的一份 CLAUDE.md、父 agent 写给它的任务,读文件填的是它自己的窗口;那一段的说明是「None of its file reads touch yours. Only the final summary comes back.」页面注明用的是代表性数字(representative numbers),所以本课不引它的 token 数;上面的三列回放就是它的真数据版。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 读很多、只要结论的活,中间结果不进主窗口,不跟着每轮重发 |
| ② 为什么一定有效? | 子 agent 的 messages 与父 agent 的是两个列表,父 agent 只追加一条工具结果;只要报告比中间结果短,主窗口的增量就更小 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:父 agent 第 k+1 轮的历史 = 第 k 轮的历史 + delegate 调用 + 报告;子 agent 的中间消息不在父 agent 的列表里。前提是父 agent 自己没先把同样的东西读进来(sd1 就是反例)。 出处待补 |
| ③ 实验上呢? | 本课项目 T4:主窗口峰值 4 组里 3 组更低;总 token 两降两升;6 次运行验收全失败 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:Qwen3-8B(4bit)、单一任务、每种配置 2 个 seed。只能说明现象存在;换前沿模型、换更大的仓库,比例会变。 出处待补 ;Anthropic 自报多 agent 约 15 倍 token 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条研究类任务,自家评测,不可复现。 出处Anthropic · 官方自报 ↗需联网 |
演进地图:你在这里
- 所有 harness 的默认形态;本课项目 T4 的「不用子 agent」两次运行解决了不需要交接:主 agent 亲眼看到每一行搜索结果,不会有信息在转手时丢失。卡在两次 grep 结果进主上下文 563 / 1,120 token,之后每轮跟着重发(A5);要判对兼容性还得读 7 个文件(内容 1,818 token),两次都没读——找到 5/6 与 5/6,兼容性判对 3/6 与 2/6。
- learn-claude-code s06、CoreCoder AgentTool、OpenCode task、Claude Code 子 agent、本课项目 delegate解决了搜索的中间结果留在子 agent 自己的窗口里;4 组对照里 3 组主上下文峰值更低。卡在sub-text-s2 的子 agent 在自己的 grep 里看到了 5 个真实调用点,报回的 9 个「调用点」里 9 个是 import、注释或定义行;父 agent 照抄,verifier 找到 0/6。
- 本课项目 with_decisions、Hermes 的 context 字段、Codex v2 的 fork_turns(默认 all)、Claude Code 的 fork解决了子 agent 拿到了父 agent 的全部约束;sub-dec-s1 的子 agent 报回的真实调用点行号 5/6。卡在两次仍然 0/6:s1 的子 agent 一个文件都没读就猜兼容性,父 agent 全程 write_file 0 次却说「已写进 CALLERS.md」;s2 的子 agent 正则写错,15 轮预算只用了 3 轮就交差。
- OpenCode subagent_depth、Codex agent_max_depth、Hermes max_spawn_depth(默认都是 1);Claude Code 默认 3 层;Hermes、CoreCoder、lcc、本课项目的子 agent 预算独立解决了递归有上界;子 agent 的轮数不挤占父 agent:sub-dec-s1 的父 agent 只需要 3 轮,父子共用一个计数时得给到 7。卡在预算防的是停不下来,不防过早收工:T4 的 4 个子 agent 各有 15 轮,最多只用了 4 轮。
- learn-claude-code s17 独立评估器、Codex Guardian(A3)、Anthropic cwc-long-running-agents 的 evaluator、Hermes 后台自省 fork解决了完成与否不再由干活的 agent 自己说了算;评审者没有写工具,不能顺手「修好」来掩盖问题。卡在T1 与 T4 里最后一句话明确声称完成 20 次,verifier 判失败 12 次;评审者也是模型,也会被说服 → 确定性的判决在 A9。
数字口径:本课项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit 本机录制的 T4(2026-09-26,找全 parse_date 调用点,6 次运行 + 4 条子 agent trace;单一模型、每种配置 2 个 seed,只说明现象存在,不说明比例)。token 取自服务端 usage;「内容 token」用录制模型的 tokenizer 现数。耗时不作比较(本机有其他任务同时占用)。
一句话
先停在这里:记住两件事——子 agent = 一份新的 messages[] + 只交回最后一段话;它让主窗口变小,但总账要把子 agent 自己的每一轮加上。
再深一层:在上面的回放里点开 seed 2「只给任务文本」那一列的 S1,看子 agent 自己的 grep 里有哪几行真实调用点,再看它交回去的报告写了什么。或者走岔路:多个 agent 同时改代码,怎么不互相踩?、Pi 为什么不内置子 agent?
下一个缺口
子 agent 只拿到一句任务描述,会丢掉什么? → 第 2 课
t4-sub-text-s2 的子 agent 在自己的 grep 里看到了 5 个真实调用点,报回去的却是 import 所在的行号;父 agent 照抄进表,verifier 一个也没对上。父 agent 任务里写着「只算真实调用,不算 import 和注释」——这句话没传下去。
你可能会问
- 「主窗口就算大一点,离 40,960 的窗口还远着,为什么要在意?」 在这个 6 个调用点的小仓库里确实远。这一课在意的是增长方式:搜索结果进了主窗口就每轮重发(A5),仓库越大、要读的文件越多,这部分越先撞墙;而且 A5 的岔路讲过,窗口没满模型也可能先变差。
- 「子 agent 为什么不直接用父 agent 的模型缓存?」 普通子 agent 的请求前缀和父 agent 不同(自己的 system prompt、自己的任务),读不到父 agent 的缓存;Claude Code 文档写明只有 fork 出来的子 agent 继承父 agent 的 system prompt、工具与历史,第一次请求能读父 agent 的缓存。第 2 课讲 fork 的代价。
- 「用了子 agent 的四次为什么全都 0/6?」 不是隔离的错:交接丢了约束(第 2 课)、子 agent 没读文件就下结论(第 2、3 课)、父 agent 照单全收子 agent 的自述(第 4 课)。这些失败逐条都能在实验课里对上 trace。