从0打造编码Agent 阅读 + 实验 ≈ 35 分钟 页内实验 ×1
A7 子 agent:一个 agent 忙不过来、或需要别人把关时,怎么分工? 第 2 课

子 agent 只拿到一句任务描述,会丢掉什么?

交接会丢掉什么
只给任务文本的两次交接,都把「import 和注释不算」「看 AGENTS.md」这些约束丢在了父 agent 那边:一个子 agent 在自己的 grep 里看到了 5 个真实调用点,报回去的 9 个「调用点」却全是 import、注释和定义行。把决定显式写进交接之后,约束都到了,却又丢了另一样东西——父 agent 自己已经做过的事。现代做法有两条:把决定写下来传,或者 fork 继承整段历史;后者又把第 1 课省下的上下文还了回去。
❓

本课一问:上一课把搜索交给了子 agent,子 agent 的全部上下文只有父 agent 写给它的那一段话。那段话要是只写了「找调用点」,子 agent 会丢掉什么?

这一课先不碰:子 agent 能不能再派子 agent、轮数算谁的(第 3 课);子 agent 交回来的报告父 agent 该不该信(第 4 课)。

从已有出发

从已有出发 · 累计数据流 点已学环节看它回答什么
每一轮循环都经过

第 1 课的子 agent 是 learn-claude-code s06 的形态:子 agent 的 messages[] 里只有一条 prompt(s06_subagent/code.py:272)。父 agent 读过什么、看出了什么、任务里有哪些硬约束,只有写进这条 prompt 的才存在。这和 A5 第 4 课的压缩是同一个问题:接手的一方只看得到你写下的东西。区别是 A5 的接手者是「压缩之后的自己」,这里是另一个 agent。

父 agent 手里的任务原文(105 token)里写着六条约束。本章把它们编成 D1-D6,检测规则写在数据文件里,组件上悬停可见:

编号约束任务原文
D1只算真实调用,不算 import 和注释List only real calls, not imports or comments.
D2每个调用点要给出行号file | line | compatible (yes/no) | reason
D3逐个判断是否兼容「按日在前」读法decide for each call site whether it is compatible with the fixed day-first behaviour
D4项目的日期约定写在 AGENTS.md(see AGENTS.md)
D5经别名调用的也要算directly or under another name
D6排除 tests/outside tests/

先自己设计:只把子任务的一句话交出去

最自然的交接:父 agent 在 delegate 里写一句子任务,别的不管。本课项目的 text_only 就是这样:子 agent 收到的 user 消息 = 父 agent 写的 task 参数,一个字不多(miniagent/subagent.py:67-68)。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案只把子任务的一句话交给子 agent(text_only)

两次 text_only 交接的原话,和父 agent 手里的任务并排看:

运行子 agent 收到的全部任务token丢了哪些约束
t4-sub-text-s1Search all files except tests/ for calls to parse_date (including aliases) and report results.19D1、D2、D3、D4
t4-sub-text-s2Find all call sites of parse_date (including aliases) outside the tests directory, and list each with file, line number, and context.28D1、D3、D4

丢掉的恰好是决定答案对错的那几条。t4-sub-text-s2 的子 agent 不知道「import 和注释不算」,就把 grep 到的每个文件的第一处出现当成调用点报了回去:docstring 2 个、import 6 个、def 1 个。它的 grep 输出里其实有 app/billing.py:33: return parse_date(FISCAL_YEAR_END) 这样的真实调用行,报告里的行号却是 8(import 那一行)。

t4-sub-text-s1 的报告连一个 file:line 都没有,还写了一句「No matches found in tests/」——它自己的 grep 输出里有 9 行来自 tests/。父 agent 收到后说要写 CALLERS.md,然后没写就停了。

改进一:把决定显式写进交接

本课项目的 with_decisions:delegate 多一个必填参数 decisions(父 agent 已拍板、子 agent 必须遵守的决定),harness 再把父 agent 的原始任务整段附上(miniagent/subagent.py:69-72)。各家也都在同一个位置留了口子:

delegate_tool.py hermes-agent@56dc01d9:tools/delegate_tool.py:4213-4228需联网 MIT
4213 "goal": {
4214 "type": "string",
4215 "description": (
4216 "What the subagent should accomplish. Be specific and "
4217 "self-contained -- the subagent knows nothing about your "
4218 "conversation history."
4219 ),
4220 },
4221 "context": {
4222 "type": "string",
4223 "description": (
4224 "Background information the subagent needs: file paths, "
4225 "error messages, project structure, constraints. The more "
4226 "specific you are, the better the subagent performs."
4227 ),
4228 },
👆 带下划线的行可以点:点哪行,讲哪行。

交接写成什么样,决定了子 agent 起跑时知道什么。下面是同一个 seed 的两次真实交接并排:

