返回博客列表

A2A 协议详解:Agent 之间该怎么对话,以及 ADK 里怎么落地

2026-10-05T20:00:00+08:00
A2AAgent2AgentADK多 AgentMCP协议开源

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 章节,含暴露与消费两条路)、以及它最值得学的那个设计——扩展机制。

本文提纲

  1. 为什么"把 Agent 包成工具"不够
  2. 协议骨架:三种绑定与五个核心对象
  3. Agent Card:可发现的契约
  4. ADK 实战之一:把 Agent 暴露成 A2A 服务
  5. ADK 实战之二:把远程 Agent 当子 Agent 用
  6. 扩展机制:不破坏协议核心做演进
  7. 什么时候该用 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-Extensions HTTP 头,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 都处于流式模式时的三个真实缺陷:

  1. 消息重复:同一个用户消息在任务历史里出现两次;
  2. 输出误分类:远程 Agent 的 ADK 输出被错误地转成"事件思考(event thoughts)";
  3. 子 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,想用新执行器要显式关掉;
  • 跨进程意味着跨信任边界:鉴权、传输安全、链路追踪这些"企业特性"不是可选项。

参考链接

你现在的多 Agent 系统,是真需要跨进程,还是只是想让它"看起来像微服务"?评论区聊聊你的判断,觉得有用点个赞。


作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

本文首发于 AI人工智能时代,转载请注明出处。

分享给朋友