回路 / The Loop — 提出有价值的问题,比直接获取答案更重要。
这个专题收集和整理我与 Gemini、ChatGPT、Claude 等 AI 的对话记录。每一篇对应一次完整的提问与回答过程。通过留存这些层层追问,还原日常思考的真实轨迹。
配套图解
为了把文中的核心角色、对象模型和任务生命周期放在同一张认知地图里,我另外做了一份七章的 A2A 协议图解。如果你更习惯先看图建立全局,可以从图解开始,再回到正文查看细节。
Table of contents
Open Table of contents
- 概要
- Q:A2A 协议要解决什么问题?它和 MCP 是什么关系?
- Q:Agent 收到一条消息,为什么要区分回 Message 还是回 Task?
- Q:contextId 是什么?它和 taskId 的分工是怎样的?
- Q:Agent 中途发现信息不够,需要向用户澄清,流程是怎么走的?
- Q:任务状态的枚举值到底有哪些?
- Q:任务不可变性(Task Immutability)到底在约束什么?换来了什么?
- Q:产物(Artifacts)的版本历史谁来管?
- Q:流式和推送这两种交付机制,差别在哪?
- Q:Agent Card 里的 skills,和 Agent 内部的”工具”是一回事吗?
- Q:Agent Card 完整包含哪些字段?
- Q:双方怎么就”交付物用什么格式”达成一致?
- Q:Agent Card 的签名为什么需要”规范化”?
- Q:把这些设计取舍串起来看,A2A 在优化什么?
- 参考资料
概要
The Loop 011 讨论了多个 Agent 如何稳定地跑完一个复杂任务,那是同一个系统内部的协同问题。这一期换一个层次:当两个 Agent 分属不同公司、不同框架、互相看不到对方代码时,它们怎么互相发现、怎么把一件事委托出去、怎么追踪进度和交付结果?
这就是 A2A(Agent2Agent)协议要解决的问题。学习的入口是它的官方文档 Life of a Task,追问的路径大致这样:
A2A 和 MCP 的分工 → Agent 回应请求时为什么要区分 Message 和 Task → contextId 怎么把多轮交互串起来 → Agent 中途需要澄清时怎么挂起等人 → 任务状态到底有哪几个、怎么分类 → “任务不可变性”这条原则换来了什么 → 产物版本谁负责管 → 流式与推送两种交付机制的差别 → Agent Card 完整包含哪些字段 → 签名前为什么必须做”规范化”。
文中所有字段名、枚举值和 MUST/SHOULD 级别的约束,均以 2026-08-05 抓取的 A2A 官方规范为准。
浓缩成一句话:
A2A 的核心取舍是把”可追踪的工作单元”设为一等公民——用不可变的任务换取溯源清晰,用中断状态换取人在回路,用只声明意图不暴露实现的能力描述,换取跨组织协作的安全边界。
Q:A2A 协议要解决什么问题?它和 MCP 是什么关系?
先把两个协议的分工划清楚,否则后面全是混乱。
MCP(Model Context Protocol) 解决的是”一个 Agent 怎么使用一个工具”。它规范工具能力如何描述、入参怎么传、结构化结果怎么回。
A2A(Agent2Agent) 解决的是”一个 Agent 怎么把活委托给另一个 Agent”。它规范 Agent 之间如何互相发现、协商交互方式、管理共享任务、交换上下文和复杂结果。
官方规范 Appendix B 的原文表述是:
MCP: Focuses on standardizing how AI models and agents connect to and interact with tools, APIs, data sources, and other external resources. […] Think of MCP as the “how-to” for an agent to use a specific capability or access a resource.
A2A: Focuses on standardizing how independent, often opaque, AI agents communicate and collaborate with each other as peers. […] It’s about how agents partner or delegate work.
两者是互补而非竞争关系。规范给的组合方式很直观:A2A 客户端 Agent 请求 A2A 服务端 Agent 完成一个复杂任务,而后者在内部可能用 MCP 调用若干工具、API 和数据源来把这件事做完。
一句话记:A2A 管”谁跟谁合作”,MCP 管”用什么把活干出来”。
这个分层还有一个直接推论:A2A 刻意不规定 Agent 内部怎么调工具、怎么跟子 Agent 说话。从客户端视角看,远端 Agent 是一个 Key Concepts 页所说的 opaque(黑盒)系统——“its internal workings, memory, or tools are not exposed”。这一点在后面理解 Agent Card 的 skills 时很关键。
Q:Agent 收到一条消息,为什么要区分回 Message 还是回 Task?
这是 A2A 最基础的一个设计决策。Agent 收到客户端消息后,有两种回应形态:
- 无状态 Message:用于即时、自包含、说完就结束的交互,不需要后续状态管理。
- 有状态 Task:当这件事需要”实质性的、可追踪的、跨越一段时间的工作”时,Agent 返回一个 Task 对象,它会走完一个定义好的生命周期。
区分的意义在于成本。如果每一次”你好”都要创建一个带 ID、带状态、带历史、可查询的任务对象,系统的记账负担会毫无必要地翻倍。反过来,如果一个要跑两小时的编译任务只回一条消息,客户端就完全失去了追踪进度、取消、恢复的能力。
规范进一步把 Agent 归为三类设计模式:
| 类型 | 行为 | 适用场景 |
|---|---|---|
| Message-only | 永远只回 Message,不管理复杂状态,用 contextId 把多条消息串起来 | 直接包装 LLM 调用和简单工具的轻量 Agent |
| Task-generating | 永远只回 Task,连简单问答也建模成一个”已完成的任务” | 想省掉”该回哪个”的判断,代价是为琐碎交互也生成任务对象 |
| Hybrid | 两者都用:先用 Message 协商能力和工作范围,范围定了再生成 Task 追踪执行 | 需要先对齐需求、再承诺资源的复杂协作 |
有一条约束值得单独记住,规范对 Task-generating 和 Hybrid 两类都强调了:一旦任务被创建,Agent 对后续消息就只返回 Task 对象;一旦任务完成,就不能再往它发消息了(“once a task is created, the agent will only return Task objects in response to messages sent, and once a task is complete, no more messages can be sent”)。
Hybrid 模式的价值就在这个”先协商后承诺”上——用便宜的消息把需求问清楚,避免用昂贵的任务去试错。
Q:contextId 是什么?它和 taskId 的分工是怎样的?
contextId 是把多个独立的 Task 和 Message 在逻辑上归为同一件事的标识符。
规范描述的机制是这样的:
- 客户端第一次发消息时,Agent 返回一个新的
contextId;如果同时启动了任务,还会有一个taskId。 - 客户端后续发消息时带上同一个
contextId,表示”还在推进之前那件事”。 - 客户端可选地带上
taskId,表示”这是针对那个具体任务的延续”。
关键在于两者的粒度不同:contextId 是会话级的(一个共享目标、一段共享上下文),taskId 是工作单元级的。一个 contextId 下可以有多个任务,甚至可以并发。
规范还提到一个实现层面的细节:A2A Agent(尤其是基于 LLM 的)在内部用 contextId 来管理自己的会话状态或 LLM 上下文。也就是说,这个字段对外是分组标识,对内往往就是”该加载哪段记忆”的钥匙。
并行后续任务是这个设计最直接的收益。规范给的例子:
- Task 1:订一张去赫尔辛基的机票
- Task 2:基于 Task 1,订酒店
- Task 3:基于 Task 1,订雪地摩托活动
- Task 4:基于 Task 2,在酒店订单上加一个 SPA 预约
四个任务共享一个 contextId,各自独立追踪,依赖关系由客户端通过引用前序任务来表达。任务 2 和 3 可以同时进行,因为它们只依赖 1。
Q:Agent 中途发现信息不够,需要向用户澄清,流程是怎么走的?
这是我在这次学习里最关心的一环——它决定了”人在回路”(Human-in-the-loop)能不能被协议自然表达。
规范的做法是给任务定义了中断状态(interrupted state)。以一条自驾路线规划为例:
- 发起:客户端发消息”帮我规划一条从珀斯出发、途经埃斯佩兰斯、最后到奥尔巴尼的自驾路线”,系统生成新的
contextId和taskId。 - 中断:Agent 开始处理,发现缺少关键参数(玩几天?开什么车?),于是把任务状态置为
TASK_STATE_INPUT_REQUIRED,并在状态里附带具体的追问。此时 Agent 挂起等待,既不猜测也不失败。 - 客户端接管:客户端收到这个状态,知道机器之间聊不下去了,把问题呈现给人类。
- 携带上下文继续:人类回答后,客户端把答案作为一条新 Message 发回,带上原来的
contextId(以及指向该任务的引用)。 - 恢复:Agent 拿回上下文,补齐缺失信息,继续往下推进,最终到达
TASK_STATE_COMPLETED并交付产物。
同一套机制也用于授权:当 Agent 走到敏感操作(读私人邮件、执行付款、改关键配置)时,它可以置为 TASK_STATE_AUTH_REQUIRED,把授权决定权交回给客户端和人。
这里有一个极容易搞错、且直接影响实现的点,值得专门强调:
中断状态不是终止状态,任务不会因此死掉。 官方文档把这两类状态明确并列且互斥:
[…] until it reaches an interrupted state (e.g.,
input-required,auth-required) or a terminal state (e.g.,completed,canceled,rejected,failed).
也就是说,input-required 期间任务只是挂起等待,补充信息后它在原 taskId 上恢复执行,不需要(也不应该)新建任务。下一节会讲到的”任务不可变性”约束的是终止状态,跟 input-required 无关。这两件事如果混为一谈,实现上就会在每次澄清时都新开一个任务,凭空制造大量碎片任务和错误的溯源链。
这个设计的好处是”不确定性”有了一个明确的落点。Agent 遇到障碍时不会卡在一个薛定谔式的未知状态里,要么明确失败,要么明确停在中断状态等人,系统行为始终可预期。
Q:任务状态的枚举值到底有哪些?
规范 §4.1.3 TaskState 的完整枚举:
| 取值 | 含义 | 分类 |
|---|---|---|
TASK_STATE_UNSPECIFIED | 未知或不确定状态 | — |
TASK_STATE_SUBMITTED | 任务已成功提交并被确认 | 活跃 |
TASK_STATE_WORKING | Agent 正在处理该任务 | 活跃 |
TASK_STATE_INPUT_REQUIRED | 需要额外的用户输入才能继续 | 中断 |
TASK_STATE_AUTH_REQUIRED | 需要鉴权才能继续 | 中断 |
TASK_STATE_COMPLETED | 成功结束 | 终止 |
TASK_STATE_FAILED | 以错误结束 | 终止 |
TASK_STATE_CANCELED | 完成前被取消 | 终止 |
TASK_STATE_REJECTED | Agent 决定不执行该任务(可发生在创建时,也可在开始后判定无法/不愿继续) | 终止 |
几个容易踩的点:
-
所有取值都带
TASK_STATE_前缀。 这是 Protocol Buffer 枚举的命名惯例,不是可省略的修饰。写COMPLETED而不是TASK_STATE_COMPLETED是无效值。 -
起始状态是
SUBMITTED,执行中是WORKING。 不是PENDING、也不是RUNNING——这两个值在 A2A 里都不存在。规范里有一处ListTasks参数校验的错误示例,恰好就是用TASK_STATE_RUNNING来演示”传了非法值会返回 400”:"message": "Invalid status value 'TASK_STATE_RUNNING'. Must be one of: TASK_STATE_SUBMITTED, TASK_STATE_WORKING, TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED, TASK_STATE_REJECTED, TASK_STATE_INPUT_REQUIRED, TASK_STATE_AUTH_REQUIRED"这份”合法值列表”正好也是最权威的对照表。
-
REJECTED不限于”一眼拒绝”。 规范原文明确它也可以发生在 Agent 开始处理之后(“or later once an agent has determined it can’t or won’t proceed”)。 -
UNSPECIFIED不算可用的过滤值。 它存在于枚举里,但从上面那份ListTasks合法值列表看,它不在其中(只有其余八个)。
Q:任务不可变性(Task Immutability)到底在约束什么?换来了什么?
规范的表述很干脆:
Once a task reaches a terminal state (completed, canceled, rejected, or failed), it cannot restart. Any subsequent interaction related to that task, such as a refinement, must initiate a new task within the same
contextId.
注意主语是终止状态。任务终止后不能重启,想继续只能在同一个 contextId 下开新任务,并通过 Message 的 referenceTaskIds 字段指向原任务。
规范列了三条收益:
- 可靠引用:客户端能稳定地引用某个任务及其状态、产物和消息,输入到输出的映射是干净的。这对编排和溯源很有价值。
- 清晰的工作单元:每个新请求、细化、追问都是一个独立任务,记账简单,能按粒度追踪 Agent 的工作量,每个产物都能追到具体的工作单元。
- 实现更简单:开发者不用再纠结”这应该是新建任务还是重启旧任务”——这个决策被协议消灭了。
第三条容易被低估。“重启还是新建”这种二选一在分布式系统里会衍生出大量边界情况(重启时旧产物算谁的?状态历史怎么合并?并发重启怎么办?)。协议直接禁掉一个选项,把一类复杂性从每个实现者的桌上拿走了。
官方文档给的例子很直观:用户先让 Agent 画一艘海上的帆船,Agent 返回 Task 1(TASK_STATE_COMPLETED)和一张图片产物;用户接着说”把帆船改成红色”,带上同一个 contextId 和 referenceTaskIds: ["task-boat-gen-123"];Agent 新建 Task 2,返回一张红色帆船的图,产物名称沿用 sailboat_image.png,但 artifactId 是新的。
把它和上一节合起来看,两条规则的边界就清楚了:中断 → 原任务恢复;终止 → 新建任务。
Q:产物(Artifacts)的版本历史谁来管?
这个问题的答案有点反直觉:A2A 协议明确把它推给客户端,并且不把这种关联关系写进协议。
规范的理由是”谁有资格判断”:
[…] the client is in the best position to manage this artifact linkage. The client determines what constitutes an acceptable result and has the ability to accept or reject new versions. Therefore, the serving agent shouldn’t be responsible for tracking artifact mutations, and this linkage is not part of the A2A protocol specification.
换句话说,“哪个版本算数”本质上是验收判断,而验收权在客户端。服务端 Agent 无从知道用户觉得第 3 版比第 5 版更好,所以协议干脆不让它承担这个责任。客户端应当自己维护版本历史,并把”最新可接受版本”呈现给用户。
作为配合,规范给服务端 Agent 两条约定:
- 生成某个既有产物的细化版本时,沿用一致的 artifact name,只更换
artifactId——这样客户端能识别”这是同一个东西的新版本”。 - 客户端发起细化任务时应显式引用要改的那个产物;如果没给引用,Agent 可以基于
contextId推断,如果推断不出或有歧义,就返回input-required要求澄清(而不是猜)。
最后那条又一次印证了中断状态的用途:歧义的正确处理方式是停下来问,而不是赌一个。
Q:流式和推送这两种交付机制,差别在哪?
先说 Agent Card 里能声明的能力(§4.4.3 AgentCapabilities)只有四个字段:
| 字段 | 类型 | 含义 |
|---|---|---|
streaming | boolean | 是否支持流式响应 |
pushNotifications | boolean | 是否支持为异步任务更新发送推送通知 |
extensions | array | 支持的协议扩展列表 |
extendedAgentCard | boolean | 鉴权后是否提供扩展版 Agent Card |
注意字段名就是 streaming 和 pushNotifications,没有 supports 前缀。
两种机制的适用场景,规范说得很清楚:
流式(Streaming) — 客户端用 SendStreamingMessage 方法发送初始消息并同时订阅该任务的更新,服务端以 Server-Sent Events(text/event-stream)持续推送。适用于产生增量结果的任务(生成长文档、流式媒体)或需要持续状态更新的场景。
For tasks that produce incremental results […] or provide ongoing status updates, A2A supports real-time communication using Server-Sent Events (SSE).
推送通知(Push Notifications) — 适用于超长任务(数分钟、数小时乃至数天),或客户端无法/不愿维持长连接的情况(移动端、Serverless 函数)。Agent 通过 HTTP POST 把更新发到客户端注册的 webhook 地址。
For very long-running tasks […] or when clients are unable to or prefer not to maintain persistent connections (like mobile clients or serverless functions), A2A supports asynchronous updates using push notifications.
推送的载荷格式和流式是同一个:StreamResponse 对象,内容为 task / message / statusUpdate / artifactUpdate 之一——也就是说推送可以携带产物更新,不只是轻量元数据。实践中收到通知并验证其真实性后,客户端通常会用 GetTask 方法凭 taskId 去取回完整的、更新后的 Task 对象。
值得说明的是:协议层面被声明、被定义的交互机制只有这两种(SSE 流式 + 推送通知)。GetTask / ListTasks 是普通的请求-响应方法,客户端当然可以反复调用它们来实现轮询,但那属于使用方式,既没有对应的能力声明位,也不是协议专门定义的交互模式。设计系统时不要去找”异步/轮询能力”的开关——它不存在。
最后是一个容易被忽略的安全细节:规范对 webhook 两端都有硬性要求。Agent 侧 MUST 在 webhook 请求中携带鉴权凭据,SHOULD 设置合理超时(建议 10–30 秒)、实现指数退避重试,并且 SHOULD 校验 webhook URL 以防 SSRF(拒绝私有网段 127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,拒绝 localhost 和链路本地地址)。客户端侧 MUST 验证 webhook 的真实性。把客户端提供的回调地址当作可信输入,是这类架构最典型的漏洞。
Q:Agent Card 里的 skills,和 Agent 内部的”工具”是一回事吗?
不是,而且这个区分是理解 A2A 边界的关键。
Agent Card 里的 skills 是对外的服务能力声明:它面向其他 Agent 和编排系统,用于被”发现”,粒度是宏观的业务级别(“交通感知路线优化”、“个性化地图生成”),内容是自然语言描述加上输入输出格式,不暴露任何实现细节。
Agent 内部执行时用的”工具”(MCP tools、function calling、框架里的 node)是可执行单元:它面向 Agent 自己体内的大模型,粒度是微观原子级别(“读 PDF”、“查数据库”、“算汇率”),需要向模型暴露明确的入参 schema。
两者是接口与实现的关系。Agent Card 上的 1 个公开 skill,背后往往封装了内部的 N 个工具调用;Agent 内部可能有 20 个底层能力,对外只声明 3 个有业务价值的 skill。
这不是实现习惯,而是协议的明确边界:A2A 不规定 Agent 如何调用工具、如何与子 Agent 通信——那属于框架层或 MCP 的职责范围。远端 Agent 在客户端眼里始终是 opaque 的黑盒。如果 Agent Card 暴露的是底层工具(比如内部连的哪个数据库),A2A 就退化成了一个微服务 RPC 协议,同时带来安全和商业机密上的风险。
Q:Agent Card 完整包含哪些字段?
Agent Card 是 Agent 的自描述清单,通常托管在 /.well-known/agent-card.json。它声明身份、能力、技能、支持的通信方式和安全要求,让别的 Agent 在不接触任何内部代码的前提下判断”你是谁、能干什么、怎么安全地跟你说话”。
规范 §4.4.1 的完整字段表:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✅ | 人类可读的 Agent 名称 |
description | string | ✅ | 说明用途,帮助人和其他 Agent 理解它做什么 |
supportedInterfaces | array of AgentInterface | ✅ | 有序的接口列表,第一项为首选 |
version | string | ✅ | Agent 版本,如 "1.0.0" |
capabilities | AgentCapabilities | ✅ | 支持的 A2A 能力集 |
defaultInputModes | array of string | ✅ | 跨所有技能支持的输入模态,以 media type 定义,可被单个技能覆盖 |
defaultOutputModes | array of string | ✅ | 支持的输出 media type |
skills | array of AgentSkill | ✅ | Agent 的能力单元 |
provider | AgentProvider | — | 服务提供方(organization + url) |
documentationUrl | string | — | 额外文档地址 |
securitySchemes | map of string → SecurityScheme | — | 鉴权方案详情 |
securityRequirements | array of SecurityRequirement | — | 联系该 Agent 的安全要求 |
signatures | array of AgentCardSignature | — | 为本卡片计算的 JSON Web Signature |
iconUrl | string | — | 图标地址 |
鉴权是 securitySchemes + securityRequirements 两个字段,前者定义有哪些方案(如 OpenID Connect、OAuth2),后者声明联系这个 Agent 需要满足什么。Agent Card 上没有叫 authentication 的字段——规范里那个 authentication 属于推送通知配置(PushNotificationConfig.authentication,用于 webhook 回调的鉴权),是完全不同的东西。
服务地址在 supportedInterfaces 里,不是一个扁平的 URL 字符串。 每个 AgentInterface 包含:
url(必填)——接口地址。HTTP 类传输在生产环境必须是合法的绝对 HTTPS URL;gRPC 用hostname:port格式。protocolBinding(必填)——该地址支持的协议绑定,官方核心支持JSONRPC、GRPC、HTTP+JSON。这是开放字符串,便于扩展。protocolVersion(必填)——该接口暴露的 A2A 协议版本,如"0.3"、"1.0"。tenant(可选)——多租户路由用的不透明字符串。
这个结构的意义是:同一个 Agent 可以把同一套功能同时暴露在多种协议绑定上,客户端按列表顺序挑自己支持的。用一个扁平的字符串是表达不了这件事的。
再看 skills(§4.4.5 AgentSkill):
| 字段 | 必填 | 说明 |
|---|---|---|
id | ✅ | 技能唯一标识 |
name | ✅ | 人类可读名称 |
description | ✅ | 详细描述 |
tags | ✅ | 描述该技能能力的关键词集合 |
examples | — | 示例提示或场景 |
inputModes / outputModes | — | 该技能的输入/输出 media type,覆盖 Agent 默认值 |
securityRequirements | — | 该技能所需的安全方案 |
id、name、description、tags 四个都是必填,tags 尤其容易漏;而 inputModes / outputModes 是可选的,缺省时继承 Agent 级的默认值。
Q:双方怎么就”交付物用什么格式”达成一致?
机制比想象的朴素:Agent 在 Agent Card 里静态声明自己能收什么、能出什么(defaultInputModes / defaultOutputModes,技能级可覆盖),客户端在发现阶段读到这些声明,据此决定要不要把活委托过来。 技能级声明优先于 Agent 级默认值。
这里有一个概念要点:A2A 的模态不是 TEXT / IMAGE / AUDIO 这样的枚举,而是标准 media type(MIME 类型)字符串。 官方示例 Agent 的声明是:Agent 级 defaultInputModes: ["application/json", "text/plain"]、defaultOutputModes: ["application/json", "image/png"];而”个性化地图生成”这个技能把自己的 outputModes 覆盖为 ["image/png", "image/jpeg", "application/json", "text/html"],其中还包括 application/vnd.geo+json 这种领域专用类型。
用 media type 而不是自定义枚举,意味着模态系统是开放的:可以直接复用现有的 IANA 媒体类型注册表,协议不需要为每种新格式扩充枚举值。
要留意的是,规范到此为止——它没有定义”格式不匹配就自动拒绝”的协议级校验,也没有定义客户端用 JSON Schema 约束响应结构的字段或流程。实践中 Agent 完全可以因为要求的格式不支持而返回 REJECTED(这落在”Agent 决定不执行该任务”的语义范围内),但那是实现的选择,不是协议的保证。别指望有一层协议级校验帮你兜底,该写的校验还是要自己写。
Q:Agent Card 的签名为什么需要”规范化”?
Agent Card 可以带 signatures 字段,内容是 JSON Web Signature(RFC 7515)。问题在于:JSON 的同一份语义内容可以有无数种字节表示——键的顺序、缩进空白、Unicode 转义方式、数字格式都可能不同。如果直接对原始 JSON 字符串做哈希,两台服务器对着同一份卡片会算出不同的签名,验证必然失败。
所以规范 §8.4.1 要求:签名前,Agent Card 内容 MUST 用 JCS(JSON Canonicalization Scheme,RFC 8785) 规范化。RFC 8785 规定了键的字典序排列、数字与字符串的一致表示、无意义空白的移除。
除此之外还有两条规则:
字段存在性语义。 规范化前,JSON 表示 MUST 遵守 Protocol Buffer 的字段存在性语义,使规范化形式能准确反映哪些字段是显式提供的、哪些是被省略的:
- 标记为
optional但未显式设置的字段 MUST 从 JSON 中省略; - 标记为
optional且被显式设置的字段 MUST 包含,即使它的值恰好等于默认值; - 标记为
REQUIRED的字段 MUST 始终存在,即使值等于默认值; - 有默认值的字段 MUST 省略,除非它是
REQUIRED或带optional关键字。
签名字段自身排除。 signatures 字段 MUST 从被签名内容中排除,避免循环依赖。
规范给的例子很能说明”显式的 false”和”缺失”不是一回事:
// 原始片段
{"name":"Example Agent","description":"","capabilities":{"streaming":false,"pushNotifications":false,"extensions":[]},"skills":[]}
streaming: false 和 pushNotifications: false 虽然值就是默认值,但因为在 JSON 里显式出现过,所以保留;extensions: [] 是非 REQUIRED 的空数组,所以省略。应用 RFC 8785 后:
{"capabilities":{"pushNotifications":false,"streaming":false},"description":"","name":"Example Agent","skills":[]}
这里的核心认知是:在签名的语境下,“我明确说了 false” 和 “我没说” 是两个不同的事实,指纹必须能区分它们。 一个把省略字段补上默认值的实现,会算出完全不同的签名。
Q:把这些设计取舍串起来看,A2A 在优化什么?
读完一圈,几个看似独立的规定其实指向同一个方向:让”一件工作”成为可被外部系统可靠引用的一等公民。
- 任务不可变性(终止后不可重启,只能新建)——换来的是干净的输入输出映射。每个产物都能追到唯一的工作单元,编排系统不需要理解”这个任务被重启过三次”这种历史。
- 中断状态与终止状态分离——换来的是可预期的挂起点。不确定性有明确落点:要么明确失败,要么停在原地等人,不存在薛定谔式的未知态。
- 产物版本归客户端管(且明确不写进协议)——按”谁有资格判断”来分配责任。验收权在客户端,那么版本取舍权也应该在客户端。
- skills 只声明意图、不暴露实现——换来的是跨组织协作的安全边界。Agent 之间以黑盒相待,A2A 才不会退化成微服务 RPC。
- 模态用 media type 而非自定义枚举——换来的是开放性,复用既有标准而不是自建一套。
- 签名前强制 JCS 规范化——换来的是分布式互信。不同语言、不同框架实现的 Agent 能对同一份卡片算出同一个指纹。
如果只记一条,我会记这个:A2A 的大部分复杂性,都花在”让一件事在跨组织、跨框架的环境里仍然可追踪”上。 它宁愿多要求几条 MUST(前缀、规范化、字段存在性语义),也不愿把”这个任务现在到底算什么状态”这种问题留给实现者各自解释——因为在分布式协作里,语义的歧义比功能的缺失代价更高。
参考资料
A2A 协议官方文档(本文所有字段名、枚举值与约束的依据,抓取日期 2026-08-05)
- A2A Protocol Specification — 完整规范。本文重点引用 §4.1.3 TaskState、§4.4.1–4.4.6 AgentCard 及子对象、§8.4.1 Canonicalization Requirements、§8.5 Sample Agent Card、§13.2 Push Notification Security、Appendix B
- Life of a Task — 本次学习的起点:Message vs Task、
contextId、任务不可变性、并行后续任务、产物变更追踪 - Streaming & Asynchronous Operations — SSE 流式与推送通知两种机制的适用场景和载荷格式
- Key Concepts — Agent Card、Task、Message、Artifact 的定义,以及远端 Agent 的 opaque(黑盒)特性
- A2A and MCP — 两个协议的分层对比:“A2A is about agents partnering on tasks, while MCP is more about agents using capabilities”
- Agent Discovery — Agent Card 的发现机制与
/.well-known/agent-card.json约定
相关规范
- RFC 8785 — JSON Canonicalization Scheme (JCS) — Agent Card 签名前规范化所依据的标准:键的字典序、数字与字符串的一致表示、空白移除
- RFC 7515 — JSON Web Signature (JWS) —
AgentCardSignature采用的签名格式 - Model Context Protocol — A2A 的互补协议,负责 Agent 与工具/资源的交互层
延伸讨论
- A2A protocol: Demystifying Tasks vs Messages — 官方 Life of a Task 文档在讲 Hybrid Agent 时引用的讨论帖
相关阅读
- The Loop 011 — 从 Hermes Kanban 到确定性 DAG — 单一系统内部的多 Agent 协同,与本期的跨组织协作互为两面
- The Loop 004 — MCP 企业多用户认证架构 — MCP 侧的鉴权设计,可与本期
securitySchemes对照 - The Loop 009 — 从 Building Effective Agents 到 Claude Code 动态工作流 — Agent 工作流模式的系统梳理