0%

A2A是「胡扯协议」吗?从Agent Card重构到真实抓包找证据

2026年6月,balaji bal发表的《A2A Is a Nonsense Protocol》把A2A批得很直接:没有把消息总线当成一等公民,导致智能体强耦合、缺少背压(下游处理不过来时,无法协调上游减速、暂停或削峰)、可观测性碎片化,而且消息无法回放。

原文将点对点A2A与消息流架构进行对比

图:原作者对“直接A2A”与“消息流优先”架构的观点示意,图片来自原文。这里不代表本文已接受其结论。

这些批评并非全无道理,但它们混合了三个不同层次的问题:协议语义、传输模型,以及生产环境的治理能力。要判断A2A到底是不是“胡扯协议”,不能只看架构观点,还要看协议为什么这样演进,以及真实报文经过网关时到底发生了什么。

本文先讲清A2A解决的问题,再追溯两个容易被误读的破坏性改动:Agent Card为什么删除根层url,以及tasks/send为什么改成message/send。最后用Wireshark抓包验证一次真实的双Agent委托链路。

A2A不是另一个MCP

MCP解决的是智能体的“垂直调用”:一个Agent怎样访问工具、数据库和API。A2A解决的是“水平协作”:当一个Agent没有权限、知识或职责边界去完成整件事时,它怎样发现另一个Agent、了解其能力,并把任务委托出去。

二者可以同时出现在一套系统中:上层Agent通过A2A发现和委托另一个Agent;每个Agent再通过MCP访问自己的工具、数据库和API。二者分工不同,不是互相替代。

A2A负责Agent协作,MCP负责能力调用

没有A2A时,不同框架通常只能互相硬编码私有接口。A2A尝试把发现、消息、任务状态、流式更新和交付物抽象为框架无关的协议契约。

A2A 1.0规范把协议拆成三层:最上层是所有实现共同理解的数据模型;中间是与传输无关的抽象操作;底层才是JSON-RPC、gRPC、HTTP+JSON/REST以及自定义协议绑定。这样,SendMessage等操作的语义不必随底层传输变化,新增协议绑定时也不需要重写Task、Message、Artifact等核心对象。

            
            graph TB
    subgraph L1 ["A2A Data Model"]
        direction LR
        A[Task] ~~~ B[Message] ~~~ C[AgentCard] ~~~ D[Part] ~~~ E[Artifact] ~~~ F[Extension]
    end

    subgraph L2 ["A2A Operations"]
        direction LR
        G[Send Message] ~~~ H[Send Streaming Message] ~~~ I[Get Task] ~~~ J[List Tasks] ~~~ K[Cancel Task] ~~~ L[Get Agent Card]
    end

    subgraph L3 ["Protocol Bindings"]
        direction LR
        M[JSON-RPC Methods] ~~~ N[gRPC RPCs] ~~~ O[HTTP/REST Endpoints] ~~~ P[Custom Bindings]
    end

    L1 --> L2
    L2 --> L3

    style A fill:#e1f5fe
    style B fill:#e1f5fe
    style C fill:#e1f5fe
    style D fill:#e1f5fe
    style E fill:#e1f5fe
    style F fill:#e1f5fe
    style G fill:#f3e5f5
    style H fill:#f3e5f5
    style I fill:#f3e5f5
    style J fill:#f3e5f5
    style K fill:#f3e5f5
    style L fill:#f3e5f5
    style M fill:#e8f5e8
    style N fill:#e8f5e8
    style O fill:#e8f5e8
    style L1 fill:#f0f8ff,stroke:#333,stroke-width:2px
    style L2 fill:#faf0ff,stroke:#333,stroke-width:2px
    style L3 fill:#f0fff0,stroke:#333,stroke-width:2px
          

图:A2A 1.0规范的三层结构。数据模型和抽象操作保持稳定,各协议绑定负责提供具体的方法名、端点和传输行为。

时间线上有几个关键节点:Google于2025年4月9日发布A2ALinux Foundation于2025年6月23日接收并托管该项目A2A v1.0.0则在2026年3月12日发布

A2A协议演进时间线

A2A 1.0为什么重构Agent Card

每个A2A Agent都可以在/.well-known/agent-card.json暴露Agent Card。它不仅描述名字和技能,也告诉客户端“应该到哪里、用什么协议、按哪个版本访问我”。

