代码语言

知识点思维导图

29 个知识节点

Harness Engineering(09) - Subagent 调度接口与委派治理

读完后,你应能完成以下任务:

  • 绘制“Harness Engineering(09) - Subagent 调度接口与委派治理 / 为什么需要子代理:单体 Agent 的三个瓶颈”的关键对象与数据流,解释“上下文爆炸:20 个模块的代码全读进一个上下文,窗口分分钟撑爆(第 06 章的痛)。 -> 慢:一个一个串行读、串行分析,干到天黑。 -> 互相干扰:读了模块 A 的细节,分析模块 B 时这些细节还堵在上下文里,又占地方又分心。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Harness Engineering(09) - Subagent 调度接口与委派治理 / 核心机制:上下文隔离 + 结果回传”设计正常与异常输入,验证“主 Agent 的上下文因此保持精简——它只拿到 3 句总结,而不是 3 个模块的全部代码。”,输出首个偏差位置与回归测试结果。
  • 实现“Harness Engineering(09) - Subagent 调度接口与委派治理 / 子代理本质上是什么?一个"工具"”的最小代码或配置,检验“从主 Agent 的角度看,"派一个子 Agent" 其实就是调用一个特殊的工具。”,输出命令、结果与 Diff,并说明不适用边界。

核心知识清单

  • Subagent 与 Multi-Agent 的适用边界
  • Supervisor 集中编排与结果汇总
  • Router 分类分发与并行专家
  • Handoff 控制权和会话状态交接
  • fan-out、fan-in 与部分失败
  • 上下文隔离、权限裁剪与预算

到这里,你已经能造一个完整的单体 Agent 了。但当任务又大又杂时,单个 Agent 会遇到瓶颈:上下文塞不下、干活太慢、容易跑偏。 解法是"分身术"——让主 Agent 把活拆开,派给多个子 Agent(sub-agent)去干。这一章讲清楚为什么要这么做,以及怎么做。


一、为什么需要子代理:单体 Agent 的三个瓶颈

想象一个任务:"分析这个项目里 20 个模块,分别总结每个模块干嘛的。"

让一个 Agent 硬扛会怎样?

  1. 上下文爆炸:20 个模块的代码全读进一个上下文,窗口分分钟撑爆(第 06 章的痛)。
  2. :一个一个串行读、串行分析,干到天黑。
  3. 互相干扰:读了模块 A 的细节,分析模块 B 时这些细节还堵在上下文里,又占地方又分心。

子代理就是来解决这三个问题的:

把大任务拆成小块,每块交给一个"全新的、干净的"子 Agent 独立处理,多个子 Agent 还能并行干。 主 Agent 只负责拆活、派活、收结果。

这就像一个项目经理(主 Agent)带一个团队(子 Agent 们):经理不亲自写每行代码,而是拆任务、分给组员并行做、最后汇总。


二、核心机制:上下文隔离 + 结果回传

子代理的精髓在于上下文隔离

主 Agent(上下文:任务全局、各子任务的"结论")
   │
   ├── 派活 ──▶ 子Agent A(独立干净的上下文:只装模块A的细节)──▶ 回传"模块A总结"
   │
   ├── 派活 ──▶ 子Agent B(独立干净的上下文:只装模块B的细节)──▶ 回传"模块B总结"
   │
   └── 派活 ──▶ 子Agent C(独立干净的上下文:只装模块C的细节)──▶ 回传"模块C总结"

关键点:

  • 每个子 Agent 有自己独立的上下文窗口,互不污染。子 A 读了多少模块 A 的细节,都不会占用主 Agent 或子 B 的窗口。
  • 子 Agent 只把"结论"回传给主 Agent,不回传一路读过的原始细节。主 Agent 的上下文因此保持精简——它只拿到 3 句总结,而不是 3 个模块的全部代码。
  • 这正是第 06 章"上下文质量 > 数量"的放大版:用隔离换取每个上下文的干净。

三、子代理本质上是什么?一个"工具"

这里有个让人豁然开朗的视角:

从主 Agent 的角度看,"派一个子 Agent" 其实就是调用一个特殊的工具。 这个工具叫 spawn_agent,输入是"子任务描述",输出是"子 Agent 干完后的结论"。

所以子代理编排没有引入全新机制——它还是第 02 章那个循环,只不过工具箱里多了个能"再开一个 Agent 循环"的工具:

主 Agent 调这个工具,就等于"招了个临时工去干一件事,干完汇报"。循环套循环,仅此而已。


四、并行:子代理最香的地方

子任务之间互不依赖时,可以并行派发,速度起飞:

串行干 20 个模块要 20 份时间,并行(5 个一批)大约只要 4 份。这是子代理相比单体 Agent 最直接的收益。

⚠️ 但记住第 04 章的提醒:只有互不依赖的子任务才能并行。如果子任务 B 需要子任务 A 的结果,就得串行——先拿到 A 的结论,再据此派 B。


五、什么时候用、什么时候别用

子代理很强,但不是银弹。它有成本:每个子 Agent 都要独立跑循环、独立烧 token,开销可能成倍增长。

适合用子代理:

  • 任务能清晰拆成互相独立的小块(分析多个文件、并行搜索多个方向)
  • 子任务会产生大量中间细节,但主 Agent 只关心结论(用隔离保护主上下文)
  • 需要并行提速

不适合用子代理:

  • 任务很简单,单个 Agent 几步就搞定——杀鸡用牛刀,徒增开销和复杂度
  • 子任务强依赖、来回交互频繁——拆开反而增加协调成本
  • 对 token 成本敏感的场景——子代理会显著放大消耗

判断口诀:

"这活能拆成几摊互不相干、又各自啰嗦的子活吗?" 能,子代理就值;不能,单体 Agent 更省心。


六、常见误区

误区 1:什么任务都拆子代理。 简单任务拆子代理是过度设计,多花钱多添乱。先问"真的需要隔离/并行吗"。

误区 2:把一路细节也回传给主 Agent。 那就失去了"上下文隔离"的意义。子 Agent 只回传结论,细节留在它自己的窗口里随用随弃。

误区 3:强依赖的子任务硬并行。 B 依赖 A 的结果却并行跑,B 拿不到 A 的产出,必然出错。有依赖就串行。

误区 4:忽视成本。 多个子 Agent = 成倍的 token。该用则用,但心里要有账。


七、最佳实践

先判断值不值得拆:能拆成独立、各自啰嗦的子活,才上子代理。 ✅ 把子代理当成一个"工具"理解(spawn_agent),就是循环套循环,不神秘。 ✅ 子 Agent 用全新干净的上下文,只回传结论给主 Agent,保护主上下文。 ✅ 互不依赖的子任务并行派发提速;有依赖的老老实实串行。 ✅ 心里记着 token 成本,别为省时间无脑开一堆子 Agent。


八、动手实践:子代理编排:主 Agent 并行指挥 3 个子 Agent

8.1 怎么跑

无需 API Key,直接跑(用 sleep 模拟每个子 Agent 的处理耗时):

python demo.py

输出大致是:

=== 串行执行 ===
子Agent 处理 a.py ... 完成
子Agent 处理 b.py ... 完成
子Agent 处理 c.py ... 完成
串行耗时:约 3.0 秒

=== 并行执行 ===
(3 个子 Agent 同时开工)
并行耗时:约 1.0 秒

=== 主 Agent 汇总 ===
主 Agent 只收到 3 句结论(没有收到 3 个文件的全部内容):
- a.py: ...
- b.py: ...
- c.py: ...

8.2 看点

  1. 上下文隔离:每个子 Agent 处理自己的文件,主 Agent 最后只拿到结论列表——对照第 09 章"只回传结论"。
  2. 并行提速:串行约 3 秒,并行约 1 秒——这就是子代理最香的收益。
  3. spawn_agent 就是个工具:看 spawn_sub_agent 函数,它对主 Agent 来说就是一次"调用"。
  4. 把文件数量加到 6 个,再看串行/并行耗时差距如何拉大。

九、总结

  • 为什么需要子代理:单体 Agent 的三个瓶颈:上下文爆炸:20 个模块的代码全读进一个上下文,窗口分分钟撑爆(第 06 章的痛)。 -> 慢:一个一个串行读、串行分析,干到天黑。 -> 互相干扰:读了模块 A 的细节,分析模块 B 时这些细节还堵在上下文里,又占地方又分心。
  • 核心机制:上下文隔离 + 结果回传:主 Agent 的上下文因此保持精简——它只拿到 3 句总结,而不是 3 个模块的全部代码。
  • 子代理本质上是什么?一个"工具":从主 Agent 的角度看,"派一个子 Agent" 其实就是调用一个特殊的工具。
  • 并行:子代理最香的地方:这是子代理相比单体 Agent 最直接的收益。
  • 什么时候用、什么时候别用:子代理很强,但不是银弹。
  • 常见误区:简单任务拆子代理是过度设计,多花钱多添乱。