交接对照:同一个任务,子 agent 拿到的东西不同(真实 T4 运行) seed
先猜:读下面两段交接原话。哪一个子 agent 报回的「真实调用点 + 正确行号」更多?

数据:本课项目 miniagent 的 T4 真实运行(Qwen/Qwen3-8B-MLX-4bit,2026-09-26)。约束检测是对交接原话做的正则匹配(规则悬停可见);引用分类按任务仓源码逐行判断,调用点匹配容差 ±2 行,与验收脚本 verify.py 相同。每种配置只有 2 个 seed:只能说明这些丢失发生过,不能说明频率。

🎯

试三件事:① 先只读两段交接原话和约束芯片,猜哪边报回的真实调用点更多,再点揭示;② 切到 seed 1 再猜一次——这一次结果和 seed 2 不一样;③ 在 seed 1「带上父决定」那一列找那条黄色提示:父 agent 交接前自己已经看到了几个调用点的行号,写进交接的有几个。

读数:

  1. 约束都传到了。两次 with_decisions 的交接里 D1-D6 全部出现(父任务原文是 harness 附上的,不靠父 agent 自觉)。
  2. 结果分化。seed 1 的子 agent 报回了 5 个真实调用点、行号全对;seed 2 的子 agent 把正则写成了 parse_date\s*$$,只匹配到 import 行,报告说「actual call sites remain undetermined」——它有 15 轮预算,用了 3 轮就交差。
  3. 行动里的决定还是没传。seed 1 的父 agent 在交接之前自己 grep 过,亲眼看到了 5 个调用点的行号;它写的 decisions 只有一句「parse_date now parses DD/MM/YYYY as day-first」,一个行号也没带上。子 agent 从头又 grep 了两遍。
  4. 最后仍然都是 0/6。seed 1 的父 agent 拿到报告后一次 write_file 都没调,就说「findings are documented in CALLERS.md」;seed 2 的父 agent 自己读了 billing.py,写了 1 行,行号还错了(第 4 课)。
t4-sub-dec-s2:子 agent 收到的交接 = 172 token
= + + +
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
这段交接的成本是一次性的:只在子 agent 的第一条消息里,不进父 agent 的窗口。它挡不住的是父 agent 没写下来的东西——比如自己已经 grep 到的行号。
⚠
常见误解一:「把决定都传下去,子 agent 就不会出错。」

传下去的只是「写下来的决定」。seed 1 的父 agent 已经做过、看过的事(那 5 个行号)没写进 decisions;seed 2 的子 agent 收到了全部约束,仍然因为自己写错一个正则而一无所获。交接是一环,子 agent 自己的执行、报告的压缩、父 agent 怎么用报告,是另外三环。

常见误解二:「多 agent 总比单 agent 强。」

Cognition 的《Don’t Build Multi-Agents》把反对理由压成两条原则:共享完整的上下文与轨迹;以及「Actions carry implicit decisions, and conflicting decisions carry bad results」 。Anthropic 在自家多 agent 研究系统的文章里也写明,大多数编码任务可以真正并行的部分比研究少,需要所有 agent 共享同一上下文、彼此依赖多的领域,今天不适合多 agent 。本课项目 T4 的 6 次运行里,找到调用点最多的恰恰是不用子 agent 的两次(各 5/6)。

改进二:fork——子 agent 带着父 agent 的整段历史出发

另一条路是不转述:让子 agent 直接继承父 agent 的对话。

