Appearance
第21章 · Subagent 与 Multi-Agent
全部动手内容用确定性 ScriptedPlanner(重放固定分解方案),只证明编排合同——分派、上下文隔离、委派深度上限、超时、结果合并——不证明 live planner 质量,与第13章 FixtureModel 同一边界。A2A 仅概念级阅读,不实操。
先修:第13章的手写 agent harness(显式状态机、typed tools、checkpoint、HITL)+ 第16章的 workflow 模式谱系(orchestrator-workers、evaluator-optimizer、workflow vs agent 决策边界)。
本章目标
- 说清 subagent 的真实收益(上下文隔离、关注点分离、并行)与成本面(token 开销、协调复杂度、错误复合),并能回答"什么时候不该用 multi-agent"。
- 在第13章 harness 思路上实现一个 orchestrator + 2 个 subagent 的最小系统:任务契约、隔离上下文、结构化结果合并、跨 agent trace。
- 概念级掌握 A2A 协议(Agent Card、任务生命周期)与 MCP 的分工,知道 A2A 管"跨组织/跨框架的 agent 互操作",不管"一个 agent 内部怎么调自己的 subagent"。
- 亲眼看到委派循环被 depth limit 兜底拦截,并能背出 multi-agent 的四类故障模式与各自的防线。
第 1 节 · 为什么需要 Subagent
第13章的单 agent loop 里,所有中间产物——工具输出、试错、草稿——都堆进同一份 messages。任务一大,主上下文就被子任务的噪声污染:真正重要的决策上下文被挤出窗口,模型开始"忘记"原始目标。Subagent 的第一收益不是"分工",而是上下文隔离:子任务在自己独立的上下文窗口里跑完,主 agent 只收到压缩后的结构化结果。
Anthropic 在构建多 agent 研究系统的工程总结(2025-06)里给出了迄今最实的一手数据:orchestrator-workers 架构(Opus 4 主导 + Sonnet 4 subagent 并行)在其内部研究评测上比单 agent Opus 4 高出 90.2%;在 BrowseComp 上,token 用量单独解释了 80% 的性能方差——multi-agent 有效的本质机制是"用隔离的上下文并行地花更多 token"。代价同样量化过:agent 交互约消耗聊天 4 倍的 token,multi-agent 系统约 15 倍;并行工具调用把复杂查询的研究时间最多压缩 90%。
三个收益与三个成本:
| 收益 | 机制 |
|---|---|
| 上下文隔离 | 子任务的搜索/试错/草稿不进主上下文,主 agent 只收结构化结论 |
| 关注点分离 | 每个 subagent 有自己的 system prompt、工具集、输出契约,可独立测试 |
| 并行 | 独立子任务同时推进,wall-clock 缩短(Anthropic 实测最多 90%) |
| 成本 | 机制 |
|---|---|
| token 开销 | multi-agent ≈ 15× 聊天成本;只有高价值任务才划算 |
| 协调复杂度 | 分解、分派、合并都是新的失败点;orchestrator 的 prompt 微调会放大到全体 worker |
| 错误复合 | 单 agent 错了你调试一个 loop;multi-agent 错了你要先归因到哪个 agent、哪一跳 |
什么时候不该用 multi-agent——与第16章的 workflow vs agent 决策边界是同一条纪律的延伸:从最简单的方案起步。子任务无法独立(需要共享中间状态反复对齐)、任务本身不可分解、或者一个上下文窗口装得下全部工作时,单 agent + 多工具就是正确答案。判断式:只有当"子任务可以独立理解、独立验证、结果可以结构化合并"三条都成立时,subagent 的隔离收益才大于协调成本。 Anthropic 自己的结论也是:multi-agent 擅长"有价值的、重并行化的、超出单一上下文窗口的"任务,不满足这三条就不要上。
第 2 节 · 单 agent → 主从架构
最小主从系统只有四个部件:orchestrator 分解目标并分派任务 → subagent 在隔离上下文里执行 → 结构化结果返回 → orchestrator 合并。参考实现 python/agent_core/multi_agent.py(stdlib,确定性):
python
@dataclass(frozen=True)
class SubTask: # 任务契约
task_id: str
goal: str
context: Mapping[str, Any] # subagent 能看到的全部输入
timeout_seconds: float = 10.0
on_failure: str = "propagate" # propagate = 失败拖垮报告; skip = 丢弃该结果
class Orchestrator:
def __init__(self, agents, *, max_depth=2, max_tasks=16, clock=time.monotonic): ...
def run(self, goal, plan) -> OrchestratorReport # 分派 + 合并 + trace
def dispatch(self, agent_name, task, *, depth) -> SubResult任务契约就是 subagent 世界的边界,四个字段各有防线含义:context 是隔离边界(subagent 看不到主 agent 的 messages,也看不到兄弟 subagent 的任何中间产物);timeout_seconds 是时间边界(超时后输出被丢弃——调用方已经不等了);on_failure 是失败语义(关键路径 propagate,可选增强 skip);task_id 是归因锚点(trace 里每个事件都带它)。
subagent 侧的唯一通道是 SubAgentContext:note() 写进自己的私有上下文,spawn() 回绕到 orchestrator 分派子任务——subagent 不能直接 new 另一个 agent 来跑,所有委派都过 orchestrator,深度和预算限制才永远生效。这就是"主从"与"自由组网"的分界:通信拓扑收敛到一个可审计的点。
动手:跑 demo_plan() 的最小系统——ScriptedPlanner(重放固定分解,FixtureModel 的编排版)把 fixture-research 拆给 researcher 与 analyst 两个 subagent,各自在隔离上下文里工作,orchestrator 把两份输出做 key 级合并。实测(run_all(),证据见 Evidence 节):2 个任务完成、合并出 3 个 key、orchestrator 自身上下文只有 goal: 与 merged: 两行——subagent 的工作笔记一行都没有漏进主上下文,这就是隔离的可测试形态。
第 3 节 · Subagent 间通信与状态
两条路线:
| 共享状态 | 消息传递 | |
|---|---|---|
| 模型 | 所有 agent 读写同一份 store/blackboard | agent 间只交换显式消息 |
| 优点 | 无需协议设计,读取即得 | 边界清晰、可审计、可跨进程/跨组织 |
| 代价 | 写冲突、隐性耦合、"谁改了什么"难以归因 | 协议设计成本、消息模式演进成本 |
| 适用 | 同进程、同团队、短生命周期 | 跨边界、长任务、需要审计链 |
本章的实现是消息传递的退化形态:subagent 不互发消息,只向 orchestrator 返回结构化结果(fan-out / fan-in)。这是故意的——worker 之间直接通信会产生耦合,破坏并行独立性(Anthropic 的工程结论:worker 执行期间共享状态是 design smell,唯一协调点应该是向 orchestrator 汇报)。
跨组织/跨框架时,消息传递需要一个协议。A2A(Agent2Agent):Google 2025-04 发布、2025-06 捐给 Linux Foundation 的开放协议,2026 年已达 v1.0(150+ 支持组织,Apache-2.0)。三个核心概念:
- Agent Card:agent 发布在
/.well-known/agent.json的 JSON 元数据——能力(skills)、端点、认证要求。解决"发现"问题:调用方不用知道对方内部实现。 - Task:委派的基本单元,有显式生命周期(
submitted → working → (input-required) → completed / failed / canceled),长任务通过 SSE 流回状态更新。 - Opaque 协作:双方不共享内部记忆、工具或提示词——只交换协议定义的消息,这既是安全边界也是 IP 边界。
MCP vs A2A 分工(两者互补,不是竞争):
| MCP | A2A | |
|---|---|---|
| 连接谁 | agent ↔ 工具/数据 | agent ↔ agent |
| 类比 | LSP(编辑器↔语言服务) | 服务间 RPC + 发现 |
| 交互单元 | tool call(tools/list / tools/call) | Task(带生命周期状态机) |
| 治理 | Anthropic 发起,开放规范 | Google 发起,Linux Foundation 托管 |
| 与本章关系 | 第13章已实操(mcp_adapter) | 仅概念阅读,不实操 |
一个容易被忽略的边界(A2A 官方明确写出):A2A 不是 sub-agent 协议——它不规定一个 agent 怎么跟自己的 subagent 说话,那是框架原语(本章的 ctx.spawn())或 MCP 的事。A2A 管的是"别人的 agent"。 layering 记忆法:MCP 在下(工具层),A2A 在上(跨边界协作层),subagent 委派在框架内部。
另需点名:clean_room/multi_agent.py 是早期练习场骨架(A2AAdapter 空接口),不在 contracts.json 合同、run_clean_room.py、run_clean_room_faults.py 的覆盖范围内,其 docstring 原承诺的"必须设计 3 类故障注入"已降级为概念阅读材料——可运行实现与故障注入证据在 python/agent_core/multi_agent.py,以后者为准。是否把该骨架正式接进合同体系是课程 owner 的更大决策,本章不动。
第 4 节 · Multi-Agent 的故障模式
| 故障模式 | 机制 | 防线(本章实现里的对应物) |
|---|---|---|
| 级联失败 | 一个 subagent 的坏输出成为下游输入,误差沿委派链放大 | 任务契约的 on_failure;subagent 崩溃不穿透 orchestrator(异常→failed SubResult);合并只收 ok 结果 |
| 无限委派循环 | A 委派 B、B 委派 A,互相"等对方先做完" | max_depth 深度上限(主防线)+ max_tasks 总预算(第二防线) |
| 结果冲突 | 两个 subagent 对同一事实给出不同答案 | 合并时冲突显式记录而非静默覆盖(merge_outputs:先到先得 + conflict 条目) |
| 观测与归因断裂 | 出问题时无法回答"哪个 agent 在哪一跳出了错" | 跨 agent 单一有序 trace:每事件带 seq/task_id/agent/depth |
动手(故障注入,必做):demo_delegation_loop() 注册 loop_a/loop_b 两个互相委派的 subagent——A 的处理函数向 B 发子任务,B 向 A 发。没有任何防线时这是无界递归;max_depth=2 下实测:depth 0→1→2 正常分派,第 3 跳被 depth_limit 事件拦截,返回 delegation_depth_exceeded,全程只跑了 3 个子任务。对照实验(测试里):把 depth 与 budget 两个兜底都拿掉,同一个循环会一路涨到 Python 递归上限才被解释器拦下(实测 tasks_used > 100)——兜底不是装饰,是唯一的终止性来源。
注意一个语义细节:depth limit 拦截发生在 spawn 处,把结果返回给发起委派的 handler——orchestrator 负责拦住,如何向用户解释"任务没做完"是 handler 的事。编排层提供机制,语义层负责政策,与第13章的 schema/policy 分工同构。
第 5 节 · LangGraph 里的 multi-agent
LangGraph 的 supervisor 模式就是本章主从架构的图化表达:supervisor 是一个路由节点,每个 subagent 被包成一个子图节点(有自己的 State 与 checkpointer),supervisor 用条件边或 Command(goto=...) 把控制交给某个 worker,worker 返回后控制权回到 supervisor——第16章第 5 节的 orchestrator-workers 行就是它。与 evaluator-optimizer 的关系:evaluator-optimizer 是"生成者+评分者"两个角色的固定回环(你预先知道要几轮、谁来评),supervisor/orchestrator-workers 是"分解者+N 个 worker"的动态扇出(子任务运行时才确定);两者共用同一套图原语,差别仍在图拓扑与路由函数里。本章不重复实操:把手写 orchestrator 映射成 supervisor 图的练习,就是第16章概念映射表的自然延伸。
数学桥接:第2章 → 第21章
本章的 Multi-Agent 编排可以用第2章的概率和优化直觉来解释:
上下文隔离与 entropy:第2章 §6 的 entropy(probs) = -Σ p·log₂(p) 衡量一个分布的不确定性。单 agent 把所有中间产物堆进同一份 messages——噪声增加 = 上下文分布的熵增加。Subagent 的隔离本质上是"减少主上下文的信息熵":只保留结构化结论,丢弃搜索/试错/草稿的中间态。Anthropic 的 80% 性能方差来自 token 用量,本质上就是"噪声信息量"对决策质量的负面影响。
Token 开销与 cross_entropy:第2章 §6 的 cross_entropy(p_dist, q_dist) 衡量模型对下一个 token 的"意外程度"。模型不确定性高时(CE 大),agent 倾向于更长的推理链、更多工具调用、更多 token。Multi-agent 的 15× token 开销来自三倍叠加:每个 subagent 有自己的上下文、每个 agent 调用都需要 I/O 往返、orchestrator 合并需要额外处理。
Orchestrator 分派与 gradient_descent_step:第2章 §7 的 gradient_descent_step(param, grad, lr) = param - lr · grad 是最简更新。Orchestrator 的工作是"决定下一步往哪里走"——它观察当前状态(gradient 方向),选择最优 subagent(更新方向),执行分派(lr × grad = 步长)。每个 subagent 返回结构化结果后,orchestrator 根据结果调整下一步策略——这和梯度下降的"观察 → 更新 → 再观察"循环同构。
归因调试与 partial_derivative:第2章 §3 的 partial_derivative(f, x, x0, eps) 回答"改变 x 对 f 的影响有多大"。Multi-agent 调试时问"最终结果偏离了,哪个 agent 应该负责"——这正是对最终结果关于每个 agent 输出的偏导数:∂output/∂agent_i_output。如果 agent_i 的输出对最终结果影响大(偏导数大),那它更可能是故障源头。
A2A 协议与 categorical_sample:第2章 §5 的 categorical_sample(probs, rng) 从能力分布中选择一个动作。A2A 的 Agent Card 描述了 agent 的"能力分布"(它能处理什么类型的任务),task lifecycle 是状态转移——从 created 到 completed 或 failed 的路径选择。MCP 管"一个 agent 内部怎么调工具",A2A 管"不同 agent 之间怎么发现彼此、怎么发起任务"。
前端类比:orchestrator-workers = Web Worker + postMessage——主线程(orchestrator)把计算分给 worker(subagent),worker 在独立线程跑完返回结构化消息,不污染主线程状态。A2A = Service Worker + Cache API——跨进程/跨框架的 agent 通过标准协议发现彼此并交换数据。
故障注入与预期信号(权威表)
| 注入 | 预期失败信号 | 修复后证据 |
|---|---|---|
| subagent 直接读写主 agent 的 messages(共享上下文) | 主上下文被子任务试错/草稿污染;兄弟任务互相可见,隔离断言失败 | subagent 只见 SubTask.context;实测 orchestrator.context 只有 goal/merged 两行,兄弟 context window 互不相交 |
| 委派循环无 depth limit(A↔B 互相委派) | 无界递归,直到解释器 RecursionError(实测 tasks_used > 100) | max_depth=2 下第 3 跳被 depth_limit 拦截,tasks_used=3,trace 有 1 条 depth_limit 事件 |
| 只设 depth limit 不设总预算 | 宽扇出(每层派生多个子任务)指数膨胀,深度合法但总量失控 | max_tasks 第二防线:预算耗尽返回 task_budget_exceeded(测试实测 4 任务封顶) |
| subagent 超时后输出仍被收下 | 过期结论进入合并,下游基于 stale 结果继续 | 超时输出丢弃(实测 elapsed 5.0s > 1.0s → status=timeout、output=None) |
| subagent 抛异常直接穿透 orchestrator | 一个 worker 崩溃炸掉整个编排 | 异常→failed SubResult;on_failure=propagate 使报告 failed、skip 使报告 partial(均有测试) |
| 结果冲突静默覆盖(后来者赢) | 两个 subagent 对同一事实的分歧被隐藏,答案取决于分派顺序 | merge_outputs 显式记录 conflict(key/kept/rejected/来源 task_id),实测 1 条冲突、保留值确定 |
| 分派给未注册的 agent 名 | 拼写错误的 agent 名被静默忽略或当成成功 | fail-closed:unknown agent failed SubResult,报告 failed |
规范与延伸
- Anthropic: How we built our multi-agent research system(2025-06)——orchestrator-workers、90.2% 提升、4×/15× token 成本、80% 方差由 token 用量解释的原始出处。
- Anthropic: Building effective agents——"从最简单方案起步"原则与五 workflow 模式(第16章已展开)。
- A2A Protocol 官方站点——Agent Card、Task 生命周期、与 MCP 的分工、"A2A 不是 sub-agent 协议"的官方边界表述;治理:Linux Foundation,Apache-2.0。
- LangGraph multi-agent(supervisor)(以执行日官方文档为准)。
前端/Agent 迁移
orchestrator-workers 就是你写过的主从进程模型:orchestrator 是主线程/事件循环,subagent 是 Web Worker——独立的上下文(Worker 全局作用域)、消息传递(postMessage 而非共享内存)、结构化结果(structured clone)、超时与预算(终止 Worker)。depth limit 等价于"任务不允许再创建 Worker 超过 N 层";max_tasks 等价于连接池上限。trace 的 seq + task_id + depth 就是你在 Sentry 里做过的跨层级链路归因:没有它,分布式系统的故障定位就退化成猜。新的只有一件事:子任务的分解来自一个不确定的模型输出,所以契约(schema、预算、失败语义)必须比人写分解时更硬。
口述与自测(不看资料,5–10 分钟)
- 什么时候该用 subagent?用"子任务可独立理解、可独立验证、结果可结构化合并"三条作判据,并说出上下文隔离的真实收益(主上下文不被子任务噪声污染)与代价(15× token 量级)。
- 画出 orchestrator-workers 与"单 agent + 多工具"的边界:给一个具体任务(如"给这个仓库写周报" vs "并行调研 10 个竞品的定价页")分别判定,并说明理由。
- 解释 multi-agent 为什么更难调试:错误复合 + 归因需要跨 agent 链路;说出本实现 trace 的四个字段各自回答什么问题。
- 背出 A2A 与 MCP 的分工(连接谁、交互单元、治理),并解释"A2A 不是 sub-agent 协议"这句话。
- 委派循环怎么防?说出 depth limit(主)与 max_tasks(副)两道防线各自拦什么形状的失控(深链 vs 宽扇出),并用实测数字(3 vs >100)作证。
- 用第2章
entropy解释上下文隔离为什么能提升决策质量:单 agent 把所有中间产物堆进同一份messages,噪声增加 = 上下文分布的熵增加;subagent 只返回结构化结论 = 降低主上下文的信息熵。 - 用第2章
partial_derivative理解 multi-agent 归因:最终结果偏离时,"哪个 agent 应该负责"等价于求 ∂output/∂agent_i_output——偏导数大的 agent 更可能是故障源头。 - 用第2章
gradient_descent_step类比 orchestrator 分派:orchestrator 观察当前状态(gradient 方向)→ 选择最优 subagent(更新方向)→ 执行分派(lr × grad = 步长)→ 根据返回结果调整策略。
动手实验
最小主从系统 + 四个故障注入 demo,全部本地确定性运行,无网络、无 API key。
环境准备
bash
cd <仓库根>
export PYTHONPATH="$PWD/python"命令与预期输出
bash
# 1) 全部 16 项测试(隔离断言 / depth limit / 预算 / 超时 / 冲突 / 归因)
.venv/bin/python -m pytest python/tests/test_multi_agent.py -q
# 2) demo 汇总指标(evidence 数据源)
.venv/bin/python python/agent_core/multi_agent.pytext
................ [100%]
16 passed
demo: 2 个 subagent 完成,合并 3 个 key,报告 completed
isolation: orchestrator 上下文无 subagent 笔记;兄弟 context window 互不相交
loop: A↔B 委派循环第 3 跳被 depth_limit 拦截(1 条事件,tasks_used=3)
conflict: 两个 subagent 对 fact:population 分歧 → 1 条冲突记录,确定性保留 "14M"
timeout: 超时输出被丢弃(status=timeout, output=None)
判定信号:
上下文隔离三条断言全部 true
委派循环被拦截且任务数有界;对照组(无兜底)tasks_used > 100
冲突显式记录而非静默覆盖;超时输出不入合并概念图
图:多 Agent 编排器 + 并行 SubAgent — ScriptedPlanner 把用户目标分解为带契约(context/timeout/on_failure)的子任务,并行委派给隔离上下文的 SubAgent(researcher + analyst)。每个 SubAgent 在独立上下文中执行,结果通过 merge_outputs 合并(冲突显式记录而非静默覆盖)。delegation_depth > max_depth 时被拦截,防止无限委派递归。trace 只记录 task_id/agent/depth 等元数据,不含 prompt 原文——沿袭第13章的 redact 纪律。
资源 / 成本 / 隐私
全部 demo 与测试本地运行、无网络、无真实模型调用,gross cost 为 0;无新增依赖(stdlib)。ScriptedPlanner 只重放固定分解,trace 只记录 task_id/agent/depth 等元数据,不含 prompt 与 payload 原文——沿用第13章的 redact 纪律。真实多 agent 系统里 subagent 输出可能含敏感数据,合并与 trace 落盘前要过同一套脱敏门。
Evidence
仓库当前机器证据(只读快照)
evidence/21-multi-agent-v1.json 是当前 checkout 的脱敏机器运行记录:test_multi_agent.py 16 项全部通过、隔离三断言、委派循环拦截(depth_limit 1 条 / tasks_used 3)、冲突合并(1 条冲突、保留 "14M")、超时丢弃的实测指标。全部 demo 用确定性 ScriptedPlanner,只证明编排合同,不证明 live planner 质量;A2A 为概念阅读未实操。模块已登记进 evidence/module-manifest-v1.json,站点导航接线(sidebar、章节总览、examples 重跑索引)已完成。
学习者提交模板(待填写,不是当前机器证据)
复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actual 和 artifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。
yaml
schema: learn-llm.evidence.v1
module: 21-multi-agent
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 10
commands:
- PYTHONPATH=python python -m pytest python/tests/test_multi_agent.py -q
- PYTHONPATH=python python python/agent_core/multi_agent.py
metrics:
- name: multi_agent_tests_passed
expected: 16
actual: <recorded-value>
- name: isolation_orchestrator_free_of_subagent_notes
expected: true
actual: <recorded-value>
- name: isolation_sibling_contexts_disjoint
expected: true
actual: <recorded-value>
- name: loop_depth_limit_events
expected: 1
actual: <recorded-value>
- name: loop_tasks_used
expected: 3
actual: <recorded-value>
- name: conflict_count
expected: 1
actual: <recorded-value>
- name: timeout_output_discarded
expected: true
actual: <recorded-value>
artifacts:
- <learner-repo-relative-artifact-path>
cost:
gross_usd: 0
credit_usd: 0
known_failures:
- <sanitized-failure-or-none>只有"跑通了 demo"截图、没有隔离断言与委派循环拦截计数(含无兜底对照组)时,本章保持 gate。
下一步
本章是第二梯队扩展章,站点导航接线(sidebar、章节总览、examples 重跑索引、module-manifest 登记)已完成;模型层专题 第20章 与本章互为补充。回顾主线可回到 第19章 Capstone 或 教程总览。