子 agent 能无限派下去吗?轮数预算算谁的?
本课一问:子 agent 自己也是一个循环。它能不能再派出自己的子 agent,一层套一层?父 agent 设的轮数上限,子 agent 的轮数算不算在里面?
这一课先不碰:子 agent 交回来的东西可不可信(第 4 课);上限本身该设多少(A1 第 4 课已经讲过)。
从已有出发
A1 第 4 课定过一件事:循环要不要设轮数上限、设多少。现在循环里多了一个工具,调用它会在工具内部再跑一整个循环。T4 的每次运行是这样一串 API 调用(P = 父 agent,S = 子 agent,按真实先后):
| 运行 | 事件序列 | 父 agent 自己的轮数 |
|---|---|---|
| t4-sub-text-s1 | P1 P2 S1 S2 P3 | 3 |
| t4-sub-dec-s1 | P1 P2 S1 S2 S3 S4 P3 | 3 |
| t4-sub-text-s2 | P1 S1 S2 P2 P3 | 3 |
| t4-sub-dec-s2 | P1 P2 S1 S2 S3 P3 P4 P5 | 5 |
子 agent 的全部轮次都发生在父 agent 那一轮的工具调用里面:父 agent 调了 delegate,就一直等到子 agent 交回报告(岔路 B1 的泳道图画的就是这个)。
先自己设计:一个计数器管到底,不限深度
最省事的做法:整个任务只有一个轮数计数器,谁调模型谁加一;delegate 工具谁都能调,想派多深就派多深。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
下面的模拟器默认就是朴素方案:t4-sub-dec-s1、父子共用一个计数、上限 5。按 ▶ 一步步走,看最后一次调用 P3 发生了什么。
数据:事件序列来自本课项目 T4 的真实 trace(父 agent 上限 30、子 agent 15、深度上限 1);预算语义移植自 Hermes 56dc01d9(MIT);期望值由 gen_a07_subagents.py 直接加载 Hermes 原 IterationBudget 类,以及 bun / rustc / python 在四份 pin 源码的逐字切片上跑出(04-实践项目/scripts/parity/)。
两件事要分开:预算管「一层之内跑多少轮」,深度闸管「能套几层」。各家都是这么拆的,而且都拆在每一层上。
改进一:每一层各带一份预算
Hermes 把轮数预算写成一个只有两个动作的小类:consume() 每次调模型之前扣一次,扣不动就返回 False;refund() 退还一次。父 agent 与每个子 agent 各持一份。
子 agent 那份是在派生时新建的:delegate_tool.py 构造子 agent 时传 iteration_budget=None,旁边的注释就是一句「fresh budget per subagent」:
每个 agent 的循环怎么用这份预算,只在两处:循环条件,和每次调模型前的 consume()。
把模拟器切成「父子各一份」,再走一遍:
数据:事件序列来自本课项目 T4 的真实 trace(父 agent 上限 30、子 agent 15、深度上限 1);预算语义移植自 Hermes 56dc01d9(MIT);期望值由 gen_a07_subagents.py 直接加载 Hermes 原 IterationBudget 类,以及 bun / rustc / python 在四份 pin 源码的逐字切片上跑出(04-实践项目/scripts/parity/)。
试三件事:① 在上面这个实例里保持父上限 5,切「父子各一份」和「父子共用一份」,看 P3 的格子从 ✓ 变成 ✕;② 把子上限拖到 2,看子 agent 在第 3 次调用前停下、父 agent 仍然跑完——在本课项目里,父 agent 收到的报告末尾会多一行 [helper stopped: max_rounds after 2 rounds];③ 退还规则切成「构造:只调 grep 的轮也退还」,看两份计数器各少了几格(T4 里没有 execute_code,按原文一次也不会退)。
各家的预算放在哪一层:
| 父 agent 的上限 | 子 agent 的上限 | 父子是否共用 | 出处 | |
|---|---|---|---|---|
| learn-claude-code | 不设(while True) | 30 轮 | 各算各的 | s01_agent_loop/code.py:88;s06_subagent/code.py:274 |
| CoreCoder v0.7.0 | 50 轮 | 20 轮 | 各算各的 | agent.py:31;tools/agent.py:50 |
| OpenCode | 每个会话 agent.steps ?? Infinity | 同左,子会话自己计步 | 各算各的 | session/prompt.ts:1178-1179 |
| Hermes | pin 500(最新版不设) | pin 50(最新版 250) | 各算各的,总数可超过父上限 | iteration_budget.py:17-30;delegate_tool.py:1463-1466,1655 |
| 本课项目 | T4 里 30 | T4 里 15 | 各算各的;子 agent 用完时报告里写明 | scripts/record_t4.py:63,70;miniagent/subagent.py:103-104 |
| Claude Code | — | maxTurns(可选,无缺省值),到了就标记为 partial | 各算各的 | 官方文档 sub-agents |
🔍 同一个仓库里,注释和代码说的不一样
Hermes 的 AIAgent 构造函数签名还写着 max_iterations: int = 90, # Default tool-calling iterations (shared with subagents)(agent_init.py:470),而派生子 agent 的代码传的是 iteration_budget=None # fresh budget per subagent。深度闸那边也一样:注释写「default 2 for parity with the original MAX_DEPTH constant」(delegate_tool.py:3181),常量却是 MAX_DEPTH = 1(:128)。读源码时以执行路径为准,注释只当线索——这也是本课把原实现真跑一遍再对照的原因。
改进二:每一层都过一道深度闸
深度闸的写法各家几乎一样:数出自己在树里的深度,和一个配置值比。OpenCode 沿着会话的 parentID 往上数:
Codex v1 把同样的判断放在两个地方:能不能看到 spawn 工具(spec_plan.rs:602-605,超过深度就不把协作工具给模型),以及调用时再查一次:
下面四个深度闸都是从 pin 源码移植的,同一组「调用者深度 × 配置值」逐格现算;期望值由 bun、rustc、python 分别在四份原实现上跑出,页面加载时逐条对照(右上角的计数)。
「调用者深度」= 发起派生的那个 agent 在树里的深度(主 agent = 0,它派出的子 agent = 1……)。✓ = 这一层还能再派子 agent;✕ = 被闸住(悬停看原实现返回的报错原文)。Claude Code 一列按官方文档(2026-09-26 核对)的「默认最多 3 层,环境变量 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH 可改」;Codex v2 一列按 pin 里的测试 multi_agents_tests.rs:2420(v2 的 spawn 不看 agent_max_depth)。
数据:事件序列来自本课项目 T4 的真实 trace(父 agent 上限 30、子 agent 15、深度上限 1);预算语义移植自 Hermes 56dc01d9(MIT);期望值由 gen_a07_subagents.py 直接加载 Hermes 原 IterationBudget 类,以及 bun / rustc / python 在四份 pin 源码的逐字切片上跑出(04-实践项目/scripts/parity/)。
试三件事:① 保持缺省配置,确认四个实现在「主 agent 能派、子 agent 不能再派」上一致;② 把 OpenCode 和 Hermes 都设成 0:一个连主 agent 都不能派,一个照样能派——Hermes 把低于 1 的值抬到 1(delegate_tool.py:700-737);③ 看 Claude Code 一列:它的缺省是最多 3 层,和其余几家的 1 不一样。
两种闸:运行时闸(调用时查深度、超了报错,OpenCode、Codex、Hermes、本课项目)和结构闸(子 agent 的工具表里根本没有派生工具,learn-claude-code、CoreCoder)。OpenCode 两样都做:深度闸之外,子会话的权限里默认还加一条 task: deny(agent/subagent-permissions.ts:25)。Codex v2 走了另一条路:spawn 不看 agent_max_depth(pin 里有一个测试专门断言这一点,multi_agents_tests.rs:2420),限制的是同时存活的线程数。
在各家的实现里都不是:子 agent 各带一份预算,Hermes 的 docstring 直接写明父子总迭代数可以超过父 agent 的上限。要约束「整棵树一共花多少」,得另加一层(按 token 或按钱的总预算),轮数上限管不到。
常见误解二:「给子 agent 足够的预算,它就会把活干完。」预算只防「停不下来」,不防「过早收工」。T4 的 4 个子 agent 各有 15 轮,最多只用了 4 轮;t4-sub-dec-s2 的子 agent 用了 3 轮就交了「无法判断」的报告。它不是被截断的,是自己停的——这正是 A1 岔路 B3 的老问题,第 4 课接着讲。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 递归有上界;子 agent 的轮数不挤掉父 agent 收尾的那几轮 |
| ② 为什么一定有效? | 深度闸让调用树的深度 ≤ 配置值;预算独立时,父 agent 能跑的轮数只由它自己的上限决定 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:在录制的事件序列上,父 agent 需要的最小上限——各自一份时等于它自己的轮数,共用一份时等于它最后一轮之前的全部调用次数。本页四条运行逐条现算。 出处待补 |
| ③ 与原实现一致吗? | 预算 792 组网格(运行 × 模式 × 上限 × 退还规则)、深度闸 95 格,JS 移植与原实现逐条一致 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条Hermes 用 python 直接加载 pin 的 IterationBudget 类(循环闸门按 conversation_loop.py:1634、:1659-1669 原文转写);OpenCode 用 bun 跑 task.ts:104-117 的逐字切片;Codex 用 rustc 编译 registry.rs:64-78 的逐字切片;Hermes 深度闸用 python 执行原文函数;本课项目真跑 DelegateTool.execute。脚本在 04-实践项目/scripts/parity/。 出处待补 |
演进地图:你在这里
- 所有 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 现数。耗时不作比较(本机有其他任务同时占用)。
一句话
先停在这里:记住预算与深度闸各管一件事,并且都按层算;预算防停不下来,不防过早收工。
再深一层:在第一个模拟器里把运行换成 t4-sub-dec-s2,找出共用计数时父 agent 至少要多大的上限才能跑完,再和它自己的轮数比。
下一个缺口
干活的 agent 评自己的活,可信吗? → 第 4 课
子 agent 自己停下、交回一段「做完了」的报告;父 agent 也自己停下、写一段「做完了」的总结。这两句话,谁来核对?
你可能会问
- 「既然子 agent 预算独立,一个失控的子 agent 会不会把钱花光?」 会花掉它自己那份的全部:Hermes pin 缺省 50 轮,最新版 250。能挡住的是数量上限(Hermes 的
max_concurrent_children缺省 3,Codex 的线程上限)和深度闸;要按钱挡,得在 harness 外面加(A8)。 - 「本课项目为什么选深度 1?」 MILESTONES.md 的 A7 决策:子 agent 没有 delegate 工具,超限调用返回显式错误(
tests/test_subagent.py的test_depth_limit_is_an_explicit_error)。和 OpenCode、Codex v1、Hermes 的缺省一致;Claude Code 的缺省是 3。