multi_agents_spec.rs codex@8cabf5a:codex-rs/core/src/tools/handlers/multi_agents_spec.rs:602-608需联网 Apache-2.0
602 (
603 "fork_context".to_string(),
604 JsonSchema::boolean(Some(
605 "True forks the current thread history into the new agent; false or omitted starts with only the initial prompt."
606 .to_string(),
607 )),
608 ),
👆 带下划线的行可以点:点哪行,讲哪行。
spawn.rs codex@8cabf5a:codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs:205-224需联网 Apache-2.0
205 fn fork_mode(&self) -> Result<Option<SpawnAgentForkMode>, FunctionCallError> {
206 if self.fork_context.is_some() {
207 return Err(FunctionCallError::RespondToModel(
208 "fork_context is not supported in MultiAgentV2; use fork_turns instead".to_string(),
209 ));
210 }
211
212 let fork_turns = self
213 .fork_turns
214 .as_deref()
215 .map(str::trim)
216 .filter(|fork_turns| !fork_turns.is_empty())
217 .unwrap_or("all");
218
219 if fork_turns.eq_ignore_ascii_case("none") {
220 return Ok(None);
221 }
222 if fork_turns.eq_ignore_ascii_case("all") {
223 return Ok(Some(SpawnAgentForkMode::FullHistory));
224 }
👆 带下划线的行可以点:点哪行,讲哪行。

同一个仓库在 v1 → v2 之间把缺省值从「不带历史」翻成了「带全部历史」,工具说明里还专门提醒:传 fork_turns="none" 时子 agent 拿不到周围的上下文,可能缺少完成任务所需的信息(multi_agents_spec.rs:768)。

fork 的代价也直接:它把第 1 课想省下的东西又带了回来——子 agent 的窗口从父 agent 的整段历史起步。Claude Code 用前缀缓存把钱省回来一部分,但窗口占用是实打实的。交接的设计空间是一条轴:一端是只给一句话(最省、丢得最多),另一端是整段 fork(最全、最贵),中间是「写下来的决定 + 原始任务」。

🔍 文件式协调:把交接写到磁盘上

Anthropic 的《Harness design for long-running application development》(2026-03-24)用 planner → generator → evaluator 三个 agent,原文说它们之间的通信经由文件:一个写、另一个读后在同一文件或新文件里回应。这让交接本身变得可检查、可回放。调研档案对这条的核查是 2-1 非满票,并附三条限定:这是实验原型;配套仓库 cwc-long-running-agents 只实现了 builder 与 evaluator 两个 agent;「纯文件」的说法略强(agent 之间还共享 git 与运行中的应用)。第 4 课会回到它的 evaluator。

证据三问

问本课的回答
① 它解决什么问题?子 agent 只看得到交接原话;父 agent 的约束、已做过的判断,要么写进去,要么经 fork 继承
② 为什么写下来的决定可能有效?约束是子 agent 判断对错的依据;没有它,子 agent 只能按自己的理解补全(T4 里把 import 当成调用点)
③ 实验上呢?约束的传递是确定的:with_decisions 两次 6/6 约束到位,text_only 两次缺 3-4 条 ;报回的真实调用点 seed 1 为 0 对 5,seed 2 为 0 对 0

演进地图:你在这里

子 agent 的演进:每一代都被上一代的某个真数字逼出来
  1. 所有 harness 的默认形态;本课项目 T4 的「不用子 agent」两次运行
    解决了不需要交接:主 agent 亲眼看到每一行搜索结果,不会有信息在转手时丢失。
    卡在两次 grep 结果进主上下文 563 / 1,120 token,之后每轮跟着重发(A5);要判对兼容性还得读 7 个文件(内容 1,818 token),两次都没读——找到 5/6 与 5/6,兼容性判对 3/6 与 2/6。
  2. 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。
  3. 本课项目 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 轮就交差。
  4. 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 轮。
  5. learn-claude-code s17 独立评估器、Codex Guardian(A3)、Anthropic cwc-long-running-agents 的 evaluator、Hermes 后台自省 fork
    解决了完成与否不再由干活的 agent 自己说了算;评审者没有写工具,不能顺手「修好」来掩盖问题。
    卡在T1 与 T4 里最后一句话明确声称完成 20 次,verifier 判失败 12 次;评审者也是模型,也会被说服 → 确定性的判决在 A9。
旁支 多个 agent 同时改代码:worktree 隔离工作目录,同一文件的写串行
learn-claude-code s13、Codex(0.156 起默认开 worktree)、CoreCoder FILE_MUTATION_LOCK、Pi withFileMutationQueue
本课项目的子 agent 是同步的、只读的,T4 里不会发生写冲突;并行写同一个文件时才需要它们。worktree 不是沙箱。
旁支 Pi:核心不内置子 agent,交给扩展
Pi 31b513e3 README「No sub-agents」;examples/extensions/subagent
不是做不出来:子 agent 扩展 1,141 行,权限确认扩展 34 行,都放在核心之外。

数字口径:本课项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit 本机录制的 T4(2026-09-26,找全 parse_date 调用点,6 次运行 + 4 条子 agent trace;单一模型、每种配置 2 个 seed,只说明现象存在,不说明比例)。token 取自服务端 usage;「内容 token」用录制模型的 tokenizer 现数。耗时不作比较(本机有其他任务同时占用)。

一句话

📌
行动里藏着决定,只传任务文本就会丢掉它们。
🚦

先停在这里:记住交接的三个档位——一句任务、写下来的决定 + 原始任务、fork 整段历史——以及每一档丢什么、多花什么。
再深一层:去实验课,在回放里对照 seed 1 父 agent 第 1 轮的 grep 输出和它第 2 轮写的 decisions,找出它看见了却没交出去的每一行。

下一个缺口

子 agent 还能再派子 agent 吗?轮数预算算谁的? → 第 3 课

seed 2 的子 agent 15 轮预算只用了 3 轮。反过来的情况呢——子 agent 一轮接一轮停不下来,或者再派出自己的子 agent,父 agent 的上限还管不管得住?

你可能会问