Appearance
第20章 · Subagent 与 Multi-Agent 架构
前置要求:掌握 第18章 · 生产编排:StateGraph 与 LangGraph 对拍 的工作流编排与 第19章 · 全栈流式交付与增量渲染 的全双工交互。
第二梯队的最后一章:从"一个 agent 调工具"走到"一个 orchestrator 带多个 subagent"。这一章的第一课其实是克制——multi-agent 的收益(上下文隔离、并行)真实但昂贵(15 倍 token 量级),大部分任务单 agent 加多工具就够了。学完你会实现一个 orchestrator + 2 个 subagent 的最小系统,并亲眼看到委派循环被深度上限拦下。
全部动手内容用确定性 ScriptedPlanner(重放固定分解方案),只证明编排合同——分派、上下文隔离、委派深度上限、超时、结果合并——不证明 live planner 质量,与第15章 FixtureModel 同一边界。A2A 仅概念级阅读,不实操。
本章目标
学完后你能做到:
- 说清 subagent 的真实收益(上下文隔离、关注点分离、并行)与成本面(token 开销、协调复杂度、错误复合),并能回答"什么时候不该用 multi-agent"。
- 在第15章 harness 思路上实现一个 orchestrator + 2 个 subagent 的最小系统:任务契约、隔离上下文、结构化结果合并、跨 agent trace。
- 概念级掌握 A2A 协议(Agent Card、任务生命周期)与 MCP 的分工,知道 A2A 管"跨组织/跨框架的 agent 互操作",不管"一个 agent 内部怎么调自己的 subagent"。
- 亲眼看到委派循环被 depth limit 兜底拦截,并能背出 multi-agent 的四类故障模式与各自的防线。
阶段一:为什么需要 Subagent
第15章的单 agent loop 里,所有中间产物——工具输出、试错、草稿——都堆进同一份 messages。任务一大,主上下文就被子任务的噪声污染:真正重要的决策上下文被挤出窗口,模型开始"忘记"原始目标。Subagent 的第一收益不是"分工",而是上下文隔离:子任务在自己独立的上下文窗口里跑完,主 agent 只收到压缩后的结构化结果。
和第3章的联系:用 entropy 直觉看——中间噪声越多,主上下文的信息越杂、决策质量越差;subagent 只返回结构化结论,等于给主上下文"降噪"。
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——与第18章的 workflow vs agent 决策边界是同一条纪律的延伸:从最简单的方案起步。子任务无法独立(需要共享中间状态反复对齐)、任务本身不可分解、或者一个上下文窗口装得下全部工作时,单 agent + 多工具就是正确答案。判断式:只有当"子任务可以独立理解、独立验证、结果可以结构化合并"三条都成立时,subagent 的隔离收益才大于协调成本。 Anthropic 自己的结论也是:multi-agent 擅长"有价值的、重并行化的、超出单一上下文窗口的"任务,不满足这三条就不要上。
阶段二:单 agent → 主从架构
最小主从系统只有四个部件:orchestrator 分解目标并分派任务 → subagent 在隔离上下文里执行 → 结构化结果返回 → orchestrator 合并。
前端类比:这就是 Web Worker + postMessage——主线程(orchestrator)把计算分给 worker(subagent),worker 在独立线程跑完返回结构化消息,不污染主线程状态。
参考实现 python/agent_core/multi_agent.py(stdlib,确定性):
python
@dataclass(frozen=True)
class SubTask: # 1. 任务派发契约:输入边界
task_id: str
goal: str
context: Mapping[str, Any] # subagent 能看到的全部输入(严格物理隔离)
timeout_seconds: float = 10.0
on_failure: str = "propagate" # propagate = 失败拖垮报告; skip = 丢弃该结果
@dataclass
class SubResult: # 2. 子任务执行返回:状态与局部产物
task_id: str
agent: str
status: str # completed | failed | timeout | delegation_depth_exceeded
output: Mapping[str, Any] | None = None
reason: str = ""
depth: int = 0
elapsed_seconds: float = 0.0
@dataclass
class OrchestratorReport: # 3. 编排器汇总结算:全景报告
status: str # completed | partial | failed
goal: str
results: list[SubResult]
merged: dict[str, Any] # 跨子任务 key 级合并结果
conflicts: list[dict[str, Any]] # 显式记录的键冲突清单
trace: list[dict[str, Any]]
tasks_used: int
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 的工作笔记一行都没有漏进主上下文,这就是隔离的可测试形态。
阶段三: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 托管 |
| 与本章关系 | 第15章已实操(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,以后者为准。
多 Agent 协作三大主流拓扑对比与通信契约 (LL-Agent-4)
在多 Agent 协同体系中,通信结构决定了系统的扩展性与容错边界。生产界主要演进出三大拓扑结构(实现见 python/agent_core/multi_agent_topologies.py):
| 架构拓扑 | 拓扑结构 | 核心通信机制 | 状态同步契约 | 失败容错与降级策略 | 适用业务场景 |
|---|---|---|---|---|---|
| Supervisor (中心化主从分发) | 辐射状星型拓扑: Supervisor | RPC / 同步分派契约 (父向下分派任务,Worker 互不通信) | 中心化局部状态聚合 Worker 上下文严格物理隔离,由 Supervisor 执行 key 级合并 | 局部故障隔离:子 Agent 失败仅标记该任务 failed,不打崩主循环,可重试或降级 | 任务边界明确、子任务完全独立的研发/数据分析流程 |
| P2P Blackboard (去中心化黑板协作) | 网状拓扑 / 事件总线: Agent A | 发布/订阅 (Pub/Sub) 共享黑板 通过全局事件总线广播 AgentMessage | 事件驱动追加日志 (Event Sourcing) 所有 Agent 监听黑板事件流,自主触发状态演进 | 心跳超时剔除与仲裁接管:某 Agent 超时无响应,其他对等节点通过多数派投票接管 | 复杂探索型博弈、开放式讨论或群体智能仿真 |
| Hierarchical (分层树状分治) | 金字塔树状拓扑: Master | 递归分级委派契约 (宏观阶段 | 分层逐级语义压缩 (Hierarchical Rollup) 底层海量细节向上汇报前强制做语义摘要 | 逐级熔断与重试预算:基层错误在中层重试(局部回环),超限逐级上报 | 超复杂系统架构设计、长程软件项目生命周期管理 |
阶段四:Multi-Agent 的故障模式与分布式熔断 (LL-Agent-2)
| 故障模式 | 机制 | 防线(本章实现里的对应物) |
|---|---|---|
| 级联失败 | 一个 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 |
海量并发下 Agent 死循环为何不能纯内存?(LL-Agent-2)
在单机开发中,我们常用 state.step >= 8 计数做循环跳出。但在海量并发的 C 端生产平台中,纯内存状态必定失效:
- 多实例无状态水平伸缩与路由漂移(Routing Drift):生产 Agent Runtime 运行在 Kubernetes Pod 集群中。一次长程 Agent 会话可能包含多次异步回调与用户介入。若网关未配置严格的会话粘性,后续请求被分发到不同 Pod。驻留在单机内存中的计数器跨实例不可见,循环检测形同虚设;
- Pod 崩溃、滚动发布与雪崩再现(Crash-loss & Avalanche):死循环 Agent 产生内存泄漏或打满 CPU 导致 Pod OOM 重启时,纯内存状态随进程清空。K8s 拉起新 Pod 后由于丢失历史轨迹,会重新加载相同的初始目标并再次陷入死循环,引发整个集群的级联雪崩;
- 全局配额击穿(Global Quota Drain):死循环往往针对特定的外部工具或模型 API。单实例内存只能看到局部,数千个并发实例各自空跑几步,就能瞬间打崩租户全天的 TPM/RPM 配额。
分布式熔断器三态机与工程草图
为了在集群维度防御死循环与工具瘫痪,必须引入外挂状态存储(Redis)的分布式熔断器(Distributed Circuit Breaker):
- 滑动窗口死循环原子检测 (Redis Lua 脚本草图):lua
-- KEYS[1]: agent:loop:{session_id}, ARGV[1]: now, ARGV[2]: window, ARGV[3]: max_limit, ARGV[4]: action_sig redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2]) local items = redis.call('ZRANGE', KEYS[1], 0, -1) local count = 0 for _, item in ipairs(items) do if item == ARGV[4] then count = count + 1 end end redis.call('ZADD', KEYS[1], ARGV[1], ARGV[4] .. ':' .. ARGV[1]) redis.call('EXPIRE', KEYS[1], ARGV[2] * 2) return count >= tonumber(ARGV[3]) and 1 or 0 - CAP / PACELC 一致性与可用性权衡:
- 在网络分区或 Redis 延迟升高时,优先保证可用性(AP):
- 只读查询/信息检索工具:执行 Fail-Open(放行执行,附带告警日志),避免用户日常体验卡死;
- 高危破坏/资金操作工具(如转账、删库、发邮件):严格执行 Fail-Closed(阻断执行,降级为人工审批 HITL),防止分布式脑裂引发不可逆灾难。
- 实现与单测见
python/agent_core/distributed_breaker.py。
阶段五:LangGraph 里的 multi-agent
LangGraph 的 supervisor 模式就是本章主从架构的图化表达:supervisor 是一个路由节点,每个 subagent 被包成一个子图节点(有自己的 State 与 checkpointer),supervisor 用条件边或 Command(goto=...) 把控制交给某个 worker,worker 返回后控制权回到 supervisor——第18章阶段六的 orchestrator-workers 行就是它。与 evaluator-optimizer 的关系:evaluator-optimizer 是"生成者+评分者"两个角色的固定回环(你预先知道要几轮、谁来评),supervisor/orchestrator-workers 是"分解者+N 个 worker"的动态扇出(子任务运行时才确定);两者共用同一套图原语,差别仍在图拓扑与路由函数里。本章不重复实操:把手写 orchestrator 映射成 supervisor 图的练习,就是第18章概念映射表的自然延伸。
Supervisor 拓扑(概念图,无实操 lab):
阶段六:动手实验
最小主从系统 + 四个故障注入 demo + 分布式熔断器仿真,全部本地确定性运行,无网络、无 API key。
环境准备
bash
cd <仓库根>
export PYTHONPATH="$PWD/python"命令与预期输出
bash
# 1) 分布式熔断器与多 Agent 协作三大拓扑测试
.venv/bin/python -m pytest python/tests/test_agent_platform.py -v
# 2) 全部 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 in 0.16s
{
"demo_tasks_completed": 2,
"demo_merged_keys": [
"fact:city",
"fact:source",
"fact:summary"
],
"demo_report_status": "completed",
"isolation_researcher_sees_only_own_task": true,
"isolation_orchestrator_free_of_subagent_notes": true,
"isolation_sibling_contexts_disjoint": true,
"loop_intercepted": true,
"loop_depth_limit_events": 1,
"loop_tasks_used": 3,
"loop_dispatch_events": 3,
"conflict_report_status": "completed",
"conflict_count": 1,
"conflict_kept_value": "14M",
"timeout_status": "timeout",
"timeout_output_discarded": true
}
判定信号:
上下文隔离三条断言全部 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 原文——沿袭第15章的 redact 纪律。
故障注入与预期信号
| 注入 | 预期失败信号 | 修复后证据 |
|---|---|---|
| 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 |
本章验收
不看资料,用 5–10 分钟回答:
- 什么时候该用 subagent?用"子任务可独立理解、可独立验证、结果可结构化合并"三条作判据,并说出上下文隔离的真实收益(主上下文不被子任务噪声污染)与代价(15× token 量级)。
- 画出 orchestrator-workers 与"单 agent + 多工具"的边界:给一个具体任务(如"给这个仓库写周报" vs "并行调研 10 个竞品的定价页")分别判定,并说明理由。
- 委派循环怎么防?说出 depth limit(主)与 max_tasks(副)两道防线各自拦什么形状的失控(深链 vs 宽扇出),并用实测数字(3 vs >100)作证。
- 背出 A2A 与 MCP 的分工(连接谁、交互单元、治理),并解释"A2A 不是 sub-agent 协议"这句话。
- 解释 multi-agent 为什么更难调试:错误复合 + 归因需要跨 agent 链路;说出本实现 trace 的四个字段(
seq/task_id/agent/depth)各自回答什么问题。
规范与延伸
- 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 模式(第18章已展开)。
- A2A Protocol 官方站点——Agent Card、Task 生命周期、与 MCP 的分工、"A2A 不是 sub-agent 协议"的官方边界表述;治理:Linux Foundation,Apache-2.0。
- LangGraph multi-agent(supervisor)(以执行日官方文档为准)。
前端/Agent 迁移
orchestrator-workers 就是你写过的主从进程模型:depth limit 等价于"任务不允许再创建 Worker 超过 N 层";max_tasks 等价于连接池上限。新的只有一件事:子任务的分解来自一个不确定的模型输出,所以契约(schema、预算、失败语义)必须比人写分解时更硬。
资源 / 成本 / 隐私
全部 demo 与测试本地运行、无网络、无真实模型调用,gross cost 为 0;无新增依赖(stdlib)。ScriptedPlanner 只重放固定分解,trace 只记录 task_id/agent/depth 等元数据,不含 prompt 与 payload 原文——沿用第15章的 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: 20-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。
下一步
进入 第21章 · 严谨评估、红队安全与受控云部署:为生产级系统建立严密的安全围栏、客观评估基准与云门禁。