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 上传的是什么、凭证在哪一层、状态存在谁那里。

A2A 客户端 远程对等 agent ① 发现 GET /.well-known/agent-card.json 返回 url · skills · capabilities ② 鉴权 凭证放 HTTP header(不进 payload) ③ 发消息 SendMessage(params.message: role=user, parts[]) ④ 建 Task 有状态 Task ⑤ 状态流转 status.state: SUBMITTED → WORKING ⑥ 取 Artifact COMPLETED + Artifact(artifactId + parts[]) 产出是 Artifact,不是裸字符串
图 2.1一次任务委托端到端。注意三处与直觉不同的边界:一·第②步凭证走的是 HTTP header,不在 JSON-RPC payload 里——Agent Card 只声明 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 一贯能做什么」,而不是「此刻、对你这个调用方,它能做什么」。

易错 · agent.json 是 v0.x 的废弃路径

大量现存博客与旧 SDK 仍写 GET /.well-known/agent.json。v0.3.0 起路径改名为 agent-card.json。看到 agent.json 一律按 v0.x 旧形态处理——这是迁移点,不是当前形态。

表 2.2 · 备选方案对照——三种发现机制
方案优势为什么没选 / 代价
静态 Agent Card
(/.well-known/ JSON)
去中心、可缓存、无注册中心要运维;像 robots.txt 一样人人会取 能力静态声明,无 live 协商;想暴露更多得靠 authenticated GetExtendedAgentCard 补丁
中央注册中心 能集中检索、按能力搜索、统一治理 多一个要高可用的服务与单点故障;跨组织谁来运营、谁可信都成问题——与「跨信任边界对等协作」相悖
运行时能力协商
(MCP 的 initialize 握手)
每个会话现场协商能力,按调用方动态裁剪 每次连接都要一轮握手往返,无法预先缓存;A2A 选了「连接前就知道」而非「连上才协商」
洞察 · 这恰好与你熟的 MCP 相反

读过 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 里声明它支持哪些、首选哪个。

底层机制:抽象方法名 = 字面 wire 方法名

在 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 形态。

SendMessage 请求 · JSON-RPC 2.0 over HTTP json
{
  "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 里声明它实现了哪些、首选哪个。下表是抽象操作到三种线缆的映射——同一行的三个写法,做的是同一件事。

表 2.3 · 方法映射(spec §5.3)——抽象操作 → 三种传输
抽象操作JSON-RPC methodgRPC rpcREST 路径
发消息SendMessageSendMessagePOST /message:send
流式发消息SendStreamingMessageSendStreamingMessagePOST /message:stream
查 TaskGetTaskGetTaskGET /tasks/{id}
取消 TaskCancelTaskCancelTaskPOST /tasks/{id}:cancel

REST 列里的 :verb 冒号语法是 Google AIP 风格的自定义方法。三种绑定语义对齐,差别只在线缆形态——选哪个取决于双方基础设施:JSON-RPC over HTTP 部署最省心,gRPC 适合内网高吞吐,REST 适合已有 RESTful 生态。

迁移注记 · v1.0 SendMessage vs v0.x message/send

v1.0 把方法名从斜杠风格的 message/send / message/stream / tasks/get / tasks/cancel 改成了与抽象名一致的 SendMessage 等。看到斜杠形态,就是在读 v0.x 的代码或文档。现存绝大多数博客和 SDK 示例仍停在 v0.x——学当前的 v1.0 形态,把斜杠形态只当迁移识别点。

表 2.3b · 备选方案对照——传输协议怎么选
方案优势为什么没选 / 代价
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 / 移动端断线客户端。
① 请求 / 响应 client agent SendMessage Task 一来一回 连接即关 ② SSE 流 client agent SendStreaming Task StatusUpdate ArtifactUpdate 一次请求 连推多帧 ③ webhook 推送 client agent 注册 webhook ↯ 断线 POST 更新 客户端要 验来源真伪
图 2.2三种交互的箭头方向。注意那条反直觉的边:模式 ③ 里服务端反向发起 POST 打到客户端的 webhook(朱红箭头从 agent 指回 client),这把信任方向也反了过来——见下面的「反向信任」要点。模式 ① 一来一回连接即关;模式 ② 一次请求换服务端连推多帧。
反向信任 · webhook 推送里要验来源的是客户端

直觉里「客户端调服务端,服务端验客户端凭证」。但 webhook 推送把方向反了:是服务端反向 POST 到客户端的 webhook url。于是要做验证的变成了客户端——它必须用密码学手段确认这个入站 POST 真的来自那个 agent,而不是攻击者伪造的(手段:JWT + JWKS 验签、HMAC、或 token + 时间戳/ID 防重放)。在这条边上,被认证的一方是服务端。这与「服务端鉴权客户端」的直觉完全相反,是 webhook 模式最容易写错、也最容易被攻击的地方(§4.4 展开 SSRF 与反向信任攻击)。

表 2.4 · 备选方案对照——推送 vs 轮询(长任务怎么拿更新)
方案优势为什么没选 / 代价
webhook 推送 只在有重大状态变化时投递,省往返;客户端可断线,serverless / 移动端不必挂连接 引入反向信任——客户端要验入站 POST 真伪;webhook url 还带来 SSRF 面(§4.4)
客户端轮询
(反复 GetTask)
实现简单,无反向连接、无 webhook 来源验证问题 浪费——多数轮询拿到「没变化」;高延迟——更新落在两次轮询之间;断线客户端干脆轮不动

2.5Task 生命周期状态机

Task 是有状态的:从 TASK_STATE_SUBMITTED 起步,经 WORKING,可在中途落到可中断的 INPUT_REQUIRED / AUTH_REQUIRED,最终收敛到四个终态之一。

底层机制:wire 上的状态值是 SCREAMING_SNAKE

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 已移除)。