9.1 可运行实验:Subagent 任务图与并发预算

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>AC-06 在线实验</title>
  <style>
    :root{color-scheme:dark;font-family:Inter,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:#0f1211;color:#e7ece9;font-size:13px}.shell{padding:16px}.top{display:flex;justify-content:space-between;gap:16px;margin-bottom:14px}h1{margin:3px 0;font-size:18px}.id,.value{color:#68e0b5;font-family:ui-monospace,monospace}.summary{margin:4px 0;color:#a5afa9}.run{border:0;border-radius:6px;background:#68e0b5;color:#07110d;padding:8px 14px;font-weight:700}.grid{display:grid;grid-template-columns:minmax(220px,.8fr) minmax(0,1.8fr);gap:12px}.panel{border:1px solid #29322e;background:#141817;padding:12px}.control{display:grid;gap:5px;margin-bottom:11px}.head{display:flex;justify-content:space-between;gap:8px}select,input{width:100%;accent-color:#68e0b5;background:#0d100f;color:#e7ece9}.toggle{display:flex;justify-content:space-between;border-top:1px solid #29322e;padding-top:9px}.toggle input{width:18px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.metric{border:1px solid #29322e;padding:8px}.metric b{display:block;color:#68e0b5;font-size:16px}.stages{display:flex;gap:6px;overflow:auto;margin:10px 0}.stage{border:1px solid #8a6230;padding:7px;min-width:90px}.stage.ok{border-color:#367a61}.stage.fail{border-color:#8b4545}table{width:100%;border-collapse:collapse}td{border-top:1px solid #29322e;padding:7px}.diagnosis{margin-top:9px;border-left:3px solid #68e0b5;background:#101412;padding:9px;line-height:1.5}.danger{border-color:#ef7f7f}@media(max-width:680px){.top,.grid{display:grid;grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,1fr)}}
  </style>
</head>
<body>
  <main class="shell">
    <header class="top"><div><div class="id">AC-06 · DETERMINISTIC LAB</div><h1 id="title"></h1><p class="summary" id="summary"></p></div><button class="run" id="run">运行实验</button></header>
    <section class="grid"><div class="panel"><div id="controls"></div><label class="toggle"><span>注入典型故障</span><input id="failure" type="checkbox"></label></div><div class="panel"><div class="metrics" id="metrics"></div><div class="stages" id="stages"></div><table><tbody id="rows"></tbody></table><div class="diagnosis" id="diagnosis"></div></div></section>
  </main>
  <script>
    const scenario = { title: 'Subagent 任务图与并发预算', summary: '根据 DAG 依赖和共享文件写集合计算并发批次与冲突。', controls: [
        { key: 'slots', label: '并发槽位', type: 'range', min: 1, max: 5, value: 3, suffix: ' 个' },
        { key: 'tasks', label: '子任务数量', type: 'range', min: 3, max: 9, value: 6, suffix: ' 个' },
        { key: 'strategy', label: '冲突策略', type: 'select', value: 'ownership', options: [['none', '不分配文件所有权'], ['ownership', '按文件所有权'], ['serial', '全部串行']] }
      ] };
    const controls = document.querySelector('#controls');
    const failure = document.querySelector('#failure');
    document.querySelector('#title').textContent = scenario.title;
    document.querySelector('#summary').textContent = scenario.summary;
    function renderControl(control) {
      const label = document.createElement('label'); label.className = 'control';
      const head = document.createElement('span'); head.className = 'head'; head.innerHTML = '<span>' + control.label + '</span><span class="value" data-value="' + control.key + '"></span>'; label.appendChild(head);
      const input = document.createElement(control.type === 'select' ? 'select' : 'input'); input.dataset.key = control.key;
      if (control.type === 'select') control.options.forEach(option => { const item = document.createElement('option'); item.value = option[0]; item.textContent = option[1]; item.selected = option[0] === control.value; input.appendChild(item); });
      else { input.type = 'range'; input.min = control.min; input.max = control.max; input.step = control.step || 1; input.value = control.value; }
      input.addEventListener('input', updateValues); label.appendChild(input); return label;
    }
    function updateValues() { scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); document.querySelector('[data-value="' + control.key + '"]').textContent = control.type === 'select' ? input.options[input.selectedIndex].text : input.value + (control.suffix || ''); }); }
    function readValues() { const values = {}; scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); values[control.key] = control.type === 'range' ? Number(input.value) : input.value; }); values.failure = failure.checked; return values; }
    function stage(name, state, detail) { return { name, state, detail }; }
    const aiStage = stage;
    function clamp(value, minimum, maximum) { return Math.min(maximum, Math.max(minimum, value)); }
    function simulate(values) { const fail = values.failure;
          /** 串行策略或并发槽位决定的实际并发数。 */
          const concurrency = values.strategy === 'serial' ? 1 : Math.min(values.slots, values.tasks);
          /** 任务需要的并发批次数。 */
          const waves = Math.ceil(values.tasks / concurrency);
          /** 未划分文件所有权时可能出现的冲突数。 */
          const conflicts = values.strategy === 'none' ? Math.max(1, Math.floor(values.tasks / 3)) : fail ? 1 : 0;
          /** 完成全部批次的估算时间。 */
          const duration = waves * 4 + conflicts * 3;
          return { metrics: [[concurrency, '实际并发'], [waves, '执行批次'], [conflicts, '文件冲突'], [duration + 'm', '估算耗时']], stages: [stage('拆分 DAG', 'ok', values.tasks + ' tasks'), stage('检查依赖', fail ? 'warn' : 'ok', fail ? '缺失边' : 'valid'), stage('分配所有权', values.strategy === 'ownership' ? 'ok' : values.strategy === 'serial' ? 'warn' : 'fail', values.strategy), stage('并发执行', conflicts ? 'warn' : 'ok', concurrency), stage('汇总', conflicts ? 'fail' : 'ok', conflicts ? '待解决' : '完成')], rows: [['可并行任务', Math.max(0, values.tasks - 2) + ' 个,根任务与汇总保持依赖'], ['冲突传播', conflicts ? conflicts + ' 个任务修改同一文件,汇总前必须解决' : '写集合互斥,可直接汇总'], ['失败策略', fail ? '一个依赖任务超时,下游任务取消' : '失败只取消依赖分支']], diagnosis: conflicts ? '增加槽位不会消除共享文件冲突,应先按文件或模块划分所有权。' : 'DAG 依赖和写集合清晰,并发能缩短关键路径。', danger: conflicts > 0 };
         }
    function render() { const result = simulate(readValues()); document.querySelector('#metrics').innerHTML = result.metrics.map(item => '<div class="metric"><b>' + item[0] + '</b><span>' + item[1] + '</span></div>').join(''); document.querySelector('#stages').innerHTML = result.stages.map(item => '<div class="stage ' + item.state + '"><b>' + item.name + '</b><div>' + item.detail + '</div></div>').join(''); document.querySelector('#rows').innerHTML = result.rows.map(item => '<tr><td>' + item[0] + '</td><td>' + item[1] + '</td></tr>').join(''); const diagnosis = document.querySelector('#diagnosis'); diagnosis.textContent = result.diagnosis; diagnosis.className = 'diagnosis' + (result.danger ? ' danger' : ''); }
    scenario.controls.forEach(control => controls.appendChild(renderControl(control))); updateValues(); document.querySelector('#run').addEventListener('click', render); render();
  </script>
</body>
</html>

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“Subagent 调度接口与委派治理”中,需要同时满足“Subagent 与 Multi-Agent 的适用边界”与“Supervisor 集中编排与结果汇总”。给定正文约束“看 spawnsubagent 函数,它对主 Agent 来说就是一次"调用"。”,哪些判断保持了原有处理机制?多选
2“Subagent 调度接口与委派治理”出现偏差:“在“Subagent 调度接口与委派治理 / Router 分类分发与并行专家”中,即使不满足“所以子代理编排没有引入全新机制——它还是第 02 章那个循环,只不过工具箱里多了个能"再开一个 Agent 循环"的工具”,结果与副作用仍会保持不变。”已成为实际行为。围绕“Router 分类分发与并行专家”与“Handoff 控制权和会话状态交接”,哪些判断能定位被改变的职责或边界?多选
3评审“Subagent 调度接口与委派治理”方案时,验收条件包含“子任务强依赖、来回交互频繁——拆开反而增加协调成本”。关于“fan-out、fan-in 与部分失败”与“上下文隔离、权限裁剪与预算”的哪些决策符合正文机制?多选