A2A 协议详解:Agent 之间该怎么对话,以及 ADK 里怎么落地
A2A 协议详解:Agent 之间该怎么对话,以及 ADK 里怎么落地
把 Agent 包成工具也能跑,但那等于让会谈判的人去看说明书。
这几天写的项目里,多 Agent 协作出现了三次:Strands 的 harness 用子 Agent 委派、Octop 用 AgentTeams 编排专家、Claude Code Mods 让插件在进程内改行为。它们解决的都是"同一个系统内怎么分工"。而 A2A(Agent2Agent 协议) 要解决的是另一个问题:不同厂商、不同框架、不同进程里的 Agent,怎么互相发现、互相委派、互相交付结果。
它由 Google 发起,现在托管在 Linux Foundation 下,官方定位写得很清楚:
An open standard, hosted by the Linux Foundation, that lets independent AI agents discover each other, delegate work, and exchange results across frameworks and vendors — without exposing their internal state, memory, or tools.
这篇文章分三层讲:协议本身(三种绑定、五个核心对象)、ADK 里怎么落地(adk.wiki 的 A2A 章节,含暴露与消费两条路)、以及它最值得学的那个设计——扩展机制。
本文提纲
- 为什么"把 Agent 包成工具"不够
- 协议骨架:三种绑定与五个核心对象
- Agent Card:可发现的契约
- ADK 实战之一:把 Agent 暴露成 A2A 服务
- ADK 实战之二:把远程 Agent 当子 Agent 用
- 扩展机制:不破坏协议核心做演进
- 什么时候该用 A2A,什么时候别用
为什么"把 Agent 包成工具"不够
官方文档里有一段论证很值得抄下来,它解释了 A2A 存在的必要性。设想你要规划一次国际旅行,需要协调四个专业 Agent:订机票的、订酒店的、推荐本地游的、换汇的。没有 A2A 的时候,开发者通常会把它们包成工具(就像 MCP 暴露工具那样)再互相调用。官方说这条路有几个硬伤:
- Agent 被降级成工具:Agent 天生是用来直接协商的,包成工具会限制它能做的事——工具是"你问它答",而 Agent 需要的是来回沟通、澄清、拒绝不合理的请求;
- 点对点定制集成:每接一个 Agent 都要写一套适配,工程量随组合数增长;
- 难扩展:Agent 数量和交互一多,系统就维护不动;
- 锁定:跨框架、跨厂商的互操作基本不可能。
所以 A2A 的定位是 "expose agents as they are, with no wrapping"——把 Agent 原样暴露成服务,而不是削成工具。
顺带把它和 MCP 的分工说清楚,因为这两个协议经常被混为一谈:A2A 覆盖 agent-to-agent 通信,MCP 覆盖 agent-to-tool 通信,两者是互补的。 一个 Agent 完全可能一边用 MCP 接数据库和搜索,一边用 A2A 跟别的 Agent 协作。
协议骨架:三种绑定与五个核心对象
先看官方的技术事实,因为很多介绍把这块讲糊了:
| 项目 | 内容 |
|---|---|
| 规范正文 | 规范性定义是 Protobuf(specification/a2a.proto);JSON Schema(2020-12)是构建时从 proto 生成的,属于非规范性产物 |
| 绑定(bindings) | JSON-RPC 2.0 over HTTP(含 SSE)、gRPC、HTTP/REST |
| 核心对象 | Agent Card(发现)、Task(有状态的工作单元)、Message、Part、Artifact |
| 官方 SDK | Python、JS/TS、Java、Go、.NET、Rust(六个) |
| 官方 CLI | a2a-cli,可在终端发现、发消息、管理 A2A Agent |
把"规范以 proto 为准"这件事单独拎出来说:这意味着跨语言实现的行为边界是明确的,而不是靠一群人对着 Markdown 各自理解。六个官方 SDK 覆盖到 .NET 和 Rust,也说明它的目标不是"某个框架的私货"。
任务的生命周期是另一个重点。协议把一次协作建模成 Task,有明确状态机(含终态与中断态),并且提供两条异步通道:SSE 流式与 Webhook 推送通知。对照我们这几天看过的项目——ADK 的 A2A 集成里就有 InMemoryPushNotificationConfigStore 专门管推送配置——可见"长任务怎么不超时"是跨进程 Agent 协作的一等公民。
Agent Card:可发现的契约
A2A 的发现机制基于一个约定位置:
/.well-known/agent-card.json客户端不需要事先知道你部署在哪、支持什么,只要 GET 这个路径就能拿到一张 Agent Card:它能做什么、怎么调、接受什么输入输出格式。在 ADK 里手工定义一张卡片长这样:
from a2a.types import AgentCard
my_agent_card = AgentCard(
name="file_agent",
url="http://example.com",
description="来自文件的测试智能体",
version="1.0.0",
capabilities={},
skills=[],
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
supports_authenticated_extended_card=False,
)几个字段值得注意:capabilities 声明协议能力(扩展也挂在这里,后面详说),skills 是给模型看的技能清单,default_input_modes / default_output_modes 约束内容类型,而 supports_authenticated_extended_card 涉及"公开卡片"与"认证后才可见的扩展卡片"的分层——这很像 OAuth 的 discovery 文档思路:先给最小必要信息,敏感能力要认证后才暴露。
对工程实践来说,Agent Card 的价值在于它把"集成"变成了"读元数据":以前接一个外部 Agent 要读文档、试接口、对齐字段;现在客户端拿卡片就能自己决定怎么调,甚至可以由模型自己读卡片来规划调用。
ADK 实战之一:把 Agent 暴露成 A2A 服务
ADK(Agent Development Kit)的 A2A 支持分"暴露"和"消费"两条路,官方文档在 adk.wiki/a2a 上有完整的中文指南,Python / Go / Java 都有。
暴露端最舒服的地方是一行代码:
from google.adk.a2a.utils.agent_to_a2a import to_a2a
# 使你的智能体兼容 A2A
a2a_app = to_a2a(root_agent, port=8001)to_a2a() 返回的是一个 Starlette 应用,也就是说它直接就是一个可以用 uvicorn 起来的 web 服务。它在背后做了一堆事,这些事才是真正值得知道的(因为它决定了你能改哪里、要配什么):
A2aAgentExecutor:A2A 协议与你的 ADK Agent 之间的桥;不提供自定义Runner时,它会自动建一个由内存服务支持的默认运行器(artifacts、sessions、memory、credentials);- 状态存储:
InMemoryTaskStore跟踪 A2A 任务,InMemoryPushNotificationConfigStore处理推送通知——注意都是内存实现,生产环境要换掉; - 请求路由:
DefaultRequestHandler把进来的 A2A HTTP 请求分发给 executor 和状态存储; - Agent Card:启动时要么加载你提供的卡片,要么用
AgentCardBuilder从你的 Agent 配置自动构建——它能从 ADK Agent 里提取 skills、capabilities 和元数据; - 挂载路由:把 A2A 的 API 路由挂到 Starlette 应用上。
参数里有几个坑点值得提前知道:
host/protocol/port只影响生成的 Agent Card 里公布的 URL,to_a2a()本身不绑端口。所以如果你用uvicorn --port 8080起服务,但to_a2a(port=8001),卡片会指向一个不可达的地址——这是最典型的新手错误。agent_card可以传AgentCard对象,也可以传 JSON 文件路径;不传就自动生成。lifespan让你接管启动/关闭逻辑(比如开数据库连接),上下文管理器会拿到 Starlette 应用实例,可以往app.state里放全局资源。- 暴露端还支持 转换器(converters) 和 执行拦截器(execution interceptors),用于定制消息如何进出协议层。
ADK 实战之二:把远程 Agent 当子 Agent 用
消费端的设计目标很明确:让远程 Agent 用起来像本地子 Agent。 ADK 提供 RemoteA2aAgent 作为客户端代理:
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
prime_agent = RemoteA2aAgent(
name="prime_agent",
description="处理质数检查任务的智能体。",
agent_card=(
f"http://localhost:8001/a2a/check_prime_agent{AGENT_CARD_WELL_KNOWN_PATH}"
),
)注意 agent_card 参数要的是卡片的 URL 而不是 Agent 的地址——这正是上一节说的"发现即集成"。拿到之后,prime_agent 可以直接当成 root_agent 的子 Agent 使用,ADK 把网络层藏掉了。
进阶配置通过 A2aRemoteAgentConfig 传:
prime_agent = RemoteA2aAgent(
name="prime_agent",
description="处理质数检查任务的智能体。",
agent_card=f"http://localhost:8001/a2a/check_prime_agent{AGENT_CARD_WELL_KNOWN_PATH}",
use_legacy=False,
config=A2aRemoteAgentConfig(
a2a_message_converter=my_a2a_message_converter,
request_interceptors=[my_request_interceptor],
),
)其中 use_legacy 是个关键的开关:它默认为 True,也就是默认走旧版集成路径;设成 False 才会启用新的 ADK-A2A 集成,并向服务端发送 A2A 扩展。为什么会有这个区别,就要讲到整篇里我认为最有价值的部分。
扩展机制:不破坏协议核心做演进
扩展(Extensions) 是 A2A 允许"加能力而不改核心协议"的机制。规则很干净:
- 扩展用 URI 标识,自己带一份规范;任何人都可以定义、发布、实现扩展;
- 服务端在 Agent Card 的
capabilities.extensions里声明自己支持哪些扩展; - 客户端在请求里选择性地 opt in;
- 传输层怎么表达?JSON-RPC 和 HTTP 用
X-A2A-ExtensionsHTTP 头,gRPC 用同名 metadata; - 服务端接受了哪些扩展,会在响应与事件的元数据里回传"已激活扩展"列表。
官方给的扩展类型也很有想象力:数据型扩展(只在 Agent Card 里暴露结构化信息,比如 GDPR 合规声明,不影响请求响应流程)、Profile 型扩展(在核心消息之上叠加结构和状态要求,比如要求所有消息用某个 schema 的 DataPart,或者在 TaskStatus.state 为 working 时定义一个 generating-image 子状态)、以及更激进的新 RPC 方法/新状态机。配套还有 extension and binding governance——一套分层晋升流程,保证核心稳定。
这个设计为什么值得学?因为它解决的正是协议最难的寿命问题:核心一旦冻结,新需求只能靠破版本;而如果谁都能改核心,规范三个月就烂了。A2A 的答案是"用 URI 命名扩展 + 卡片声明 + 请求 opt in + 响应确认",把创新放进可协商的旁路里。任何在做内部 Agent 平台、需要长期演进接口的人,都可以直接抄这个模式。
ADK 自己就有一个现成例子。它的 A2A 扩展(要求 Python v1.27.0+) 就是靠这套机制发布的:更新后的 A2aAgentExecutor 修掉了旧实现在 A2A 与 ADK 都处于流式模式时的三个真实缺陷:
- 消息重复:同一个用户消息在任务历史里出现两次;
- 输出误分类:远程 Agent 的 ADK 输出被错误地转成"事件思考(event thoughts)";
- 子 Agent 数据丢失:当远程 Agent 的子 Agent 树里嵌套了多个 Agent 时,输出会丢。
它的用法就是上一节的 use_legacy=False:客户端据此在请求扩展里加上 https://google.github.io/adk-docs/a2a/a2a-extension/,服务端检测到就用新实现,并在响应元数据里回传"已激活扩展"。服务端也能显式 use_legacy=True 选择退出,旧客户端不受影响。
这三个 bug 我觉得比扩展机制本身更有教育意义:它们全都只在"双方都在流式"时才出现。跨进程的多 Agent 协作,难点从来不在协议头,而在这种组合条件下才暴露的数据一致性问题。
另外 ADK 的 A2A 集成明确覆盖三类能力,也正好对应跨进程协作最容易丢的东西:推理(保留模型的思考痕迹)、长时间运行的工具(跟踪超过标准响应时长的调用,避免超时)、制品(在 Agent 之间传递生成的文件)。
什么时候该用 A2A,什么时候别用
官方把两个概念分得很清,这也是最实用的判断依据:
- 本地子 Agent:和主 Agent 在同一个应用程序进程中运行,像内部模块或库。通信在内存里发生,没有任何网络开销。
- 远程 Agent(A2A):作为独立服务运行,通过网络通信,A2A 定义的就是这套通信标准。
于是判断标准可以简化成两条:
该用 A2A 的场景:跨团队/跨厂商/跨框架的 Agent 需要协作;Agent 是独立部署、独立扩缩容、独立发版的服务;你想让外部开发者接入你的 Agent 而不暴露内部状态、记忆和工具(这正是官方那句 "without exposing their internal state, memory, or tools" 的价值);需要跨组织鉴权与审计。
不该用 A2A 的场景:拆出来的两个 Agent 反正要一起部署、一起重启——那它们就是同进程的两个模块,中间加一层 HTTP + Agent Card + 任务状态机,只是把一个函数调用变成了分布式事务,白白引入网络失败模式、序列化成本和调试难度。"看起来更像微服务"不是拆分理由。
几条落地提醒:
InMemoryTaskStore/InMemoryPushNotificationConfigStore是默认内存实现,生产要替换,否则重启即丢任务状态;to_a2a()不绑端口,卡片里的 URL 必须和实际监听端口一致;RemoteA2aAgent的use_legacy默认为True,想用新执行器要显式关掉;- 跨进程意味着跨信任边界:鉴权、传输安全、链路追踪这些"企业特性"不是可选项。
参考链接
- ADK 的 A2A 指南(中文) — 本文 ADK 部分的主要来源,含 Python / Go / Java
- A2A 简介 — 本地子 Agent vs 远程 Agent、暴露与消费的工作流
- 快速入门:暴露你的智能体 —
to_a2a()参数、内部组件与生命周期管理 - 快速入门:消费远程智能体 —
RemoteA2aAgent与A2aRemoteAgentConfig - A2A 扩展 - V2 实现 — 新执行器修掉的三个流式缺陷与激活方式
- A2A 协议官方网站 — 规范、主题文档与六个官方 SDK
- What is A2A? — "为什么包成工具不够"的官方论证
- A2A 规范 — 数据对象、RPC 方法、错误码与示例
- 规范定义(proto 与生成的 JSON Schema) — 规范性来源
- 扩展机制 — 扩展类型、声明与 opt-in 规则
- 扩展与绑定的治理流程 — 分层晋升如何保持核心稳定
- Agent 发现 —
/.well-known/agent-card.json约定 - 流式与异步操作 — SSE 与 Webhook 推送
- a2a-samples 示例仓库 — 各框架集成样例
你现在的多 Agent 系统,是真需要跨进程,还是只是想让它"看起来像微服务"?评论区聊聊你的判断,觉得有用点个赞。
作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
本文首发于 AI人工智能时代,转载请注明出处。