A2A 1.0对这张”名片”做了不向前兼容的重构。最显眼的变化,是删除根层urlpreferredTransportadditionalInterfaces这三个字段,并把protocolVersion从根层迁移到每个接口项,统一改成有序的supportedInterfaces[]

A2A 1.0中的核心结构是一个有序数组:

1
2
3
4
5
6
7
8
9
10
{
"name": "Data Analysis AI Agent",
"supportedInterfaces": [
{
"url": "http://127.0.0.1:3001/",
"protocolBinding": "HTTP+JSON",
"protocolVersion": "1.0"
}
]
}

第一个接口是首选项。一个Agent若同时支持不同传输或不同协议版本,可以发布多个接口项。url没有消失,只是从Card根层移进了每个接口项。

为什么删除根层url

早期Card把”首选接口”和”其他接口”拆成两种结构:根层url配合preferredTransport描述首选接口,additionalInterfaces[]再描述其余接口。客户端既要读两处数据,还要理解它们之间的优先级;新增协议绑定时也容易出现重复或冲突。

这两套结构要怎么合并才不至于越改越乱?1.0把它们合并为一个有序数组:supportedInterfaces[0]就是首选接口,后续元素是备选接口。所有接口都采用同一种AgentInterface结构,地址、协议绑定和版本放在一起,不再需要根层字段表达特例。这是删除url最直接的原因。

具体过程如下:2025年11月20号合入的PR #1160先把旧字段标记为弃用,对应commit可以直接看到三个根层字段的弃用标记;随后11月24号的Issue #1227提出趁1.0清理旧字段,2026年1月7号的PR #1301完成删除并保留旧字段号、防止被重新使用。它是数据模型去重和协议绑定重构的结果,不能单独归因于Issue #1104(这里提出了url表达上存在的问题)。

为什么协议版本也放进每个接口

生产环境中常见的问题是升级节奏:服务端可能已经支持新版本,但不可能让所有客户端同一时间升级。那如果Card根层只能写一个protocolVersion会怎样?服务端切换版本后,旧客户端可能直接失联。

Issue #1104公开提出了这个多版本兼容问题。社区讨论过以下几种方案:

方案 为什么没有成为最终方案
根层保留protocolVersion,再增加版本数组 两个字段会产生“以谁为准”和如何同步的问题
直接把根层protocolVersion改成数组 会立刻破坏旧Card的结构兼容性
用不同子域名区分版本 需要额外部署和DNS管理,而且Card仍无法完整表达能力
把版本放进每个AgentInterface 同时说明“这个地址、这种绑定支持哪个版本”,语义最完整

经过根层版本数组的过渡实现PR #1259以及兼容性讨论#1416后,PR #1401最终选择了最后一种;最终数据结构中,每个AgentInterface声明一个必填的protocolVersion。如果同一个Agent同时支持0.3和1.0,就发布两个接口项,可以使用相同或不同URL。客户端先从Card选择合适的接口,请求时再通过A2A-Version明确所用版本。服务端不支持时返回VersionNotSupportedError

因此,supportedInterfaces[]是一张明确的兼容性清单:每一项完整回答“去哪个地址、用哪种协议绑定、按哪个A2A版本访问”。

为什么1.0可以接受不向前兼容

这些字段在1.0中直接删除,意味着只认识旧字段的0.3客户端不能读懂新Card。A2A能够接受这一代价,一方面因为1.0是允许清理历史设计的主版本升级,另一方面因为继续保留两套表达会让每个后续版本都背负歧义。

那些还没升级的0.3客户端怎么办?过渡期仍需要实现层双栈:例如同时支持0.3与1.0版本、根据A2A-Version分发请求,或者给旧客户端提供兼容Card。协议本身只是把版本边界和协商方式明确化了。

相关改动经过公开的问题与设计讨论、代码评审逐步落地。PR #1160的投票结果PR #1401的投票结果都可直接查阅,后者还保留了代码审查批准记录。这里体现开放治理的不是某位提案人的身份背景,而是设计理由、替代方案、审查和表决结果都可追溯。

Signed Agent Card为什么加入JWS

