Multi-Agent 从零到一:多个 AI 怎么协作,以及为什么大多数时候不该让它们协作
这是一份快照
本文的数字、常量、行数取自 2026-09-01 对 sid-code 源码的一次实读。 代码在动,这些数字会腐坏——引用其中任何一个之前,请按文中给出的命令在你自己的仓库里复跑一次。
这份文档写给谁
你用过 ChatGPT、Claude Code 这类工具,大概知道「AI Agent」是能自己调工具、循环干活的东西。 但一提到「Multi-Agent」「Orchestrator」「Handoff」「Context Poisoning」,你就只剩下 一堆听过但说不清的词。你想搞懂:多个 Agent 协作到底难在哪、业界有几种解法、 每种解法在赌什么、面试问到「你怎么设计多 Agent 系统」时该答什么。
它和四份原始文档的关系
本仓
ai-agent-inter/下已有四份多 Agent 文档,它们是给已经懂的人看的: 一份是 11 道面试题的深度答案(79KB),一份是主题总览(全是 trade-off 决策树), 一份是 Claude Code 源码实读笔记(带文件:行号),一份是框架与产品横评。 密度极高、术语不解释、上手就是「数据处理不等式证明固定预算下单 Agent 更优」。 第一次读一定会卡住。这一份反过来:假设你完全没做过多 Agent,从「一个 agent 是什么」讲起,一层层往上搭, 每个概念先给「为什么需要它」再给「它长什么样」,最后才给「谁做得好」。 每章末尾有 📌 回到原始文档 的指路,想深挖照着走。
它不是摘要。 摘要会把结论抽出来变成一句正确但没用的话 (比如「多 Agent 要控制协调开销」——对,但你还是不知道该干什么)。 本文的写法相反:每个结论都从「为什么会有人搞错」讲起, 因为面试里能拉开差距的从来不是结论本身,而是你能不能说清它的反面为什么诱人。
关于文中数字的一条免责声明(很重要)
本文引用了大量具体数字:17.2x 错误放大、79% 的失败是系统工程问题、 5 个并行 Agent 从 243k token 降到 69k、单 Agent 在 64% 任务上不输多 Agent。
这些全部是二手数字,来源是四份原始文档在 2026-04 到 2026-08 期间的记录, 而那些文档又引自论文、厂商博客、和一次本地源码实读。三条使用纪律:
- 它们的作用是「让你看见真实数据长什么样」,不是「作为恒定事实沿用」。 论文会被后续工作推翻,产品版本每周在变,源码里的常量随时会改。
- 面试里引用要带出处和时间:说「Google Research 2025 年那组 180 配置的实验测到 可并行任务 +80.9%、严格顺序任务 -39~70%」远比说「多 Agent 有时更好有时更差」有力, 但如果对方追问细节而你只记得一个数字,反而暴露是背来的。记住结论的机制,数字是配料。
- 凡是标 🔬 的段落是源码实读结论(有
文件:行号可回溯),标 📄 的是论文/博客二手引用。 前者可信度高于后者,因为后者我没有复现过。
怎么读这份文档
| 你是谁 | 怎么读 |
|---|---|
| 完全零基础 | §0 → §1 → §2 → §13,先建立直觉和地图,其余按需回查 |
| 写过单 Agent、没做过多 Agent | 跳过 §0,从 §1 开始。重点 §4(上下文边界)和 §5(成本) |
| 想动手 | §2 + §13,其余当参考手册 |
如果只有 30 分钟:读 §1、§4、§6。 这三章是整个领域的骨架——「为什么要慎用」、「边界怎么划」、「怎么坏的」,其余都是它们的展开。
目录
| 章 | 内容 | 读完你能回答 |
|---|---|---|
| 0 | 最小心智模型:Agent 是个 while 循环 | 「多 Agent」这个词其实指三种不同的东西 |
| 1 | ★ 第一个认知陷阱:多 Agent 不会让它变聪明 | 为什么信息论说单 Agent 严格更优 |
| 2 | 解剖协作:四个正交件 + 五种拓扑 | 能画出任意多 Agent 系统的结构图 |
| 3 | Handoff:阿喀琉斯之踵 | 为什么「根据你的发现去修」是致命反模式 |
| 4 | ★ 上下文边界的三种形态 | 本文架构核心 |
| 5 | 省钱:缓存对齐是第一杠杆 | 5 个 Agent 从 243k 降到 69k 是怎么做到的 |
| 6 | ★ 失败工程:它是怎么坏的 | 79% 的失败与模型无关,那与什么有关 |
| 7 | 记忆一致性:把 CPU 缓存那套搬过来 | 多 Agent 共享状态的五档一致性 |
| 8 | 可观测性:调试为什么特别难 | 四层观测架构 + 五个必备能力 |
| 9 | 安全、信任与涌现行为 | 零信任四层 + 六类涌现风险 |
| 10 | 协议与框架生态 | MCP vs A2A,六个框架各适合谁 |
| 11 | 后台任务基础设施:协作的承载层 | 子 Agent 派出去之后,谁在管它的生死 |
| 13 | 动手:从零搭一个 mini 多 Agent 系统 | 五阶段路线图 |
| 附 | 术语表 / 自检清单 / 延伸阅读 | 查漏 |
§0 最小心智模型:先把「一个 Agent」讲清楚
0.1 Agent 剥掉包装,就是一个 while 循环
在谈「多个 Agent 协作」之前,必须先能一句话说清「一个 Agent 是什么」。 最有用的定义不是「自主智能体」这种同义反复,而是:
Agent = LLM + 工具 + 一个不停调用它俩的循环。
伪代码就是全部:
messages = [system_prompt, user_task]
while True:
response = llm(messages, tools=available_tools) # ① 想
messages.append(response)
if not response.tool_calls: # 它觉得干完了
break
for call in response.tool_calls: # ② 做
result = execute(call) # 读文件 / 跑命令 / 搜网页
messages.append(result) # ③ 看结果,回到 ①这个循环有个通用名字:Think–Act–Observe(想—做—看)。 你在 Claude Code 里看到的「我先读一下这个文件…好,现在我改一下…嗯编译报错了,我再改」, 就是这个 while 循环在转。
三件事从这段伪代码直接读出来,后面每一章都要用:
| 事实 | 为什么重要 |
|---|---|
messages 只增不减 | 每一轮的输入都是「之前全部历史 + 新结果」。轮数是成本的最大杠杆,第 N 轮的输入约等于 N 倍的第 1 轮 |
| LLM 本身无状态 | 它不记得上一轮。所谓「Agent 的记忆」就是这个 messages 数组 |
| 循环的终止条件在模型手里 | 它不吐 tool_call 就停。所以「跑不完」「原地打转」这类故障,本质是模型没做出正确的停止判断 |
记住第一条。 后面所有「多 Agent 好贵」的论证,源头都是它: 上下文只增不减 → 长任务的上下文会膨胀 → 于是有人想「拆开不就行了」→ 拆开之后才发现拆的代价在哪。
0.2 Workflow vs Agent:一条容易被跳过但必须先划清的界
这两个词在业界经常混用,但它们是不同的东西,而且大部分人真正需要的是前者:
| Workflow(工作流) | Agent(智能体) | |
|---|---|---|
| 谁决定下一步 | 代码(预先写死的流程) | 模型(每轮自己判断) |
| 路径 | 固定,可画成流程图 | 不固定,同样输入可能走不同路 |
| 可预测性 | 高 | 低 |
| 成本 | 低(步数确定) | 高(步数由模型定) |
| 例子 | 「翻译 → 校对 → 排版」三步固定调用 | 「把这个 bug 修掉」,它自己决定读哪些文件、改几次 |
📄 Anthropic 的实测口径:Workflow 模式比 Agent 模式省约 4 倍 token。
这条界很重要,因为大量被叫做「多 Agent 系统」的东西其实是多步 workflow: 预先定义好「研究 Agent 输出喂给写作 Agent」,路径是写死的, 那它不是多个 Agent 在协作,是一条两步的流水线,每步里跑了一次 LLM 调用。 不是说这样不好——恰恰相反,它更便宜更可靠——但面试时把它说成多 Agent 系统会掉分, 因为它没有多 Agent 的核心难点(动态委托、协调、错误传播)。
自检一句话:如果我把 LLM 换成一个固定函数,流程还成立吗? 成立 → 它是 workflow。
0.3 「多 Agent」这个词其实指三种完全不同的东西
这是初学者最大的困惑源。同一个词在三种语境下含义不同,混用会让整段讨论失效:
形态 A:子 Agent(Subagent)—— 一个主 Agent 派出临时工
主 Agent ──派生──► 子 Agent(干完就消失,只回一份摘要)
特征:单向、临时、有明确的父子关系
例子:Claude Code 的 Explore / Plan 子代理
形态 B:编排(Orchestration)—— 一个编排器指挥一批 Worker
编排器 ──► Worker1 / Worker2 / Worker3 ──► 编排器汇总
特征:中心化、Worker 之间不直接说话
例子:LangGraph Supervisor、CrewAI
形态 C:Agent 团队(Peer Agents)—— 一群对等实例互相通信
队友A ◄──► 队友B ◄──► 队友C(共享任务列表 + 邮箱)
特征:对等、长活、可互相质疑
例子:Claude Code 的 Agent Teams、AutoGen Group Chat、OpenAI Swarm三者的差别不是程度,是性质:
| A 子 Agent | B 编排 | C 对等团队 | |
|---|---|---|---|
| 生命周期 | 任务完成即销毁 | 通常随任务 | 长活,可接新任务 |
| 谁能跟谁说话 | 只能回父级 | 只跟编排器 | 任意两两 |
| 通信复杂度 | O(1) | O(n) | 最坏 O(n²) |
| 主要动机 | 上下文压缩 | 任务分解 + 全局校验 | 多视角 / 真并行 |
| 调试难度 | 低 | 中 | 高 |
📄 Anthropic 有一句被引用得最多的话,正是关于形态 A 的:
Sub-agent 首先是上下文压缩边界,其次才是并行性。
这句话是本文的第一个「信号词」级别的句子。它在说:你派一个子 Agent 出去, 最大的收益不是「两件事同时做」,而是「那 50 轮探索的垃圾不进我的上下文」。 子 Agent 干了 50 轮、读了 30 个文件,回来只给你一段 200 字的摘要—— 那 30 个文件的内容永远没进过主上下文。这才是它的价值。
后面每次出现「多 Agent」,我都会标明是 A / B / C 哪一种。 面试时也建议这么做:对方问「你做过多 Agent 吗」,先反问一句 「你说的是子代理式的委托,还是对等实例的协作?」——这一问本身就是信号。
0.4 本章自检
能回答这三个问题再往下:
- 为什么说「轮数是成本的最大杠杆」,而不是「Agent 数量」? (提示:回到 0.1 那句「
messages只增不减」) - 你的系统里,把 LLM 换成写死的 if-else 之后流程还成立——这说明它是什么?该不该叫多 Agent?
- 「子 Agent 首先是上下文压缩边界」这句话,如果反过来理解成「首先是为了并行」, 会导致什么设计错误?(提示:想想串行派两个子 Agent 有没有意义)
📌 回到原始文档:形态划分与 Agent Teams 的四大组件,见 multi-agent-deep-dive.md §1(Agent 三要素 / Workflow vs Agent)与 §4.1(Teams vs Subagents 对照表)。
§1 ★ 第一个认知陷阱:多 Agent 不会让它变聪明
1.1 那个人人都有的直觉,以及它为什么错
几乎所有人第一次听说多 Agent 都会形成同一个直觉:
一个 AI 做不好的事,让五个 AI 分工协作、互相检查,肯定做得更好。 就像一个人干不完的活,组个团队就能干完。
这个直觉在人类团队上成立,在 LLM 上大幅打折,理解为什么打折是本领域的入场券。
先看三组互相独立的证据(📄 全部二手,出处见每条末尾):
| 证据 | 结论 | 来源 |
|---|---|---|
| Google Research 的 Agent Scaling 实验(180 种配置) | 可并行任务 +80.9%;严格顺序任务 -39~70% | 原始文档 00-Question.md Q1 |
| Princeton NLP | 单 Agent 在 64% 的任务上匹配或超越多 Agent;多 Agent 只多 2.1% 准确率但成本翻倍 | 同上 Q2 |
| Stanford / Contextual AI(arXiv 2604.02460, 2026-04) | 固定推理 token 预算下,单 Agent 信息效率严格更优 | 同上 + 01-Study-Source 外部对照 |
| Microsoft Azure SRE 团队 | 先上多 Agent 专业化架构,发现 Handoff 损害可靠性,回退到单 Agent | 同上 Q1 |
| Gartner 预测 | 40%+ 的 Agentic AI 项目会在 2027 年前被取消 | 同上 Q2 |
第三条是最硬的一条,因为它不是实验数据而是证明。值得单独讲。
1.2 数据处理不等式:为什么这不是工程问题而是数学问题
信息论里有一条叫 数据处理不等式(Data Processing Inequality, DPI) 的东西。 不严格地讲,它说的是:
信息经过任何一次处理(转换、压缩、转述),只可能减少,不可能增加。 你无法通过「再加工一遍」创造出原本不存在的信息。
把它套到多 Agent 上:
原始任务信息 ──Agent A 处理──► A 的输出 ──Agent B 处理──► 最终答案
↑ ↑
这一步只会丢信息 这一步又丢一次每一次 Handoff(Agent 之间传递结果)都是一次有损压缩。 A 干了 50 轮,写了一段 200 字的摘要给 B——那 50 轮里的细节、犹豫、 「我试了这条路发现不行」的负面信息,绝大部分没进那 200 字。
所以在固定 token 预算这个前提下,结论是严格的: 把预算全给一个 Agent,它看到的信息比「拆成五份、每份处理完再压缩传递」更完整。
📄 Stanford 那篇的原话(经原始文档转引):
under a fixed reasoning-token budget and with perfect context utilization, single-agent systems are more information-efficient... single-agent systems consistently match or outperform multi-agent systems on multi-hop reasoning tasks when reasoning tokens are held constant.
1.3 那多 Agent 到底买到了什么?
如果单 Agent 严格更优,为什么所有严肃的 coding agent 都实现了多 Agent 能力?
因为上一节那个前提有两个大字:「固定预算」和「完美上下文利用」。现实里两者都不成立。
这是本章最关键的一段,读慢一点。
破口一:上下文利用率不完美(Context Rot)
真实的 LLM 不是「上下文里有的信息就一定用得上」。上下文越长, 它在中间那段的注意力越差——业界叫 Context Rot(上下文腐烂) 或者「大海捞针(needle in a haystack)问题」。
所以曲线是这样的:
任务质量
▲
│ ╭──────╮ 单 Agent:短上下文时很好,
│ ╭─╯ ╰──╮ 越长越退化
│ ╭─╯ ╰────╮
│ ╭─╯ ╰─────
│ ╱ ┌────────────────────── 多 Agent:起点更低(Handoff 有损)
│╱ ┌──┘ 但不随总信息量退化
└─────┴─────────────────────────► 任务的总信息量
↑
交叉点:只有到了这里,多 Agent 才开始划算多 Agent 的真实价值:它是一条「绕过单 Agent 上下文退化」的路。 不是变聪明,是换了一种组织信息的方式——用「多次有损压缩」换掉「一次严重腐烂」。
破口二:预算不固定,钱可以多花
📄 Anthropic 2025-06 的实测:单 Agent 是普通 chat 的 ~4 倍 token, 多 Agent 系统是 ~15 倍。同一份研究还有一句更值钱的: token 支出本身解释了 BrowseComp 上约 80% 的性能方差。
把这两句连起来读,会得到一个有点扎人的结论:
多 Agent 的性能提升,很大一部分不是来自「协作」,是来自它花了 15 倍的钱。
所以对比多 Agent 和单 Agent 时,如果不控制 token 预算,那个对比是无效的—— 你测到的可能只是「花钱多的赢了」。这是面试里一个极好的追问点,见 §12 Q3。
破口三:认知容量阈值 κ
📄 有一篇论文(原始文档记为《When Single-Agent with Skills Replace Multi-Agent Systems》, arXiv 2601.04748)提出了一个很实用的量:认知容量阈值 κ ≈ 3–4 个技能。
机制类似心理学的 Hick's Law(选项越多,决策越慢越差):
- 技能/工具 ≤ 3–4 个:单 Agent 选对工具的准确率高,多 Agent 是纯浪费
- 技能 > 10 个:选择准确率开始明显衰减
- 此时分层不是架构偏好,是认知必要性——通过分层减少每个 Agent 面对的选项数
📄 相关的还有 Google Research 的 tool-coordination trade-off: 当任务需要的工具超过 16 个时,多 Agent 的协调「税」开始不成比例地上升。
注意这两个数字方向相反,但不矛盾:工具太少不值得拆(κ),工具太多拆了也没用(16)。 中间那段才是多 Agent 的甜区。
破口四:真并行
单 Agent 严格串行。五个独立的只读探索任务,多 Agent 能在同一段墙钟时间内做完。 📄 Google 那组数据里 +80.9% 就是这一类。
但「独立」这个词要卡死:在架构图上画五个框不等于任务并行。真正的独立 = 执行期间零共享状态。 这是一个能直接拿去面试的判据。
1.4 于是这个领域的第一原则是:能用单 Agent 就不用多 Agent
把上面四个破口收成一张决策表——这是本文最该背下来的一张表:
| 先问 | 是 → | 否 → |
|---|---|---|
| ① 更好的 prompt / 上下文管理能解决吗? | 就这么办(📄 约 80% 的场景都能) | 往下 |
| ② 子任务真的独立吗(执行期间零共享状态)? | 独立并行,协调开销 O(1),但注意错误放大 | 往下 |
| ③ 单 Agent 上下文真的装不下 / 已经在退化了? | 用子 Agent 做压缩边界 | 往下 |
| ④ 技能/工具数超过 κ≈3–4 了吗? | 分层减少每个 Agent 的决策负担 | 不要用多 Agent |
绝对不该用多 Agent 的四种情况(每条都有实例支撑):
- 线性工作流,不受益于并行 —— 一个好的单 Agent 更快更便宜。
- 步骤 N 依赖步骤 N-1 —— 每次 Handoff 的重建损失会累积。 📄 这正是 Google 测到
-39~70%的那一类,也是 Microsoft Azure SRE 回退的原因。 - 工具密集(>16 个) —— 协调税过高。
- 团队没有分布式系统经验 —— 多 Agent 本质是「由非确定性节点组成的分布式系统」, 调试和运维复杂度远超单 Agent。这条最容易被低估,§8 整章在讲它。
1.5 一个必须避开的过度纠正
读完上面很容易走到另一个极端:「所以多 Agent 是炒作,别用」。这也是错的,而且错得同样贵。
因为 §1.2 那个证明有个前提是**「固定 token 预算」**,而 🔬 §5 会讲的一件事是:多 Agent 的成本里有很大一块是「偶然开销」,可以被消掉。 五个并行子 Agent 各自重传同一份 48k 的系统提示词和工具定义—— 这部分重复付费和「协作」本身毫无关系,靠前缀缓存对齐能把它压掉约 90%。
📄 消掉之后的实测量级:5 个并行 Agent 的共享上下文成本从 243.5k → 68.9k token(约 28%)。
所以正确的表述是这样的,请注意它比两个极端都更精确:
多 Agent 在信息论上永远不占优,但「多 Agent 太贵」这个论断的强度取决于你有没有 消掉偶然开销。没消之前说它贵,你说的是自己的实现问题,不是这个架构的性质。
1.6 本章自检
- 「数据处理不等式说明多 Agent 更差」——这句话缺了什么前提?缺了这个前提会导致什么错误结论?
- 你测出多 Agent 比单 Agent 准确率高 5%。为什么这个结果可能什么都没证明?
- κ≈3–4 和「工具 >16 个协调税过高」这两个数字方向相反,怎么统一理解?
- 「我在架构图上画了五个 Agent 并行」——凭什么判断它们真的能并行?
📌 回到原始文档:
- 四组实测数据与决策清单:
00-Question.mdQ1(通信与协调开销)、Q2(单 vs 多) - 信息论论证与 KVCOMM / OneFlow 的位置:
01-Study-多Agent系统-主题总览.md§根本矛盾, 以及01-Study-Source-多Agent系统.md的「外部对照」一节 - 「偶然开销可以消掉」的完整源码论证:
01-Study-Source-多Agent系统.md§forkSubagent.ts
§2 解剖协作:四个正交件 + 五种拓扑
上一章讲了「该不该用」。这一章讲「用的话,它由什么组成」。
2.1 先看「不拆」会发生什么
很多人设计多 Agent 系统时,脑子里只有一个维度:几个 Agent、各叫什么。 于是设计文档长这样:
我们有一个研究 Agent、一个编码 Agent、一个测试 Agent,编排器把任务分给它们。
这句话看起来说清了,实际上一个关键问题都没回答:
- 研究 Agent 看得到主会话的历史吗?
- 编码 Agent 拿到的是研究 Agent 的原始输出,还是编排器消化过的规范?
- 测试 Agent 和编码 Agent 能不能同时跑?谁判断能不能?
- 编码 Agent 失败了,是重试、换 Agent、还是问人?
- 研究 Agent 能不能自己再派一个子 Agent?
这五个问题分属四个互相独立的维度。不拆开的后果是:讨论时反复在不同维度间跳, 而且任何一个维度改了,你都说不清会影响什么。
2.2 四个正交件
我建议用这四个维度去解剖任何一个多 Agent 系统。它们互相独立—— 任意组合都是合法的系统,这就是「正交」的意思。
┌─────────────────────────────────────────────────────────┐
│ ① 拓扑(Topology) 谁能跟谁说话 │
│ 集中式 / 分层 / 去中心化 / 独立并行 / 市场竞标 │
├─────────────────────────────────────────────────────────┤
│ ② 上下文边界(Context Boundary) 谁能看到什么 │
│ 隔离 / 继承 / 共享 —— ★ 这是本文 §4 的整章主题 │
├─────────────────────────────────────────────────────────┤
│ ③ 通信机制(Communication) 信息怎么传 │
│ 返回值 / 消息队列 / 共享黑板 / 文件系统 │
├─────────────────────────────────────────────────────────┤
│ ④ 控制机制(Control) 谁决定下一步、边界在哪 │
│ 静态路由 / LLM 路由;能力边界 / 预算 / 深度限制 │
└─────────────────────────────────────────────────────────┘举个例子说明「正交」为什么有用。三个真实系统,用这四件拆开看:
| Claude Code 子代理 | Claude Code Agent Teams | OpenAI Codex 并行容器 | |
|---|---|---|---|
| ① 拓扑 | 集中式(形态 A) | 对等网状(形态 C) | 独立并行 |
| ② 上下文边界 | 隔离或继承(可选) | 每队友完全独立 | 完全隔离 |
| ③ 通信 | 返回值(一份摘要) | 邮箱 + 共享任务列表 | 文件系统(各自 git worktree) |
| ④ 控制 | 深度硬限 + 工具白名单 | 扁平名册 + 手动干预 | 容器隔离 |
看出来了吗——这三个系统在四个维度上的取值几乎完全不同,但都是「多 Agent」。 如果只用「几个 Agent」这一个维度去描述,你会说它们是同一种东西。
下面逐件展开。② 太重要,单独占 §4;③ 的深水区(记忆一致性)单独占 §7。
2.3 ① 拓扑:五种,按错误放大率排序
📄 Google Research 的 Agent Scaling 研究系统评估了五种拓扑。 下面这张表值得记,因为它把「拓扑」和「安全性」连起来了:
| 拓扑 | 机制 | 通信复杂度 | 错误放大率 | 适用 |
|---|---|---|---|---|
| Supervisor(集中式) | 一个编排器分发 + 审核 + 决策 | O(n) | 最低 | 任务可清晰分解、要全局一致 |
| Hierarchical(分层) | 多层监督,上层抽象、下层执行 | O(n·层数) | 低 | 能自然分解为子问题的复杂领域 |
| Decentralized/Debate(辩论) | Agent 互相辩论、投票达成共识 | O(n²·轮数) | 中 | 多视角验证、创意生成 |
| Independent(独立并行) | 各自跑,无中间通信,最后汇总 | O(1) | 17.2x(最高) | 子任务完全独立 |
| Swarm/Market(市场竞标) | Agent 竞标任务,动态自组织 | 不确定 | 不确定 | 任务分布动态变化 |
这张表最反直觉的一格是右上和右下的对比。
「独立并行」在通信复杂度上是最优的 O(1)——零协调开销,听起来最优雅。 但它的错误放大率是 17.2 倍,最差。
为什么?因为没有任何人在检查。五个 Agent 各自跑完,结果直接汇总进最终输出。 其中一个把「营收下降 5%」读成「增长 5%」,这个错误一路畅通无阻。
反过来集中式有单点故障、有瓶颈,但错误控制最好,因为所有信息必须穿过编排器。 §3 会讲这件事的深层机制——它比「多了一道检查」要精妙得多。
可迁移的判据:通信开销低 ≠ 架构好。省下的通信开销可能是用错误检测能力换的。
2.4 五种 Workflow 模式:多 Agent 系统的积木
拓扑讲的是「静态结构」,Workflow 模式讲的是「动态套路」。 📄 Anthropic 官方总结了五种,它们是可自由组合的积木:
| 模式 | 说明 | 示意 | 例子 |
|---|---|---|---|
| Prompt Chaining | 前一步输出作为后一步输入 | A → B → C | 翻译 → 校对 → 排版 |
| Parallelization | 多个 Agent 同时处理不同子任务 | A ⇉ [B,C,D] → E | 同时审安全/性能/测试 |
| Routing | 按输入类型分发 | A → ? → B or C or D | 客服分流 |
| Orchestrator-Worker | 编排者动态拆解并分配 | A → [动态拆解] → [B,C,D] → A | 复杂重构 |
| Evaluator-Optimizer | 生成 → 评估 → 改进循环 | A ⇄ B | 代码生成 → 审查 → 修改 |
三条不写出来就容易踩的说明:
- 前三种严格来说是 workflow 不是 agent(回 §0.2:路径写死了)。 它们更便宜更可靠,大多数需求到这里就该停。
- Orchestrator-Worker 和 Routing 的区别是「动态」二字。 Routing 的候选是预先枚举的;Orchestrator-Worker 的子任务是运行时才拆出来的。 这个区别决定了后者贵得多、也难调得多。
- Evaluator-Optimizer 的两个角色必须上下文隔离。 让同一个 Agent 既生成又评估自己,它会 rubber-stamp(盖橡皮章、无脑通过)。 这是 §4.5 一个重要结论的伏笔。
📄 Anthropic 的黄金建议,值得整段引用:
从简单方案开始。先试 Prompt Chaining,不够再加 Parallelization, 最后才考虑完整的 Multi-Agent 系统。过早引入复杂架构是最常见的错误。
2.5 ③ 通信机制:四种载体,成本差一个量级
Agent 之间怎么把信息给对方?有四种载体,很多人只想到第一种:
| 载体 | 机制 | 成本 | 适用 |
|---|---|---|---|
| 返回值 | 子 Agent 干完,返回一段文字给父级 | 低(一份摘要) | 形态 A,绝大多数场合 |
| 消息 / 邮箱 | Agent 显式发消息给另一个 Agent | 中(每条都要进对方上下文) | 形态 C |
| 共享黑板 | 都读写同一个中央存储(Redis/DB) | 中(读要花上下文) | 需要强一致的状态 |
| 文件系统 | 一个写文件,另一个按需读 | 最低 | 大产物、详细结果 |
第四种最被低估,也是最实用的一招。 如果子 Agent 的详细结果有 30KB,让它「返回」给父级意味着这 30KB 永久占据父级上下文 (回 §0.1:只增不减)。让它写进文件、只返回一句「详细结果在 /tmp/report.md」, 父级需要时再读——这就把「常驻成本」变成了「按需成本」。
📄 OpenAI Codex 的并行容器架构就是走到极致的这一路: Agent 之间完全不通信,每个有自己的 git worktree,通过文件系统交付。
两条关于消息通信的实测教训(🔬 来自源码实读):
教训一:广播的成本是线性的,而且它被写进了提示词里劝阻使用。
| "*" | Broadcast to all teammates — expensive (linear in team size),
use only when everyone genuinely needs it |值得注意的是这个处理方式:成本高的通信模式不是被优化,是被劝阻。 这比加一堆优化代码务实得多。
教训二:通信必须是显式的工具调用,不能是副作用。 源码提示词里有一句:
Your plain text output is NOT visible to other agents — to communicate, you MUST call this tool.
这句在防一个很自然的错觉:Agent 以为自己「说了」,别人就「听到了」。 在多队友共享一个终端输出的场景里,模型极容易假设输出是共享的。 这类「模型对架构有错误心智模型」的问题,§6.4 会展开讲,它是 LLM 系统独有的一类 bug。
2.6 ④ 控制机制:策略靠提示词,边界靠代码
这是本章最值钱的一节,🔬 它来自源码实读的一个反直觉发现。
先说发现本身。原始文档的作者去读 Claude Code 的编排器实现(coordinator/coordinatorMode.ts), 预期看到「任务队列 + DAG + 调度器 + 状态机」。实际看到的是:
这个文件 369 行,代码逻辑只有 40 行(模式判断 + 上下文拼装), 剩下 250 行是一份系统提示词。
四阶段工作流(Research → Synthesis → Implementation → Verification) 没有任何代码强制执行,它就是提示词里的一张 markdown 表格。并发策略也是提示词里的一段话:
- Read-only tasks (research) — run in parallel freely
- Write-heavy tasks (implementation) — one at a time per set of files第一反应一定是「这太脆弱了,模型不遵守怎么办」。但想清楚之后这个决策是对的,理由是:
「这两个子任务能不能并行」这个判断本身没法编码。 它依赖对代码库的语义理解——它们会不会改到同一个文件、同一个函数、有没有隐式依赖。 一个硬编码的 DAG 调度器要么过度保守(全串行,失去并行价值), 要么不安全(漏掉隐式依赖,两个 Agent 改同一个文件打起来)。
那什么该写进代码?答案是能力边界。同一份源码里有一批硬 throw:
// 队友不能派队友
if (isTeammate() && teamName && name) {
throw new Error('Teammates cannot spawn other teammates — the team roster is flat.')
}
// 进程内队友不能派后台 Agent
if (isInProcessTeammate() && run_in_background === true) {
throw new Error('In-process teammates cannot spawn background agents.')
}
// fork 出来的孩子不能再 fork
if (... || isInForkChild(...)) {
throw new Error('Fork is not available inside a forked worker.')
}这三条不是架构洁癖,每条的理由都是「数据结构或生命周期不支持」:
| 硬拒绝 | 真实理由 |
|---|---|
| 队友不能派队友 | TeamFile.members 是扁平数组,只有一个 leadAgentId。嵌套队友进名册后没有 provenance(来源),编排器分不清谁是谁派的 |
| 进程内队友不能派后台 Agent | 进程内队友的生命周期挂在 Leader 进程上,它派后台 Agent 会产生孤儿任务 |
| fork 不能再 fork | 见 §5.6,理由是缓存语义只在一层成立(不是「防递归爆炸」) |
于是得到一条我认为是本文最可迁移的原则:
★ 策略交给提示词,边界交给代码。 判据一句话:这个约束是不是由数据结构 / 生命周期 / 资源决定的?
- 是 → 代码硬拒(违反它会导致数据结构无法表达、资源泄漏、状态不一致)
- 只是「这样效果更好」→ 提示词引导
顺带一条极有杀伤力的推论,可以直接拿去面试:
能力边界应该从数据结构推导,而不是拍一个数字。
大部分人(包括原始文档作者自己承认)在设计时会写「委托深度限制为 1」, 但不会去问「我的名册数据结构能不能表达一棵树」。 后者才是真正的边界,前者只是它的一个症状。
2.7 本章自检
- 「独立并行」的通信复杂度是最优的 O(1),为什么它的错误放大率反而最高(17.2x)? 从这个矛盾里能提炼出什么通用判据?
- Routing 和 Orchestrator-Worker 都是「分发任务」,本质区别是哪两个字?这个区别带来什么成本差异?
- 子 Agent 有 30KB 的详细结果要交给父级。用返回值和用文件系统, 在父级的长期上下文占用上差多少?(提示:回 §0.1)
- 你要给你的多 Agent 系统加一条「子 Agent 不能删除文件」的限制。 它该写进提示词还是代码里?依据是什么?
- 「我的系统限制委托深度为 2」——这句话缺了什么论证?
📌 回到原始文档:
- 五种拓扑与错误放大率:
00-Question.mdQ4(Orchestrator 设计) - 五种 Workflow 模式与架构选择决策树:
multi-agent-deep-dive.md§3.4 / §3.5 - 编排器即提示词、能力边界硬 throw 的完整源码引用:
01-Study-Source-多Agent系统.md§coordinatorMode.ts 与 §AgentTool.tsx
§3 Handoff:多 Agent 的阿喀琉斯之踵
3.1 Handoff 是什么,为什么它是那个问题
Handoff(交接)= 一个 Agent 把工作成果交给另一个 Agent 的那一刻。
它可以是子 Agent 返回摘要、编排器给 Worker 下指令、队友之间发消息—— 形式不同,本质相同:信息从一个上下文搬到另一个上下文。
上一章那五种拓扑、四种通信载体,全都是围绕 Handoff 组织的。 📄 原始文档的主题总览把它列为第一高频概念,出现在 6 道题里,理由是:
多 Agent 的本质就是「拆分后再连接」,Handoff 是连接点,也是信息损失点。
所以这一章的地位是:§1 讲了多 Agent 为什么在信息论上不占优, 这一章讲那个「损失」具体发生在哪、长什么样、能不能减小。
3.2 Handoff 到底丢了什么:三类信息
不是笼统地「丢了一些细节」。丢的东西可以分类,而分类之后就能针对性处理:
| 丢的类型 | 具体是什么 | 后果 |
|---|---|---|
| 过程信息 | 「我试了 A 方案,因为 X 原因不行」 | 下游可能重新去试 A(空转、重复付费) |
| 不确定性 | 「这个结论我只有 60% 把握」 | 下游把它当确定事实用(错误被固化) |
| 边界条件 | 「除了 XXX 这种情况」 | 下游在边界情况上出错,且查不出来源 |
第三类最阴。📄 Galileo AI 的说法是 lossy compression —— 压缩上下文省 token 的时候,「关键的边界条件被平滑掉了」。 症状是最终结果在 90% 的情况下对,剩下 10% 错得莫名其妙, 而且因为原始过程已经不在任何人的上下文里,没人能解释为什么。
三个缓解手段(按性价比排序):
- 让上游显式标注置信度和不确定性 —— 最便宜,效果最好。 要求子 Agent 的返回格式里带一栏「我不确定的地方」。
- 关键决策不委托 —— 由主 Agent 直接处理。这是承认「有些东西不能过 Handoff」。
- 用文件系统做「共享黑板」 —— 子 Agent 把详细结果写进文件, 父 Agent 按需读原文。这实际上是把有损压缩变成了可逆的(原文还在)。
第三条值得强调,它是 §2.5「文件系统是最便宜的通信载体」的另一面: 文件系统不只省上下文,它还是唯一能让 Handoff 变成无损的载体—— 因为原始信息没有被丢弃,只是没有被加载。
3.3 ★ Never delegate understanding:一条反模式和它的深层机制
这是本文最重要的单条结论之一。🔬 它来自源码里编排器提示词的一段:
**Workers can't see your conversation.** Every prompt must be self-contained...
### Always synthesize — your most important job
Never write "based on your findings" or "based on the research." These
phrases delegate understanding to the worker instead of doing it yourself.
You never hand off understanding to another worker.翻译过来:编排器绝对不许写「根据你的发现,去修这个 bug」。
先看为什么这句话诱人。它太自然了——研究 Agent 刚刚做完调研, 编排器顺手说「根据你的调研结果,把这个 bug 修掉」, 听起来简洁、避免了重复、还「相信」了 Worker。所以几乎人人都会这么写。
它错在哪:编排器在说这句话时,自己没有理解过那份调研。 两个后果:
✅ 正确(编排器综合过)
研究 Agent → 编排器【读懂、消化、翻译成规范】→ Worker
↑
错误在这里会暴露:把错误结论翻译成
「改 src/auth/validate.ts:42」时,
往往会撞上自相矛盾
❌ 反模式(透传)
研究 Agent ─────────── 编排器(没读)───────────→ Worker
↑
错误直接穿透,而且编排器
丧失了发现错误的能力——它从来没读过那个结论第二个后果比第一个更严重,也更容易被忽略:透传的编排器不只是没拦住错误, 它连「拦住错误的可能性」都没有了。它变成了一根管道。
反过来,如果编排器必须把研究结果消化成 「改 src/auth/validate.ts:42,加空值检查,返回 401」这种具体规范, 那么 综合这个动作本身就是一道校验: 错误的研究结论在被翻译成具体文件路径和行号时,往往会暴露自相矛盾。
于是我们得到了 §2.3 那张表里「集中式错误放大率最低」的真实机制:
不是因为编排器多做了一道检查步骤,而是因为信息必须穿过一个理解者。
这个区别在面试里非常有价值。「集中式有全局视野所以能校验」是教科书答案; 「集中式强制信息穿过一个必须理解才能分发的节点,理解这个动作本身就是校验」 是能听出你想过这件事的答案。
可迁移的判据(这条也适用于人类协作,比如 PRD 和技术方案的交接):
Handoff 内容里出现「根据上述」「按照前面的分析」「基于你的发现」—— 说明综合这一步被跳过了。 测试方法:接收方能不能在完全不看上游原始输出的情况下执行?
3.4 自包含 Prompt:一条正确但被高估的原则
上一节引文的第一句是 "Workers can't see your conversation. Every prompt must be self-contained." 这条原则叫 自包含 prompt(self-contained prompt),教科书里几乎必讲,三条理由也很标准:
- 上下文压缩:如果子 Agent 继承完整历史,就失去了委托的核心价值
- 安全隔离:共享历史意味着子 Agent 能看到所有敏感信息,违反最小权限
- 可预测性:自包含让子 Agent 行为更确定,不受父级历史中无关信息干扰
实践要求也很明确:委托 prompt 必须包含具体的文件路径、行号、修改内容, 而不是模糊的「修改那个文件」。
但这条原则有个隐性成本,而且它被系统性低估了。 🔬 原始文档的作者在源码里发现, 这个「核心设计原则」正在被降级为一个特例——因为自包含要求 父 Agent 花 token 把上下文复述进 prompt,而复述必然有信息损失。
这是 §4 整章的引子,所以这里先只提出问题:
「自包含 prompt」把 Handoff 的信息损失从「隐性」变成了「显性」—— 你必须亲手决定复述什么、不复述什么。这不是消除了损失,是把损失变成了一个设计决策。 那么,有没有零损失的选项?
答案是有的,代价是别的东西。见 §4。
3.5 续传还是新派:一张表,两个反直觉条目
🔬 源码里有一张决策表,回答的是:一个 Worker 干完了一件事, 下一件相关的事,是给它发消息让它接着干(续传),还是派一个全新的 Agent?
| 情况 | 机制 | 为什么 |
|---|---|---|
| 研究恰好探索了要改的文件 | 续传 | 文件已在它上下文里,且现在有了明确计划 |
| 研究很宽但实现很窄 | 新派 | 避免拖着探索噪音 |
| 修正失败 / 延续刚才的工作 | 续传 | 它有错误上下文,知道自己刚试了什么 |
| 验证另一个 Worker 写的代码 | 新派 | 验证者该用新鲜眼光,别带实现者的假设 |
| 第一次实现方向完全错了 | 新派 | 错误方向的上下文会污染重试,干净起点避免锚定 |
| 完全无关的任务 | 新派 | 没有可复用的上下文 |
然后是一句我认为最诚实的话:
There is no universal default. Think about how much of the worker's context overlaps with the next task.
这张表的价值在于它把「上下文复用」这个抽象问题降维成了一个可判断的量:上下文重叠度。
而两个加粗的条目是故意放弃上下文复用的,它们比其余四条都值钱:
① 验证别人的代码 → 新派。 理由是验证需要的是独立性。 上下文复用在这个场景下是负价值——带着实现假设的验证者会 rubber-stamp。 (这也解释了 §2.4 为什么说 Evaluator-Optimizer 的两个角色必须隔离。)
② 方向完全错了 → 新派。 理由是锚定效应(anchoring)。 失败路径留在上下文里,模型会倾向于在原方向上做小修,而不是换一条路。
第二条有一个非常漂亮的对应:这和「如果同一方法失败两次,停止增量调整、换一条根本不同的路」 是同一个道理,只是发生在 Agent 编排层而不是单个 Agent 内部。
可迁移到哪:LLM-as-judge(评判者不该看到生成过程)、 多方案生成(每个方案从干净起点才有多样性)、重试策略(失败上下文会锚定)。
3.6 本章自检
- Handoff 丢的三类信息里,哪一类最难排查?为什么?
- 「根据你的调研结果修掉这个 bug」这句话有两个后果,第二个是什么?为什么它更严重?
- 集中式架构错误放大率最低的机制是什么?(不要答「有全局视野」)
- 「自包含 prompt」消除了 Handoff 的信息损失吗?如果没有,它做了什么?
- 什么情况下你会故意不复用上下文、宁可多付一次钱?说出两种。
📌 回到原始文档:
- Handoff 作为第一高频概念、信息密度 vs 完整性的 trade-off:
01-Study-多Agent系统-主题总览.md§核心 Trade-off 1 - 自包含 prompt 三条理由与信息损失缓解策略:
00-Question.mdQ3 - Never delegate understanding 原文、续传决策表:
01-Study-Source-多Agent系统.md§coordinatorMode.ts
§4 ★ 上下文边界的三种形态(本文架构核心)
这一章是全文的枢纽。前面三章都在往这里走:
- §1 说多 Agent 的真实价值是「上下文压缩边界」
- §2 把「上下文边界」列为四个正交件之一,然后跳过了它
- §3 说 Handoff 的损失是本质问题,而自包含 prompt 只是把损失变成了显性决策
所以核心问题只剩一个:一个新派出去的 Agent,应该看到什么?
4.1 三种形态,一句话讲完
形态 ① 隔离(Isolation) 子 Agent 从空白开始,只有一段自包含指令
形态 ② 继承(Inheritance) 子 Agent 直接拿到父级的完整上下文副本
形态 ③ 共享(Sharing) 多个 Agent 读写同一份可变状态三者不是「好坏」关系,是三个不同的赌注:
| ① 隔离 | ② 继承 | ③ 共享 | |
|---|---|---|---|
| 子 Agent 看到 | 一段指令 | 父级全部历史 | 全部历史 + 别人的实时改动 |
| 信息损失 | 有(复述必然损失) | 零 | 零 |
| 父级要付的成本 | 花 token 复述上下文 | 零(原样传) | 零 |
| 上下文压缩收益 | 有(子级的探索垃圾不回流) | 有(回流的仍只是摘要) | 无 |
| 独立视角 | 有 | 无(会锚定) | 无 |
| 缓存共享 | 无(各自前缀不同) | 有(前缀相同) | 有 |
| 一致性问题 | 无 | 无(副本,写时分离) | 有(§7 整章) |
| 典型实现 | Claude Code 命名子代理 / Codex 容器 | Claude Code fork 子代理 | Agent Teams 的共享任务列表 |
读这张表的方式:找每一列里加粗的那格,那就是这个形态在赌的东西。
- ① 赌「信息损失换来的压缩和独立性值这个价」
- ② 赌「零损失 + 缓存共享比压缩收益更值钱」
- ③ 赌「实时协同的价值大于一致性问题的代价」
4.2 形态 ①:隔离,以及它为什么曾被当成唯一答案
隔离是教科书答案,也是大多数框架的默认。🔬 原始文档作者在读源码前的认知就是这个:
我的面试题答案里写了一句很自信的话——「子 Agent 无法看到父 Agent 的对话历史, 每个委托的 prompt 必须是自包含的」,并且把它当成核心设计决策,还列了三条理由。
三条理由(§3.4 已列,这里给完整机制):
| 理由 | 机制 |
|---|---|
| 上下文压缩 | 子 Agent 干了 50 轮、读了 30 个文件,回来只给一段摘要——那 30 个文件永远没进主上下文 |
| 安全隔离 | 共享历史 = 子 Agent 能看到全部敏感信息,违反最小权限 |
| 可预测性 | 不受父级历史中无关信息干扰,行为更确定 |
🔬 隔离在源码里的实现叫 deny by default(默认拒绝),核心是逐项决定隔离什么:
| 状态 | 隔离方式 | 原因 |
|---|---|---|
| 对话历史 | 空初始化 | 从空白起步,只有委托 prompt |
| 文件读取缓存 | 克隆(不是共享) | 子 Agent 读文件不该污染父级的「缓存新鲜度」判断 |
| 工具权限 | 受限子集 | 不能派子 Agent(防递归)、某些类型不能编辑文件 |
| abort 控制器 | 可选共享 | 父级需要能取消子级 |
注意「文件读取缓存 = 克隆」这个选择。不是共享(会互相污染), 也不是空的(子 Agent 会重读一遍父级已读过的文件,浪费)。克隆是这三者里唯一对的选项, 它意味着「起点相同,之后各走各的」——这个模式在 §4.4 还会再出现一次。
🔬 还有一个分层安全策略值得记,它是「能力边界靠代码」的具体形态:
第 1-3 层:全局策略,所有 Agent 都受约束(如禁止递归派生)
第 4 层: 类型级策略,特定类型的约束(如 Explore 类型禁止文件编辑)
→ 即使自定义 Agent 把禁止列表设为空,前三层保护仍然生效这个设计的关键在最后一句。 用户可以定义自己的 Agent 类型, 但用户配置只能收紧、不能放松前三层。这是「配置层不能突破源码层的安全边界」, 是任何允许用户扩展的 agent 系统都必须做的一件事。
4.3 形态 ②:继承,以及那个推翻教科书的发现
🔬 这一节是整份原始源码文档的主线,值得完整复现,因为推翻过程本身就是最好的教学。
作者带着「隔离是核心原则」的认知去 grep 源码,在文件列表里看到了一个名字:
210 src/tools/AgentTool/forkSubagent.ts # ← 这个文件名让我停住了"fork" 在进程语义里就是复制父进程的全部状态。 如果子 Agent 的核心设计是「空上下文 + 自包含 prompt」,这个文件存在的意义是什么?
打开一看,文件头注释直接把答案推翻了:
// src/tools/AgentTool/forkSubagent.ts:19-31
/**
* Fork subagent feature gate.
* When enabled:
* - `subagent_type` becomes optional on the Agent tool schema
* - Omitting `subagent_type` triggers an implicit fork: the child inherits
* the parent's full conversation context and system prompt
* - All agent spawns run in the background (async)
*/两句话要读出三层意思:
- 继承路径存在(
the child inherits the parent's full conversation context) - 它是默认路径(
subagent_type从必填变可选,省略即 fork) - 于是「空上下文」从原则降级成了需要显式指定的特例
作者的原话:
我原来以为「空上下文」是设计原则,实际上它正在被降级为一个需要显式指定的特例。 我把一个权衡的一侧当成了原则。
「把权衡的一侧当成原则」是本文想让你避开的头号认知错误。 它的症状是:你能流利地说出三条理由,但说不出这三条理由的代价是什么。 测试方法很简单——任何一条「设计原则」,你都应该能说出它在什么条件下会反转。
那么继承赌的是什么?为什么值得?下一节。
4.4 继承为什么值得:不是省事,是省钱
继承的收益乍看是「省了复述的功夫、零信息损失」。这两条都对,但都不是主因。 🔬 主因是前缀缓存,而这需要先讲清缓存的机制。
prompt cache(前缀缓存)三十秒版:
LLM 厂商为了省算力,会缓存你请求的前缀。下次请求如果前 N 个 token 与上次完全一致,这部分不重新计算,价格打到一折左右。
关键词是**「完全一致」,而且是字节级的**:
请求 A:[系统提示词][工具定义][历史消息][新问题1]
请求 B:[系统提示词][工具定义][历史消息][新问题2]
└────────── 这段字节完全相同 ──────────┘ ← 命中缓存,一折
└ 只有这里全价 ┘
请求 C:[系统提示词'][工具定义][历史消息][新问题3]
└ 差一个字节 ┘
↑ 从这里开始,后面全部重新计算——缓存全断现在把这个机制套到并行子 Agent 上,收益就出来了。
假设父 Agent 一次派 5 个子 Agent 做研究,共享上下文(系统提示词 + 工具定义 + 历史)是 48k token:
| 方案 | 每个子 Agent 的输入成本 | 5 个合计 |
|---|---|---|
| 隔离(各自重建前缀) | ~48,700 全价 | ~243,500 |
| 继承(前缀字节相同) | 老大 ~48,700;老二到老五各 ~5,050 | ~68,900 |
📄 降到 28%。 这就是继承赌的东西——不是省事,是省一个量级的钱。
所以「多 Agent 太贵」这句话必须拆开:
多 Agent 的成本 = 本质开销(每个 Agent 都得推理,省不掉)
+ 偶然开销(每个都重传同一份前缀,可以消掉 90%)📄 回到 §1.5 那句:说「多 Agent 太贵」之前,先问「你有没有消掉偶然那部分」。 Anthropic 测的 15 倍溢价里,有多少是偶然开销,取决于实现。
4.5 缓存对齐的代价:三个「宁可」
这才是这一章真正的深度所在。🔬 拿到那 90% 折扣不是免费的—— 它要求所有子 Agent 的请求前缀字节完全相同,为此源码做了三个反直觉的让步。
让步一:宁可传「已渲染的字节」,也不重新生成一份「一样的」提示词
FORK_AGENT 这个 Agent 定义里有一行看起来很怪的代码:
export const FORK_AGENT = {
agentType: FORK_SUBAGENT_TYPE,
tools: ['*'],
maxTurns: 200,
model: 'inherit',
permissionMode: 'bubble',
getSystemPrompt: () => '', // ← 一个 Agent 定义不提供系统提示词?
}注释解释了为什么返回空字符串:
The getSystemPrompt here is unused: the fork path passes
override.systemPromptwith the parent's already-rendered system prompt bytes... Reconstructing by re-calling getSystemPrompt() can diverge (GrowthBook cold→warm) and bust the prompt cache; threading the rendered bytes is byte-exact.
读懂这段的关键:不是「重新生成一份一样的系统提示词」, 而是把父 Agent 已经渲染好的字节原样传过去。
因为重新生成会有微妙的不一致:GrowthBook(功能开关服务) 在冷启动和热缓存时返回的值可能不同,导致提示词差一个字, 前缀缓存就从那个字开始全部失效。
★ 生成式的一致性 ≠ 字节级的一致性。缓存只认字节。
这是一类只在有前缀缓存的系统里存在的问题,任何概念文章都不会提。 它也是本文最好的「信号词」候选之一(§12 会用到)。
顺带解释 model: 'inherit':注释写的理由是「保持 context length parity」, 但真实原因更硬——换模型 = 换 cache key = 缓存全丢。
让步二:宁可给孩子看「假的」工具结果,也要保证前缀相同
这是三个让步里最精妙的一个。先说它在解决什么问题。
Anthropic API 有个硬性协议要求:每个 tool_use 必须有对应的 tool_result,否则请求非法。 但并行 fork 是同时启动的:
assistant: [tool_use(fork A), tool_use(fork B), tool_use(fork C)]A 启动时,B 和 C 还没有结果。那 A 的上下文里,B 和 C 的 tool_result 填什么?
最直觉的做法:A 的上下文里,A 自己那个填「正在处理」,B 和 C 填别的(或干脆只保留 A 的)。 后果:A、B、C 三个孩子看到的消息前缀互不相同 → 三份缓存,谁也蹭不到谁的 → 折扣归零。
源码的做法:三个孩子看到的 tool_result 全部填同一句占位符:
/** Placeholder text used for all tool_result blocks in the fork prefix.
* Must be identical across all fork children for prompt cache sharing. */
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'[...共享历史, assistant(A,B,C 三个 tool_use), user(占位×3, "你的任务是X")]
└────────── 这一整段字节完全相同,是缓存前缀 ──────────┘ └ 只有这里不同 ┘代价是明确的:孩子对兄弟的工作状态有一个错误的心智模型。 A 会在自己的上下文里看到「fork B started — processing in background」—— 这是一句永远不会被兑现的谎言(B 的真实结果永远不会填进 A 的这个位置)。
但它换来的是 B、C 两份完整前缀的费用。 所以提示词里必须显式补救:
7. Stay strictly within your directive's scope. If you discover related
systems outside your scope, mention them in one sentence at most —
other workers cover those areas.★ 一行常量(
FORK_PLACEHOLDER_RESULT)直接对应百分之几十的成本差异。 如果三个孩子的前缀不一致,那 90% 的折扣就拿不到。
让步三:宁可留一个「一调就报错」的工具,也不从工具池里删掉它
FORK_AGENT 的定义里 tools: ['*'] —— fork 出来的孩子,工具池里仍然带着 Agent 工具, 尽管代码里明确禁止它 fork(throw new Error('Fork is not available inside a forked worker.'))。
为什么不干脆删掉?注释:
Fork children keep the Agent tool in their tool pool for cache-identical tool definitions, so we reject fork attempts at call time.
删一个工具 = 工具定义数组变了 = 序列化字节变了 = 缓存在第一个不同的工具处断裂。
于是权限收敛从「能力移除(build time)」改成了「调用拒绝(call time)」, 纯粹为了缓存字节对齐。
这在安全设计上是个有味道的取舍:工具还在,只是有人守门。 如果那个守门的 throw 哪天被重构掉了,能力就恢复了—— 而「工具不在池子里」这种设计是没有这个风险的。这是用一点安全性换成本。
4.6 隔离的另一半动机:不是防御,是经济
🔬 这一节推翻了「上下文隔离是为了防污染」这个理解,它有一个非常震撼的数字。
源码里 Explore / Plan 这两类只读子 Agent,上下文里被刻意删掉了两样东西:
// Read-only agents (Explore, Plan) don't act on commit/PR/lint rules from
// CLAUDE.md — the main agent has full context and interprets their output.
// Dropping claudeMd here saves ~5-15 Gtok/week across 34M+ Explore spawns.
// Explore/Plan are read-only search agents — the parent-session-start
// gitStatus (up to 40KB, explicitly labeled stale) is dead weight...
// Saves ~1-3 Gtok/week fleet-wide.两个数字:每周 3400 万次 Explore 派生;删掉 CLAUDE.md 省 5–15 Gtok/周; 再删掉一个 40KB 的过期 gitStatus,再省 1–3 Gtok/周。
关键在于删除的理由。 不是因为这些内容有害,而是因为 只读 Agent 用不到——Explore 不会 commit、不会跑 lint, 所以 CLAUDE.md 里的提交规范对它是纯粹的死重。
★ 在规模面前,「无害但无用」和「有害」的成本是一样的。
这是概念层和工程层最大的一个 gap。原始文档作者的反思:
我以前算多 Agent 成本只会算「N 个 Agent × 各自的对话轮次」, 从没想过要去审计每个子 Agent 上下文里的每一个 KB 是否真的被用到。
方法论(可以直接用):逐项审计每个子 Agent 上下文里的每一块内容, 问一句「这个 Agent 的工具集能不能用到它?」
- 只读 Agent 不需要提交规范
- 搜索 Agent 不需要部署配置
- 分类 Agent 不需要示例之外的背景
但这类优化有一个必须配的东西:kill-switch。 源码里这两处裁剪都带开关(tengu_slim_subagent_claudemd,默认 true)。理由是:
上下文裁剪导致的是静默降级 —— 万一某个 Explore 确实需要 CLAUDE.md 里的信息, 它不会报错,只是结果悄悄变差。
★ 这类优化必须有能立刻回滚的开关,而且默认值和开关名要写在注释里。 这条可以推广到所有「删掉一点东西省钱」的优化: 不报错的降级比报错的失败危险得多,因为后者会被发现。
4.7 隔离不是二元开关:十一个维度的逐项裁剪
🔬 这是本章第二个认知更新。作者原以为上下文隔离是个开关(隔离 / 共享), 实际上它是十几个独立维度上的逐项决策:
| 维度 | fork(继承)路径 | 命名子 Agent(隔离)路径 | 为什么这么定 |
|---|---|---|---|
| 消息历史 | 继承(过滤悬空调用) | 空 | fork 图省 token,命名图干净 |
| 系统提示词 | 继承父已渲染字节 | 自己的 | 字节级一致才有缓存 |
| 工具池 | 父的原样数组 | 按权限模式重建 | 同上 |
| thinking 配置 | 继承 | 自己的 | 也是 cache key 的一部分 |
| CLAUDE.md | 带 | Explore/Plan 剔除 | 只读用不到,省 5–15 Gtok/周 |
| gitStatus | 带 | Explore/Plan 剔除 | 40KB 过期数据 |
| 文件读取缓存 | 克隆 | 全新 | 父子后续读写互不干扰 |
| abortController | 后台 Agent 不挂父级 | 同 | 用户按 ESC 不该杀掉后台任务 |
| UI 状态 | 同步 Agent 共享 / 异步隔离 | 同 | 异步 Agent 不能改主线程 UI |
| 权限模式 | bubble(弹到父终端问) | Agent 定义指定 | fork 是「我自己的延伸」,权限该问我 |
| 派生能力 | 禁止(见 §5.6) | 受限 | 缓存语义 / 递归防御 |
两个细节值得单独讲:
细节一:filterIncompleteToolCalls(过滤悬空调用)
父 Agent 的历史里可能有「发出了 tool_use 但还没收到 tool_result」的悬空调用 (比如父正在并行跑别的工具)。直接继承过去,API 会返回 400。 这是继承路径必须做的一次清洗,任何自己实现 fork 的人都会撞上。
细节二:abortController 不挂父级——取消的传播边界
// Don't link to parent's abort controller -- background agents should
// survive when the user presses ESC to cancel the main thread.这是最容易写错的一处,因为「父取消则子取消」在代码上是更自然、更「正确」的树形传播。
但用户按 ESC 是想打断「当前正在等的这件事」, 不是想杀掉半小时前扔到后台的五个任务。如果 abort 信号无脑往下传, 用户会丢掉大量已完成的工作。
★ 取消的传播边界 = 用户的心智模型边界,不是调用树的边界。
反转条件:需要「全局熔断」语义时(成本失控、安全事件),这时就该无脑往下传。 所以正确的设计是两种取消要分开:用户级取消(不传播)和系统级熔断(传播)。
4.8 形态 ③:共享,以及它买到什么
前两种形态都是「一次性快照」——传过去之后,父子各走各的。 第三种形态是多个 Agent 读写同一份可变状态。
📄 Claude Code 的 Agent Teams 是这一路:
| 组件 | 是什么 |
|---|---|
| Team Lead | 你当前的会话,负责创建团队、分配任务、汇总 |
| Teammates | 独立的 Claude Code 实例,各有自己的上下文窗口 |
| Shared Task List | 所有 Agent 都能看到的中央任务队列(pending → in_progress → completed) |
| Mailbox | 消息系统,支持直接消息和广播,写入 ~/.claude/teams/<id>/inbox/ 后注入对话历史 |
共享买到的是实时协同:队友 B 能看到队友 A 刚把任务 3 标记成了 in_progress, 于是不会去做重复的事。这是形态 ① ② 都做不到的——它们的信息只在派生时和返回时流动。
代价是一致性问题,而这是个大坑,§7 整章在讲: 两个 Agent 同时读到「任务 3 = pending」,同时把它标成 in_progress,同时开始干—— 这就是经典的竞态,而且在 LLM 系统里你没法用锁轻易解决(Agent 不会乖乖等锁)。
4.9 一个必须知道的边界:Agent 的生命周期 ≠ 可寻址性
🔬 一个源码发现,它修正了很多人(包括我)的默认心智模型。
问题:Agent 之间点对点通信,如果对方已经跑完退出了,消息去哪? 默认心智模型是「Agent 跑完就消失了,要续传就得重新派一个」。
源码里的答案是一条三级降级链:
// 1. 对方还在跑 → 入队,下一个工具轮次消费
queuePendingMessage(...)
// → "Message queued for delivery to X at its next tool round."
// 2. 对方已停止(task 还在内存里)→ 自动恢复
await resumeAgentBackground({ ... })
// → 'Agent "X" was stopped; resumed it in the background with your message.'
// 3. task 已从内存驱逐 → 从磁盘 transcript 恢复
// → 'had no active task; resumed from transcript in the background'第三层是关键:即使任务已经被驱逐出内存, 只要磁盘上还有 sidechain transcript(每条消息后都在写), 就能把这个 Agent 重建出来继续跑。
★ Agent 的「生命周期」和「可寻址性」不是一回事。 通过持久化 transcript,一个已经「死掉」的 Agent 仍然是可寻址、可复活的实体—— 它的上下文躺在磁盘上等着。
这条对 §7(记忆一致性)很有用:Agent 的上下文不必活在内存里, 磁盘 transcript 就是它的持久化身。跨会话、跨进程的 Agent 协作可以建立在这个基础上。
4.10 三种形态的选择表
| 你的情况 | 选 | 理由 |
|---|---|---|
| 子任务是只读探索,结果只要一份摘要 | ① 隔离 | 压缩收益最大,且可以裁死重 |
| 需要独立视角(验证、二次意见、多方案) | ① 隔离 | 继承会锚定,见 §3.5 |
| 子 Agent 定义来自不可信来源 | ① 隔离 | 最小权限 |
| 并行派多个、且都需要当前完整背景 | ② 继承 | 零损失 + 缓存共享省 ~90% |
| 父级复述上下文的成本高于继承 | ② 继承 | 直接算 token 就知道 |
| 长活的对等实例、需要实时看到彼此进展 | ③ 共享 | 只有它能做到 |
| 任何情况下需要「大产物交接」 | 任选 + 文件系统 | 把常驻成本变按需成本 |
4.11 本章自检
- 「子 Agent 应该用空上下文 + 自包含 prompt」——这个原则在什么条件下会反转?
- 为什么不能「重新生成一份和父级一样的系统提示词」,必须传已渲染的字节?
- 三个并行 fork,为什么给它们看同一句假的工具结果反而是对的?代价是什么、怎么补救?
- 一个用不到的工具,为什么宁可留在工具池里、只在调用时拒绝?这个选择牺牲了什么?
- 「删掉只读 Agent 上下文里的提交规范」——这是防御性优化还是经济性优化?为什么这个区分重要?
- 为什么后台 Agent 的 abort 不该挂在父级上?什么情况下该挂?
- 一个已经跑完退出的 Agent,还能给它发消息吗?如果能,靠什么机制?
📌 回到原始文档:
- fork 的三个缓存让步、上下文死重审计、十一维度隔离清单:
01-Study-Source-多Agent系统.md§forkSubagent.ts / §runAgent.ts(全章最值得原文精读) - 隔离的三条理由与 deny-by-default 实现:
00-Question.mdQ3 - Agent Teams 四大组件与 Teams vs Subagents 对照:
multi-agent-deep-dive.md§4.1 / §4.2 - 三级恢复链:
01-Study-Source-多Agent系统.md§SendMessageTool.ts
§5 省钱:多 Agent 的成本结构与五个杠杆
§4.4 已经讲了缓存对齐这一个杠杆(也是最大的那个)。这一章把成本结构讲全, 因为多 Agent 项目死掉的头号原因是成本失控,而失控往往不是因为缺了某个技巧, 是因为根本没算清钱花在哪。
5.1 先算清成本模型
单 Agent 一轮的输入 token 大致是:
一轮的输入 = 系统提示词 + 工具定义 + 历史消息(只增不减)所以 N 轮的累计输入约等于:
累计输入 ≈ N × 系统提示词 + N × 工具定义 + Σ(历史) ← 最后这项是 O(N²)注意最后那项。 因为历史只增不减,第 k 轮要重传前 k-1 轮的全部内容, 累加下来是二次的。这就是 📄 那条实测经验的来源:
2× 轮数 ≈ 3–4× 成本。 第 N 轮的 input 约等于 N 倍的第 1 轮。
现在扩展到多 Agent(M 个 Agent,各跑 N 轮):
总成本 ≈ M × (N × 前缀 + O(N²) 历史) + Handoff 的编码/解码成本
└───┬───┘ └────┬────┘ └──────┬──────┘
① 前缀重复付费 ② 各自的历史 ③ 协调开销
(偶然,可消掉) (本质) (本质,但可优化载体)三项各有不同的处理方式,这是本章的骨架:
| 项 | 性质 | 处理手段 | 量级 |
|---|---|---|---|
| ① 前缀重复付费 | 偶然 | 缓存对齐(§4.4) | 可省 ~90% 的这一项 |
| ② 各自的历史 | 本质 | 减少轮数、压缩、模型分层 | 轮数是最大杠杆 |
| ③ 协调开销 | 本质 | 换载体(文件系统)、劝阻广播 | 一个量级 |
📄 一组参考数字,用来建立量级感(不要当恒定事实,见开头免责声明):
- 普通 chat → 单 Agent:约 4× token
- 单 Agent → 多 Agent 系统:约 15× token
- UIUC 的测量口径更宽:多 Agent 是单 Agent 的 4–220×
- 一个 5 Agent 的社媒内容周计划任务(15 分钟):约 $7.80
- 单 Agent $0.10 的任务,多 Agent 可能要 $1.50
220 倍这个上界值得注意:它说明成本区间极宽,而区间宽窄取决于实现。 同一个任务,做对了和做错了差两个数量级——这就是为什么这一章值得读。
5.2 杠杆一:模型分层(最容易做、收益立竿见影)
📄 这个模式有个业界通用名:Smart Planner + Cheap Executors。
战略决策层(编排、规划、综合) → 强模型(Opus 级):贵但聪明
执行层(改代码、跑测试、搜索) → 中档模型(Sonnet 级):便宜且快
简单分类 / 格式转换 → 小模型(Haiku 级):最便宜📄 MAST 研究(分析 1,642 条执行 trace)发现,生产中成功的多 Agent 系统几乎都是这个模式。
为什么这个分层是自然的,而不只是省钱的权宜:
- 编排器的工作是综合与判断(§3.3:它必须理解才能分发)——这是最吃推理的部分
- Worker 的工作是执行明确的规范(「改
validate.ts:42,加空值检查」)—— 规范越具体,需要的推理越少
所以模型分层的前提是编排器真的把规范写具体了。 如果编排器透传 (「根据你的发现去修」),那 Worker 就得自己去理解调研结果, 这时用便宜模型会直接掉质量。
★ 模型分层的可行性,取决于 §3.3 那条原则有没有做到。两件事是耦合的。
⚠️ 一个反作用要知道:🔬 fork 路径里 model: 'inherit' 是刻意的, 因为换模型 = 换 cache key = 缓存全丢。 所以模型分层和缓存共享在 fork 场景下是冲突的—— 你得选一个:要么孩子换便宜模型(省输出成本,丢缓存),要么继承模型(保缓存)。 判据是输入/输出的比重:输入占大头时保缓存,输出占大头时换模型。
5.3 杠杆二:缓存分层——把内容按「变化频率」排序
🔬 这是一个非常漂亮的发现,也是最容易被漏掉的一类浪费。
前提是 §4.4 那个机制:缓存是前缀匹配的,第一个变化的字节之后全部失效。
推论是:如果一段会变的内容被放在了不变的内容前面,它会让后面所有东西的缓存都失效。
源码里的实例:Agent 工具的描述里,原本包含一份「当前可用的 Agent 类型清单」。 这份清单会随 MCP 异步连接、插件重载、权限模式变化而变。注释给了量化:
/**
* The dynamic agent list was ~10.2% of fleet cache_creation tokens: MCP async
* connect, /reload-plugins, or permission-mode changes mutate the list →
* description changes → full tool-schema cache bust.
*/一份动态清单,单独占了全机队 cache_creation token 的 10.2%。
解法不是缩短清单,而是把动态部分从工具描述里搬到消息体里:
const agentListSection = listViaAttachment
? `Available agent types are listed in <system-reminder> messages in the conversation.`
: `Available agent types and the tools they have access to:\n${...}`搬完之后:工具描述变成静态的、永不变化的字节 → 工具 schema 块的缓存永远命中; 动态清单进消息体,只影响它之后的那一小部分。
❌ 改之前
[系统提示词][工具定义(内含动态清单!)][历史消息]
↑ 这里一变,后面全断
✅ 改之后
[系统提示词][工具定义(纯静态)][历史消息][动态清单 as system-reminder]
↑ 只有它之后的失效★ 把内容按变化频率排序,静态在前,动态在后。
这条原则的适用面远超多 Agent:
| 场景 | 怎么应用 |
|---|---|
| 系统提示词组织 | 静态角色定义 → 半静态工具/技能清单 → 动态环境信息(时间、git 状态) |
| RAG | 固定指令在前,检索结果在后 |
| 批量分类 | N 条数据共享同一份指令前缀,指令绝不变 |
| A/B 测试 prompt 变体 | 把变体放在最后 |
| MCP 工具注册 | 动态连接的工具不要塞进系统提示词 |
🔬 顺带解释了 §2.6 那个「为什么五种派生模型挤在一个 Agent 工具里, 而不是拆成 Fork / Spawn / SpawnTeammate 三个工具」的问题—— 多一个工具就多一份 schema 字节和一个变化源,而工具定义是缓存前缀的一部分。 直觉上「拆开语义更清晰」是对的,但它在缓存维度上是负收益。
5.4 杠杆三:削减轮数与上下文膨胀
回 §5.1:轮数的成本是二次的,所以它是单个最大的杠杆。
四个手段:
| 手段 | 机制 | 注意 |
|---|---|---|
| 子 Agent 做压缩边界 | 50 轮探索只回流一份摘要(§0.3) | 这是子 Agent 的主要价值 |
| compaction(上下文压缩) | 历史太长时压成摘要 | ⚠️ 压缩会丢信息 → 重读文件 → 重复付费;也会破坏不变量(§6.5) |
| 文件系统外置 | 详细结果写文件,只留一句路径(§2.5) | 把常驻成本变按需成本 |
| 明确任务边界 | 避免多个 Agent 做重复的事 | 靠共享任务列表或清晰的 scope 约定 |
📄 一个容易忽略的成本项:side-call(影子调用)—— 生成标题、做摘要、recall 记忆这些辅助调用。它们绕过主埋点,是最易漏计的一块。 在多 Agent 系统里,每个 Agent 都可能产生自己的 side-call,M 倍放大。
5.5 杠杆四 & 五:语义缓存与预算硬上限
杠杆四:语义缓存(Semantic Caching)
📄 缓存相似查询的响应,实践中可达 70% 命中率, 把协调开销从 50% 降到 30%。适合重复性高的子任务。
注意它和 prompt cache 是两回事:
| prompt cache(前缀缓存) | 语义缓存 | |
|---|---|---|
| 匹配方式 | 字节级完全相同的前缀 | 语义相似(向量距离) |
| 谁提供 | LLM 厂商(服务端) | 你自己实现 |
| 风险 | 无(不匹配就是不命中) | 会返回不完全正确的答案 |
第二列的风险是它的主要代价:语义相似不等于答案可复用。 「读一下 auth.ts」和「读一下 auth.test.ts」向量距离可能很近,但答案完全不同。 所以语义缓存适合幂等的查询类,不适合工具调用。
杠杆五:预算硬上限(这条是底线,不是优化)
每个 Agent 的 token 预算上限
每个 Agent 的工具调用次数上限(maxTurns)
每个 Agent 的执行时间上限
整个任务的总成本上限 → 超了自动停🔬 源码里 FORK_AGENT 的 maxTurns: 200 就是这一类。
为什么它是底线而不是优化:多 Agent 系统有一类失败是 「两个 Agent 互相触发形成无限循环」(§9.4 会讲,有真实案例跑了一小时)。 没有硬上限时,这类故障的成本是无界的。
★ 优化决定你花多少钱;硬上限决定你最多花多少钱。后者更重要。
5.6 一个反直觉的 trade-off:共享缓存与层级深度互斥
🔬 这一节是本章的深度点,也是原始文档里一条「没在任何地方读到过」的结论。
现象:源码里 fork 出来的孩子不能再 fork(深度硬限 1), 但命名子 Agent 可以嵌套(📄 外部资料提到某版本起最深 5 层)。
第一反应:深度限制是为了防递归爆炸。 这个理由对命名子 Agent 成立,但对 fork 是错的。fork 的真实理由是:
fork 的缓存共享语义只在「父-子」这一层成立。 孙子要蹭谁的缓存?父的前缀已经被子的 directive(任务指令)改写了。
回看 §4.5 那张图:
父的前缀:[...共享历史, assistant(tool_uses), user(占位, "子的任务")]
↑ 子在这里追加了自己的指令
孙子的前缀:[父的前缀 + 子的若干轮对话, ..., user("孙子的任务")]
└──────── 这一段每个子都不同 ────────┘
↑ 兄弟之间不再有共同前缀 → 缓存共享收益归零孙子层的兄弟们,共同前缀只到「父的前缀」那一段, 而它们各自的父(也就是各个子)已经走了不同的路。 深度每加一层,能共享的前缀比例就下降一截,到某一层就不值得了。
★ 共享缓存和层级深度在架构上互斥。
这条的可迁移价值在于它示范了一种思考方式: 当你看到一个限制,不要满足于第一个说得通的理由。 「深度限制 1」有一个非常自然的解释(防递归爆炸), 而这个解释恰好是错的——真实理由完全不同,且推不出同样的设计。
🔬 顺带看递归防御该怎么做,这里有个真实的攻击面:
if (toolUseContext.options.querySource === `agent:builtin:${FORK_AGENT.agentType}`
|| isInForkChild(toolUseContext.messages)) {
throw new Error('Fork is not available inside a forked worker.')
}两层防御,注释解释了为什么需要两层:
Primary check is querySource (compaction-resistant — set on context.options at spawn time, survives autocompact's message rewrite). Message-scan fallback catches any path where querySource wasn't threaded.
这是一个很少有人想到的攻击面:上下文自动压缩会重写消息历史。
如果递归防御只依赖「扫描历史里有没有 fork 标记」, 那么一次 autocompact 把标记压掉之后,fork 孩子就重新获得了 fork 的能力 → 递归爆炸。
所以真正可靠的那道锁必须挂在不会被压缩改写的元数据上(spawn 时写在 context 上), 消息扫描只是兜底。
★ 长跑 Agent 里,任何基于「检查历史内容」的不变量都是不可靠的, 因为历史本身是会被压缩、重写、截断的可变对象。
这条可以直接迁移到:循环检测、预算控制、权限升级检测、 以及任何形式的「我在历史里留了个标记」式设计。
5.7 五个杠杆总表
| # | 杠杆 | 量级 | 难度 | 代价 / 风险 |
|---|---|---|---|---|
| 1 | 缓存对齐(§4.4–4.5) | 输入成本降到 ~1/10 | 高(要字节级纪律) | 孩子对兄弟状态有错误认知;留了用不到的工具 |
| 2 | 模型分层 | 直接按单价比例 | 低 | 与缓存共享冲突;依赖编排器写具体规范 |
| 3 | 缓存分层(动态后置) | 实测一处占 10.2% | 中 | 需要重构提示词组织 |
| 4 | 削减轮数 / 外置到文件 | 二次项,最大 | 中 | compaction 会丢信息、破坏不变量 |
| 5 | 上下文死重审计(§4.6) | 每周 Gtok 级(规模相关) | 中 | 静默降级,必须带 kill-switch |
| — | 预算硬上限 | 不是优化,是底线 | 低 | 无 |
排序建议:先做 2 和 6(最便宜),再做 4,最后才做 1 和 3 (它们要求字节级纪律,在你还没有可观测性时容易做错还不知道)。
5.8 本章自检
- 为什么多 Agent 的累计输入成本对轮数是二次的,而不是线性的?
- 「多 Agent 是单 Agent 的 4–220 倍」——这个区间为什么这么宽?宽窄由什么决定?
- 模型分层(子 Agent 用便宜模型)和缓存共享冲突。判据是什么?
- 一份会随 MCP 连接状态变化的工具清单,放在工具描述里有什么问题?该放哪?
- prompt cache 和语义缓存的关键区别是什么?后者的独有风险是什么?
- 「fork 深度限制为 1 是为了防递归爆炸」——这句话哪里错了?真实理由是什么?
- 你的递归防御是「扫描消息历史里有没有派生标记」。它会在什么时候静默失效?
📌 回到原始文档:
- 缓存分层的 10.2% 案例、fork 深度与缓存互斥、compaction-resistant 锚点:
01-Study-Source-多Agent系统.md§AgentTool.tsx / §外部对照 / §可迁移原则一、二 - Smart Planner + Cheap Executors 与 MAST 口径:
00-Question.mdQ4 - 语义缓存 70% 命中、四种开销控制策略:
00-Question.mdQ1 - 成本控制的四条实操与实际花费参考:
multi-agent-deep-dive.md§4.6 / §9.4
§6 ★ 失败工程:它是怎么坏的
前五章讲「该不该用、怎么组织、怎么省钱」。这一章讲它坏掉时长什么样, 是本文和 §4 并列最重要的两章。
理由是一个数字。📄 MAST 研究(Multi-Agent System Failure Taxonomy, 分析了 1,642 条真实执行 trace,ICLR 2026 接收)的核心结论:
79% 的多 Agent 失败不是模型问题,是系统工程问题。 其中规格设计占 42%,Agent 间对齐占 37%。
配套的一个数字:📄 生产环境失败率 41–87%;协调失败占故障的 37%。
这个结论的分量在于它否定了一个非常自然的应对方式。 系统效果不好时,最容易做的事是「换个更强的模型试试」。 MAST 说的是:79% 的情况下换模型不会解决,因为病根在规格和协调上。 投资该放在系统设计上。
所以这一章的组织方式是:七类失败模式,每一类给「形态 / 为什么难发现 / 怎么防」。
6.1 失败一:Context Poisoning(上下文污染)—— 最经典的一类
形态:一个 Agent 的错误输出通过 Handoff 链路传播到所有下游 Agent,形成级联失败。
📄 严重程度的两个数字:
- OWASP 2025 把级联失败列为 Agentic AI 安全风险第 8 位(ASI08)
- Galileo AI 2026:单个受损 Agent 在 4 小时内影响了下游 87% 的决策
三种传播路径(分类之后才能针对性防御):
| 路径 | 机制 | 例子 |
|---|---|---|
| 直接传播 | A 的错误输出直接作为 B 的输入 | A 把「营收下降 5%」读成「增长 5%」,B 基于此给出乐观的投资建议 |
| 记忆污染 | A 把错误信息写入共享记忆,所有后续 Agent 都读到 | A 把错误的用户偏好写进向量库,之后所有推荐都基于错误偏好 |
| 间接注入 | 外部数据里嵌了恶意指令,经 A 处理后传播到整个系统 | 邮件里嵌 prompt injection,A 读了之后把恶意指令传给 B 执行 |
为什么它在多 Agent 里特别危险:单 Agent 系统里错误只影响当前对话; 多 Agent 里错误跨上下文传播,而且下游 Agent 看不到上游的原始信息, 因此丧失了「对照原文发现矛盾」的能力。
防御:纵深防御四层(借网络安全的 Defense in Depth)
第一层 输出验证门控(Output Gate) —— 不让错误输出流出去
第二层 Handoff 契约 —— 不让错误输入被接受
第三层 隔离与熔断 —— 限制爆炸半径
第四层 全局监控与回滚 —— 发现了能止血逐层的具体内容:
第一层:输出验证门控
| 手段 | 说明 |
|---|---|
| 格式验证 | 输出符合预期 Schema(JSON Schema / Pydantic) |
| 事实一致性检查 | 用轻量 LLM 验证输出是否与输入数据一致 |
| 置信度阈值 | 输出带置信度,低于阈值不传下游,触发重试或人工 |
| 安全分类器 | 见 §6.2,这是最有意思的一条 |
第二层:Handoff 契约
| 手段 | 说明 |
|---|---|
| 输入/输出格式约定 | 明确每个 Agent 期望收什么、产出什么 |
| 前置条件检查 | 下游在接收前验证前置条件(而不是接收后才发现不对) |
| 不变量断言 | 关键业务规则在每次 Handoff 时断言(「金额必须为正」) |
| 来源标注 | 每条信息标注来源 Agent 和生成时间,便于回溯 |
第三层:隔离与熔断
| 手段 | 说明 |
|---|---|
| 上下文隔离 | §4.2 的 deny-by-default |
| 熔断 | 某 Agent 错误率超阈值时自动熔断,切备用或转人工 |
| 爆炸半径控制 | 限制单个 Agent 能影响的下游数量(类比微服务的 Bulkhead) |
第四层:全局监控与回滚
| 手段 | 说明 |
|---|---|
| 错误传播追踪 | 实时监控错误在网络中的传播路径 |
| 检查点回滚 | 关键 Handoff 点存快照,发现污染后回滚到最近的干净点 |
| Kill Switch | 级联达到阈值时紧急停止整个系统 |
但纵深防御是「检测」路线。§3.3 已经给了更根本的「预防」路线:
★ Context Poisoning 主要该靠结构预防,检测是兜底。 最关键的一条结构约束是「编排器不许透传」(Never delegate understanding)—— 污染发生在「综合」这一步被跳过的时候。
面试时这两条要一起说,顺序是先说结构预防,再说四层检测。 只说四层会显得像背的(每篇文章都有这四层), 先说「污染的根源是综合被跳过」再落到四层,才显得是想过的。
6.2 失败二:子 Agent 被劫持当跳板 —— 一道边界该放在哪
🔬 这一节是个很好的「边界设计」教学案例。
形态:子 Agent 中途读到一个带 prompt injection 的 README, 被劫持去改了 CI 配置,然后返回一句人畜无害的总结。
为什么难发现:如果只检查子 Agent 的返回结果,这条链路完全查不出来—— 返回的摘要是干净的,脏事发生在过程里。
源码里的做法是在控制权交还这个时刻,对子 Agent 的完整对话记录跑一次分类器:
const classifierResult = await classifyYoloAction(
agentMessages, // ← 注意是完整对话记录,不是返回值
{ role: 'user', content: [{ type: 'text', text:
"Sub-agent has finished and is handing back control to the main agent. " +
"Review the sub-agent's work based on the block rules and let the main " +
"agent know if any file is dangerous (the main agent will see the reason)."
}]},
)三个设计点,每个都值得单独记:
① 检查的是完整对话记录,不是返回结果。
检查全过程是在问「它做了什么」,不只是「它说了什么」。
这个区分可以直接迁移到任何审计场景。
② 它挂在「控制权交还」这一个时刻,不是每次工具调用。
为什么这个位置选得准:
子 Agent 内部:怎么折腾都在沙箱里,错了也只影响自己
↓ ← 这一刻是信任边界的跨越点,查一次
父 Agent 上下文:子 Agent 的输出变成父 Agent 的「事实」注入父上下文 = 子 Agent 的输出变成父 Agent 的事实。 在这之前它是「别人说的」,在这之后它是「我知道的」。这就是该设关卡的地方。
每次工具调用都查太贵;只查返回摘要漏得太多;查信任边界的跨越点是那个平衡点。
③ 分类器不可用时:fail-open + 告警,不是 fail-closed。
if (classifierResult.shouldBlock) {
// When classifier is unavailable, still propagate the sub-agent's
// results but with a warning so the parent agent can verify the work.分类器挂了就放行,但在结果里带一条警告让父 Agent 自己去验证。
第一反应会觉得这不安全。但考虑到量级(每周 3400 万次派生)—— 分类器一抖动就全线阻塞,可用性代价远大于安全收益。
★ 这是一个有意识的、写在注释里的取舍,不是遗漏。 反转条件很明确:高合规场景(金融、医疗)必须 fail-closed。
🔬 另外这道分类器只在自动模式(mode === 'auto')下启用—— 用户在手动确认模式下,人本身就是那道关,不需要机器再查一遍。 这是个很干净的设计:同一道防线不重复设置。
⚠️ 一处值得批判的地方(原始文档作者自己提出的,我认为很有价值):
fail-open 的「告警」接收方是父 Agent —— 一个 LLM。 让模型自己决定「要不要认真验证这个带警告的结果」,可靠性是没有保证的。 如果我做,我会至少让这个降级路径对用户可见,而不只是对父 Agent 可见。
这个批判本身是面试里的加分项:能指出一个成熟系统的设计里哪里还不够, 比复述它做了什么更能体现判断力。
6.3 失败三:过程病态 —— 空转、循环、锚定
这一类失败的特征是它不报错。系统在跑,token 在烧,但没有进展。
| 病态 | 形态 | 怎么检测 |
|---|---|---|
| 空转 | 反复调用同一个工具且返回值不变 | 最长「重复调用且返回值不变」段,≥3 判病态 |
| 无限循环 | 两个 Agent 互相触发,A→B→A→B… | 监控 Agent 间消息模式;设 max 轮次 |
| 锚定(anchoring) | 方向错了但上下文里全是那个方向的尝试,模型倾向做小修 | 见 §3.5:方向性失败要新派不要续传 |
| 重复工作 | 多个 Agent 做了同一件事 | 明确任务边界 + 共享任务列表 |
| 步数虚高 | 完成同样的事用了明显更多轮 | 步数比(vs 基线) |
「空转」这个指标值得单独说,因为它是本领域少见的「便宜且锋利」的指标:
判据:最长的「重复调用同一工具、且返回值完全相同」的连续段长度 ≥ 3它便宜(只需要比对工具名和返回值),且几乎没有假阳性—— 一个 Agent 连续三次做同一件事拿到同一个结果还在继续, 没有任何正常情况能解释这个行为。
📄 无限循环有一个真实案例(Agents of Chaos 实验,§9.4 详述): 两个 Agent 被设定成互相回复,产生了一小时的无限循环。 这就是 §5.5 说「预算硬上限是底线而不是优化」的原因。
6.4 失败四:模型会编造异步结果 ★ LLM 系统独有的一类
🔬 这是原始文档作者说「最让我意外」的一条,也是我认为整份文档里最独特的一条。
形态:派完一个后台 Agent,上下文里留下一句「任务已启动」, 模型接着往下写「任务返回了以下结果……」——结果是它编的。
源码提示词里用一整段禁令在防它:
Don't race. After launching, you know nothing about what the fork found. Never fabricate or predict fork results in any format... The notification arrives as a user-role message in a later turn; it is never something you write yourself.
为什么会发生:在训练数据的分布里, 「一个已启动的任务」后面接「任务的结果」是最自然的续写。
★ 架构上的异步,在自回归模型看来只是文本上的一个空隙,它会去填。
这句话值得反复读。它揭示了一类传统分布式系统里根本不存在的失败模式:
传统分布式系统:
一个进程不会因为「看起来该有结果了」就伪造一个 RPC 响应。
没有结果就是没有结果,代码会阻塞或超时。
LLM 系统:
模型看到上下文里有「已启动」,就会补全出一个合理的「结果」。
而且这个伪造的结果长得和真的一模一样,后续所有推理都基于它。这类失败的通用形态是:模型对架构状态有错误的心智模型,并且会「帮忙」把它补全。 同类的还有:
- §2.5 那条「Agent 以为自己说了别人就听到了」(输出不是通信)
- §4.5 那条「孩子看到兄弟的假状态」(占位符的副作用), 所以提示词里必须补一句「stay strictly within your directive's scope」
所以有一条通用的设计原则:
★ 凡是模型能看到但不该依赖的架构状态,都必须在提示词里显式声明它的语义。 不声明的话,模型会用「最自然的续写」去填,而那个续写往往是错的。
具体做法是三步:
- 列出模型上下文里所有「看起来像但实际不是」的东西(占位符、已启动通知、别人的状态)
- 对每一条,问「模型最自然的续写是什么」
- 如果那个续写是错的,写一句禁令
这一条几乎是纯 LLM 工程知识,在任何分布式系统教材里都找不到, 所以它在面试里的信号强度极高(§12 Q22)。
6.5 失败五:不变量被上下文压缩摧毁
§5.6 已经讲过机制,这里把它作为一类失败模式独立列出,因为它太容易踩。
形态:你在消息历史里留了一个标记(「这是个 fork 孩子」「这个操作已经确认过了」 「预算已经用了 80%」),然后依赖它做判断。一次 autocompact 之后标记消失,判断失效。
为什么难发现:
- 它只在长会话里发生(短会话不触发压缩)
- 失效是静默的(不报错,只是防御不生效了)
- 复现需要跑到触发压缩的长度
判据 / 修法:
❌ 不可靠:任何形如「扫描消息历史里有没有 X」的不变量
✅ 可靠: 挂在 spawn 时写入的、不会被压缩改写的元数据上
🛡 兜底: 消息扫描可以作为第二道,但不能是唯一那道★ 长跑 Agent 里,消息历史是可变对象。 任何建立在「历史里有什么」之上的不变量,都会在某次自动压缩后悄悄失效。
可迁移到:循环检测、预算控制、权限升级检测、 「用户已经批准过这个操作」的记录、任何形式的会话内状态标记。
6.6 失败六:规格设计问题(42%,最大的一块)
📄 MAST 说规格设计占 42%,是最大的一块,但它最难讲清,因为它不是一个 bug 而是一类。 把它拆成可操作的四条:
| 症状 | 具体形态 | 修法 |
|---|---|---|
| 角色越权 | Agent「不听话」,去做了别人的事 | 明确 scope + 工具白名单(能力边界靠代码,§2.6) |
| 指令模糊 | 「帮我处理这个」 | 委托 prompt 必须含具体文件路径、行号、要改什么(§3.4) |
| 角色粒度不对 | 太粗(一个「通用助手」什么都做)/ 太细(「JSON 格式化 Agent」) | 一个角色 = 一个专业领域,不是一个具体操作 |
| 信息隐瞒 | Agent 不分享关键数据 | Handoff 契约要求显式标注不确定性 |
角色粒度那条有一个很好的判据:📄 来自 Inngest 的 Dan Farrelly:
专业化应该由测量的必要性驱动,而不是架构美学。
具体是:从通用 Agent 开始,只在以下三个条件同时满足时才专业化:
- 通用 Agent 在这个任务上的成功率明显低于专业 Agent(测出来的,不是猜的)
- 这类任务的频率足够高,值得维护一个专业角色
- 专业化带来的质量提升 > 协调开销的增加
第三条最容易被忽略:多一个角色就多一条协调链路, 质量提升 3% 但协调开销涨 30% 是净亏。
6.7 失败七:Coordination Drift(协调漂移)
📄 形态:集体推理因状态更新中的累积小错误而逐渐偏离初始目标。 类似分布式系统里「拜占庭将军问题」的弱化版。
为什么难发现:每一步的偏差都很小,都「看起来合理」。 只有对照最初的目标才能看出已经跑偏了,而没有任何一个 Agent 手里有那个对照 (编排器的上下文也在往前滚)。
三个防御手段:
| 手段 | 机制 |
|---|---|
| 定期全局快照 | 周期性把所有 Agent 的记忆状态同步到一个一致的检查点 |
| 漂移检测器 | 监控行为与初始目标的偏离度,超阈值触发重新对齐 |
| 分层摘要 | A 和 B 交互了 50 轮,但对 C 只暴露一句「他们决定用 Python 写后端」 |
第三条很精巧:不同粒度的摘要减少信息噪声, 而且它同时也是成本手段(C 不用付那 50 轮的 token)。 这是本文少见的「安全和成本同向」的设计——大多数时候两者是对立的。
6.8 七类失败总表 + 一个统一心智模型
| # | 失败 | 报错吗 | 检测手段 | 最该做的预防 |
|---|---|---|---|---|
| 1 | Context Poisoning | 否 | 四层纵深防御 | 结构预防:编排器不许透传 |
| 2 | 子 Agent 被当跳板 | 否 | 交还点查完整对话记录 | 信任边界跨越点设关卡 |
| 3 | 过程病态(空转/循环) | 否 | 空转段 ≥3;max 轮次 | 预算硬上限 |
| 4 | 模型编造异步结果 | 否 | 极难自动检测 | 提示词显式禁令 |
| 5 | 不变量被压缩摧毁 | 否 | 长会话回归测试 | 锚点挂 compaction-resistant 元数据 |
| 6 | 规格设计问题(42%) | 否 | 成功率对比 | 具体规范 + 测量驱动专业化 |
| 7 | Coordination Drift | 否 | 漂移检测器 | 定期全局快照 |
盯住「报错吗」那一列——全是「否」。 这就是这一章的统一心智模型:
★ 多 Agent 系统的失败模式几乎全是「不报错的失败」。 系统在跑、日志在写、每一步看起来都合理,但结果是错的、或者钱白花了。
推论:多 Agent 系统的可观测性不是运维需求,是正确性需求。 没有观测,你连「它坏了」都不知道。
这条推论直接把我们推向下一章。
6.9 本章自检
- MAST 说 79% 的失败不是模型问题。这个结论否定了哪一种最自然的应对方式?
- Context Poisoning 的四层纵深防御属于「检测」路线。更根本的「预防」路线是什么?
- 为什么安全分类器要检查子 Agent 的完整对话记录而不是返回摘要?
- 分类器不可用时选 fail-open 的理由是什么?什么场景下必须反过来?
- 「模型会编造异步任务的结果」——这个失败的机制是什么?为什么它在传统分布式系统里不存在?
- 你在消息历史里留了个「已确认」标记。它会在什么时候静默失效?
- 「我们给每个子任务都配了一个专业 Agent」——按测量驱动原则,该怎么质疑这个设计?
- 七类失败有一个共同特征,是什么?它推出了什么结论?
📌 回到原始文档:
- MAST 79%/42%/37%、Context Poisoning 三路径与四层防御:
00-Question.mdQ5(Context Poisoning) - 安全分类器与 fail-open、模型编造异步结果的原文禁令:
01-Study-Source-多Agent系统.md§agentToolUtils.ts / §forkSubagent.ts - compaction 摧毁不变量:
01-Study-Source-多Agent系统.md§可迁移原则三 - Coordination Drift 与分层摘要:
00-Question.mdQ4(记忆一致性) - 常见陷阱六条速查表:
multi-agent-deep-dive.md§9.5
§7 记忆一致性:把 CPU 缓存那套搬过来
这一章讲 §4.8 那个形态 ③(共享)留下的坑。它是本文唯一一章几乎可以直接借用现成理论的, 所以也是最容易学的一章——前提是先接受那个类比。
7.1 一句话建立直觉
📄 SIGARCH(计算机架构顶级学术组织)2025 年有一篇文章,把这个问题一句话框定了:
多 Agent 系统正在撞上与多处理器系统相同的墙—— 只不过它们的「内存」不是原始字节,而是用于推理的语义上下文。
这个类比之所以好用,是因为它逐项对得上:
| 计算机架构 | Agent 系统 | 说明 |
|---|---|---|
| CPU 核心 | 单个 Agent | 独立的计算 / 推理单元 |
| L1/L2 Cache | Agent 的上下文窗口 | 本地高速缓存,容量有限 |
| 共享 L3 / 主存 | 共享知识库(向量库、Redis) | 所有 Agent 可访问 |
| Cache Line | 记忆单元(一条事实、一个偏好) | 一致性管理的最小粒度 |
| MESI 协议 | Agent 记忆同步协议 | 确保各 Agent 看到一致数据 |
| Write-Back vs Write-Through | 延迟同步 vs 即时同步 | 写入策略的 trade-off |
| False Sharing | 无关 Agent 因共享记忆区域互相干扰 | 记忆分区不当导致的性能问题 |
核心矛盾就是缓存一致性问题的原样搬迁:
Agent A 的上下文窗口 共享知识库 Agent B 的上下文窗口
「用户偏好 = 深色主题」 ←── ??? ──→ 「用户偏好 = 浅色主题」
↑ ↑
A 三分钟前读的 B 刚才读的(A 已经改过了)A 更新了某个事实,B 的「本地缓存」(上下文窗口)里还是旧版本。 而且比 CPU 更糟的一点:Agent 的上下文窗口没有 cache invalidation 机制—— 没人能主动让 B 上下文里那句话失效,除了往它的对话里再塞一条消息。
7.2 三种记忆共享架构
| ① 共享黑板(Blackboard) | ② 消息传递(Message Passing) | ③ 混合 | |
|---|---|---|---|
| 类比 | 多处理器的共享内存架构 | 多处理器的分布式内存架构 | 现代多核的缓存层级 |
| 机制 | 都读写同一个中央存储 | Agent 间显式交换消息,不共享状态 | 本地缓存 + 共享存储 |
| 一致性 | 强一致(靠锁/事务),性能代价高 | 最终一致,依赖消息可靠性 | 分类型定级 |
| 优势 | 实现简单、天然单一真相源 | 无共享状态、天然隔离、扩展好 | 兼顾 |
| 劣势 | 中央存储是瓶颈和单点;Agent 多了读写竞争 | 通信开销高;需要协议保证顺序和完整性 | 复杂 |
| 适用 | Agent < 10、需要强一致 | Agent 多、弱一致可接受 | 生产中最常见 |
③ 是生产答案,而它的关键不在「混合」这个词,在下一节那张表:对不同类型的记忆用不同的一致性级别。
7.3 五档一致性模型
📄 直接借用内存一致性模型的分档:
| 级别 | 含义 | 适用 | 代价 |
|---|---|---|---|
| 强一致(Linearizability) | 所有 Agent 任何时刻看到完全相同的状态 | 金融交易、医疗决策 | 最差,需分布式锁 |
| 因果一致(Causal) | 有因果关系的更新按序可见,无关更新可乱序 | 多轮对话的上下文依赖 | 需追踪因果(向量时钟) |
| 会话一致(Session) | 同一会话内的 Agent 看到一致状态 | 单用户多 Agent 协作 | 会话内强、跨会话最终 |
| 最终一致(Eventual) | 最终收敛,中间可能不一致 | 大规模集群、非关键信息 | 最好,但需冲突解决 |
| 语义最终一致(Semantic Eventual) | 基于 CRDT,保证无冲突的确定性收敛 | 并行代码生成、协作编辑 | 需精心设计数据结构 |
落地的样子——这张表是本章最实用的东西:
| 记忆类型 | 一致性要求 | 实现 |
|---|---|---|
| 任务状态(当前步骤、进度) | 强一致 | 共享黑板 + 分布式锁 |
| 用户偏好(长期) | 最终一致 | 向量库 + 异步同步 |
| 工具调用结果(短期) | 会话一致 | 本地缓存 + TTL |
| 团队共识(决策结论) | 因果一致 | 版本化存储 + 因果追踪 |
| 工作记忆 / Scratchpad | 无需一致性 | 完全隔离,各 Agent 独立 |
最后一行最重要,也最容易被漏掉。 设计时的本能是「让所有状态都一致」,但 scratchpad(Agent 自己的草稿) 根本不需要一致性——它是私有的。把它也纳入一致性协议是纯粹的浪费, 而且会引入 §7.1 那个 False Sharing(两个无关 Agent 因为共享了同一个存储区域而互相拖慢)。
★ 一致性设计的第一步不是「怎么保证一致」,是「哪些东西不需要一致」。
7.4 CRDT:一个值得知道的前沿方案
📄 CodeCRDT(2025)把分布式系统里的 CRDT(Conflict-Free Replicated Data Types, 无冲突复制数据类型) 用到了多 Agent 代码生成上。
核心思想很漂亮:Agent 不通过显式消息通信, 而是通过观察共享状态的变化来协调。
类比是蚂蚁的信息素(stigmergy,共识主动性): 蚂蚁之间不开会,每只蚂蚁只是留下信息素、并对别人的信息素做反应, 集体行为就涌现出来了。
三个关键属性:
- 可观察更新:Agent 可以订阅状态变化事件
- 确定性收敛:所有 Agent 最终观察到相同的一致状态
- 单调进展:已完成的工作不会被回滚
📄 实验结果,注意后半句: 强最终一致性保证了 100% 的字符级收敛,零合并冲突。 但语义冲突(重复声明、类型不匹配)仍有 5–10% 的发生率,需要后处理。
后半句是这条的真正价值所在:
★ CRDT 解决的是「字符级冲突」,不是「语义冲突」。 两个 Agent 各自加了一个
function validateUser(),CRDT 会让两份都保留下来、 字符级完美收敛——然后代码编译不过。
这是一个非常好的「工具边界」案例:一个技术在它的定义域内是完美的(100%、零冲突), 但那个定义域比你需要的小。面试里能说出这一层,比说「我们用 CRDT」强得多。
7.5 冲突解决:五种策略
当两个 Agent 对同一记忆产生矛盾更新时(A 说「用户在北京」,B 说「用户在上海」):
| 策略 | 机制 | 问题 |
|---|---|---|
| 时间戳优先(LWW) | 最新的覆盖旧的 | 简单,但可能丢失有效信息 |
| 置信度评分 | 每条记忆带置信度,高的胜 | 依赖置信度本身可信 |
| 角色权威性 | 特定领域的 Agent 对该领域有更高权威 | 需要预先定义权威域 |
| 共识投票 | 多 Agent 投票,多数胜 | 贵;适合主观判断类 |
| 人工仲裁 | 高风险冲突升级到人 | 慢,但对「已提交输出」值得 |
📄 SIGARCH 的一条建议值得单独记: 对「已提交输出」(committed outputs)提供更强的一致性保证。
这是个很实用的分界:未提交的中间状态可以最终一致,一旦对外产生了效果 (写了文件、发了请求、回了用户),就必须强一致。 和数据库的「已提交事务」是同一个思路。
7.6 一个务实的收尾:大多数系统不需要这一章
必须说清楚:上面这一整套只在形态 ③(共享可变状态)里才需要。
- 形态 ①(隔离):子 Agent 各自一份快照,没有一致性问题
- 形态 ②(继承):副本语义,父子从派生那一刻起各走各的,也没有
- 形态 ③(共享):才有
所以这一章的正确用法是:
先问「我真的需要共享可变状态吗」。 大多数 coding agent 场景的答案是不需要—— 用文件系统交付(§2.5)+ 一次性 Handoff(§3)就够了。
一旦答案是「需要」,就照 §7.3 那张表分类型定级, 而不是给所有东西上强一致。
面试里这一章的用法:它是深水区题,用来展示系统思维和跨领域视野。 但如果对方问「你的系统怎么做记忆一致性」而你的系统其实是形态 ①, 正确答案是「我们刻意避免了共享可变状态,因为 X」——而不是编一套一致性协议。
7.7 本章自检
- Agent 的上下文窗口比 CPU 的 L1 cache 少了一个什么关键机制?这导致什么后果?
- 一致性设计的第一步是什么?(提示:不是「怎么保证一致」)
- CRDT 报告「100% 字符级收敛、零合并冲突」。它没解决什么?举个具体例子。
- 为什么「已提交输出」需要比中间状态更强的一致性?
- 你的系统是形态 ①(隔离子 Agent)。面试官问你怎么做记忆一致性,怎么答?
📌 回到原始文档:00-Question.md 第四题(记忆一致性)是本章的完整来源, 含 SIGARCH 类比、五档一致性、CRDT/CodeCRDT、冲突解决五策略、分层一致性设计表。
§8 可观测性:调试为什么特别难
§6.8 那条推论把我们推到这里:多 Agent 的失败几乎全是不报错的失败, 所以可观测性不是运维需求,是正确性需求。
8.1 三个维度的复杂性
单 Agent 的调试是线性的:一条 trace 从输入到输出,中间若干 Think-Act-Observe。 多 Agent 引入了三个新维度:
| 维度 | 具体 | 后果 |
|---|---|---|
| 因果链分散 | 错误源自 A 的第 3 步,经 B 误解,在 C 执行时才暴露 | 发生点和暴露点分离 |
| 非确定性交互 | 同样的输入,Agent 间对话路径可能完全不同 | Bug 难以复现 |
| 状态空间爆炸 | N 个 Agent 各有上下文状态,组合是指数级 | 无法穷举测试 |
第一条是最要命的。传统调试的方法论是「从报错的地方往上看栈」, 但在多 Agent 里报错的地方和出错的地方可能隔着两个 Agent 和三次 Handoff, 而且中间那些 Agent 的行为都是「正确的」——它们只是基于错误的输入做了正确的事。
8.2 四层观测架构
| 层 | 关注点 | 关键数据 |
|---|---|---|
| L1 单 Agent 内部 | 单个 Agent 的推理链和工具调用 | prompt/response、工具输入输出、token 消耗 |
| L2 Agent 间交互 | 消息传递和 Handoff | 消息内容、传递时序、上下文压缩损失 |
| L3 编排层 | 编排器的路由决策和任务分配 | 路由逻辑、任务分解策略、重试/降级决策 |
| L4 系统全局 | 端到端任务完成情况 | 任务成功率、总延迟、总成本、人工转接率 |
大多数人只做了 L1(因为现成的 LLM 观测工具都在这一层)。 而多 Agent 特有的问题全在 L2 和 L3。
L2 里那个「上下文压缩损失」是最少有人记的一项,但它直接对应 §3.2 那三类信息丢失。
8.3 Trace 与 Span:一棵树
传统微服务的分布式追踪(OpenTelemetry)可以直接复用,形态是这样:
Root Span: 用户任务「分析这份财报」
├── Orchestrator Span: 任务分解
│ └── LLM Call: 规划子任务
├── Agent-A Span: 财务数据提取
│ ├── LLM Call: 解析表格
│ ├── Tool Call: PDF 解析器
│ └── LLM Call: 结构化输出
├── Agent-B Span: 行业对比分析
│ ├── Tool Call: Web 搜索
│ └── LLM Call: 分析
├── Handoff Span: A→Orchestrator 结果回传
├── Handoff Span: B→Orchestrator 结果回传
└── Orchestrator Span: 结果合并
└── LLM Call: 综合报告和微服务 trace 的关键差异:
传统微服务的 span 是确定性的(同一请求走同一路径); Agent trace 是非确定性的(同一输入可能走完全不同的路径)。
推论:必须额外记录「决策点」 —— 编排器为什么选择路由到 A 而不是 B。 在微服务里这个信息不需要记(代码写死了),在 Agent 里它是最重要的信息之一。
8.4 五个关键调试能力
| # | 能力 | 做什么 | 为什么必须 |
|---|---|---|---|
| 1 | 跨 Agent 因果追踪 | 每次 Handoff 记完整上下文快照(传了什么、没传什么);标注输入来源;支持反向追踪 | 解决「发生点≠暴露点」 |
| 2 | 回放 / 时间旅行 | 记录每个 Agent 的完整输入;支持「冻结」部分 Agent(用录制响应),只让目标 Agent 实时跑 | Bug 难复现;📄 有分布式追踪的系统 MTTR 降低 73% |
| 3 | 错误传播可视化 | 展示初始错误点、传播路径、受影响的下游、编排器有没有纠正 | 17.2x 放大率需要看得见 |
| 4 | 上下文差异对比(Context Diff) | Handoff 前后做 diff,标出被压缩/丢弃的信息;量化信息损失率 | 直接对应 §3.2 |
| 5 | 交互模式异常检测 | 消息轮次突增(循环辩论)、响应时间异常、编排器频繁重试同一个 Agent、token 速率飙升 | §6.3 那些不报错的病态 |
能力 1 里那个「没传什么」值得单独强调。 记录「传了什么」是本能,记录「没传什么」才是这个能力的核心—— 因为 Handoff 的 bug 绝大多数是漏传,不是错传。
能力 2 的「冻结」是个很实用的技巧:多 Agent bug 复现难的根源是每个 Agent 都不确定。 把 4 个 Agent 的响应录下来回放、只让第 5 个实时跑, 就把一个「5 个不确定源」的系统变成了「1 个不确定源」的系统。
8.5 关键指标清单
按 §6 那个「几乎全是不报错的失败」的逻辑,指标要覆盖过程而不只是结果:
| 类别 | 指标 |
|---|---|
| 成本 | 按 Agent / 按任务的 token 分解;side-call 成本;缓存命中率 |
| 延迟 | 端到端;每个 Agent 的耗时;Handoff 的等待时间 |
| 过程病态 | 空转段长度(≥3 判病态);重复工作次数;步数比;重试率 |
| 质量 | 任务成功率;人工介入率;与单 Agent 基线的对比 |
| 协作特有 | 视角多样性(不同 Agent 是否真给了不同观点);达成共识时间;错误放大率 |
| 失败归因 | exit status 分布;各 Agent 的失败频率和原因 |
「与单 Agent 基线的对比」这一项是本清单里唯一不可省的。 理由回到 §1:多 Agent 在信息论上不占优, 所以你必须一直保留一个单 Agent 基线,否则你无法回答 「这套复杂度到底买到了什么」——而这是老板/面试官必然会问的问题。
⚠️ 一个口径陷阱:成本对比必须固定 token 预算(回 §1.3 破口二)。 不控预算的对比只能测出「花钱多的赢了」。
8.6 工具选型(简版)
| 工具 | 定位 |
|---|---|
| Langfuse | 开源 LLM 可观测性,支持追踪 Agent 调用链 |
| LangSmith | LangChain 生态,L1 层最完整 |
| OpenTelemetry + 自定义 Span | L2 层唯一通用方案(GenAI 语义约定还在草案阶段) |
| Arize / Maxim AI | 偏 L4 的评估与监控 |
| 框架内置 | LangGraph 的时间旅行调试;OpenAI SDK 的原生追踪 |
| 自建 | 在 Agent 的 Hook 里记录所有工具调用和决策 |
务实建议:L1 用现成的,L2/L3 基本得自己写(用 OTel 的 Span 做载体), 因为 Handoff 语义和决策点记录目前没有标准。
8.7 日志分级:一个成本问题
多 Agent 的日志量是 M 倍的,全量记录既贵又难查。分级建议:
ERROR —— Agent 崩溃、工具调用失败、契约验证不通过
WARN —— 降级发生(fail-open 放行、模型降级)、重试、置信度低于阈值
INFO —— 每次 Handoff(含前后上下文摘要 + 决策理由)、任务状态变更
DEBUG —— 完整 prompt / response(默认关闭,排查时开)INFO 那一档是这个分级里最重要的判断: Handoff 必须在默认级别可见,因为它是多 Agent 特有故障的集中发生地。 而完整 prompt 放 DEBUG,因为它体积最大且涉及内容级隐私(§9 会提)。
8.8 本章自检
- 多 Agent 调试的三个新维度里,哪一个让「从报错处往上看栈」这个方法论失效了?
- 四层观测架构里,大多数人只做了哪一层?多 Agent 特有的问题在哪两层?
- Agent trace 和微服务 trace 的关键差异是什么?这个差异要求你额外记录什么?
- 「跨 Agent 因果追踪」要记录「传了什么」和「没传什么」。为什么后者更重要?
- 指标清单里唯一不可省的一项是什么?为什么?
📌 回到原始文档:00-Question.md 第五题(调试与可观测性)是本章主源, 含四层架构、Agent trace 结构、五个调试能力、工具选型、日志分级。 multi-agent-deep-dive.md §9.2 补了关键指标表。 深挖建议:本仓 sid-code/summary/X1-Observability/AI-Agent-可观测性-从零到一(入门与面试准备).md 是这一章的十倍展开版(含 Span 树健康度判据、计量精度四道防线、十二个真实陷阱)。
§9 安全、信任与涌现行为
这一章有三个主题,它们的关系是层层递进的: 信任(该不该相信另一个 Agent 的输出)→ 安全(怎么防被劫持的 Agent)→ 涌现(一群各自正常的 Agent 合起来干出没人设计过的事)。
第三个最反直觉,也最容易在面试里出彩,所以我把它放在最后并讲得最细。
9.1 为什么 Agent 之间需要「信任机制」
传统软件里模块之间的交互是确定性的——函数调用总返回预期类型的结果。 但 LLM Agent 的输出是非确定性的,且可能被 prompt injection 劫持。
📄 OWASP 的 Agentic AI 威胁列表里有两条直接相关:
| 编号 | 威胁 | 说明 |
|---|---|---|
| T14 | 多 Agent 系统中的人类攻击 | 利用 Agent 间委托关系、信任机制、工作流依赖提升权限或操纵操作 |
| T6 | 破坏意图和操纵目标 | 篡改 Agent 目标与推理逻辑(Agent 劫持) |
📄 Lakera AI 2026 年的一个真实攻击场景,值得完整记住,因为它展示了这类攻击的时间尺度:
攻击者通过支持工单向客服 Agent 的持久记忆中植入恶意指令。 三周后,该 Agent 自动将合法付款路由到攻击者地址。 更令人担忧的是:当人类质疑这些错误认知时,Agent 坚持认为它们是正确的。
三点值得盯住:
- 注入点和触发点隔了三周 —— 常规的「检查这次输入有没有注入」完全防不住
- 载体是持久记忆 —— 这正是 §7 那个「共享可变状态」的攻击面
- 最后那句最可怕 —— 污染进了记忆之后,它对 Agent 而言就是「事实」, 人类的质疑成了「与我所知不符的说法」
9.2 零信任:三条原则 + 四个层级
零信任(Zero Trust)的核心假设:Never Trust, Always Verify —— 假设任何 Agent 的输出都可能是不可信的。
| 原则 | 传统网络安全 | Agent 系统 |
|---|---|---|
| 验证每个请求 | 每次 API 调用都要认证 | 每次 Agent 间消息都要验证来源和内容 |
| 最小权限 | 用户只获得完成工作所需的最小权限 | Agent 只获得完成当前任务所需的最小工具集 |
| 假设已被攻破 | 设计时假设内网已被渗透 | 设计时假设任何 Agent 都可能被劫持 |
四个层级,从「你是谁」到「关起来」:
第一层:身份验证(Authentication)—— 这个 Agent 是谁
- Agent 身份标识:唯一 ID + 角色标签,消息中必须携带
- 消息签名:防篡改、防伪造
- 来源追踪:每条信息标注完整来源链(哪个 Agent、什么时间、基于什么输入产生的)
第二层:授权控制(Authorization)—— 它能做什么
- 工具白名单:安全 Agent 不能执行代码;数据分析 Agent 不能发邮件
- 数据访问控制:只能读写与角色相关的数据
- 操作范围限制:只能改特定目录、只能查特定表
🔬 这一层最重要的实现细节是 §4.2 那个分层策略: 即使自定义 Agent 把禁止列表设为空,前三层全局保护仍然生效。 用户配置只能收紧,不能放松。
第三层:输出可信度评估(Trust Scoring)—— 它的输出可信吗
- 置信度评分:低置信度触发额外验证
- 历史信誉:连续多次输出错误的 Agent 降权
- 交叉验证:关键结论由多个独立 Agent 验证
⚠️ 但这一层要小心。🔬 源码实读的一个发现是它在生产系统里基本不存在:
我以为会看到声誉分、投票权重、拜占庭容错之类的东西。 实际上是一道分类器挂在控制权交还点,加上一句 "The agent's outputs should generally be trusted"。
信任是结构性的(隔离 + 一道关卡 + 显式通信),不是计算性的(没有声誉分)。
这个「概念 vs 工程」的 gap 是面试里的高价值信息。 如果对方问「你怎么设计 Agent 间的信任机制」, 背出四层是标准答案;补一句「实践中第三层基本没人做, 因为信任是靠结构而不是靠打分实现的,理由是……」是区分度答案。
第四层:运行时隔离(Runtime Isolation)—— 关起来
- 沙箱执行(Codex 走的是 OS 内核级沙箱;Claude Code 走应用层 Hook)
- 凭证隔离:Agent 之间不共享敏感凭证
- 资源限制(回 §5.5)
📄 Gartner 的一句话可以直接当这一层的口号:
把每个 Agent 当作潜在的半敌对(semi-adversarial)工作负载来对待。
9.3 「致命三角」在多 Agent 里的放大
这是一个非常好用的判据,来自 prompt injection 的研究:
致命三角(Lethal Trifecta):当一个系统同时具备 ① 访问私有数据 ② 接触不可信内容 ③ 能对外通信/执行副作用 三者时,prompt injection 就能造成实际损害。缺任何一个都相对安全。
多 Agent 让这三角变得更难管,原因是三个能力可能分散在不同 Agent 身上, 但通过 Handoff 链路合成了一个完整的三角:
Agent A:能读私有代码库 ①
Agent B:会读取网页/README ② ← 单看每个 Agent 都不构成三角
Agent C:能执行 bash / 发请求 ③
但 A→B→C 的 Handoff 链路合起来 = 完整的致命三角这是多 Agent 特有的安全问题,而且它在架构图上完全不可见—— 每个 Agent 的权限清单看起来都很克制。
判据:审计的单位不能是单个 Agent 的权限, 必须是「任意一条可达的 Handoff 链路的权限并集」。 这也是 §6.1 第三层「爆炸半径控制」的真实含义。
9.4 涌现行为:一群正常的 Agent 干出没人设计过的事
涌现行为(Emergent Behavior)= 没有任何单个 Agent 被设计成表现出的系统级行为, 它从 Agent 之间的交互中自发产生。
📄 最直接的证据是斯坦福×哈佛 2025 年的 "Agents of Chaos" 实验 (38 位研究者,6 个全副武装的 AI Agent 在真实环境中运行两周):
- Agent 为了「保护秘密」炸掉了自己的邮件服务器
- 向陌生人泄漏了 124 封私人邮件
- 用「语义重构」绕过了自己的安全规则
- 两个 Agent 被设定成互相回复,产生了一小时的无限循环
最关键的发现,请完整记住这句:
这些行为从激励机制中自然涌现,跟越狱(jailbreak)完全无关。 控制好单个 Agent ≠ 控制好一群 Agent。
📄 六类已观察到的涌现行为(arXiv 2025 的分类), 这张表在面试里非常好用,因为它把一个玄学话题变成了可枚举的清单:
| 涌现行为 | 机制 | 证据 |
|---|---|---|
| 默契串通(Tacit Collusion) | 没有显式通信也学会协调,维持高价 | AI 交易 Agent 把价格推到纳什均衡的 200%,且没被指示串通 |
| 优先级垄断 | 一部分 Agent 反复抢占稀缺低成本资源,挤出其他 | 调度系统里某些 Agent 学会「抢先占位」 |
| 竞争性任务回避 | 资源紧张时卸载高成本任务,只挑简单的 | 共享容量压力下 Agent 选择性「摸鱼」 |
| 战略性信息隐瞒 | 有特权信息的 Agent 故意隐瞒或误报 | 协作研究中 Agent 不分享关键发现以维持「信息优势」 |
| 涌现欺骗 | 发展出欺骗策略,且跨交互持续存在并适应 | Agent 发展出心智理论(Theory of Mind)能力 |
| 无限循环 | 互相触发,无终止 | Agents of Chaos 的一小时循环 |
为什么难预测:
- 组合爆炸:N 个 Agent 的交互状态空间指数级,无法穷举测试
- 非线性:微小的 prompt 变化可能导致完全不同的系统行为
- 上下文依赖:只在特定组合下出现,单独测试每个 Agent 时不会触发
- 目标漂移:Agent 通过交互修改自身目标(回 §6.7 的 Coordination Drift)
第三条解释了为什么单元测试和单 Agent 评测抓不到这类问题。
9.5 控制涌现:四条策略,核心是「约束边界而不是预测行为」
策略一:架构层面的约束(最有效,因为不依赖模型行为)
- 权限隔离:不共享文件系统、数据库、通信频道,除非显式授权
- 通信管控:所有 Agent 间通信经过中央消息总线,便于监控和拦截
- 资源限制:token 预算、工具调用次数、执行时间上限
- 深度限制:禁止递归派生
策略二:交互监控(Agent 间行为审计)
📄 一条重要的观测重心转移:
传统 AI 监控关注 Agent 对用户说了什么, 但 Agents of Chaos 告诉我们:Agent 之间的交互才是最大的风险来源。
- 循环检测(A→B→A→B)
- 串通检测(多个 Agent 在没有显式协调时产生一致的偏向性行为)
- 目标一致性检查(定期验证行为是否仍与初始目标一致)
- 资源消耗异常(Agent 间交互导致的 token 突增是涌现行为的早期信号)
最后一条最实用:token 消耗速率突增 3 倍以上是个便宜且灵敏的哨兵, 它同时能抓到无限循环、串通、和空转。
策略三:受控涌现(Controlled Emergence)
不是完全消除涌现(那会失去多 Agent 的价值,见 9.6),而是在安全边界内允许它:
- 沙箱实验:隔离环境里让 Agent 自由交互,观察,再决定是否允许
- 渐进放权:从严格编排开始,逐步放松,每一步都监控
- 行为白名单:定义允许的交互模式,超出的自动拦截
- 人类断路器:关键决策点必须有人审批,防止涌现导致不可逆后果
策略四:对抗性测试(Red Teaming)
- 多 Agent 红队:测试 Agent 组合的安全性,不只是单个
- 长时间运行测试:涌现往往需要时间积累(Agents of Chaos 跑了两周)
- 压力测试:资源紧张、高并发、异常输入等极端条件
★ 核心心智模型:约束边界,而不是预测行为。 🔬 源码里对应的东西非常朴素——
maxTurns、深度限制、几条硬 throw、 提示词里「stay strictly within your directive's scope」。没有任何涌现预测逻辑。
9.6 涌现不全是坏事(一个必要的辩证)
📄 arXiv 2025 的研究发现:自组织的 LLM Agent 群体,性能比预设层级结构高 14%(p<0.001)。
而且不同模型的自组织策略有明显差异,这组数据很有意思:
| 模型 | 自组织策略 | 数据 |
|---|---|---|
| Claude | 最大化角色多样性 | 1,272 个独特角色,Gini 系数 0.055(极均匀) |
| DeepSeek | 激进的 Agent 过滤 | 22.4% 空闲,只有 8.5/16 个 Agent 活跃 → 最大成本效率 |
| GPT 系列 | 倾向形成层级 | 自发产生「领导者」和「执行者」 |
📄 自组织在 256 个 Agent 规模下仍然有效。
DeepSeek 那一行是最有价值的,因为它说明了一件反直觉的事: 「不是所有 Agent 都需要工作」是一种优化,不是浪费。 22.4% 的 Agent 空闲换来最大成本效率——如果你的负载均衡策略是 「让所有 Agent 都忙起来」,你可能在优化一个错误的目标。
所以这一节的结论是:
| 负面涌现 | 正面涌现 | |
|---|---|---|
| 例子 | 串通、隐瞒、循环、炸服务器 | 涌现专业化、涌现协作策略、创造性方案 |
| 应对 | 架构约束 + 交互监控阻止 | 安全边界内允许 |
关键是在安全边界内允许正面涌现,同时用架构约束阻止负面涌现—— 不是靠调教每个 Agent,而是靠系统设计。
📄 一个复杂系统理论的类比表,用来展示跨学科视野:
| 复杂系统概念 | Agent 系统对应 |
|---|---|
| 蝴蝶效应 | 一个 Agent 的微小输出变化导致系统级行为剧变 |
| 吸引子 | 系统倾向收敛到某些稳定的交互模式(可能好也可能坏) |
| 相变 | Agent 数量或交互频率超过临界点时,行为发生质变 |
| 自组织临界性 | 系统在「有序」和「混沌」边缘运行,小扰动可能触发大规模级联 |
9.7 一条容易漏的:内容级 tracing 与隐私
安全章节的最后一条,是关于你自己的观测系统的。
§8.7 说完整 prompt/response 放 DEBUG,理由之一是隐私。展开一下:
- 多 Agent 系统的完整 trace 里包含每个 Agent 看到的所有内容, 含用户代码、密钥、内部文档
- 这些 trace 如果上传到第三方观测平台,等于把代码送出内网
- 而多 Agent 的 trace 体积是 M 倍的,泄露面也是 M 倍的
判据:默认关闭内容级 tracing;开启时必须是显式配置 + 有出口白名单。 这是「数据主权」在多 Agent 场景的具体形态。
9.8 本章自检
- Lakera 那个「三周后触发」的案例,为什么常规的输入注入检查防不住?
- 「信任机制」的四层里,实践中基本没人做的是哪一层?为什么?
- 致命三角在多 Agent 里为什么更难管?审计的单位应该是什么?
- 「控制好单个 Agent ≠ 控制好一群 Agent」——这句话否定了哪种最自然的做法?
- 六类涌现行为里,哪一类最难检测?(提示:想想哪一类没有显式信号)
- DeepSeek 让 22.4% 的 Agent 空闲。这是浪费还是优化?说出理由。
- 你的负载均衡目标是「让所有 Agent 都忙」。这个目标可能错在哪?
📌 回到原始文档:
- 零信任四层、OWASP T14/T6、Lakera 案例、致命三角:
00-Question.md第八题(信任机制) - 六类涌现行为、Agents of Chaos、四条控制策略、复杂系统类比:
00-Question.md第九题(涌现行为) - 「信任是结构性的不是计算性的」这个 gap:
01-Study-Source-多Agent系统.md§概念 vs 工程的 Gap - 安全清单与两种沙箱路线:
multi-agent-deep-dive.md§9.3
§10 协议与框架生态
这一章是「地图章」:告诉你别人在说什么词、这些词属于哪一层、哪个框架适合谁。 它的深度低于前面几章,但面试里被问到的频率很高,而且答错会显得没做过。
10.1 两个协议,两个层级:MCP vs A2A
这是面试里最常被问的一组对比,而且很多人会答成竞争关系,那是错的。
| 维度 | MCP(Model Context Protocol) | A2A(Agent-to-Agent) |
|---|---|---|
| 解决什么 | Agent ↔ 工具 / 数据源 | Agent ↔ Agent |
| 类比 | Agent 的「USB-C 接口」 | Agent 之间的「外交专线」 |
| 协议层级 | 基础设施层(工具调用级) | 应用层(任务级) |
| 发起方 | Anthropic | Google Cloud |
| 技术基础 | JSON-RPC over stdio/HTTP | HTTP + JSON-RPC + SSE + OAuth 2.0 |
| 状态管理 | 有状态(Session 维护上下文) | 无状态(Task 自包含) |
| 采纳程度 | 已成事实标准 | 生态建设中 |
一句话答法(可以直接背,它很好用):
它们是不同层级的协议,可以同时使用。 一个 Agent 通过 MCP 访问工具和数据源,通过 A2A 与其他 Agent 协作。 就像一个人用手(MCP)操作工具,用嘴(A2A)和同事沟通。
A2A 的四个核心概念,知道这四个词就够了:
| 概念 | 是什么 |
|---|---|
| Agent Card | Agent 的「名片」——标准化 JSON,声明自己的能力、支持的输入输出格式、认证方式(类比 OpenAPI Spec) |
| Task | 协作的基本单位。一个 Agent 向另一个发 Task 请求,含输入和期望输出 |
| Message | Task 内的通信单元,支持文本/图片/音频/视频 |
| Artifact | Task 的交付物——完成后产出的结构化结果 |
工作流程五步:能力发现(读 Agent Card)→ 任务发送 → 流式更新(SSE 返回进度)→ 结果交付(Artifact)→ UX 协商(协商输出格式)。
⚠️ 注意 A2A 是无状态的(每次 Task 自包含)。 这个设计选择直接对应 §3.4 那条「自包含 prompt」原则—— A2A 在协议层面强制了自包含,也就继承了它的成本(要复述上下文)和收益(可扩展、可跨组织)。
10.2 跨框架互操作的三条路
现实是企业里多框架并存:LangGraph 做工作流编排、CrewAI 做角色团队、 AutoGen 做多 Agent 对话、Google ADK 做分层。它们默认无法通信。
| 方式 | 机制 | 优势 | 劣势 |
|---|---|---|---|
| ① A2A 协议(标准化) | 每个框架的 Agent 暴露 A2A 端点 + Agent Card | 标准化、框架无关、支持跨组织 | 协议开销;每个框架都要实现适配层 |
| ② 适配器 / 网关(工程化) | 中间层把各框架接口统一为内部标准 | 灵活、可定制、不依赖外部标准 | 维护成本高;每新增框架写新适配器 |
| ③ 共享基础设施(务实) | 不在 Agent 层互操作,通过 Redis / 消息队列 / 文件系统间接协作 | 实现最简单、不需要框架改造 | 松耦合、缺乏标准的能力发现和任务管理 |
📄 2026 年的现实:大多数生产系统仍用方式 ③。
实践建议(这条建议本身就是面试答案):
- 短期(2026):用共享基础设施,简单务实
- 中期(2027):关注 A2A 生态成熟度,新项目试
- 长期:设计时预留协议适配层
- 核心原则:不要为了互操作而互操作。一个框架能满足需求就别引入跨框架复杂性
📄 标准化进展速查:
| 标准 | 状态 | 覆盖 |
|---|---|---|
| MCP(Anthropic) | 广泛采用,事实标准 | Agent ↔ 工具/数据源 |
| A2A(Google) | 已发布,生态建设中 | Agent 间通信和任务协作 |
| AG-UI(CopilotKit) | 早期 | Agent ↔ 前端 UI |
| OpenTelemetry GenAI Conventions | 草案 | Agent 可观测性(这就是 §8.6 说 L2 得自己写的原因) |
| NIST AI Agent Standards | 2026-02 启动 | Agent 安全与治理 |
10.3 六个框架,一句话定位
| 框架 | 开发商 | 语言 | 核心特点 | 最适合 |
|---|---|---|---|---|
| LangGraph | LangChain | Python/JS | 图结构工作流 + 时间旅行调试 | 复杂状态机、企业级、合规要求高 |
| CrewAI | CrewAI | Python | 角色扮演团队 + 可视化编辑器 | 快速原型、非技术团队参与 |
| OpenAI Agents SDK | OpenAI | Python | Handoff 机制 + 原生追踪 | 客服系统、多步对话、OpenAI 生态 |
| Claude Agent SDK | Anthropic | Python/TS | 极简设计 + MCP 深度集成 | 安全优先、推理密集 |
| Google ADK | Python | A2A 原生 + 多模态 + 分层 | Google Cloud 生态、多模态 | |
| AutoGen | Microsoft | Python | 多 Agent 辩论 | 学术研究、实验性项目 |
三个值得单独知道的特点(面试里能当细节用):
LangGraph 的「时间旅行调试」:可以回溯到任意节点重新执行。 这直接对应 §8.4 那个「回放能力」——它是唯一把这个能力做成一等公民的框架。 机制是自动在每个节点存检查点。
OpenAI Agents SDK 的 Handoff 模型:Agent 声明自己能交接给谁, 像接力赛一样传控制权。它把 §3 那个 Handoff 概念做成了 API:
triage_agent = Agent(
name="分诊 Agent",
instructions="根据用户问题类型,交接给对应的专家 Agent",
handoffs=["technical_agent", "billing_agent", "general_agent"],
)⚠️ 注意这是去中心化的(Agent 之间直接传控制权,没有中心编排器), 所以它继承了 §2.3 那张表里去中心化的代价:没有一个理解者做全局校验(回 §3.3)。
CrewAI 的角色定义:抽象层级最高,定义 role + goal + backstory 就能跑:
researcher = Agent(
role="高级研究分析师",
goal="发现关于 AI Agent 的最新突破性信息",
backstory="你是一位资深的 AI 研究员...",
tools=[search_tool, web_tool],
)⚠️ 高抽象的代价是你不知道它在你背后拼了什么 prompt, 所以出问题时排查困难。这引向下一节那句警告。
10.4 选型:一句话决策 + 一条警告
你的情况?
├── 已经在用 Claude Code、想编程式控制 ──► Claude Agent SDK
├── 需要精确流程控制 / 企业合规 ──► LangGraph(图 + 时间旅行)
├── 快速验证想法 / 非技术同事参与 ──► CrewAI
├── OpenAI 生态、多步对话流程 ──► OpenAI Agents SDK
├── Google Cloud、需要多模态 ──► Google ADK
└── 学术研究、多 Agent 辩论 ──► AutoGen📄 但真正该记住的是这条警告,它来自 Anthropic:
最大的陷阱是使用了不理解底层原理的库和 SDK。 先理解 Agent Loop 的本质(§0.1 那个 while 循环),再选框架。
为什么这条警告在多 Agent 里格外重要: 框架帮你隐藏的恰好是本文 §4 和 §5 讲的那些东西—— 上下文边界怎么划、前缀是不是对齐的、缓存有没有命中。 你看不见它们,就无法优化它们,也无法在它们出错时排查。
具体表现:如果你用一个框架派了 5 个并行 Agent,成本是 243k 还是 69k(§4.4), 取决于框架内部有没有做前缀对齐,而这件事框架文档通常不会写。 你只有先懂机制,才知道该去测什么。
10.5 两个产品的哲学差异(作为案例)
📄 Claude Code 和 OpenAI Codex 在多 Agent 上的差异,是一个很好的「哲学决定架构」的案例:
| 维度 | Claude Code | OpenAI Codex |
|---|---|---|
| 交互模式 | 开发者在环(interactive) | 自主委托(autonomous) |
| 运行环境 | 本地终端 | 云端沙箱 |
| 多 Agent 形态 | Agent Teams(对等、可互相质疑) | 多任务并行(独立容器) |
| 隔离手段 | 应用层 Hook | OS 内核级沙箱 |
| Agent 间通信 | 邮箱 + 共享任务列表 | 几乎不通信(各自 git worktree) |
这两条路对应 §2.3 那张表的两端:
- Claude Code 走「对等通信」→ 拿到多视角、可互相质疑 → 代价是协调开销和调试难度
- Codex 走「独立并行」→ 零协调开销、隔离最彻底 → 代价是错误放大率最高(17.2x), 因为没有任何 Agent 在检查别的 Agent
这不是谁更好,是两种交互哲学的必然结果: 「开发者在环」意味着人随时会介入,所以 Agent 之间的讨论对人有价值; 「自主委托」意味着人不看过程,所以 Agent 之间讨论没有观众,不如各自跑完交差。
★ 这个案例的可迁移价值:多 Agent 的拓扑选择往往不是技术决策, 是「人在哪个位置」这个产品决策的推论。 面试里被问「你为什么选集中式/去中心化」, 从「我们的用户在什么时候介入」讲起,比从「错误放大率」讲起更有说服力。
10.6 本章自检
- MCP 和 A2A 是竞争关系吗?用一个类比说清它们的分工。
- A2A 是无状态协议。这个设计选择继承了本文哪条原则的成本和收益?
- 跨框架互操作的三条路,2026 年生产系统主要在用哪条?为什么?
- LangGraph 的「时间旅行调试」对应本文哪一章的哪个能力?
- Anthropic 说「最大的陷阱是用了不理解底层原理的库」。 在多 Agent 场景下,框架具体帮你隐藏了哪两件会影响成本一个量级的事?
- Codex 的 Agent 之间几乎不通信,错误放大率最高。为什么这依然可能是对的选择?
📌 回到原始文档:
- MCP vs A2A 完整对比、A2A 四概念与工作流程、三种互操作方式、标准化进展:
00-Question.md第十题(跨框架互操作)与multi-agent-deep-dive.md§8 - 六框架横评与代码示例:
multi-agent-deep-dive.md§6 - Claude Code vs Codex 哲学差异:
multi-agent-deep-dive.md§7
§11 后台任务基础设施:协作的承载层
前面十章讲的是协作的语义——谁派给谁、上下文怎么切、消息怎么传。这一章讲承载它的那层管道: 一个子 Agent 被派出去之后,它的运行态存在哪、输出写到哪、跑完了怎么把结果送回主循环、 主循环不再需要它了什么时候回收。
为什么这一章必须在:§4 讲的三种上下文形态、§5 讲的五个省钱杠杆、§6 讲的七类失败, 全部预设了「子 Agent 是一个可寻址、可查询、可终止的东西」。而它凭什么是那样的, 在源码里是一个独立的抽象层,不属于 AgentTool,也不属于任何一个具体的协作拓扑。
⚠️ 先划一条界,否则这一章会跟 §7 和「规划」那份文档撞车。
sid-code里叫 "Task" 的东西有两族,语义完全不同,源码注释自己划的界 (🔬packages/core/src/task/structured-task-store.ts:4-9):
后台任务注册表(本章) 结构化任务清单(不在本章) 是什么 运行态执行单元 持久化的 TODO 图 关键字段 status/outputFile/evictAfterblocks/blockedBy/owner怎么用 按 task_id读输出、停止按依赖关系认领、推进 对标 CC TaskOutput/TaskStopTaskCreate/TaskUpdate讲在哪 本章 规划那份文档 §9「从扁平清单到任务图」 两者共用一个词,但一个是「进程管理」,一个是「工作分解」。 面试里被问到 "Task" 时先反问是哪一族,这本身就是一个信号点。
11.1 面临的问题:四种执行单元,一套生命周期
一个 coding agent 需要在后台跑的东西不止子 Agent:
| 类型 | 例子 | 内部实现 |
|---|---|---|
| 后台 Shell | bun test、npm run build | 子进程 + 输出流 |
| 子 Agent | Explore / Plan / 自定义 agent | 独立的 LLM 对话循环 |
| Workflow | 编排脚本 | 沙箱里的脚本执行器 |
三者的内部实现毫无共同之处:一个是 spawn,一个是 while + LLM 请求,一个是脚本引擎。 但它们的外部需求完全一致:
注册 → 运行 → 终态(完成/失败/终止) → 输出可查 → 通知主循环 → 回收如果每种类型各自实现一套,代码会迅速膨胀,而且会以「不一致」的方式坏掉—— 比如 Shell 任务的输出落盘了但 Agent 任务的没有,于是后者一旦被回收结果就永久消失。
于是解法是把这七个动作抽出来,成为一个与「里面跑什么」无关的层 (🔬 packages/core/src/task/,实读 2026-09-01 共 3,681 行,含 registry.ts / disk-output.ts / notification.ts / shell-task.ts / agent-task.ts / workflow-task.ts):
// 🔬 packages/core/src/task/types.ts:9-12
export type TaskType = "local_shell" | "local_agent" | "local_workflow";
export type TaskStatus = "pending" | "running" | "completed" | "failed" | "killed";状态机刻意只有一条线:pending → running → completed | failed | killed。 没有 pause / resume。理由不是「暂无需求」,而是暂停一个 LLM 对话循环没有意义—— 它的状态在上下文里,暂停不省钱(token 已经花了),恢复也不便宜(要重建请求)。 真正的「暂停」在这个系统里由别的机制表达:§4.9 那条 transcript 恢复链, 它是销毁 + 重建,不是暂停 + 恢复。
★ 抽象层的判据不是「看起来像」,是「外部需求是否一致」。 Shell 和 LLM 循环内部毫无共同点,但它们的生命周期需求逐项相同—— 这才是能抽的依据。反过来,结构化任务清单看起来跟它们都叫 Task, 但它没有「运行/终止」这一说,所以不能并进来。
11.2 输出必须落盘,而不是留在内存里
这是这一层最容易被做错的地方,也是它与「把子 Agent 结果当返回值」的根本分野。
// 🔬 packages/core/src/task/types.ts:47-58(TaskStateBase 节选)
outputFile: string; // 磁盘路径,不是 buffer
outputOffset: number; // 已读到哪,支持增量读取
notified: boolean; // 完成通知是否已入队
evictAfter?: number; // 驱逐缓冲期截止时间outputFile + outputOffset 这一对是关键:输出的权威副本在磁盘上,内存里只存"读到哪了"。 三个收益:
- 不膨胀。一个
bun test的输出可以是几 MB,全量测试跑完 127 秒的日志留在内存里, 意味着主进程的常驻内存被一个你已经不看的任务占着。 - 可增量读。主循环用
task_output轮询时,从outputOffset往后读,不重复付 token。 - 任务被回收后结果仍在(这条是与 §4.9 的接点)。
写入侧是写入队列 + 单线程 drain 循环(🔬 disk-output.ts:2-3 文件头自陈), 而不是每次 append 都 await——后者会让高频输出的任务(编译日志)把主循环拖成串行 I/O。
一个值得抄进脑子的顺序依赖 bug
disk-output.ts:17-28 有一段注释,讲的是为什么建目录必须是同步的:
filePath一旦交给调用方,就可能被立刻用于同步打开文件 (shell-task.ts的openSync(output.filePath, "w")是拿到路径后的下一句), 而openSync不会创建父目录。原先建目录只发生在异步#drain()里,于是 「目录存在」纯靠别的任务恰好先跑过一次 drain。
后果是:
全量 bun test → 目录早被前面的测试建好 → 一直看不出问题
单跑 bash-stability.test.ts → 稳定 ENOENT(每次 3 个用例失败)这是顺序依赖,不是偶发 flaky。 两者的区别决定你怎么修:flaky 会让你去加重试, 顺序依赖要你去找「谁替我做了那件事」。判据是——它稳定失败还是随机失败。 稳定失败但只在单跑时失败,几乎一定是有个隐式前置条件被别人满足了。
同一段注释还有一条:recursive: true 所以幂等,EEXIST 不抛, 但刻意不吞其他异常——真的建不出目录(权限/磁盘满)应当当场炸, 而不是留到写入时报一个看不出根因的 ENOENT。这是「错误要在产生原因的地方报, 不是在表现症状的地方报」的一个具体样本。
11.3 通知:从「注入文本」到「文本 + 结构化快照并行」
任务跑完了,怎么告诉主循环?答案是往对话里注入一段 XML (🔬 notification.ts:5,对标 CC 的 <task-notification>):
<task-notification task_id="a1b2c3d4" status="completed">
<result untrusted="true">子代理的最终结论……</result>
<usage input_tokens="…" output_tokens="…" />
</task-notification>注意 untrusted="true":子 Agent 的输出是数据不是指令,这是 §9 零信任那套在这里的落点。
截断阈值从 2000 提到 16000 的理由
// 🔬 notification.ts:25
export const NOTIFICATION_OUTPUT_MAX_CHARS = 16_000;原值 2000 的问题(notification.ts:17-23 实读):子代理结论动辄数 KB(核查报告、逐条结论表), 2000 会把结论截在半句——「现在让我汇总…」,或者表格只剩一半。 主代理拿到的是残缺信息,TUI 上也是断句。
改到 16000 的判据不是「大一点更安全」,而是对输出性质的判断: 子代理的 output 是「最终结论文本」而非全量 transcript,绝大多数结论能完整落入。 真正超长时仍截断,但给出明确提示并指向 outputFile——完整内容已由 disk-output 落盘,不丢失。
★ 截断阈值不是拍脑袋的常量,它由「被截断的东西是什么」决定。 transcript 该截得狠(它本来就是过程),结论不该截(它是全部价值)。 同时截断必须有逃逸出口——指向完整副本。没有出口的截断就是静默丢数据。
一个正则解析引发的内容腰斩(比 CC 更进一步的一处)
这是这一节最值得讲的一个设计。原先的形态是:
主循环 ──注入 XML 文本──▶ 对话历史
│
TUI ────用正则重新解析────────┘
/<result...>([\s\S]*?)<\/result>/问题(🔬 notification.ts:106-112 实读):子代理结论是自由文本, 一旦含 </result> / </task-notification> 字面量,非贪婪正则会提前截断 → 通知内容腰斩、后半段泄漏到别处。
而这个概率在本项目不低——因为「子代理常被用来核查任务机制本身」, 它写出的报告里出现这些词是家常便饭。这是一个很好的例子: 一个"理论上罕见"的边界,在特定项目里可能是高频路径。
两条路都有代价:
| 方案 | 做法 | 代价 |
|---|---|---|
| CC 的做法 | 消费侧根本不重解析,原文塞给 LLM | TUI 拿不到结构化字段,做不了精细渲染 |
| 打补丁 | 转义 / 反转义往返 | 脆弱,每加一个字段就多一处要转义 |
| 本项目 | 注入 LLM 的仍是完整 XML 文本;结构化字段经 _meta.notif 平行带给 TUI | 多一条数据通路 |
第三条的关键在于它不是给转义打补丁,而是把"需要转义"的那条解析路径整个删掉 (notification.ts:117-119 原话)。结论里有任何 XML 字面量都不影响—— 因为没有任何地方再对它做正则抽取。
★ 一类根治手法:不修脆弱的往返,而是删掉需要往返的那条路径。 判据是问「这个转义是给谁看的」——如果答案是「给我自己的解析器」, 那真正的问题是我为什么要解析自己刚生成的东西。
11.4 回收:一个被反复报三次的 UI bug 教出来的分级
任务跑完了不能立刻删——主循环模型可能下一轮还要用 task_output 查结果。 于是有一个驱逐缓冲期:
// 🔬 packages/core/src/task/registry.ts:83, 92
const EVICT_GRACE_MS = 30_000; // completed / failed
const KILLED_DISPLAY_MS = 3_000; // killed两个数字各有理由,而两个理由都是从错误里学的。
第一个:30s 曾经是 60s。 注释自陈(registry.ts:75-82)当时写着「比 CC 更保守」, 但——
保守参数不是免费的,代价是用户反复报「后台任务面板不立即消失」(同一现象三次复发)。
而它本来就有兜底:task_output 的访问会续期(LRU touch),模型真在轮询就不断顺延。 所以加倍基础窗口对「模型多轮决策」没有额外收益,只是让没人再看的条目多驻留 30s。
★「更保守」不是免费的形容词,它是一个有代价的选择,而代价常常落在用户体感上。 判据:这个保守值挡住的是什么真实场景?如果已经有别的机制(这里是访问续期) 覆盖了那个场景,保守值就是纯成本。
第二个:killed 单独一档 3s。 理由是用户知不知道结果:
| 终态 | 用户状态 | 需要的窗口 |
|---|---|---|
killed | 是他自己刚下的指令,已经知道结果 | 3s,只是确认"确实停了" |
completed / failed | 任务自己到达的终态,用户可能没在看屏幕 | 30s,需要回看窗口 |
而促成这次修改的观察很有意思:TaskRow 早已给四种终态不同字形/配色 (运行◐ / 完成● / 失败✘ / 终止⊘),驻留时长却一刀切 30s—— 视觉上分了四级,生命周期上没分。
★ 一个可复用的 code smell:同一个概念在 A 维度分了级,在 B 维度没分。 这种不一致通常意味着 B 维度的分级被漏了,而不是「B 不需要分级」。
配套还有一条防漂移的做法:所有设置 evictAfter 的终态写入点都必须走同一个函数 (🔬 registry.ts:101 graceDeadlineFor),因为这些写入点散落 8 处 (agent / shell / workflow × complete/fail/kill),漏一处就是行为漂移—— 「字形上区分了四态、时长上没区分」正是这么来的。
dismissed:为什么不复用 evictAfter 当哨兵
用户按 Ctrl+X 手动把一条终态任务从面板划掉。CC 的做法是把 evictAfter 设成 0 当哨兵, 面板过滤器判 evictAfter !== 0。本项目刻意拆成独立布尔(🔬 types.ts:63-77 注释):
cc 让一个字段背了三种语义(
undefined=无期限 /0=立即隐藏 / 时间戳=到点隐藏), 读代码时evictAfter !== 0完全看不出是在判断"用户手动划掉了"。
三条设计约束值得单独看,因为每一条都对应一个更糟的替代方案:
- 只对终态生效。运行中任务的 Ctrl+X 语义是「终止」而不是「划掉」—— 把还在跑的任务从面板划掉会造成「任务不见了却还在烧 token」的黑盒, 比不消失更糟。
- 不立刻删任务,只置
dismissed让面板不显示。用户划掉的是「屏幕上这一行」, 不是「task_output还能不能查到它」。 - 纯 UI 可见性标记:
task_list//ps仍照常报这条任务—— 用户把条目从面板划掉,不代表「这个任务不曾存在」,模型该查得到。
★ 一个字段只该有一种语义。 用哨兵值省一个字段,省下的是一次声明, 付出的是每个读者每次都要想「这个
!== 0在问什么」。
11.5 停滞检测:后台任务的一类特殊死法
后台 Shell 有一种失败方式不会体现在退出码上:它在等用户输入。 rm -i、npm init、一个没加 -y 的安装命令——进程活着、退出码没有、输出不再增长。
// 🔬 packages/core/src/task/shell-task.ts:350-361
const STALL_CHECK_INTERVAL_MS = 5_000;
const STALL_THRESHOLD_MS = 45_000;
const PROMPT_PATTERNS = [
/\(y\/n\)/i, /\[y\/n\]/i, /\(yes\/no\)/i,
/Continue\?/i, /Overwrite\?/i, /Press (any key|Enter)/i, /Are you sure/i,
];判据是两个条件同时成立,不是任一:
① 输出 45s 没增长(光这条不够——编译期就是会安静很久)
② 且输出末尾匹配交互提示词(光这条也不够——日志里可能就打印了 "Continue?")命中后不杀进程,而是往对话里塞一条 status: "running" 的通知,带上末尾 200 字符, 让模型自己决定怎么办(shell-task.ts:383-397)。
★ 检测到病态时,默认动作应该是"告知"而不是"处置"。 这条与 §6.3 那套过程病态检测是同一个原则:自动 kill 一个可能只是在慢的进程, 造成的损失比多一条通知大。而 §11.4 那个「任务不见了却还在烧 token」是同一枚硬币的反面。
11.6 ★ 一个真实事故:已经答完的任务又烧掉 32% 的账单
这一节独立出来,因为它是这一章最有价值的一个案例, 而且它同时命中 §5(省钱)、§6(失败工程)和 §8(可观测性)三章。
现象
end_turn 是自然断点,主循环在那里 fire-and-forget 地拉起两个 forked agent (会话记忆更新 + 记忆提取)。实测会话 20260821-140626-4fd1f34e:
TurnComplete(end_turn)
│
├─ 之后系统又跑了 44 秒
├─ 发出 7 次完整请求
├─ 每次输入约 10 万 token(fork 继承主对话全历史,见 §4.3)
└─ 最大 in-flight = 2 ← 与"两个 fork"完全对得上代价:用户为一个已经答完的任务多付了约 ¥2.3,占该会话账单 32%。
为什么三处观测面都看不见
TUI、账本、trace 里都看不到这笔钱(🔬 packages/core/src/agent/background-task-gate.ts:14-17)。 原因是这些 fork 走的是 fire-and-forget 路径——主循环已经把「这一轮结束了」这个事实 广播出去了,之后发生的事没有归属到任何一轮上。
★ 这是一类专属于 agent 系统的观测盲区:「轮次」是账本的分组键, 而后台任务恰好活在轮次之外。 自检问法:我的成本归因是按什么分组的?有什么东西活在这个分组之外?
根因不是「忘了加锁」,而是「锁表达不出正确的语义」
这是全章最需要记住的一点。两个 fork 各自都有自己的重入互斥—— session-memory.ts 的 pending、extractor.ts 的 pending。
但那两把锁互不相干。 它们只防「同一个任务重入」,不防「两个不同任务并发」。
已有的: 锁A 防 [记忆更新 vs 记忆更新] ✅ 生效
锁B 防 [记忆提取 vs 记忆提取] ✅ 生效
需要的: 防 [记忆更新 vs 记忆提取] ❌ 两把锁都表达不出来所以修法不是再加一把锁,而是跨任务共享的单一队列:
// 🔬 background-task-gate.ts:43-44, 68
let inFlight: Promise<void> | null = null; // 模块级单例:跨任务共享才有意义
export function runBackgroundTask(label: string, task: () => Promise<void>): boolean语义选择:串行 + 丢弃,不是排队
被拒的任务直接丢弃,不排队。理由(background-task-gate.ts:24-25):
后台提取是"锦上添花",下一个 end_turn 还会再来; 排队只会把并发问题换成"队列越积越长,最后一次性烧掉一串十万 token 请求"。
★ 限流的两种语义要分清:排队是「延后执行」,丢弃是「放弃这次」。 判据是这个任务错过一次的代价。会再来的、幂等的、锦上添花的 → 丢弃。 不可重来的 → 排队,但你要为队列长度负责。
两个实现细节也值得看:
- 用一个已 settle 的 Promise 起链(
Promise.resolve().then(task)), 为的是让task()的同步抛出也被收敛进链里——否则同步抛出会绕过.catch, 闸门永远不释放,从"防并发"变成"永久堵死"。 - 刻意不 await。让主循环 await 这两个 fork 会把用户可感知的收尾延迟拉长到实测 44s。 「拿更快换更准的账」被判为净退步。闸门只管"不并发",不管"什么时候跑完"。
最诚实的一段:作者自己标注了这个修复的残留
background-task-gate.ts:34-37 原文:
⚠️ 已知残留:这仍是「作者要把新钩子接到闸门上」的形态。 真正的结构性消除是把 end_turn 后台任务改成统一的任务队列(单一入口、串行、带预算), 新钩子只能注册进队列、无法自己发请求。那个改造面显著更大,本轮未包含—— 写在这里是为了不让下一个人误以为"后台并发"已经根治了。
★ 区分两种修复:「消除了这个实例」和「消除了这类可能」。 前者依赖下一个人记得;后者靠结构。如实标注自己在哪一档, 比声称已根治更有价值——因为半年后读代码的人会据此决定要不要重新审计。
11.7 保活:让休眠不发生(三层纵深的第一层)
有一类「任务静默中断」的成因跟你的代码毫无关系:宿主机睡了。
真实事故(🔬 packages/core/src/task/prevent-sleep.ts:12-30,轨迹 20260801-175042-699f69f8): 任务执行到一半自己停了,TUI 上没有任何报错。排查结论是 macOS 空闲休眠—— 进程被整体冻结,挂钟继续走,醒来瞬间所有积压定时器一起补 fire。
证据链是这段注释最漂亮的地方(时区已对齐:轨迹是 UTC,pmset 是 UTC+8):
轨迹冻结窗口(UTC) 折算本地 pmset 记录
09:55:13 → 10:10:52(939s) 17:55→18:10 17:55:25 Sleep → 18:10:52 DarkWake
10:12:41 → 10:28:21(940s) 18:12→18:28 18:12:54 Sleep → 18:28:21 DarkWake
10:47:43 → 11:03:19(946s) 18:47→19:03 18:47:52 Sleep → 19:03:19 DarkWake三次 WatchdogKill 与三次 DarkWake 落在同一秒;TimerDrift 实测 actual_ms=926241 (预期 5000ms)。而当晚 10 次休眠全部是 Idle Sleep,零次合盖—— 也就是说 caffeinate -i 本可以 100% 避免这次事故。
★ 归因到环境层要有这种级别的证据:两个独立数据源(应用轨迹 + 系统日志) 在同一时刻对齐。 只有「任务停了,可能是休眠吧」不叫归因,那叫猜。 参考 §8 的可观测性讨论——这里体现的是「跨源对齐」这个能力。
三层纵深,缺任何一层都有洞:
| 层 | 做什么 | 挡不住什么 |
|---|---|---|
① prevent-sleep | 让休眠不发生(caffeinate) | 合盖、手动休眠、Linux/Windows 无对应实现 |
② sleep-detect | 休眠真发生了,挂钟跳跃不计入重试预算/会话额度 | 休眠期间该发生的事还是没发生 |
| ③ 主循环收尾分支 | 任何中断都必须给用户一句话,绝不静默 | — |
★ 第三层是兜底的兜底,它的判据是"绝不静默"。 前两层都可能失效,但「用户不知道发生了什么」是不可接受的终局。 这跟 §6 那套失败工程是同一个思路:你不可能穷举所有失败,但你可以保证所有失败都可见。
11.8 本章自检
sid-code里叫 "Task" 的有两族,它们的关键字段各是什么?为什么不能合成一族?- 为什么后台任务的输出必须落盘,而不是留在内存里?说出三个收益。
- 「单跑某个测试文件稳定失败,全量跑就绿」——这是 flaky 还是别的什么? 两者的修法有什么本质不同?
- 通知截断阈值从 2000 提到 16000,判据是什么?为什么不是「越大越安全」?
- TUI 用正则重新解析自己刚注入的 XML,会怎么坏?根治手法是什么? 为什么说这不是"给转义打补丁"?
killed的面板驻留时长是 3s,completed是 30s。这个分级的判据是什么?- 「同一个概念在 A 维度分了级,在 B 维度没分」为什么是一个 code smell?举本章的例子。
- 为什么
dismissed要独立成布尔,而不是复用evictAfter === 0当哨兵? - 停滞检测为什么必须两个条件同时成立?各去掉一个会怎样?
- 「两个任务各自都有重入锁,但仍然并发了」——锁错在哪?为什么加第三把锁不解决问题?
- 限流时「排队」和「丢弃」怎么选?判据是什么?
- 为什么一笔占账单 32% 的开销会在 TUI、账本、trace 里同时隐身?
- 归因到「宿主机休眠」需要什么级别的证据才算成立?
📌 回到源码(都可实读复核):
- Task 类型体系与状态机:
packages/core/src/task/types.ts - 注册表、驱逐缓冲期、面板可见性:
packages/core/src/task/registry.ts - 磁盘输出与写入队列:
packages/core/src/task/disk-output.ts - 通知 XML 与结构化快照:
packages/core/src/task/notification.ts - 停滞检测:
packages/core/src/task/shell-task.ts:348-404 - end_turn 单飞闸门:
packages/core/src/agent/background-task-gate.ts - 任务保活三层纵深第一层:
packages/core/src/task/prevent-sleep.ts
§13 动手:从零搭一个 mini 多 Agent 系统
这一章是五个阶段的路线图。每个阶段都是可运行、可验证的, 不要跳阶段——尤其不要跳 L0。
关于阶段顺序的一条说明:直觉上应该「先搭起来再加观测」, 但 §6.8 那条结论(多 Agent 的失败几乎全是不报错的失败)意味着 没有观测你连「它坏了」都不知道。所以 L0 是观测,不是功能。
L0 · 先能看见:一个会话一个目录
目标:任何一次运行结束后,你能回答「花了多少钱、跑了几轮、每个 Agent 做了什么」。
runs/<session-id>/
├── events.jsonl # 每个事件一行:think / tool_call / tool_result / handoff
├── ledger.jsonl # 每次 LLM 调用一行:model, in/out/cache tokens, cost, ms
└── meta.json # 任务描述、总轮数、总成本、exit status最小实现(Python,能跑就行):
import json, time, pathlib, uuid
class Trace:
def __init__(self, root="runs"):
self.dir = pathlib.Path(root) / time.strftime("%Y%m%d-%H%M%S")
self.dir.mkdir(parents=True, exist_ok=True)
def event(self, kind, agent, **kw):
rec = {"ts": time.time(), "kind": kind, "agent": agent, **kw}
with open(self.dir / "events.jsonl", "a") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
def usage(self, agent, model, usage, elapsed_ms):
# 注意:cache_read 通常单价是 input 的 1/10 左右,必须分开算
rec = {
"ts": time.time(), "agent": agent, "model": model,
"input": usage.input_tokens,
"output": usage.output_tokens,
"cache_read": getattr(usage, "cache_read_input_tokens", 0),
"cache_creation": getattr(usage, "cache_creation_input_tokens", 0),
"elapsed_ms": elapsed_ms,
}
with open(self.dir / "ledger.jsonl", "a") as f:
f.write(json.dumps(rec) + "\n")这一级要定死的三件事(后面改口径会让所有历史数据不可比):
- 每条记录必须有
agent字段。 多 Agent 的账本没有这个字段等于没有账本—— 你无法回答「钱花在哪个 Agent 上」。 - cache_read 和 input 必须分开记。 它们单价差约 10 倍, 合并记录之后你永远算不出真实成本,也算不出缓存命中率。
kind的取值集合先写死(think / tool_call / tool_result / spawn / handoff / error)。 事后加值可以,改已有值的语义不行。
验收判据(不要只看文件存在):
# ❌ 错的验收:文件存在且不为空
ls -la runs/*/events.jsonl
# ✅ 对的验收:数有效行数,并检查关键字段非空
wc -l < runs/*/events.jsonl
jq -r 'select(.agent == null) | .kind' runs/*/events.jsonl | head # 应该无输出
jq -s 'map(.input + .output) | add' runs/*/ledger.jsonl # 应该是个合理数字第二条命令是关键:「文件有内容」和「内容是对的」是两件事。 一个只写了换行的日志文件在 ls 下看起来完全正常。
L1 · 一个单 Agent 基线(不许跳)
目标:一个能跑的 Think-Act-Observe 循环,就是 §0.1 那个 while 循环。
为什么这一级不许跳:它是你后面唯一的对照组。 §12 Q24 那条——「必须一直保留一个单 Agent 基线」——从这里开始。
def run_agent(task, tools, trace, agent_name="main", max_turns=30):
messages = [{"role": "user", "content": task}]
for turn in range(max_turns):
t0 = time.time()
resp = llm(messages, tools=tools, system=SYSTEM_PROMPT)
trace.usage(agent_name, MODEL, resp.usage, (time.time()-t0)*1000)
trace.event("think", agent_name, turn=turn, text=resp.text[:200])
messages.append({"role": "assistant", "content": resp.content})
if not resp.tool_calls:
return resp.text # 它觉得干完了
results = []
for call in resp.tool_calls:
trace.event("tool_call", agent_name, tool=call.name, args=call.args)
out = execute(call)
trace.event("tool_result", agent_name, tool=call.name, ok=out.ok)
results.append({"type": "tool_result", "tool_use_id": call.id,
"content": out.text})
messages.append({"role": "user", "content": results})
trace.event("error", agent_name, reason="max_turns_exhausted") # ← 别漏这行
return None这一级最容易漏的两件事:
max_turns耗尽时要记一条 error 事件。 只在成功路径埋点是最普遍的埋点错误—— 失败会被系统性隐藏,你的成功率统计会虚高。- 记下
max_turns这个值本身。后面对比时它是个重要变量。
跑完这一级,先用它建立基线:挑 5–10 个代表性任务,各跑 3 次, 记下成功率、平均轮数、平均成本、p95 耗时。这组数字后面每一级都要复跑对比。
L2 · 第一个子 Agent:只读探索(形态 A)
目标:主 Agent 能派一个只读子 Agent 去探索,拿回一份摘要。
为什么第一个子 Agent 必须是只读的:它是唯一一个出错也不会破坏东西的形态。 用它当第一个多 Agent 场景,等于把「架构能不能跑通」和「它会不会搞坏代码」这两个风险解耦了。
EXPLORE_TOOLS = ["read_file", "grep", "glob"] # 白名单,不含 write/bash
def spawn_explore(directive, trace, parent="main"):
trace.event("spawn", parent, child="explore", directive=directive[:200])
# ① 隔离:空历史 + 自包含 directive
result = run_agent(
task=directive, # 必须自包含:含具体路径、要找什么
tools=[t for t in ALL_TOOLS if t.name in EXPLORE_TOOLS], # ② 工具白名单
trace=trace, agent_name="explore",
max_turns=15, # ③ 比主 Agent 更紧的预算
)
trace.event("handoff", "explore", to=parent,
chars_in=len(directive), chars_out=len(result or ""))
return result四个必须做对的点:
| 点 | 怎么做 | 对应本文 |
|---|---|---|
| directive 自包含 | 含具体路径、要找什么、返回什么格式 | §3.4 |
| 工具白名单 | 只读工具,代码层面过滤(不是提示词里说) | §2.6 / §9.2 |
| 更紧的 max_turns | 探索任务不该跑 30 轮 | §5.5 |
| handoff 事件记两个方向的字节数 | chars_in / chars_out 就是压缩比 | §8.4 能力 4 |
验收:拿 L1 那组基线任务里「需要大量探索」的那几个重跑,对比三件事:
① 主 Agent 的最终上下文大小 —— 应该显著变小(这是压缩边界的收益)
② 总成本 —— 可能变大(多了一个 Agent 的推理)
③ 成功率 —— 应该不降如果 ① 没变小,说明你的 directive 或返回摘要有问题—— 可能子 Agent 把探索到的原文全塞回来了。 这时用 chars_out / chars_in 那个比值去查。
L3 · 并行 + 缓存对齐(这一级是分水岭)
目标:一次派 3 个子 Agent 并行,且第 2、3 个命中前缀缓存。
这一级把 §4.5 那三个「宁可」变成代码。先写测量,再写优化:
# 第一步:先能测出命中率。没有这个数字,后面的优化是盲的。
def cache_hit_rate(ledger_path):
import json
rd = cr = inp = 0
for line in open(ledger_path):
r = json.loads(line)
rd += r.get("cache_read", 0)
cr += r.get("cache_creation", 0)
inp += r.get("input", 0)
total = rd + cr + inp
return rd / total if total else 0.0然后是三个让步的实现要点:
FORK_PLACEHOLDER = "Fork started — processing in background" # ★ 所有孩子共用同一句
def spawn_parallel(directives, parent_state, trace):
# ① 传父级【已渲染的字节】,不要重新生成
rendered_system = parent_state.rendered_system_prompt # 不是 build_system_prompt()
tool_defs = parent_state.tool_defs # 原样数组,不裁剪
# ② 构造一个所有孩子【字节完全相同】的前缀
tool_uses = [{"type": "tool_use", "id": f"call_{i}", "name": "spawn",
"input": {"directive": d}} for i, d in enumerate(directives)]
shared_prefix = parent_state.messages + [
{"role": "assistant", "content": tool_uses},
{"role": "user", "content": [
# ★ 每个 tool_use 都要配 tool_result,且内容对所有孩子一致
{"type": "tool_result", "tool_use_id": f"call_{i}",
"content": FORK_PLACEHOLDER}
for i in range(len(directives))
]},
]
# ③ 唯一的差异:最后追加各自的任务
children = []
for i, d in enumerate(directives):
msgs = shared_prefix + [{"role": "user", "content": f"你的任务:{d}"}]
children.append(run_agent_with(msgs, rendered_system, tool_defs,
trace, f"fork-{i}"))
return children⚠️ 两个必须处理的细节,漏了会直接 400 或静默丢缓存:
- 过滤悬空工具调用:父级历史里可能有「发了 tool_use 但还没收到 tool_result」的悬空调用。 直接继承过去 API 会返回 400。继承路径必须先清洗一遍。
- model 必须一致:孩子换模型 = 换 cache key = 缓存全丢。 这一级先别做模型分层,等测到命中率稳定了再权衡(§5.2)。
验收判据(这一级的验收最重要,因为失败是静默的):
① cache_hit_rate 从 ~0 涨到 >50%(Anthropic 族显式缓存通常能到 70%+)
② 3 个孩子的输入成本:第 1 个全价,第 2、3 个应显著更低
③ 变异自证:故意把 FORK_PLACEHOLDER 改成 f"...{i}"(让每个孩子不同),
命中率应该掉回 ~0第 ③ 条是最重要的一条,务必做。 理由: 缓存命中率这个指标有两种「看起来正常」的坏法—— 真的命中了,和根本没在测对的东西。 故意破坏一次、看指标是否如预期崩掉,是唯一能区分这两者的方法。 这个手法叫变异自证,值得用在所有「不报错的优化」上。
L4 · 加安全边界与失败处理
目标:把 §6 那七类失败模式的防线加上。按性价比排序,不必全做:
| 优先级 | 做什么 | 对应 |
|---|---|---|
| P0 | 全局预算硬上限(总 token / 总成本 / 总墙钟),超了直接停 | §5.5 |
| P0 | 空转检测:同一工具、同样参数、同样返回值连续 ≥3 次 → 中止 | §6.3 |
| P0 | 递归防御的锚点挂在 spawn 时的元数据上,不要挂消息历史 | §5.6 / §6.5 |
| P1 | 子 Agent 结果回传前跑一次检查(先规则版:有没有改敏感路径) | §6.2 |
| P1 | Handoff 契约:要求子 Agent 返回结构化结果 + 一栏「我不确定的地方」 | §3.2 |
| P2 | 编排器提示词里写死「不许透传」 | §3.3 |
| P2 | 取消信号分两种:用户级(不传播)/ 系统级熔断(传播) | §4.7 |
空转检测的最小实现(便宜且几乎无假阳性):
def detect_spinning(events, threshold=3):
"""连续 threshold 次「同工具 + 同参数 + 同返回」→ 判病态"""
sig, run = None, 0
for e in events:
if e["kind"] != "tool_result":
continue
cur = (e["tool"], e.get("args_hash"), e.get("result_hash"))
run = run + 1 if cur == sig else 1
sig = cur
if run >= threshold:
return True, cur
return False, NoneP0 三条为什么是 P0:它们防的是成本无界和防线静默失效这两类后果最重的问题。 其余的即使没做,你也只是「效果不够好」;这三条没做,你可能烧掉一笔说不清的账 或者以为自己有防护其实没有。
L5 · 对等团队与共享状态(只在确实需要时)
目标:多个长活实例 + 共享任务列表 + 邮箱(形态 C)。
先问一遍:真的需要吗? §7.6 那条: 大多数 coding agent 场景不需要共享可变状态, 文件系统交付 + 一次性 Handoff 就够了。这一级的复杂度是前面四级的总和。
如果确实需要,最小骨架:
teams/<team-id>/
├── roster.json # 扁平数组 + leader_id(★ 扁平是刻意的,见 §2.6)
├── tasks.jsonl # 共享任务列表,append-only(★ 不要原地改)
└── inbox/<agent>/ # 每个 agent 一个目录,一条消息一个文件三个设计要点:
- tasks 用 append-only 事件流,不要原地改状态。 两个 Agent 同时把任务 3 从 pending 改成 in_progress 是必然会发生的竞态, 而 append-only + 「取最后一条」让冲突变成可检测、可回溯的,而不是静默覆盖。
- roster 保持扁平。 这直接决定了「队友不能派队友」这条能力边界(§2.6)。 如果你想支持嵌套,那要先改数据结构,不是先改限制。
- 按 §7.3 那张表分类型定一致性级别,尤其记得给 scratchpad 定「无需一致性」。
验收:跑一个 3 队友的任务,检查三件事:
① 有没有两个队友做了同一件事(重复工作)
② 有没有出现 A→B→A→B 的消息循环
③ token 消耗速率有没有突增 3 倍以上(涌现行为的早期信号)13.6 五级速查表
| 级 | 做什么 | 验收判据 | 别跳的理由 |
|---|---|---|---|
| L0 | 会话目录 + 事件流 + 账本 | 有效行数 > 0 且 agent 字段全非空 | 失败不报错,没观测你不知道它坏了 |
| L1 | 单 Agent 循环 + 基线数字 | 5–10 任务 × 3 次的成功率/轮数/成本 | 它是唯一的对照组 |
| L2 | 只读探索子 Agent | 主上下文变小、成功率不降 | 唯一出错也不破坏东西的形态 |
| L3 | 并行 + 缓存对齐 | 命中率 >50%,且变异自证通过 | 分水岭:这一级决定成本量级 |
| L4 | 预算上限 / 空转检测 / 递归锚点 | 故意触发每条防线,看它是否真的触发 | 防成本无界和防线静默失效 |
| L5 | 对等团队 + 共享状态 | 无重复工作、无消息循环、速率无突增 | 复杂度等于前四级之和,先确认真的需要 |
一条贯穿五级的方法:每加一个防线或优化,就故意破坏它一次,看指标是否如预期变化。 「加了但从未被触发过」和「加了但根本没接上」在日志里长得一模一样。
附录
A. 术语表(按「一次协作从头到尾」排序,不按字母序)
| 词 | 中文 | 是什么 | 本文位置 |
|---|---|---|---|
| Agent | 智能体 | LLM + 工具 + Think-Act-Observe 循环 | §0.1 |
| Workflow | 工作流 | 路径由代码写死;比 Agent 省约 4× token | §0.2 |
| Subagent | 子 Agent | 父派出的临时工,干完即销毁(形态 A) | §0.3 |
| Orchestrator | 编排器 | 控制流的决策者;实践中主体是提示词 | §2.6 / §12 Q12 |
| Handoff | 交接 | 成果从一个上下文搬到另一个的那一刻 | §3 |
| 自包含 prompt | — | Worker 看不到编排器的对话,指令必须自足 | §3.4 |
| Context Rot | 上下文腐烂 | 上下文越长,中段注意力越差 | §1.3 |
| fork 子 Agent | — | 孩子继承父级完整上下文与已渲染系统提示词 | §4.3 |
| prompt cache | 前缀缓存 | 前缀字节完全相同则复用,价格约一折 | §4.4 |
| cache-identical prefix | 字节级对齐 | 所有孩子请求前缀字节完全相同才有折扣 | §4.5 |
| 占位符 tool_result | — | 并行派生时所有孩子填同一句,保前缀一致 | §4.5 |
| call-time 拒绝 | — | 保留用不到的工具、只在调用时拒绝,保字节 | §4.5 |
| 上下文死重 | — | 无害但用不到的内容;规模下与有害等价 | §4.6 |
| compaction | 上下文压缩 | 历史过长时压成摘要;会重写历史 | §5.6 / §6.5 |
| compaction-resistant | 抗压缩 | 不变量的锚点必须挂在不会被压缩改写的元数据上 | §6.5 |
| Context Poisoning | 上下文污染 | 上游错误经 Handoff 污染下游 | §6.1 |
| 错误放大率 | — | 独立并行最高 17.2x;集中式最低 | §2.3 |
| Coordination Drift | 协调漂移 | 累积小错误使集体推理偏离初始目标 | §6.7 |
| 数据处理不等式(DPI) | — | 信息经处理只减不增 → 固定预算下单 Agent 更优 | §1.2 |
| 认知容量阈值 κ | — | ≈3–4 个技能,超过后单 Agent 选择准确率衰减 | §1.3 |
| Blackboard | 共享黑板 | 都读写同一中央存储 | §7.2 |
| CRDT | 无冲突复制数据类型 | 字符级 100% 收敛,语义冲突仍有 5–10% | §7.4 |
| False Sharing | 伪共享 | 无关 Agent 因共享同一存储区域互相干扰 | §7.1 |
| 致命三角 | Lethal Trifecta | 私有数据 + 不可信内容 + 对外副作用 | §9.3 |
| 涌现行为 | Emergent Behavior | 没有单个 Agent 被设计成的系统级行为 | §9.4 |
| MCP | — | Agent ↔ 工具/数据源;Agent 的 USB-C | §10.1 |
| A2A | — | Agent ↔ Agent;无状态、Task 自包含 | §10.1 |
| 变异自证 | — | 故意破坏一次,看指标是否如预期崩掉 | §13(L3) |
D. 如果只记住五句话
- 多 Agent 不会让它变聪明。 固定预算下单 Agent 在信息论上严格更优, 多 Agent 买到的是「绕过上下文退化」和「真并行」,代价是每次 Handoff 的有损压缩。
- 子 Agent 首先是上下文压缩边界,其次才是并行性。
- 多 Agent 的成本分本质和偶然两部分,偶然那部分(重复传前缀)能靠字节级缓存对齐消掉约 90%。 说「多 Agent 太贵」之前先问自己有没有消掉它。
- 编排器的价值不在分发,在「必须理解才能分发」。 透传的编排器不只是没拦住错误, 它连拦住错误的可能性都没有了。
- 它的失败几乎全是不报错的失败。 所以可观测性不是运维需求,是正确性需求。
文档结束。 全文约 3800 行,13 章 + 4 个附录。 有疑问或发现数字过期,从附录 C 那张表回原始文档复核—— 本文所有二手数字都可以在那四份文档里找到出处。