多 Agent 协作的基础设施与冲突治理(Multi-Agent Coordination & Conflict Governance)
所属:01-fundamentals / 04-agent-architecture
面试权重:★★★
来源:《深入理解 AI Agent:设计原理与工程实践》第 10 章「多 Agent 协作」(李博杰,github.com/bojieli/ai-agent-book)。书中标注的框架(A2A、AutoGen、Swarm、MetaGPT、灵台等)与数字均有出处,非本仓库实测
相关知识点:multi-agent.md(姐妹篇:多 Agent 的收益判据、代价、六种失败模式清单、LangGraph 共享状态 —— 本篇不重复其内容,只补机制层)、10-tool-discovery.md(协作工具三组原语spawn/cancel/send_message/list_agents)、11-async-event-driven.md(安全点与取消语义)、memory.md(冲突处理与知识更新)、../../02-interview/design/02-multi-agent-service.md(多智能体设计题)
一句话#
多 Agent 的难点不在「怎么让它们分工」,而在「怎么让它们不互相踩」 —— 而这两件事各有一套成熟答案:分工靠数据平面(共享文件系统交换产物)+ 控制平面(消息、状态、终止、调度),不互相踩靠冲突治理(乐观锁、工作副本隔离、幂等结算点、级联终止)。这套抽象不新鲜 —— 它几乎是操作系统与分布式系统的 LLM 版,所以成熟经验可以整段借用。
为什么重要(面试角度)#
multi-agent.md已经回答了「该不该上多 Agent」和「会怎么坏」。这一篇回答「真上去了,靠什么撑着」。 两篇是判据层与机制层的关系,面试官问完前者,下一句通常就是后者。- 它把「多 Agent」接到你已经会的知识上。 多 Agent 系统与操作系统有完整的对应关系(进程 / 内核 / 系统调用 / fork / kill / ps),你不是在学一套新东西,是在给旧知识换名字。这比背框架 API 值钱得多。
- 它有一个全仓此前零覆盖的高频词:A2A 协议。 Agent Card、任务生命周期、不透明协作三要素,以及它与 MCP 的分工(MCP 管 Agent ↔ 工具,A2A 管 Agent ↔ Agent)。
- 它给「冲突」的答案比「加个锁」深一层。 乐观锁只能防同一个文件;跨文件的语义冲突要靠工作副本隔离;而并行收口真正难的是**「第一个已验证成功」的幂等结算点** —— 这三个层次能分开讲的人很少。
- 和你的项目接得上:Writing Copilot 的多智能体故障复盘里,面试官追问的正是「多 agent 冲突谁裁决」(见
../../02-interview/projects/writing-copilot.md)。这一篇给你的是那题的机制层答案。
⚠️ 边界声明:本篇是《AI Agent》第 10 章的知识梳理,不是你的实战经验。你没搭过多 Agent 平台、没实现过 A2A、没做过并发冲突治理。面试用「这套结构我会这么设计」的口径讲,不要讲成「我们线上就是这么跑的」。可讲成一手经验的只有 Writing Copilot 的中断控制那一小块。
核心内容#
1. 两个正交的设计维度:先把「共享什么」和「听谁的」分开#
维度一:上下文是否共享。 这是最基础的架构决策,它决定信息怎么传。
| 共享上下文 | 不共享上下文 | |
|---|---|---|
| 形态 | 后续 Agent 接收前一个的完整轨迹(换 system prompt 和工具集后是新角色,但保留前任的全部记忆) | 每个 Agent 有完全独立的上下文与轨迹,看不到对方的"内心活动" |
| 优势 | 信息不丢失,任何阶段的细节都还能回看 | 模块化与隔离更好;新增 Agent 不改现有 Agent;某个 Agent 故障不传染 |
| 代价 | 上下文快速膨胀,当前 Agent 被历史干扰 | 信息同步(状态一致性、丢失与重复);调试要拼多份日志 |
一个常见误判:以为「共享上下文」就是「一个 Agent 分几个阶段」。书里的定义更严:每阶段切换 system prompt 和工具集后,它就是一个新的 Agent(身份、职责、能力都变了),只是继承了前任的记忆。
不共享上下文就必须显式通信,而通信机制只有三种 —— 它们恰好落在操作系统的 IPC 两大范式里:
| 机制 | 对应 IPC | 适合 |
|---|---|---|
| 工具调用的参数(把下游 Agent 封装成工具) | 消息传递(同步) | 需要类型确定、结构清晰 |
| 共享文件系统 | 共享内存 | 产物较大或需持久化 |
| 消息总线(Message Bus) | 消息传递(异步投递) | 数量多、需解耦 |
Go 那句广为流传的话可以直接引用来收口:「不要通过共享内存来通信,而要通过通信来共享内存。」
维度二:协作拓扑。 控制权和信息按什么结构流动:
| 拓扑 | 形态 | 适用 |
|---|---|---|
| 对等协作 | 少量(2–3 个)平等 Agent 多轮互评、迭代改进 | 需要独立视角与认知多样性 |
| 管理者模式 | 一个中心 Manager 规划调度,子 Agent 各管一块 | 子任务多、有复杂依赖、需动态调度 |
| 去中心化 | 没有运行时中心控制者,Agent 之间自行发起沟通与移交 | 需要制衡;或管理者本身是单点故障时 |
两个维度是正交的:你可以「不共享上下文 + 管理者模式」,也可以「共享上下文 + 对等」。先分别定这两维,再谈具体框架 —— 这也是绕过「Agent Swarm」这类营销词的唯一办法:同一个词既可能指 Swarm 式 handoff 网络,也可能指「主 Agent 动态创建上百个子 Agent」的规模化管理者模式,名字一样、结构相反。
一个高频追问:角色转换该替换 system prompt,还是加载 Skill?
| 选择 | 工具可见性 | 前缀缓存 | 约束能力 |
|---|---|---|---|
transfer_to_agent(替换 system prompt + 工具集) |
只暴露当前角色工具 | 每次切换都改请求前缀,从变化点起缓存通常无法复用 | 强:越界工具在 schema 层就不可见 |
Skill(固定 prompt,按需把 SKILL.md 追加到轨迹) |
通常固定暴露工具全集 | 静态前缀不变,Skill 内容落在轨迹末尾,已有缓存可继续复用 | 弱:Skill 是行为指令,硬权限仍要 Harness 门 |
判据:差异只在知识、流程、写作风格 → 用 Skill;差异涉及权限、工具隔离、合规边界,或需要在运行时强制禁止某类动作 → 用独立 Agent / transfer_to_agent,并在 Harness 层用代码限制工具调用。
2. 数据平面:Agent 眼里的文件系统有四个区域#
Agent 访问的不是单一存储,而是一棵虚拟文件系统 —— 来源、生命周期、权限各异的存储被"挂载"到同一目录树下,Agent 通过统一的 read_file / write_file / list_dir 访问,底层可能是本地临时盘、持久对象存储、第三方云盘 API 或只读资源包。
一句值得记住的话:相当一部分并发冲突与信息泄露,源于把本应隔离的区域混置。 这棵目录树相当于 Agent 的地址空间,四类区域就是权限各异的"内存段"。
| 区域 | 可见性 | 生命周期 | 读写 | 并发控制 |
|---|---|---|---|---|
| ① Agent 专属工作区(Scratchpad) | 仅该 Agent | 随实例销毁 | 读写 | 不需要(私有) |
| ② 多 Agent 共享空间 | 所有协作 Agent + 用户 | 随任务,需持久化 | 读写 | 需要(乐观锁 / worktree) |
| ③ 外部挂载资源(Drive / Notion / Wiki…) | 视外部授权 | 由外部源决定 | 多为只读,写需谨慎 | 由外部源负责 |
| ④ 系统内置资源(Skills / 模板 / 共享工具定义) | 所有 Agent | 跨会话稳定 | 只读 | 不需要(只读) |
每一区域解决一个具体问题,也就各有一处要防的东西:
- ① 私有工作区 一举两得:既避免多个 Agent 的临时文件互相覆盖,又保持主 Agent 上下文精简 —— 子 Agent 的试错留在自己工作区,只把最终产物提交到共享空间。这正是「子 Agent 返回结构化摘要而非全量轨迹」在存储层的体现。
- ② 共享空间 是并发冲突的高发处(乐观锁、worktree 都作用在这里),也是用户上传 / 下载的入口。
- ③ 外部挂载 有三项不同于本地存储的特性,必须显式处理:受外部权限约束(用户在源系统的权限决定可见范围)、延迟高且一致性弱(每次读是网络往返、数据可能已被外部改动,只能按最终一致性对待)、以按需只读为主(写回会污染用户的真实数据)。统一文件接口把这些差异掩盖了,所以要在挂载层显式管理只读/可写、超时与凭证边界。
- ④ 内置资源 全局共享、只读、跨会话稳定,可被所有 Agent 并发读取而无需并发控制 —— 这是四区域里唯一"免费"的一类。
「文件路径作为通用接口」是这套设计的价值所在:Agent 之间传产物、主 Agent 给子 Agent 交接输入、乃至跨组织 A2A 交换产物,传的都是轻量的路径字符串,而不是把内容塞进上下文窗口。
3. 控制平面:消息、状态、终止、调度#
文件系统解决了产物交换,协作还需要一条控制平面。这一节就是操作系统里 fork / ps / kill / wait 的对应物。
① 消息传递:点对点 vs 消息总线,都要带"信封"。 点对点(A 直接调 send_message_to_agent_b)适合拓扑固定、Agent 少的场景;Agent 一多就出问题 —— 连接数随 Agent 数呈平方增长,且要求收发双方同时在线,此时改用消息总线:发布到总线、按订阅转发,发送方不必知道消费者是谁。两种形态下消息都应携带结构化信封:发送者 ID、目标(指定或广播)、消息类型(task_assigned / status_update / result / terminate)和 JSON 负载。统一的信封保证接收方能可靠路由与解析,也让协作链路可追溯 —— 这是多 Agent 调试的关键。
② 状态查询:这是控制平面里最容易被低估的一环。 主 Agent 派出子 Agent 后,若无从获知其进展,既无法判断该不该继续等,也无法在它卡住时介入。直觉做法是定义 get_subagent_status(agent_id) —— 但这个拉取式接口并不实用:子 Agent 一经创建就开始跑,完成或失败时自然会通知,并不需要主 Agent 显式轮询。更自然的做法是回到两大通信范式:
- 用消息传递获取状态:主 Agent 发一句"进展如何?",子 Agent 在合适时回复,全程异步、不阻塞自己 —— 就像经理用 IM 问进度,不要求下属立刻停手。反过来说,子 Agent 也该在关键节点主动汇报。无论问还是报,状态本身宜采用统一的状态机词汇(执行中 / 需要输入 / 已完成 / 失败)—— A2A 协议正是把任务生命周期标准化成这一组状态。
- 用共享文件系统获取状态:最彻底的形态是轨迹持久化(trajectory persistence)—— 子 Agent 把自己的完整轨迹实时序列化为 JSON,追加写入日志文件(通常每会话一个文件、每行一个事件,即 JSONL)。主 Agent 无需任何状态上报协议,直接读文件就能看到它全部执行过程:正在调哪个工具、最近在想什么、是不是卡在反复失败的重试上。用进程的语言说,这相当于直接读另一个进程的内存。
⚠️ 但它不宜作为主要方式:轨迹动辄数万 token,主 Agent 读完还得自行提炼,费时费 token。
- 更合理的是约定进度文件:启动子 Agent 时约定"把进度写到
progress.md",子 Agent 每完成一项就更新。主 Agent 随时读这个轻量文件即可 —— 相当于两块进程在共享内存里划出一小块约定格式的状态区。它还有一个额外用途:检测卡住状态 —— 文件最后修改时间超过 N 分钟没变,即可判定无活动、触发超时兜底。
这三条合起来是一张分层表,值得背下来当检查清单:要完整过程用轨迹持久化(重、用于调试与审计);要进展概览用进度文件(轻、用于调度决策与卡死检测);要低延迟通知用消息(异步、用于事件驱动的协调)。
③ 执行终止:两种强度,以及"孤儿 Agent"。 并行协作里常见「一个成功、其余无需继续」,此时终止有两种强度 —— Unix 用户会认出这正是 SIGTERM 与 SIGKILL 的区别:
- 优雅终止(首选):发
terminate信号 → 子 Agent 在当前步骤的安全点响应 → 先清理资源(关浏览器会话、写未完成文件、释放锁)→ 返回 ack → 退出。 - 强制终止(兜底):直接终止进程,仅在优雅信号无响应时用,代价是可能遗留悬挂资源与未完成写入。
还剩一个问题:主 Agent 终止后,仍在跑的子 Agent 怎么办? 最简洁的工程做法借鉴 Go 的 context —— 终止沿创建关系向下级联:取消一个 Agent,它派生的所有子 Agent 随之取消,从根上杜绝无人认领的孤儿 Agent。反过来,若确实需要脱离父级长期运行的后台 Agent(类似 Unix 的 nohup),就让它从一棵新的生命周期树起步(对应 context.Background()),显式声明不随父级终止。
④ 资源与调度:稀缺的不是 CPU,是 token 和并发额度。 进程世界稀缺的是 CPU 时间和内存,Agent 世界稀缺的是 token 和并发额度 —— 并发数设上限,避免几十个 Agent 同时打满 API 配额。调度通常落在管理者 Agent 或运行时身上:启动子 Agent 时设定步数或 token 预算、超限即止;困难任务给强模型、机械任务给低成本模型;并发到上限而更紧急的任务到来时,打断执行中的子 Agent,这就是抢占。
管理者 Agent 相对传统调度器的独特优势:它有推理能力。 它可以并行启动多个子 Agent 探索同一问题,根据各自进展决定给谁更多资源、砍掉哪个看起来误入歧途的 —— 像公司内部赛马。但这一领域远不如操作系统调度成熟,它决定系统的成本上限,应在架构阶段就纳入考虑。
4. 冲突治理:三个层次,别停在「加个锁」#
multi-agent.md 的六种失败模式已给出分类与对策;这一节补它的机制层 —— 每一条对策具体怎么做才不出漏洞。
层次一:同文件写冲突 → 乐观锁。 每个文件维护版本号(或最后修改时间戳):读时记下版本号,写时校验是否仍与读取时一致;不一致就写入失败,Agent 重新读最新版、在此基础上重做。代价是偶尔需要重试,换来的是数据一致性。
层次二:跨文件语义冲突 → 乐观锁管不了,要更高层的语义校验。 典型案例:Agent A 重编全书图片编号,Agent B 同时改某章内容并引用旧编号。两者操作的是不同文件,文件层面完全没有冲突,但结果是 B 的引用全部失效。这一类更隐蔽也更危险,需要的是"这个改动会不会让别处的引用失效"这种语义级校验,而不是文件锁。
层次二的最实用工程解:工作副本隔离。 在「多个 Coding Agent 并发改同一代码库」这个最常见场景里,业界主流做法是给每个 Agent 分配独立的 Git 分支或 worktree,各自在副本上并行修改、互不干扰,把冲突集中推迟到最后的合并点。合并点是人或另一个 Agent 可以显式检查与裁决的地方 —— 这比"事中互相踩"可控得多。
层次三:并行收口的竞态 —— 结算点必须是「第一个已验证成功」。 这一层最少人讲。多个 Agent 并行搜索、一个命中后其余应停,于是要定义结算点:
event = next_event()
if event.type == RESULT:
if verify(event.artifact, hidden_checks): # 先验证
if not settle_once(event): # 再原子地认领
continue
broadcast_cancel(to = workers - {event.worker_id})
await_all_ack_or_timeout()
return assemble(event.artifact, evidence = event.evidence)两个细节决定成败:
verify不能省:「第一个声称成功」不等于「第一个已验证成功」。 直接按第一个返回结果收口,就是把一个未验证的结论当成结论。settle_once必须幂等(通常由锁或事务保护)。 否则两个几乎同时到达的成功事件会触发两次汇总 —— 这就是竞态条件(Race Condition):Agent A 和 B 在同一毫秒各自报告"我找到了",若主 Agent 收 A 的报告就开始汇总、紧接着又收 B 的触发第二次汇总,就会产生重复结果或互相矛盾的状态。解法是第一个报告到达后立即锁定状态,后续报告识别为重复并忽略。
这一层还有一个前置纪律:区分「失败」与「未完成」。 广播 terminate 之后要 await_all_ack_or_timeout() —— 等所有确认或超时,因为你无法保证每个子 Agent 都还活着并会回复。
横向的一层:防共因失效。 同一模型 + 相似上下文产生的多条结论不能自动当作独立证据;工程上要有意识地制造差异(不同模型、不同上下文、不同数据来源),并用命名空间、资源配额、速率限制防止相同决策同时冲击共享资源(实证:30 个同时上线的 Agent 有 18 个创建了同名 Git 分支 —— 详见 multi-agent.md)。
一句话把三层串起来:同文件用乐观锁,跨文件用工作副本,收口用幂等结算点。 别人只答第一层时,你答到第三层就是分层。
5. 跨组织:A2A 协议,以及它和 MCP 的分工#
前面所有机制都假设所有 Agent 由同一个团队开发、跑在同一个系统内 —— 此时参数传递、共享文件、消息总线三种机制足够用。但当协作跨越组织边界(你的 Agent 要调用另一家公司的 Agent),就需要标准化的互操作协议。 2025 年 Google 发布、后捐赠给 Linux 基金会托管的 A2A(Agent2Agent)就是干这个的。它之于 Agent,就是网络协议之于进程。
三个核心要素:
| 要素 | 内容 | 解决什么 |
|---|---|---|
| Agent Card | 一份描述 Agent 能力的元数据文档(发布在约定公开地址下):能做什么、支持哪些输入输出模态、如何认证 | 跨组织的能力发现(相当于 Agent 的"名片") |
| 任务生命周期管理 | 把协作单元建模为任务(Task),带明确状态机(已提交 / 进行中 / 需要输入 / 已完成 / 失败),原生支持长任务与流式进度更新 | 跨组织任务的状态对齐 |
| 不透明协作 | Agent 之间只交换任务与产物(Artifact),不暴露内部提示词、思考过程和工具实现 | 与「不共享上下文」原则一致,也是跨组织必要的安全属性 |
与 MCP 的分工要说准 —— 这是最容易被问混的一处:
MCP 解决 Agent ↔ 工具的互操作;A2A 解决 Agent ↔ Agent 的互操作。 前者管"Agent 能调什么",后者管"Agent 能找谁、怎么托付任务"。A2A 并不取代三种通信机制,而是在它们之上、跨信任边界的标准化层。
因此它有一个明确的适用边界:同一团队内部的多 Agent 系统直接用消息总线即可;只有协作方互不信任、实现互不可见时,才需要 A2A 这样的公开协议。上来就说"我们用 A2A 做内部编排",是典型的用力过猛。
⚠️ 别和另外两个缩写混:
09-coding-agent.md里提到的 A2UI(界面描述协议)与 AG-UI(事件/传输协议)都不是 agent-to-agent 协议。三个缩写的字母表差不多,管的事完全不同。
6. 这一篇和 multi-agent.md 怎么配合着答#
两篇是同一章的两个切面,面试时按问题类型分流,不要互相顶替:
| 面试官问的是 | 去哪一篇 | 关键词 |
|---|---|---|
| 该不该上多 Agent? | multi-agent.md |
判据层:是否引入单 Agent 拿不到的新信息;先单 Agent、遇瓶颈再拆 |
| 多 Agent 会怎么坏? | multi-agent.md |
六种失败模式、错误复合 、崩溃故障 vs 拜占庭故障、《Why Do Multi-Agent LLM Systems Fail?》(14 模式 / 3 类 / kappa 0.88) |
| 真上去了靠什么撑着? | 本篇 | 数据平面(虚拟文件系统四区域)+ 控制平面(消息 / 状态 / 终止 / 调度) |
| 两个 Agent 打起来了怎么办? | 本篇 §4 | 乐观锁 → 工作副本 → 幂等结算点;预设优先级 / 所有权 / 权限边界 |
| 跨公司协作怎么做? | 本篇 §5 | Agent Card / 任务状态机 / 不透明协作;A2A vs MCP |
一个能立住的开场:「多 Agent 值的判断在 multi-agent 那一层,我先说结论 —— 多数场景先单 Agent;如果真的上,真正的工程工作量不在编排逻辑,而在数据平面和控制平面,以及冲突治理的三个层次。」
常见面试问法#
Q:多个 Agent 之间怎么通信?三种机制怎么选? A:先承认它们就是 IPC 的两大范式。共享文件系统 = 共享内存(Agent 世界的"共享内存"),适合产物较大或需持久化;工具调用参数 = 同步消息传递,适合类型确定、结构清晰;消息总线 = 异步消息传递,适合 Agent 多、需解耦(点对点的连接数随 Agent 数平方增长,且要求双方同时在线)。可以引 Go 那句收口:不要通过共享内存来通信,而要通过通信来共享内存。再加一句实操:无论哪种,消息都应带结构化信封(发送者、目标、类型、JSON 负载),否则链路无法追溯、多 Agent 调试会变成拼图。
Q:不共享上下文的多 Agent,主 Agent 怎么知道子 Agent 在干什么?
A:先否掉"轮询状态接口"这个直觉答案 —— get_subagent_status() 并不实用,因为子 Agent 一经创建就开始跑,完成或失败自然会通知。更自然的是回到两大通信范式:① 用消息 —— 主 Agent 发"进展如何"、子 Agent 择机回复,全程异步不阻塞;反过来子 Agent 也该在关键节点主动汇报。状态本身要用统一的状态机词汇(执行中 / 需要输入 / 已完成 / 失败)—— A2A 正是把这组状态标准化了。② 用文件 —— 最彻底的是轨迹持久化(JSONL,每会话一文件、每行一事件),主 Agent 直接读就能看到它全部过程,相当于读另一个进程的内存;但它动辄数万 token、太重。所以常规做法是约定进度文件(如 progress.md),轻量、够用,还能靠"最后修改时间超过 N 分钟没变"检测卡住。
Q:一个 Agent 卡住了或者失控了,怎么终止?
A:终止有两种强度,对应 SIGTERM 与 SIGKILL。 优雅终止是首选:发 terminate 信号,子 Agent 在当前步骤的安全点响应,先清理资源(关会话、写未完成文件、释放锁),回 ack 后退出。强制终止只在优雅信号无响应时用,代价是遗留悬挂资源和未完成写入。还要回答一个容易漏的问题:主 Agent 终止后,它派生的子 Agent 怎么办? 借鉴 Go 的 context,终止沿创建关系向下级联 —— 取消一个 Agent,它派生的全部随之取消,从根上杜绝孤儿 Agent;若确实需要脱离父级长期运行的后台 Agent,就让它从新的生命周期树起步并显式声明不随父级终止。最后补一句并发收口的纪律:广播取消后要 await_all_ack_or_timeout() —— 因为不能保证每个子 Agent 都还活着并会回复。
Q:两个 Agent 同时写同一个文件怎么办?乐观锁够吗?
A:乐观锁只解决第一层,三层要分开答。 ① 同文件写冲突 → 版本号 + 写前校验,不一致就重新读最新版重做,代价是偶尔重试。② 跨文件的语义冲突 → 乐观锁完全管不了:A 重编图片编号、B 在别处引用旧编号,文件层面毫无冲突,但 B 的引用全失效 —— 这类更隐蔽也更危险,要语义级校验。最实用的工程解是工作副本隔离:给每个 Agent 独立的 Git 分支 / worktree,各自改、把冲突推迟到显式的合并点(这一点在多个 Coding Agent 并发改同一仓库时是业界主流做法)。③ 并行收口的竞态 → 结算点必须是**"第一个已验证成功"而不是"第一个声称成功",且认领动作 settle_once 必须幂等**(锁或事务保护),否则两个几乎同时到达的成功事件会触发两次汇总。一句话总结:同文件用乐观锁,跨文件用工作副本,收口用幂等结算点。
Q:多个 Agent 并行搜索,一个先找到,怎么收口?
A:先把"找到"和"验证找到"分开。 步骤是:收到结果 → 先 verify(隐藏检查)→ 通过后再 settle_once 原子认领 → 广播 terminate 给其余 → await_all_ack_or_timeout() → 汇总。两个坑:① 省掉 verify 就等于把第一个未验证的返回值当结论;② settle_once 不幂等就会出现竞态 —— A、B 同一毫秒各报"我找到了",收 A 后开始汇总、紧接着 B 又触发一次汇总,结果是重复或矛盾的状态;解法是第一个到达即锁状态,后续识别为重复并忽略。另外别忘了超时兜底:等 ack 要有上限。"一个 Agent 命中就全部停下"这件事,看起来简单,真正难的是幂等和超时。
Q:什么时候需要 A2A?它和 MCP 什么关系? A:A2A 解决的是"跨信任边界的 Agent ↔ Agent 互操作",MCP 解决的是"Agent ↔ 工具互操作" —— 一句话分开:MCP 管能调什么,A2A 管能找谁、怎么托付任务。三要素:Agent Card(能力发现的名片)、任务生命周期管理(任务状态机:已提交 / 进行中 / 需要输入 / 已完成 / 失败,原生支持长任务与流式进度)、不透明协作(只交换任务与产物,不暴露内部提示词、思考与工具实现)。关键是说清它不取代什么:它不取代参数传递 / 共享文件 / 消息总线这三种机制,而是架在它们之上的一层。所以适用边界很窄:同一团队内部用消息总线就够了,只有协作方互不信任、实现互不可见时才需要。最后提一句别混:A2UI(界面描述)和 AG-UI(事件/传输)都不是 agent-to-agent 协议。
Q:多 Agent 为什么可以直接借用操作系统的经验?
A:因为两者的抽象几乎一一对应:静态前缀(system prompt + 工具定义)↔ 程序可执行文件;轨迹 ↔ 进程内存;LLM ↔ CPU;Agent 运行时 ↔ 内核;工具调用 ↔ 系统调用;spawn_subagent ↔ fork;cancel_subagent ↔ kill;list_agents ↔ ps;子 Agent 返回的结构化摘要 ↔ 退出码与 wait();共享文件系统 / 消息 ↔ 共享内存 / 消息传递。这套设定其实就是 1970 年代 Actor 模型(私有状态、异步消息、可创建新成员)的 LLM 版。所以私有状态、异步消息、生命周期级联、孤儿回收、抢占调度这些成熟经验可以整段借用 —— 也正因为如此,判据不该是"像不像人",而是"这一步在操作系统里对应什么"。
易错点#
| 易错说法 | 问题 | 应该说 |
|---|---|---|
| 「共享上下文就是同一个 Agent 分几个阶段」 | 定义偏松 | 切换 system prompt 和工具集后就是一个新 Agent(身份 / 职责 / 能力都变了),只是继承了前任的记忆 |
| 「Agent 之间通信方式很多」 | 没归类 | 只有三种,且落在 IPC 两大范式里:共享文件系统 = 共享内存;工具参数 = 同步消息;消息总线 = 异步消息 |
「主 Agent 应该有 get_status 轮询接口」 |
直觉但不对 | 子 Agent 完成或失败会自然通知;更自然的是用消息(异步问答 / 主动汇报)或进度文件获取状态 |
| 「用轨迹持久化做 Agent 间的信息传递」 | 太重 | 轨迹动辄数万 token、还要主 Agent 自己提炼;常规做法是约定进度文件(progress.md),轨迹留给调试与审计 |
| 「发个 terminate 就停了」 | 漏了两件事 | ① 优雅终止要先在安全点清理资源(含释放锁)并回 ack;② 要等 ack 或超时,不能假设对方还活着 |
| 「取消主 Agent 就行了」 | 漏了孤儿 | 终止要沿创建关系向下级联(Go context 的做法),否则留下无人认领的子 Agent;需要长驻的要显式起新的生命周期树 |
| 「加乐观锁就解决并发冲突了」 | 只答了一层 | 乐观锁只防同一文件;跨文件语义冲突要更高层校验,常用**工作副本隔离(Git 分支 / worktree)**把冲突推迟到合并点 |
| 「谁先返回就听谁的」 | 把"声称"当"验证" | 结算点必须是第一个已验证成功;先 verify 再收口 |
| 「收口逻辑怎么写都行」 | 漏了竞态 | settle_once 必须幂等(锁 / 事务),否则两个几乎同时的成功事件触发两次汇总 |
| 「同一模型给的意见就是独立证据」 | 共因失效 | 相似上下文的多个结论不能自动视为独立;要刻意制造差异,并用命名空间 / 配额 / 限流防同批决策冲击同一资源 |
| 「内部编排也用 A2A」 | 用力过猛 | 同团队内部用消息总线即可;A2A 只在跨信任边界、实现互不可见时才需要 |
| 「A2A 和 MCP 是一回事 / 是竞争的」 | 混淆分工 | MCP = Agent ↔ 工具;A2A = Agent ↔ Agent;A2A 不取代三种通信机制,是架在其上的标准化层 |
| 「A2A / A2UI / AG-UI 差不多」 | 缩写陷阱 | A2UI 是界面描述协议、AG-UI 是事件/传输协议,都不是 agent-to-agent 协议 |
| 「共享区域越方便越好」 | 混置区域是冲突源 | 相当一部分并发冲突与信息泄露,源于把本应隔离的区域混置;四区域各有各的可见性与生命周期,先定布局再谈机制 |
| 把本章讲成「我们线上就是这么跑的」 | 触碰仓库硬规矩 | 这是书里的框架与出处,非一手经验;只能用「这套结构我会这么设计」的口径,一手经验只有 Writing Copilot 的中断控制 |
自测#
1. 一个多 Agent 文档翻译系统:术语表 Agent、章节翻译 Agent(多实例并行)、全文审校 Agent、一个 Manager。你会怎么设计它的数据平面和控制平面?
答案
先在数据平面上把四个区域划出来(这是最容易被跳过、却最容易出事的一步):
| 区域 | 在这个系统里是什么 | 并发控制 |
|---|---|---|
| ① 专属工作区 | 每个翻译 Agent 实例的草稿、术语试译、调试日志 | 不需要(私有)——同时避免临时文件互相覆盖、并让 Manager 上下文保持精简 |
| ② 共享空间 | 术语表(JSON/CSV)、各章译文(chapter1_zh.md…)、审校报告;用户在此上传原书、下载成稿 |
需要:多人(多 Agent)读写同一批文件,术语表尤其会被反复读写 —— 用乐观锁(版本号 + 写前校验);译文按章一文件、天然分片,冲突面小 |
| ③ 外部挂载 | 若术语来源是外部词典 / Wiki | 按需只读,写回要谨慎、受外部权限约束、一致性弱 |
| ④ 内置资源 | 翻译指南、风格模板、共享工具定义 | 只读,可并发读取、无需控制 |
控制平面四条:
- 消息:Manager 与各 Agent 之间的任务分派与汇报,带结构化信封(发送者 / 目标 / 类型 / 负载),类型如
task_assigned/status_update/result。Agent 多时用消息总线,避免点对点连接数平方增长。 - 状态查询:不要给 Manager 做
get_status轮询。约定每个翻译 Agent 维护一份进度文件("第 3/12 章完成"),Manager 随时读;顺便用它检测卡住 —— 某实例进度文件超过 N 分钟没更新就判定无活动、触发超时。 - 执行终止:审校发现某章不合格时,Manager 要能取消对应的翻译实例 —— 优雅终止:发
terminate,实例在安全点收尾(写完当前章、释放文件锁)后回 ack。Manager 终止时,它派生的所有翻译实例要沿创建关系级联取消,不留孤儿。 - 资源与调度:给每个翻译实例设步数与 token 预算;并行的章节数设并发上限,避免打满 API 配额;复杂章节用强模型、机械段落用便宜模型。
这个案例里最值得主动讲的一点:术语表是这个系统最容易出并发问题的地方 —— 它被 Glossary Agent 写、被所有 Translation Agent 读、被 Proofreading Agent 复核。它应该放在共享空间、用乐观锁保护,而不是放在某个 Agent 的私有工作区。
2. 上面那个系统里,两个翻译 Agent 同时发现了一个术语表里没有的新术语,各自推断了一个译法并写回术语表。会发生什么?你怎么防?
答案
先说会发生什么 —— 分两种情况:
- 同一文件的写冲突(简单冲突):两个 Agent 几乎同时读术语表、各自新增自己的译法、先后写回,后写的把先写的覆盖掉 —— 第一个 Agent 的译法消失,而且它自己并不知道。
- 更隐蔽的是语义冲突:假设两者写的是不同文件(比如各自的和译法记在自己的笔记里),文件层面毫无冲突,但全书术语一致性被破坏 —— 这正是"跨文件语义冲突"的形态,读者会在第 3 章看到"智能体"、第 9 章看到"代理"。
防法分三层:
- 第一层:乐观锁。 术语表带版本号;Agent 读时记版本,写时校验 —— 发现版本已变就写入失败,重新读最新版再决定(可能发现自己推断的译法和已有的冲突,于是改为沿用或标注待审)。代价是偶尔重试,收益是不丢数据。
- 第二层:语义级校验。 乐观锁只防同一文件的覆盖,防不住"译法不一致"本身。这里需要的是职责隔离:书里的架构本来就规定 Translation Agent 遇到对照表里没有的新术语,只"推断翻译并标记为待审查",不直接改术语表 —— 把唯一的写权限收敛给 Glossary Agent(或由 Proofreading Agent 统一裁决)。这比加锁更根本:不是让并发写变安全,而是让它不发生。
- 第三层(若坚持要并发写):工作副本 + 合并点。 让每个 Agent 在自己的副本上改,把冲突集中到显式的合并 / 审校环节由 Proofreading Agent 或人裁决 —— 与多个 Coding Agent 用 Git 分支 / worktree 是同一个套路。
这个题真正的考点是第二层:能不能从"加锁"上升到"改权限与职责划分"。加锁是让并发安全,收敛写权限是让并发消失 —— 后者通常更好。
3. 你的多 Agent 系统有 10 个子 Agent 并行搜索,找到一个就全部停止。为什么"第一个返回就收口"是错的?正确流程是什么?
答案
错在把一个"声称"当成了"结论"。 第一个返回的不一定是第一个真的找到的 —— 它可能返回了一个看似匹配、实际不对的结果;按它收口,整个系统的产出就建立在一个未验证的前提上。这是多 Agent 里最典型的拜占庭式失败:Agent 不会报错,它会继续给出看似可信的错误结论。
正确流程(五步):
- 收到
RESULT事件; - 先
verify(artifact, hidden_checks)—— 用验证器判定,而不是信任返回方; - 验证通过后
settle_once()原子认领 —— 这一步必须幂等(由锁或事务保护); - 广播
terminate给其余 worker; await_all_ack_or_timeout()—— 等全部确认或超时,再assemble结果。
三个必须讲到的细节:
- 为什么
settle_once要幂等? 因为竞态条件:Agent A 和 B 可能在同一毫秒各自报告"我找到了"。若主 Agent 收 A 的报告就开始汇总、紧接着收 B 的又触发第二次汇总,就会产生重复结果或互相矛盾的状态。解法是第一个报告到达即锁定状态,后续报告识别为重复并忽略。 - 为什么要
await_all_ack_or_timeout? 因为你无法保证每个子 Agent 都还活着、并且会回 ack —— 没有超时兜底,收口可能永远等下去。 - 终止本身要走优雅路径:发
terminate后,子 Agent 在安全点响应并清理资源(关浏览器会话、释放锁)再 ack;只有无响应时才强制终止。
如果全部失败,也要有明确出口:汇总各失败原因、向用户报告"未找到"及各失败原因统计,而不是静默返回空。
4. 有人提议「我们内部这几个 Agent 用 A2A 来编排」。你怎么评价?
答案
大概率是用力过猛 —— 但先别急着否定,先问清一件事:这几个 Agent 是不是同一个团队开发、跑在同一个系统内、彼此信任且实现可见。
如果是(绝大多数内部编排都是),那 A2A 用错了地方。 A2A 的定位是跨信任边界的 Agent ↔ Agent 互操作协议 —— 它的三个要素里,「不透明协作」这一条就说明了它是为"互不信任、实现互不可见"设计的(Agent 之间只交换任务与产物,不暴露内部提示词、思考过程与工具实现)。内部协作里这些恰恰是共享的,用 A2A 只是给消息总线套了一层额外的协议开销。
内部编排应该用什么? 回到三种通信机制:工具调用参数(结构化、同步)、共享文件系统(大产物、持久化)、消息总线(解耦、异步)。书里给的判据很清楚:同一团队内部的多 Agent 系统直接用消息总线即可;只有当协作方互不信任、实现互不可见时,才需要 A2A 这样的公开协议。
那什么时候 A2A 确实正当? 例如:你的 Agent 要调用另一家公司(或另一个部门、另一个安全域)的 Agent —— 你既不知道对方怎么实现,也不该把内部提示词和思考过程交出去,同时需要一种标准方式发现对方能做什么(Agent Card)、对齐任务状态(已提交 / 进行中 / 需要输入 / 已完成 / 失败)。这才是它解决的问题。
顺带把两个容易混的分工说清:MCP 管 Agent ↔ 工具的互操作,A2A 管 Agent ↔ Agent 的互操作 —— 一个管"能调什么",一个管"能找谁、怎么托付"。并且要强调 A2A 不取代三种内部通信机制,它是架在其上的一层。
5. 「多 Agent 系统不就相当于分布式系统吗?那套经验能直接用?」你怎么答?
答案
方向对,而且可以答得更具体 —— 因为对应关系几乎是逐项成立的(书里给了完整对照表):
| 操作系统 / 分布式系统 | 多 Agent 系统 |
|---|---|
| 程序(可执行文件) | 静态前缀(system prompt + 工具定义) |
| 进程的内存 | 轨迹 |
| CPU | LLM |
| 内核 | Agent 运行时 |
| 系统调用 | 工具调用 |
fork |
spawn_subagent |
kill |
cancel_subagent |
ps |
list_agents |
退出码与 wait() |
子 Agent 返回的结构化摘要 |
| 共享内存 / 消息传递 | 共享文件系统 / 消息 |
这套抽象并不新鲜:私有状态、异步消息、可创建新成员,正是 1970 年代 Actor 模型的基本设定 —— 多 Agent 系统不妨看作它的 LLM 版本。所以成熟经验确实可以整段借用:生命周期级联与孤儿回收(Go 的 context)、SIGTERM / SIGKILL 式的两级终止、生产者消费者式的消息总线、锁与版本号、抢占与配额。
但必须紧接着说清楚三处不能照搬的地方:
- 故障类型不同 —— 这是最根本的一条。 分布式容错理论把故障分崩溃故障(停摆)与拜占庭故障(继续工作但给错误信息)。传统分布式系统大多只需防崩溃;Agent 的故障天生是拜占庭式的 —— 它很少径直停止,而是继续给出看似可信的错误结论,且错误不会主动声明自己是错误。所以传统系统里"等它报错"的兜底思路在这里失效,交叉验证与多数表决这类拜占庭容错手段成了必需品。
- 传递是有损的。 进程之间传字节是逐位保真的;Agent 之间传语义,每转述一次都是有损的重新编码 —— 这就是"错误的级联放大"的根源。
- 调度远不成熟。 操作系统调度器有明确的资源与目标函数;Agent 调度的稀缺资源是 token 和并发额度,实践中很大程度上靠管理者 Agent 的推理能力现场判断(谁该多给资源、谁该被砍掉),远不如 OS 调度成熟,而且它直接决定成本上限。
收口:照搬的是结构(数据平面 / 控制平面 / 生命周期),不能照搬的是故障模型 —— 前者省了大量设计工作,后者是 Agent 特有的坑。