Agent Card挂在公开URL上,任何客户端都能读取;同样地,如果缺少完整性与来源校验,攻击者也可能篡改Card,替换接口地址、能力或安全要求,把请求引向错误的Agent。A2A 1.0因此加入Signed Agent Card,并按签名规范使用JWS为Card签名。

Signed Agent Card使用非对称密钥完成签名与验签

生成签名时,签名方先移除Card里的signatures字段,再按照JCS(RFC 8785)对JSON做确定性规范化,最后生成JWS签名。验证方使用可信公钥验证签名,就能判断Card内容签发后是否被改动,以及签名者是否持有对应私钥。

但JWS不能证明Card里的自我描述是真的,也不能证明Agent的输出可靠。它证明的是“这份Card由谁签过、签后有没有被改”,信任仍取决于公钥来源和组织的信任策略。

tasks/send为什么改成message/send

这里先澄清版本:tasks/send -> message/send不是A2A 1.0才发生的,而是2025年5月、v0.1向v0.2演进时的改动。

这项改动最早出现在Phil Stephens(GitHub账号pstephengoogle)的提交4921066,由Issue #432跟踪设计,再通过PR #415“Protocol updates: Support stateless transactions”实现。PR于2025年5月7日合入next分支,随后经PR #599进入v0.2.0

PR #415:为无状态交互重构消息方法

资源所有权的改变

旧的tasks/send默认每次交互都围绕Task展开,客户端需要预先提供Task ID。这对长任务合理,对一句问答却很别扭:Agent即使只想直接返回一条Message,也被迫创建Task。

PR #415把模型改成“消息优先”:

  • 消息创建者生成messageId
  • 客户端发送Message,而不是预先创建Task;
  • 服务端判断是否需要持久任务;
  • 需要追踪时由服务端创建并拥有taskId
  • 不需要状态管理时可以直接返回Message;
  • sessionId同步重构为服务端创建的contextId
  • 流式入口相应从tasks/sendSubscribe迁到message/stream

所以方法名换成message/send是在表达:输入对象始终是Message,Task只是服务端可能创建的处理结果。

依旧不向前兼容

对照v0.1规范v0.2规范,可以看到它不保证新旧版本之间的请求与响应格式兼容性。

PR #415合入时,旧方法曾短暂保留并标记为弃用,给实现者一个迁移窗口;随后提交41437f7从示例实现中移除了旧方法,PR #459继续同步规范、Schema和测试,最终v0.2.0只保留新方法。更麻烦的是,变化不仅是路由名,还包括Task ID归属和返回类型。服务端即便给旧方法设置别名,也不能自动消除所有语义差异。

这类破坏性修改之所以被接受,主要有两个背景:其一,项目当时仍处于0.x草案阶段;其二,新模型解决了真实的无状态交互问题,而不是纯命名偏好。

到1.0版本,A2A把核心操作与具体传输绑定分开(感受下当下的AI相关领域迭代有多快吧),命名又发生变化:

阶段 JSON-RPC/核心操作 HTTP+JSON映射
v0.1 tasks/send 当时未形成1.0式映射
v0.2 / v0.3 message/send 以JSON-RPC为主
v1.0 SendMessage POST /message:send

根据1.0迁移说明方法映射表,当前1.0规范的JSON-RPC方法是SendMessage,gRPC方法也是SendMessage,REST绑定则是POST /message:send。统一的是抽象操作语义,各传输绑定负责把它映射到自己的报文格式。

A2A真正提供了什么

前面的字段变化不是目的,A2A要提供的核心价值仍然是下面四项:

  • 框架无关的互操作契约:双方不必使用同一种Agent框架,只要实现相同版本和绑定,就能发现能力并交换消息。
  • 任务生命周期管理:除了即时Message,还可以创建可查询、可订阅、可取消并具有终态的Task,承载长耗时和人工介入流程。
  • 对等委托:Agent可以根据能力边界自主寻找另一个Agent,不必把对方降格为本地工具。
  • 与MCP互补:MCP解决“怎样调用工具、数据库和API”,A2A解决“应该找哪个Agent协作”。

网关不是A2A运行的强制依赖,两个Agent可以点对点通信。但进入规模化环境后,Agent发现、路由、凭证、策略和观测通常需要一个集中治理角色,这才是A2A网关或者说是AI网关的价值。

真实双Agent抓包验证