start SUBMITTED 已收下 WORKING 执行中 INPUT_REQUIRED 要补输入 · 可中断 AUTH_REQUIRED 要补凭证 · 可中断 暂停 / 续 暂停 / 续 COMPLETED ✓ FAILED CANCELED REJECTED 刚提交即可拒 终态
图 2.3Task 生命周期状态机(wire 值为 TASK_STATE_*,图中省略前缀)。注意上下两个中途态是双向的(虚线进、虚线出):INPUT_REQUIRED / AUTH_REQUIRED 不是终态,而是 WORKING 中途的可中断暂停——agent 能把一个跑着的任务暂停下来要更多输入或凭证,拿到后原地续,不必重新开一个 Task。四个右侧框是终态,任务一旦进入就不再流转。
洞察 · auth-required 不是开头那个 401

容易把 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 搞混。

表 2.6 · A2A 的五个核心设计取舍
设计选择选了什么放弃了什么
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 框架世界翻了过来

兄弟教程 多 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

合上教程再答。每题答完再点开对照——能复述出「为什么这么设计」「代价在哪」才算过。

  1. 在 A2A v1.0,把一条消息发给远程 agent,JSON-RPC 请求里 "method" 字段的字面值是什么?v0.x 的旧写法是什么?
    答案

    v1.0 是字面的 SendMessage(抽象名与 wire 方法名同字符串)。v0.x 旧写法是斜杠风格的 message/send,已弃用。看到斜杠形态即在读 v0.x 代码或文档。

  2. 一个 Task 处于 TASK_STATE_AUTH_REQUIRED,这与「客户端一开始没带凭证、在 HTTP 层被 401 挡住」有什么本质区别?
    答案

    开头鉴权失败发生在传输层、建 Task 之前,请求根本进不来。AUTH_REQUIRED 是 Task 已经在跑(WORKING)之后的可中断中途态——agent 中途需要一份新凭证,把任务暂停、等客户端补、再原地续,不重开 Task。前者是「没进门」,后者是「进门后跑到一半停下来要东西」。

  3. webhook 推送模式里,SendMessage 模式下「服务端验证客户端凭证」的信任方向,发生了什么变化?是谁要验什么?
    答案

    信任方向反了。webhook 推送是服务端反向 POST 到客户端的 webhook url,所以要做验证的是客户端——它必须用密码学手段(JWT+JWKS 验签 / HMAC / token + 时间戳防重放)确认这个入站 POST 真来自那个 agent。在这条边上被认证的一方是服务端。这与「服务端鉴权客户端」的直觉相反。

  4. (跨机制综合)一个客户端 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 不兼容当成一个独立的失败模式来拆。