01 · 核心概念
核心概念:两个 agent 如何彼此发现、对话,又互为黑盒
index 把 A2A 定位成 MCP 的水平对应面——MCP 让一个 agent 往下调工具,A2A 让一个 agent 横向托付给另一个 agent。这章把这条定位拆成五个能各自下定义的概念。读完,看到任意一对 A2A 协作的 agent,能说清它们靠什么互相发现、交换哪些对象、各自向对方藏起了什么,并在「该用 A2A 还是 MCP」这道题上给出判据。事实基线 A2A v1.0.1(2026-05-28);下文凡涉及 wire 的字段与方法名,均取自该版规范。
本章你将建立的 schema
- A2A 解决的是 agent 层的 M×N 互操作——M 个调用方 × N 个远程 agent,各写适配是 M×N,统一协议后降到 M+N。与 MCP 同源动机,但发生在 agent↔agent 这条水平线上。
- 两个远程 agent 靠 Agent Card(发布在
/.well-known/agent-card.json的能力声明 JSON)互相发现,交换 Task / Message / Artifact 这套数据模型来协作。 - 不透明 agent(opaque)是设计灵魂:对方的提示词、记忆、内部工具一律不暴露,只暴露 Agent Card 声明的能力 + 显式交换的消息。每个 agent 对另一个都是黑盒。
- A2A 与 MCP 正交不竞争:MCP 垂直(agent→工具),A2A 水平(agent→对等 agent)。一个 agent 常常对外用 A2A 当对等体、对内用 MCP 调工具。
1.1为什么需要 A2A:M×N 与水平 vs 垂直
A2A 把 agent↔agent 的两两适配(M×N)收敛成对一份公共协议的适配(M+N),解决的是 agent 层的互操作,处在 agent↔agent 这条水平线上。
没有公共协议时,让 M 个调用方各自对接 N 个远程 agent,要写 M×N 套适配——每加一个 agent,已有的每个调用方都得改。这正是 MCP 在工具层解决过的同一道题:MCP 出现前,每个应用对每个工具各写一遍集成。A2A 把这套思路搬到 agent 层。先把这条坐标装进脑子——后面四个概念都挂在它上面:A2A 治的是"横向"那条线。
底层机制:M×N→M+N 省的是哪一类工作
把它说具体:M×N 里每一格"适配"包含三样东西——怎么发现对方能干什么、用什么线缆格式发请求、对方用什么数据结构回话。没有公共协议时,这三样每一对 agent 都要重新约定一遍,且是私有约定,换个对端全部作废。A2A 把这三样固定成规范:发现统一走 Agent Card(§1.2),线缆统一走 JSON-RPC / gRPC / REST 三选一加 SendMessage 方法,数据统一走 Task / Message / Artifact(§1.3)。于是每个调用方只需实现一次"说 A2A",每个 agent 只需实现一次"听 A2A",总工作量从 M×N 降到 M+N。
这里有一条容易被"M+N 省事"盖过去的代价:统一协议必然是各端的最小公约数。私有点对点协议能为某一对 agent 量身定制(比如塞进只有它俩懂的二进制字段),A2A 为了让任意两端都能对话,只能用通用的 HTTP + 文本载体,牺牲了专用协议的紧凑与效率。规范用"额外提供 gRPC、REST 两种绑定,由 Agent Card 的 preferredTransport 声明首选"来部分对冲这条代价——这条权衡 §2.3 会展开。
另一个关键区分:A2A 治的是水平方向(agent 和对等 agent 之间),而同样喊"M×N→M+N"的 MCP 治的是垂直方向(agent 和它脚下的工具之间)。同源的动机,发生在两个不同的层。把这两条线分清楚,是看懂整套 A2A 的第一步,也是 §1.5 判别口诀的根。
SendMessage + Task)。黑色为垂直线:每个 agent 各自向脚下的工具用 MCP(tools/call)。注意:两条线方向相互垂直,A2A 与 MCP 不在同一条线上抢位置——它们正交。既然 MCP 已经把"一个 agent 调外部能力"标准化了,为什么不直接把"调用另一个 agent"也当成一次 MCP 工具调用,省掉 A2A 这套东西?
1.2Agent Card:能力声明与去中心发现
Agent Card 是一个 agent 公开发布的能力声明 JSON,固定位于 GET /.well-known/agent-card.json,让别的 agent 无需注册中心即可发现"它能干什么、怎么连、怎么认证"。
M+N 的"发现"那一格要落到一个具体动作上:一个调用方在还没建立任何连接、不知道对端任何细节时,怎么得知"这个 agent 提供哪些技能、用哪种传输、需要哪种凭证"?Agent Card 把这份信息固定在一个可预测的 URL 上,谁都能用一次普通 HTTP GET 取到,无需先去某个注册表查号。它是 A2A 的起点——没有 Card,连"对端能不能干这件事"都无从判断。
底层机制:它像 robots.txt——静态、可缓存、声明类型而非凭证
Agent Card 的几个设计点,比"一份能力 JSON"这句话深一层:
- 固定路径 = 去中心发现。路径写死成
/.well-known/agent-card.json(v1.0;v0.x 旧路径agent.json已弃用)。这和robots.txt同一个思路:约定一个人人皆知的位置,于是不需要中央注册中心来"查这个 agent 在哪"——知道域名就能拼出 Card 的 URL。代价是没有注册表那种"按能力检索全网 agent"的能力,发现是"已知道域名、去确认它能干什么",而非"去找谁能干这件事"。 - 静态、可缓存。Card 是一份静态文档,调用方可以缓存。代价:它声明的是静态快照,没有 MCP
initialize握手那种"每次会话现场协商能力"的动态性——能力是预先声明死的(认证后可通过GetExtendedAgentCard取到更详细的一份,这是 §2.2 的话题)。 - 声明能力类型,不携带凭证本身。Card 里的
securitySchemes声明的是它采用 OAuth2 / API Key / mTLS 这类方案,而不包含任何实际的 token 或密钥。凭证由调用方带外另行获取,发请求时放进 HTTP 头——绝不出现在 Card 里,也不进 JSON-RPC 载荷。把"声明用哪种锁"和"递上哪把钥匙"分开,是 Card 能公开发布而不泄密的前提。
Card 的核心字段:url(A2A 服务端点)、skills(技能列表,每个含 id / 描述 / 输入输出模式)、capabilities(如 streaming、pushNotifications 这些可选能力开关)、securitySchemes(认证方案类型)、provider(提供方信息)。一个调用方读完这五样,就知道"调它走哪个地址、它会哪几手、怎么跟它认证、它归谁"。
url=去哪连、skills=会哪几手、capabilities=支持哪些可选能力、securitySchemes=用哪种锁。注意:底部那格——securitySchemes 只声明锁的类型,凭证本身从不写进 Card,所以 Card 能公开发布。场景走查
需求:"一个差旅编排 agent 想把'订机票'这件事托付给某航司的远程 agent"。第一步,编排 agent 用域名拼出 https://air.example.com/.well-known/agent-card.json 并发 HTTP GET——这是Agent Card 发现。读到 skills 里有 book-flight、capabilities.streaming=true、securitySchemes 声明走 OAuth2、url 指向 https://air.example.com/a2a。于是编排 agent 知道:这个对端确实会订票、支持流式、得先按 OAuth2 带外拿到 token。注意此刻还没发任何任务,凭证也还没递——Card 只解决了"能不能、怎么连、用哪种锁"。下一步把订票请求真正发出去、对端建出一个有状态的工作单元来跟进,需要的是 §1.3 的数据模型。
1.3数据模型:Task / Message / Part / Artifact
A2A 的协作内容由四类对象承载:Task(有状态的工作单元)包住 history(Message[])和 artifacts(Artifact[]);Message 由 Part[] 组成(文本 / 文件 / 结构化数据);Artifact 是任务的产出。
M+N 的"数据结构"那一格,需要一套双方都认的对象,来表达"一次托付从开始到产出"全过程。如果只有一来一回的请求/响应,就只能表达"问一句答一句"这种瞬时交互;可托付给对等 agent 的任务往往要多轮、要跑很久、要产出多个结果。数据模型用 Task 把这一整段"包"起来,于是协作有了一个可反复查询、可承载中间状态的载体。
底层机制:Task 是有状态、长生命周期的工作单元,不是一次 request/response
这套对象的关键,不在字段多少,而在 Task 的有状态、长生命周期这一点——它正是 A2A 能撑起小时级、天级任务的原因:
- Task:一次托付对应一个 Task,有自己的
id和contextId,并带一个status(含state与时间戳)。它持续存在——调用方可以隔一段时间用GetTask再来查它现在到哪一步了。这与"发一个 HTTP 请求、等一个响应、连接关掉就结束"的模型根本不同:Task 在服务端有生命周期,跨越多次交互。它内部包两样东西——history(迄今的 Message 列表)和artifacts(已产出的 Artifact 列表)。 - Message:一条消息,带
role(user或agent)和一个parts数组。注意一条 Message 可以同时含多个 Part——这是它能表达多模态的原因。 - Part:消息的内容单元,靠
kind字段区分类型——text(TextPart,纯文本)、file(FilePart,文件,可内联或给 URI)、data(DataPart,结构化 JSON 数据)。模态无关的设计让同一条 Message 能混装文字、文件、结构化数据。 - Artifact:任务的产出,有
artifactId,内容同样是 Part[]。它和 Message 的区别在语义:Message 是"对话过程中的往来",Artifact 是"任务最终或阶段性的成果"。
这套有状态设计的代价:服务端必须存储并管理 Task 的生命周期(建、改状态、留 history、存 artifacts),不能像无状态 HTTP 端点那样处理完即忘。这份存储与状态管理的复杂度,是换取"长任务、可重查、可中断续接"的成本——Task 在哪些状态之间转移、怎么转,是 §2.5 的生命周期状态机。
Task 的 state 在实际 JSON 载荷里是 SCREAMING_SNAKE 风格,例如 "status":{"state":"TASK_STATE_COMPLETED","timestamp":"…Z"}。全集为 TASK_STATE_SUBMITTED / WORKING / INPUT_REQUIRED / AUTH_REQUIRED / COMPLETED / FAILED / CANCELED / REJECTED。很多旧博客里写的小写 completed 是 v0.3.0 的形式,已弃用;另外注意是美式拼写 CANCELED(v0.x 的 cancelled 已移除)。
kind 分 text / file / data。注意:history 装"对话往来"、artifacts 装"成果"——两者都是 Part[],区别在语义而非结构;而最外层那只有状态的 Task 才是承载长任务的容器。Message 和 Artifact 内容都是 Part[],结构几乎一样。既然如此,为什么规范要分成两类对象,而不是统一用 Message 把产出也一并表示?
展开答案(先答再展开)
因为它们在 Task 里扮演不同语义角色,分开能让调用方一眼区分"过程"与"结果"。history 里的 Message 是协作过程的逐条往来(谁说了什么、对端追问了什么);artifacts 是这次托付要交付的成果(一份报告、一个文件、一段结构化数据)。若混成一类,调用方就得自己从一堆 Message 里猜"哪几条才是最终产物"。分成两类,等于把"给你看的过程"和"交给你的东西"在数据模型层面就标清楚——流式传输时也正好对应两种事件(TaskStatusUpdateEvent 推进度、TaskArtifactUpdateEvent 推产出),这是 §2.4 的内容。
1.4不透明 agent 原则(opaque)
远程 agent 对调用方是黑盒:它不暴露内部提示词、记忆、工具或推理过程,只暴露 Agent Card 声明的能力 + 显式交换的 Message / Task。
A2A 协作的对端,常常是另一家公司、另一套信任域里的 agent。要让两个互不隶属、互不信任的 agent 能协作,就不能要求一方把内部实现摊给另一方看——没有公司愿意把自己 agent 的提示词、私有工具、客户记忆交出去。不透明原则把"协作"和"暴露内部"解耦:你只需声明能干什么、交换消息,不必、也不该让对方看见你怎么干的。这是 A2A 能跨组织、跨信任边界用起来的前提,也是它的设计灵魂。
底层机制:动机是 IP 保护 + 安全面收缩 + 解耦,代价是无法深度自省
"黑盒"不是含糊其辞,它对应三条具体动机和一条具体代价:
- IP 保护:agent 的竞争力常常就在它的提示词、工具编排、私有数据上。不透明让供应商能对外提供能力,而不泄露这些核心资产。
- 安全面收缩:对方能触达的只有 Agent Card 声明的接口 + 消息通道,内部状态、工具、记忆都不在攻击面上。暴露得越少,可被利用的入口越少。
- 解耦:双方各自换实现、换模型、换内部架构,只要 Agent Card 声明的契约不变,对端无感。这正是 §1.1 那套 M+N 互操作能成立的微观保证。
- 代价:调用方无法深度自省或细粒度协调对端——看不到它的中间推理,不能干预它内部用哪个工具,只能在"发消息 / 查 Task 状态"这个粒度上交互。要更细的协同?不透明原则就堵死了。
这是 A2A 与本站 multi-agent-patterns 教的那类多 agent 框架的分水岭。那类框架里,多个 agent 跑在同一套你掌控的系统内,常共享一块 state(LangGraph 的 State 就充当各 agent 之间的公告板)——一个 agent 写进去的中间产物,别的 agent 默认读得到。A2A 把这个前提整个翻转:对端是你不掌控的黑盒,甚至属于另一家公司,连它的内部状态都看不见,更谈不上共享一块 state。一句话对照:那边是"同一系统内共享状态的协作",这边是"跨信任边界、对内部彼此不可见的对等体协作"。看清这条分界,就不会再试图用 A2A 去做进程内编排,也不会把它和那类框架混为一谈。
场景走查
接 §1.2 那个差旅例子。编排 agent 把"订一张明早飞上海的票"发给航司 agent(Message 跨过边界,§1.3),航司 agent 建出一个 Task 开始处理。整个过程里,编排 agent 看不到航司 agent 内部用了哪个订座系统、用什么提示词解析需求、查了哪些库存——这些都在虚线框里,是不透明的。它能依据的,只有航司 agent Agent Card 当初声明的 book-flight 技能,以及对端通过 Message / Task 状态显式回传的内容。若编排 agent 想"接管航司 agent 内部、改它选座逻辑",不透明原则直接堵死——跨信任边界的协作,本就只允许在声明的能力这个粒度上发生。这份"看不见内部"既是 IP 与安全的保护,也是协调粒度的天花板。
1.5A2A ⊥ MCP:正交,不是竞争
A2A 与 MCP 正交:MCP 垂直(agent→工具,wire 上是 tools/call),A2A 水平(agent→对等 agent,wire 上是 SendMessage + Task);同一个 agent 常对外用 A2A、对内用 MCP。
"A2A vs MCP 谁取代谁"是最常见的误解,市面上大量"vs"文章把它们摆成对手。把这条厘清,是为了让你在设计时不纠结"二选一",而是知道两者在不同的层各司其职、可以同时用。把 §1.1 的水平/垂直坐标和 §1.4 的"对端是工具还是对等体"合到一处,就得到一个能当场判断该用哪个的口诀——这是本章五个概念收束成可操作判据的地方。
底层机制:被调对象的性质决定了用哪个协议
两者的分界不在口号,而在线缆上搬的是什么、对端是什么东西:
- MCP(垂直):线缆上搬的是
tools/call、resources/read这类调用,对端是一个无状态原语——一个工具或数据源,给输入、回结构化输出,调用方完全掌控。方向是"往下"伸手够能力。 - A2A(水平):线缆上搬的是
SendMessage+ Task 对象,对端是一个有状态、不透明、自治的对等 agent——它会推理、能多轮、可能反问、任务往往很长(§1.3 的 Task + §1.4 的 opaque)。方向是"横向"托付给一个平级的智能体。
两者组合使用是常态,也是 2026 年企业级的默认栈:一个 agent 对外用 A2A 暴露成对等体,供别的 agent 托付;对内用 MCP 调自己的工具去完成那次托付。它们不在同一条线上抢位置,所以是正交(⊥)。
| 维度 | MCP(垂直) | A2A(水平) |
|---|---|---|
| 对端是什么 | 无状态工具 / 数据源 | 有状态、不透明、自治的对等 agent |
| 方向 | agent ↓ 往下调能力 | agent ↔ 横向托付 |
| wire 上搬什么 | tools/call / resources/read | SendMessage + Task 对象 |
| 交互形态 | 一次调用、结构化 I/O、即结束 | 可多轮、长生命周期、对端会推理 |
| 关系 | 正交(⊥)· 同一 agent 常对外 A2A、对内 MCP | |
场景走查 · repair-shop
用修车行串起来:顾客 → 修车行——顾客(一个 agent)把"我的车有异响"托付给修车行(另一个 agent),对端是个会诊断、会追问、要花时间的自治体,这是 A2A。修车行内部,修车行 → 技师——前台把活分派给某个技师 agent,技师同样是个自治对等体,仍是 A2A(agent↔agent,无论跨不跨公司)。而 技师 → 诊断仪——技师拿起诊断仪读取故障码,诊断仪是个无状态工具,给输入回数据,这是 MCP。三跳里前两跳水平、第三跳垂直,A2A 与 MCP 在同一个系统里各管一段,正交共存。
设计时卡在"用 A2A 还是 MCP",就问一句:你是在调一个工具(给输入、要结构化结果、一次就完),还是在托付给一个对等 agent(对方会自己推理、多轮交互、可能跑很久)?前者 MCP,后者 A2A。对端的性质,而非它叫什么,决定了答案。
有人把一个"内部封装了 LLM、会自己规划多步、可能反问你"的服务,做成了一个 MCP 工具挂给上游 agent 调。按本节口诀,这个选择哪里别扭?换成什么更合适?
展开答案(先判断再展开)
别扭在用错了层:被调对象其实是个有状态、会推理、可能多轮反问的自治体,符合"对等 agent"的画像,却被硬塞进 MCP"无状态工具"的模型。后果是丢掉它的自治特征——MCP 的 tools/call 是一次调用即结束的形态,承载不了"对端反问、任务跑很久、跨多轮"。更合适的是把它做成一个 A2A 对等 agent:发布 Agent Card 声明技能,用 SendMessage + Task 承载这次有状态的托付,对端的多轮反问正好落在 input-required 这类中间状态上(§2.5)。口诀在这里给的判断是"托付给对等 agent"而非"调工具"——所以选 A2A。
§本章 self-check
合上教程,把答案先写在纸上或编辑器里。卡住的题,就是心智模型还没建好的地方——回对应小节再来。
- A2A 把 agent 层的 M×N 互操作降成 M+N,省掉的是哪三类两两重复的工作?它治的是"水平"还是"垂直"方向,跟 MCP 治的方向差在哪?
- Agent Card 位于哪个固定路径?它声明的
securitySchemes里有没有实际凭证?为什么这一点决定了 Card 能不能公开发布? - 为什么 A2A 用有状态的 Task 而不是一次请求/响应来承载一次托付?这份"有状态"换来了什么、又要付出什么代价?
- 判别题:给一个对象——"它内部跑着 LLM、会规划多步、可能反过来问你要信息、一次任务可能跑十几分钟"。该把它当 MCP 工具还是 A2A 对等 agent 来接?用本章口诀说出判断依据。
答案(先做完再展开)
- 省掉的三类重复工作:怎么发现对端能干什么、用什么线缆格式发请求、对端用什么数据结构回话——A2A 把这三样分别固定成 Agent Card、JSON-RPC/gRPC/REST +
SendMessage、Task/Message/Artifact,于是每端只实现一次。它治水平(agent↔agent);MCP 治垂直(agent↓工具)。同源动机,不同层。 - 路径是
GET /.well-known/agent-card.json(v0.x 旧agent.json已弃)。securitySchemes只声明认证方案的类型(如 OAuth2 / API Key / mTLS),不含任何实际 token 或密钥——凭证由调用方带外获取、放进 HTTP 头。正因为 Card 只说"用哪种锁"而不放"钥匙",它才能作为公开文档发布而不泄密。 - 因为可托付给对等 agent 的任务常常要多轮、跑很久、产出多个结果,一次请求/响应只能表达"问一句答一句"。有状态的 Task 有
id和持续的status,可隔段时间用GetTask重查、可承载中间状态、可中断续接——这是它能撑小时/天级任务的原因。代价:服务端必须存储并管理 Task 的整个生命周期,不能处理完即忘。 - 当 A2A 对等 agent 接。判断依据(本章口诀):被调对象是在"被托付一件事"而非"被当工具调一次"——它有状态、会自己推理、可能多轮反问、任务长,符合"自治对等体"画像,不符合 MCP"无状态、一次调用即结束的工具"画像。所以发 Agent Card + 用
SendMessage/Task,反问落在input-required这类中间状态上。
把"不透明"这条原则推到它的代价那一面
本章把不透明 agent 讲成 A2A 的优点(IP 保护、安全面收缩、解耦)。现在反过来想——不查后面章节,只凭"对端是看不见内部的黑盒"这一个事实推导:
- 如果一个恶意的远程 agent 在自己的 Agent Card 的技能描述里塞进一段诱导性文字,而调用方是靠 LLM 读 Card 来挑选 agent 的,会发生什么?这个攻击发生在"认证"之前还是之后?
- 既然对端内部不可见,调用方怎么确认"收到的这条消息真的来自它声称的那个 agent,而不是被人冒充或篡改"?Agent Card 默认有没有帮你解决这件事?
- 这两个问题共同指向不透明原则的同一个软肋——它是什么?
提示(卡住再展开)
不透明把"内部不可见"当成安全收益,但它没有顺带保证"对外声明的东西可信"。① 技能描述是一段会被 LLM 当指令读、却不可信的文本——恶意 Card 可以靠诱导性描述赢得 agent 选择,而选择发生在认证之前,于是即便它后续认证失败,也已经被选中、被发了消息(这类"Agent-in-the-Middle / 描述注入"是 04 章的头号陷阱)。② Agent Card 默认不签名,调用方无法仅凭 Card 确认对端身份——冒充与篡改是默认可能的(规范提供可选的 JWS 签名,但不强制)。③ 共同软肋:不透明保证了"看不见内部",却把"声明是否可信、身份是否真实"整个留给了实现者——所以 "A2A-compliant ≠ secure"。这条线正是 04 失败模式 整章的起点。