实验拓扑包含一个协调Agent和一个数据分析Agent,二者都位于agentgateway之后。客户端先经3000端口访问协调Agent;协调Agent收到任务后,再从1176端口建立一条独立TCP连接,经3001端口把分析任务委托给数据分析Agent:

双Agent委托链路及两条独立TCP连接

注意:这次实验仅验证网关转发、Agent Card处理和双Agent委托链路。

下面Wireshark中的HTTP请求/响应总览。前八条是两个Agent Card分别经过网关的请求与响应;随后是四段POST /message/send,其中帧44开始的是协调Agent自主发起的第二条连接。

Wireshark中的Agent Card与委托链路

关键帧如下:

帧号 相对时间 源端口 -> 目标端口 内容
13 0.027537s 9998 -> 1172 Orchestrator原始Agent Card
15 0.027972s 3000 -> 1171 网关转发后的Orchestrator Card
29 0.059025s 9999 -> 1174 Data Analyst原始Agent Card
31 0.059171s 3001 -> 1173 网关转发后的Data Analyst Card
35 0.059879s 1171 -> 3000 客户端发给协调Agent
37 0.060099s 1172 -> 9998 网关转发给协调Agent
44 0.285413s 1176 -> 3001 协调Agent建立新连接并委托
46 0.285535s 1174 -> 9999 网关转发给分析Agent
115 89.367648s 1171 -> 3000 兼容性测试:/tasks/send
117 89.367830s 1172 -> 9998 网关继续转发

Message与Artifact的差异

其实可以看到Agent的响应同时包含messageartifacts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"result": {
"id": "task-a2a-98765",
"message": {
"role": "assistant",
"parts": [{"type": "text", "text": "分析完成"}]
},
"artifacts": [
{
"artifactId": "report-q3-001",
"name": "Q3 Sales Analysis Report",
"parts": [{"text": "..."}]
}
]
}
}

message是对话中的即时回复,artifacts是任务生成的正式交付物,例如报告、文件。二者并行存在,不能把响应里的所有文本都当成Artifact。

这次返回的“14%环比增长”“电子产品增长22%”并非真实经营数据。测试Agent没有联网,也没有连接真实数据库,这些数字是模型生成的。实验验证的是CrewAI确实执行了模型调用,以及A2A委托链路真实发生;它没有验证报告内容的事实性。

网关修改了什么

网关转发Agent Card时到底动没动内容?逐字节比较响应后发现,JSON Body只差一个字符:

1
2
- "url": "http://127.0.0.1:3001"
+ "url": "http://127.0.0.1:3001/"

同样的尾斜杠归一化也出现在3000端口的Card中。这表明网关至少对Agent Card做了结构化解析与重新序列化,而不是逐字节盲转发。

另一方面,本次抓包中message/send请求和响应的Body经过网关后完全一致,HTTP响应头也未见网关注入自己的server或链路追踪字段。就这组样本而言,agentgateway对Card做轻量协议感知,对业务消息保持透传。

这次拓扑也有明确边界:配置文件中每个网关端口只对应一个固定backend,网关根本不需要在多个候选Agent之间做技能路由或负载均衡。因此,本次实验不能用来证明agentgateway的动态路由、限流或熔断能力。

agentgateway官方资料还把以下能力定位为网关层职责,但它们没有在本次抓包中验证:

  • 凭证代理:真实凭证由网关在转发时注入,避免Agent长期持有密钥。
  • 策略执行与急停:在多Agent间的异常调用扩散时集中执行访问策略,必要时切断链路。
  • 统一可观测性:汇总原本分散的Agent间调用、指标和追踪数据。

回看开头文章的四条批评

原文的四项批评可以分成两组来看:前两项讨论点对点调用的耦合与流量控制,后两项讨论跨节点观测与历史留存。

“没有消息总线,所以强耦合”

这是架构偏好,不是充分的协议缺陷证明。A2A选择点对点委托,HTTP和gRPC同样如此。消息总线适合需要持久日志、消费组和重放的场景,但它也会引入Broker治理、消息顺序和投递语义等问题。

“没有背压”

背压是指:当下游的消费速度低于上游的生产速度时,下游能够通过一套协调机制让上游减速、暂停、排队或丢弃部分负载,避免请求持续堆积直至内存、连接池或线程池耗尽。超时只是“等不下去了”,重试甚至可能放大拥塞,它们都不等于背压。

