Chapter 02 · 工作原理与设计权衡
wire 上发生了什么,以及为什么这么设计
01 建立了七个抽象的静态结构(Agent Card、Task、Message、Part、Artifact)与不透明原则——本章讲它们在 wire 上如何运转、为什么这么设计、代价落在谁头上。每个设计选择都附带一张备选方案表:被放弃的方案不是错的,是换了一组不同的代价。事实锚点全部来自 A2A v1.0.1 规范(2026-05-28),正文给出 spec-cited 的真实方法名与状态值。
本章你要建立的心智模型
- 一次任务委托端到端走六步:发现 → 鉴权 → 发消息 → 建 Task → 状态流转 → 取 Artifact——凭证走 HTTP header 不进 payload,产出是 Artifact。
- 线缆上是
SendMessage(v1.0 的字面 JSON-RPC 方法名),同一抽象有 JSON-RPC / gRPC / REST 三个对等绑定,agent 在 Card 里声明支持哪些。 - 三种交互模式(请求/响应、SSE 流、webhook 推送)的箭头方向不同;webhook 推送里信任是反向的——是客户端要验证这个入站 POST 真来自 agent。
- Task 是有状态的,生命周期里
TASK_STATE_INPUT_REQUIRED/TASK_STATE_AUTH_REQUIRED是可中断的中途态,不是终态。 - 五个设计取舍都指向同一个灵魂——不透明(opaque):拿到了去中心、抗篡改、可复用,放弃了对等体的内部可见性。
2.1整体:一次任务委托端到端
先把 01 章的七个静态抽象放进一条时间线,看它们在 wire 上依次怎么动。一个客户端 agent 要把一件事委托给一个远程对等 agent,从「不知道它在哪」到「拿回结果」,恰好走六步。本章后面每一节,都是在这条时间线的某一步上展开取舍——所以先盯住每一步在 wire 上传的是什么、凭证在哪一层、状态存在谁那里。
securitySchemes 的类型,从不携带凭证本身;二·第④步建出来的 Task 是有状态的,状态存在服务端,客户端后续靠 Task id 回查;三·第⑥步交回的是 Artifact(带 id 和 Part[]),不是一个裸字符串响应。第②步凭证放在 HTTP header 而不是 JSON-RPC payload 里。为什么这个看似随意的位置选择,恰恰是让 A2A 能直接跑在现有企业基础设施上的关键?
展开答案(先停 10 秒再点)
凭证在 HTTP header(如 Authorization)意味着鉴权发生在传输层而非应用层——现有的反向代理、API 网关、负载均衡器、auth proxy 早就懂怎么在 header 上做鉴权、限流、审计,无需理解 A2A 的 JSON-RPC body。如果凭证混进 payload,这些中间设施就得解析 A2A 协议才能鉴权,A2A 也就没法「drop 进」既有 infra 了。这是 §2.6 第一条取舍(建立在既有 HTTP 标准上)在最小处的体现。
2.2发现机制的取舍
A2A 把发现做成「去固定 URL 取一份静态 JSON」——像 robots.txt 一样可缓存、无需注册中心,代价是能力只能静态声明,没有运行时协商。
01 章 §1.2 介绍了 Agent Card 是「声明与发现」的载体,回答「这个 agent 是谁、能干什么」。这一节回答下一个、也是设计时真正要拍板的问题:客户端到底怎么拿到这份 Card,以及这个选择放弃了什么。发现机制有三条路可走,A2A 选了最轻的那条——客户端对固定路径发一个普通 HTTP GET:GET https://{domain}/.well-known/agent-card.json(spec §8.2)。
底层机制:静态文件发现,谁也不用跑注册中心
/.well-known/ 是 IETF 为「众所周知的资源」预留的路径前缀,robots.txt 是同一族的老熟人。把 Agent Card 放在这里,意味着发现完全是去中心、可缓存的:没有一个中央注册中心要运维、要保证高可用、要成为单点故障;客户端可以像缓存任何静态资源一样缓存这份 Card。代价直接写在机制里——Card 是一份静态声明,agent 在写下它的那一刻就把能力固定了,客户端在连接前读到的是「这个 agent 一贯能做什么」,而不是「此刻、对你这个调用方,它能做什么」。
大量现存博客与旧 SDK 仍写 GET /.well-known/agent.json。v0.3.0 起路径改名为 agent-card.json。看到 agent.json 一律按 v0.x 旧形态处理——这是迁移点,不是当前形态。
| 方案 | 优势 | 为什么没选 / 代价 |
|---|---|---|
| 静态 Agent Card (/.well-known/ JSON) |
去中心、可缓存、无注册中心要运维;像 robots.txt 一样人人会取 | 能力静态声明,无 live 协商;想暴露更多得靠 authenticated GetExtendedAgentCard 补丁 |
| 中央注册中心 | 能集中检索、按能力搜索、统一治理 | 多一个要高可用的服务与单点故障;跨组织谁来运营、谁可信都成问题——与「跨信任边界对等协作」相悖 |
| 运行时能力协商 (MCP 的 initialize 握手) |
每个会话现场协商能力,按调用方动态裁剪 | 每次连接都要一轮握手往返,无法预先缓存;A2A 选了「连接前就知道」而非「连上才协商」 |
读过 mcp 教程会记得:MCP 客户端连上 server 后要先做 initialize 握手,运行时协商双方能力。A2A 走了相反的路——能力在 Card 里静态声明,连接前就可读、可缓存。代价是没有 per-session 的现场协商;spec 留的补丁是鉴权后可拉取的 GetExtendedAgentCard,让 agent 对已认证的调用方多暴露一些能力。但这是补丁,不是 MCP 那种第一性的会话协商。两种协议在「何时确定能力」上做了相反的取舍,对应它们各自的部署形态。
2.3线缆:SendMessage + 三传输绑定
委托一件事,wire 上发的是 SendMessage——同一个抽象方法有 JSON-RPC 2.0 / gRPC / HTTP+REST 三个对等绑定,agent 在 Card 里声明它支持哪些、首选哪个。
在 A2A v1.0+ 里,抽象操作名和 JSON-RPC "method" 字段的值是同一个字符串。「发送一条消息」这个抽象操作,落到 JSON-RPC over HTTP 上,"method" 就是字面的 SendMessage。这与许多现存博客里写的 message/send 不同——后者是 v0.x 的命名约定,v1.0 已弃用(迁移注记见下)。
下面是一条真实的 SendMessage JSON-RPC 请求(spec 示例)。注意 params.message 就是 01 章的 Message 抽象:一个 role 加一个 parts 数组,数组里每个 Part 带自己的 kind 和内容——这就是 01 章模态无关 Part 的 wire 形态。
{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"role": "user",
"parts": [
{ "kind": "text", "text": "把这份季度财报里的风险段落抽出来" }
],
"messageId": "9229e770-767c-417b-a0b0-f0741243c589"
}
}
}
method字面值就是 SendMessage,与抽象名一致(v1.0+)。
params.message即 01 章的 Message:role + parts[]。
parts[].kind标出这个 Part 的模态(此处 text,也可是 file / data)。
三个对等绑定:同一抽象,三种线缆
A2A 不绑死一种传输。同一组抽象操作,规范定义了三个对等的绑定(spec §5.3),agent 在 Agent Card 的 capabilities / preferredTransport 里声明它实现了哪些、首选哪个。下表是抽象操作到三种线缆的映射——同一行的三个写法,做的是同一件事。
| 抽象操作 | JSON-RPC method | gRPC rpc | REST 路径 |
|---|---|---|---|
| 发消息 | SendMessage | SendMessage | POST /message:send |
| 流式发消息 | SendStreamingMessage | SendStreamingMessage | POST /message:stream |
| 查 Task | GetTask | GetTask | GET /tasks/{id} |
| 取消 Task | CancelTask | CancelTask | POST /tasks/{id}:cancel |
REST 列里的 :verb 冒号语法是 Google AIP 风格的自定义方法。三种绑定语义对齐,差别只在线缆形态——选哪个取决于双方基础设施:JSON-RPC over HTTP 部署最省心,gRPC 适合内网高吞吐,REST 适合已有 RESTful 生态。
v1.0 把方法名从斜杠风格的 message/send / message/stream / tasks/get / tasks/cancel 改成了与抽象名一致的 SendMessage 等。看到斜杠形态,就是在读 v0.x 的代码或文档。现存绝大多数博客和 SDK 示例仍停在 v0.x——学当前的 v1.0 形态,把斜杠形态只当迁移识别点。
| 方案 | 优势 | 为什么没选 / 代价 |
|---|---|---|
| JSON-RPC 2.0 over HTTP(+ 备选 gRPC / REST) | 建立在既有标准上,直接落进企业 infra(防火墙 / LB / auth proxy),零新基础设施;人类可读、易调试 | 不如专用协议高效;用额外提供 gRPC + REST 绑定对冲,吞吐敏感场景换 gRPC |
| 自研二进制 agent 协议 | 为 agent 通信量身定制,编解码最省、吞吐最高 | 要全套新基础设施(新代理、新网关、新调试工具);跨组织没人愿为一个新二进制协议改 infra——采用成本压垮一切 |
2.4三种交互模式
同一个委托,按任务时长和客户端形态有三种交互:请求/响应(短任务)、SSE 流(要实时进度)、webhook 推送(长任务 / 断线客户端)——三者箭头方向不同。
§2.3 的 SendMessage 解决了「发出去」,但 agent 的任务短则几秒、长则几小时。一个客户端 agent 不可能为一个跑三小时的调研任务一直挂着 HTTP 连接;一个 serverless 或移动端客户端甚至随时会断线。三种交互模式就是为不同任务时长和客户端形态准备的——它们传同样的 Task / Artifact,但谁主动、连接怎么活,完全不同。
底层机制:三种模式的箭头方向不一样
- ① 请求/响应——客户端调
SendMessage,在同一个 HTTP 响应里直接拿回 Task(或已是TASK_STATE_COMPLETED,或仍在TASK_STATE_WORKING,客户端事后用GetTask回查)。适合秒级短任务。 - ② 流式(SSE)——客户端调
SendStreamingMessage,服务端回Content-Type: text/event-stream,在一条长连接上依次推帧:先是 Task,然后是若干TaskStatusUpdateEvent(状态变化)和TaskArtifactUpdateEvent(产出分块,带 append / lastChunk 标志)。流在终态时关闭,断了可用 resubscribe 续。适合要实时看进度的中长任务。 - ③ 异步推送(webhook)——客户端先注册一个
PushNotificationConfig(webhook url + token + auth),然后可以断开连接;服务端在 Task 有重大状态变化时,主动 POST 到那个 webhook url。客户端随时可用GetTask主动回查。适合跑几小时的长任务、serverless / 移动端断线客户端。
直觉里「客户端调服务端,服务端验客户端凭证」。但 webhook 推送把方向反了:是服务端反向 POST 到客户端的 webhook url。于是要做验证的变成了客户端——它必须用密码学手段确认这个入站 POST 真的来自那个 agent,而不是攻击者伪造的(手段:JWT + JWKS 验签、HMAC、或 token + 时间戳/ID 防重放)。在这条边上,被认证的一方是服务端。这与「服务端鉴权客户端」的直觉完全相反,是 webhook 模式最容易写错、也最容易被攻击的地方(§4.4 展开 SSRF 与反向信任攻击)。
| 方案 | 优势 | 为什么没选 / 代价 |
|---|---|---|
| webhook 推送 | 只在有重大状态变化时投递,省往返;客户端可断线,serverless / 移动端不必挂连接 | 引入反向信任——客户端要验入站 POST 真伪;webhook url 还带来 SSRF 面(§4.4) |
| 客户端轮询 (反复 GetTask) |
实现简单,无反向连接、无 webhook 来源验证问题 | 浪费——多数轮询拿到「没变化」;高延迟——更新落在两次轮询之间;断线客户端干脆轮不动 |
2.5Task 生命周期状态机
Task 是有状态的:从 TASK_STATE_SUBMITTED 起步,经 WORKING,可在中途落到可中断的 INPUT_REQUIRED / AUTH_REQUIRED,最终收敛到四个终态之一。
01 章 §1.3 说 Task 有状态,但没说状态怎么在 wire 上表示。真实 JSON-RPC / REST payload 里,状态是全大写下划线串:"status": {"state": "TASK_STATE_COMPLETED", "timestamp": "...Z"}。全集八个:TASK_STATE_SUBMITTED、WORKING、INPUT_REQUIRED、AUTH_REQUIRED、COMPLETED、FAILED、CANCELED、REJECTED(另有零值 TASK_STATE_UNSPECIFIED,真实 Task 上不会出现)。小写的 completed 是 v0.3.0 旧形态。注意美式拼写 CANCELED(v0.x 的 cancelled 已移除)。
TASK_STATE_*,图中省略前缀)。注意上下两个中途态是双向的(虚线进、虚线出):INPUT_REQUIRED / AUTH_REQUIRED 不是终态,而是 WORKING 中途的可中断暂停——agent 能把一个跑着的任务暂停下来要更多输入或凭证,拿到后原地续,不必重新开一个 Task。四个右侧框是终态,任务一旦进入就不再流转。容易把 AUTH_REQUIRED 理解成「一开始没带凭证被挡」。不是——开头的鉴权失败发生在 §2.1 第②步、HTTP 层,根本到不了建 Task。AUTH_REQUIRED 是任务已经在跑(WORKING)之后,agent 中途发现需要一份新凭证(比如要代客户访问某个需额外授权的下游),于是把 Task 暂停在这个中途态、等客户端补凭证、再续跑。这个「跑到一半暂停要凭证」的能力很强,但也正是钓鱼攻击的入口——恶意 agent 可以伪造一个 INPUT_REQUIRED / AUTH_REQUIRED 骗客户端重新交出凭证(§4.6)。
2.6五个设计取舍 + opaque 的代价
把前五节的取舍并到一张表上看:每一条都是「拿了什么 / 放弃了什么」,而它们最终都收束到同一个灵魂——把对等体当不透明的黑盒。
§2.2 到 §2.5 各自讲了一个机制的取舍。这一节把它们排成一行行「选了什么 / 放弃了什么」,让取舍之间的共性显出来——前四条都在为同一件事服务:让远程对等体保持不透明(opaque)。看清这一点,就不会再试图用 A2A 去做进程内编排,也不会再把它跟 MCP 搞混。
| 设计选择 | 选了什么 | 放弃了什么 |
|---|---|---|
| JSON-RPC 2.0 / HTTP + SSE(§2.3) | 直接落进既有企业 infra,零新基础设施 | 专用协议的编解码效率——用额外的 gRPC + REST 绑定对冲 |
| 静态 Agent Card 发现(§2.2) | 去中心、可缓存、无注册中心要运维 | per-session 的 live 能力协商——只能静态声明 |
| 不透明 agent(贯穿全章) | IP 保护(藏住提示词/工具)、更小攻击面、解耦 | 对内部的深度自省与协调——黑盒不可见 |
| 有状态长任务 + 推送(§2.4 / §2.5) | 支持跑几小时的任务、断线 / serverless 客户端 | 纯请求/响应的简单——多了 Task 状态机与反向信任 |
| 模态无关 Part(§2.3) | 多模态 + 按 Part 协商内容类型给 UI 渲染 | 纯文本 string-API 的简单 |
代价收束在 opaque:黑盒换来的一切,都有同一笔账
不透明是这张表的中心行,也是全章每个取舍最终指向的地方。把远程对等体当黑盒——只暴露 Agent Card 上的能力声明,绝不暴露它的提示词、记忆、内部工具——直接换来三样东西:厂商能藏住自己的 IP,攻击面更小,双方解耦得彻底。但同一个「黑盒」属性,反过来就是代价:你看不进去。你不能像在自己掌控的多 agent 系统里那样,读对等体的中间状态、对它的推理过程做断点、把它的内部记忆当共享黑板来协调。
兄弟教程 多 Agent 协作模式 教的协作,发生在一个你自己掌控的系统内部——共享状态、读得到彼此的上下文、能在一处加约束。A2A 把这件事反了过来:对等体是一个你不掌控的黑盒,往往是另一家公司的 agent。一旦接受了「跨信任边界的不透明对等体」这个前提,你就会停止用 A2A 去做进程内编排(那是框架的活),也会停止把它跟 MCP 那种「调一个工具」搞混(MCP 的对端是工具,A2A 的对端是对等 agent)。这就是为什么 opaque 是整套协议的灵魂——它定义了 A2A 该用在哪、不该用在哪。
§跨概念综合:给一个长任务选型
把这一章的四个机制——发现、传输、交互模式、生命周期——串到一个具体场景上走一遍。这是本章每个零件第一次合在一起用。
你的 orchestrator agent 要把一件事委托给一个第三方「深度调研」agent:给它一个题目,它要爬一批来源、交叉验证、写一份带引用的报告,预计跑约 10 分钟。你的 orchestrator 跑在一个 serverless 函数里(请求超过 60 秒就会被平台杀掉)。问:发现怎么做?传输与交互模式选哪个?生命周期里要预备处理哪些状态?
把四个维度各拍一个板:① 怎么拿到这个调研 agent 的能力声明?② 同步请求/响应、SSE 流、还是 webhook 推送?③ 为什么?④ 生命周期里除了 COMPLETED,还要预备处理哪两个非终态?
展开选型推导(先自己写下答案)
① 发现——对调研 agent 的域名发 GET /.well-known/agent-card.json,读它的 skills(确认有「调研」能力)、capabilities.streaming 与 capabilities.pushNotifications(确认它支持哪些交互模式)、securitySchemes(确认怎么鉴权)。这一步在委托前就能做、还能缓存。
②③ 交互模式——选 webhook 推送。任务约 10 分钟,远超 serverless 的 60 秒上限:请求/响应会在拿到结果前就被平台杀掉;SSE 流要 orchestrator 一直挂着长连接,serverless 函数同样撑不住。webhook 推送让 orchestrator 注册一个 PushNotificationConfig 后立刻返回、释放函数,等调研 agent 在 Task 完成时 POST 回那个 webhook url 再被唤醒。代价是:收到那个 POST 时,orchestrator 这一侧必须验证它真来自调研 agent(§2.4 反向信任)——这是选 webhook 必须一起接受的负担。
④ 生命周期——除了 TASK_STATE_COMPLETED,至少要预备处理两个非终态:TASK_STATE_AUTH_REQUIRED(调研 agent 跑到一半可能要一份访问某付费来源的新凭证,把任务暂停下来等你补)和 TASK_STATE_INPUT_REQUIRED(它可能中途要你澄清调研范围)。这两个是可中断的中途态,不处理,任务就卡在那里永远等不到 COMPLETED。当然也要处理 FAILED。
一句话收束:任务时长 + 客户端形态决定交互模式(长任务 + 易断客户端 ⇒ 推送),发现在委托前就把这些能力问清楚,生命周期提醒你长任务会在中途停下来要东西。四个零件是这么咬合的。
self-check
合上教程再答。每题答完再点开对照——能复述出「为什么这么设计」「代价在哪」才算过。
-
在 A2A v1.0,把一条消息发给远程 agent,JSON-RPC 请求里
"method"字段的字面值是什么?v0.x 的旧写法是什么?答案
v1.0 是字面的
SendMessage(抽象名与 wire 方法名同字符串)。v0.x 旧写法是斜杠风格的message/send,已弃用。看到斜杠形态即在读 v0.x 代码或文档。 -
一个 Task 处于
TASK_STATE_AUTH_REQUIRED,这与「客户端一开始没带凭证、在 HTTP 层被 401 挡住」有什么本质区别?答案
开头鉴权失败发生在传输层、建 Task 之前,请求根本进不来。
AUTH_REQUIRED是 Task 已经在跑(WORKING)之后的可中断中途态——agent 中途需要一份新凭证,把任务暂停、等客户端补、再原地续,不重开 Task。前者是「没进门」,后者是「进门后跑到一半停下来要东西」。 -
webhook 推送模式里,
SendMessage模式下「服务端验证客户端凭证」的信任方向,发生了什么变化?是谁要验什么?答案
信任方向反了。webhook 推送是服务端反向 POST 到客户端的 webhook url,所以要做验证的是客户端——它必须用密码学手段(JWT+JWKS 验签 / HMAC / token + 时间戳防重放)确认这个入站 POST 真来自那个 agent。在这条边上被认证的一方是服务端。这与「服务端鉴权客户端」的直觉相反。
-
(跨机制综合)一个客户端 agent 要委托一个预计跑 2 分钟的任务,且它运行在一个 30 秒就会超时的环境里。请把「发现读哪个字段 → 选哪种交互模式 → 为什么 → 生命周期要额外预备处理什么」串成一条选型链。
答案
① 发现:读 Agent Card 的
capabilities.pushNotifications确认对端支持推送,读securitySchemes定鉴权方式。② 选 webhook 推送:任务 2 分钟 > 环境 30 秒上限,请求/响应和 SSE 长连接都会被超时杀掉;推送让客户端注册后立刻返回、释放资源,靠服务端 POST 回来唤醒。③ 代价:收到 POST 时客户端要验来源真伪(反向信任)。④ 生命周期:除COMPLETED/FAILED,要预备处理INPUT_REQUIRED/AUTH_REQUIRED两个可中断中途态,否则长任务可能停在中途等不到终态。串起来:任务时长 + 客户端形态选交互模式,发现在委托前问清能力,生命周期提示长任务会中途停下来要东西。
同一抽象、三种线缆,为什么 v0.x↔v1.0 仍会 wire 不兼容?
§2.3 说同一抽象有 JSON-RPC / gRPC / REST 三个对等绑定。既然抽象对齐,为什么社区报告 v0.x 的 agent 与 v1.0 的 agent 跨版本通信仍会失败(哪怕都用 JSON-RPC over HTTP)?想一想:除了方法名从 message/send 改成 SendMessage,wire 上还有哪些东西会因版本而不同,使得「同一种传输」也不保证互通?
提示
「对等绑定」对齐的是当前版本内部的三种传输,不跨版本。跨 v0.x↔v1.0 至少有三处会变:(1) 方法名(斜杠 vs PascalCase);(2) 状态值(小写 completed vs TASK_STATE_COMPLETED、cancelled vs CANCELED);(3) 流式响应的 Content-Type 约定(v1.0.1 倾向 application/a2a+json,而某些 v0.x 实现对 application/json 与 text/event-stream 的处理不一致——这正是社区 issue #885 的症结)。结论:传输相同 ≠ 版本兼容,方法名只是最显眼的那一处。§4.7 会把版本 wire 不兼容当成一个独立的失败模式来拆。