这项批评有一部分合理,但不能只看单次HTTP连接。A2A通过异步Task、查询、订阅和Push Notification减少调用方持续占用连接等待结果的压力,缓解了同步等待耦合。

“可观测性碎片化”

点对点调用确实不像消息总线那样天然经过一个中心关口。但这通常由网关、Service Mesh、OpenTelemetry和策略执行点补齐,不一定硬塞进应用协议本身。

“消息无法回放”

这条批评容易被误读,需要先排除一种不成立的理解方式,再谈真正站得住的部分。

先说清楚”回放”不是什么:如果”回放”指的是”把同一条请求重新丢给Agent,期待它重新生成一模一样的内容”,这个诉求本身在生成式AI场景下就不成立——LLM的输出是概率采样的结果,同一个问题问两次,措辞、数字、结论都可能不一样。这不是A2A协议的缺陷,是生成式模型的本质决定的,换成任何协议都解决不了,除非在应用层做语义缓存把结果锁死(而语义缓存本身要解决”两条措辞不同但语义相同的请求算不算同一件事”,是个更难的问题,跟协议设计没关系)。所以如果批评的靶子是”A2A没法保证重放出相同内容”,这是个无效指控,不该算在协议头上。

真正值得讨论的,是”回放”这个词底下压着的另外两件事,它们都跟”内容是否可重新生成”无关:

第一件事:安全重试。客户端发了一条消息,网络抖动导致不确定服务端有没有收到、有没有处理完,能不能放心再发一次,而不用担心产生第二次真实副作用(比如再触发一次计费、再生成一个重复Task)。A2A规范提到messageId可以用于这个场景:”Send Message operations MAY be idempotent. Agents may utilize the messageId to detect duplicate messages.”——工作原理不是”重新算一遍指望算出一样的结果”,而是”记住第一次算出来的结果,第二次看到同一个messageId直接把存好的结果原样吐回去,根本不会再调用一次LLM”。这跟LLM的不确定性完全无关,是个纯粹的缓存查找问题。

但这个安全网是有有效期的:服务端不可能永久保留每一个messageId对应的结果,这几乎肯定是个有TTL的短期缓存(几分钟到几小时量级),不然等于自己维护了一份持久化信息。在去重窗口内,重放同一个messageId能拿回原来那个结果,跟LLM的非确定性无关;过了去重窗口,服务端已经不再识别这个messageId了,会当成一条全新消息,真的重新调用一次LLM——这时候两次结果确实会不一样,”回放”这个词在这里已经名不副实。而且这个机制还只是MAY,不是MUST,协议没有强制要求任何Agent实现它。

第二件事:审计留痕。这跟”以后能不能重新生成同样内容”完全无关,要求的是”当时到底发生了什么、系统当时确实产出过什么内容,事后能不能查”——不管这个内容是不是能被同一个模型再生成一遍,它作为历史记录的价值不受影响。这才是原文批评真正对准的目标:A2A定义了Message、Task、Artifact的交换语义,但没有承诺一份可以长期查询、支持多消费者独立回放的持久化事件日志。这一层能力,Kafka这类消息总线协议是把它做成协议核心(分区日志+offset,本来就是为持久化和重放设计的),FIX协议是在协议层显式定义了Resend Request和序列号补发机制(金融交易的合规要求逼出来的)。A2A没有这么做,这一点和HTTP、gRPC等大多数RPC式协议保持一致,不算特别异常,但如果业务确实需要审计重演或事件溯源,仍然要在A2A之外自己引入持久化系统。

所以更精确的结论是:A2A能做到“短期内安全重试”,做不到“长期审计回放”——这条批评说的是这一层,跟”LLM是概率生成的”这件事不矛盾,也没有被生成式模型的不确定性反驳掉。

结论

A2A不是消息总线,也不试图成为消息总线。它解决的是独立Agent之间的发现、消息交换、任务管理和结果交付。批评它时,应把“协议没有定义”“协议通过异步模型部分缓解”和“生产治理层应承担”这三种情况分开。后续我们再来分析真实的A2A网关到底怎样在生产治理层中解决这些问题的。

参考资料