M12 Google ADK for Go 与 A2A 协议
M11 的 Eino 编排层解决了一个进程内的组件组织和 Agent 编排。实际项目中的能力边界往往跨越团队、语言和机器:物流 Agent 可能由另一个团队维护,风控 Agent 可能使用 Java 开发,数据分析 Agent 可能部署在独立集群。此时,进程内 Graph 无法直接连接这些远程 Agent,需要一套跨框架、跨语言、跨进程的通信标准。
A2A(Agent2Agent)协议用于解决这类 Agent 互联问题。本章同时介绍 Google ADK for Go,通过框架与协议的对照,理解 AgentCard、Message、Task、Artifact、流式更新和人工介入,并手写一个 A2A 客户端完成发现与调用。
v1.0.1,Google ADK-Go 最新稳定版本为 v1.4.0。A2A 1.0 的 JSON-RPC 方法使用 SendMessage、GetTask、CancelTask 等 PascalCase 名称,并使用 ProtoJSON 枚举值。Google 官方 Go 模块路径是 google.golang.org/adk,不要与社区同名项目混淆。项目落地时仍应在 go.mod 中锁定版本,并按对应版本核对实验性子能力。模块概述
学习目标
学完本章,你应该能够:
- 区分 Eino ADK 与 Google ADK for Go;
- 说明 MCP、A2A 与 ADK 分别解决什么问题;
- 理解 Google ADK-Go 的 Agent、Model、Tool 和 Runner;
- 了解 code-first、YAML Agent Config、OpenTelemetry、工具确认和 Vertex AI 集成;
- 讲清 A2A 的 AgentCard、Task 生命周期、JSON-RPC 交互、签名与治理;
- 使用 ADK-Go 暴露 A2A Agent,并用 Go 手写客户端调用;
- 根据 A2A 与 MCP 决策树选择合适的连接方式。
课程位置
本章是框架篇 M11-M12 的第二章。M11 11.5解决项目内部编排,本章补充对外 Agent 互联。M13 起进入项目篇时,平台可以通过 A2A 对接外部业务 Agent。
前置要求
建议先完成 M06 6.6 MCP stdio 客户端和 M11 11.3 Eino 框架系统。M06 的 JSON-RPC 与 MCP 客户端会在本章复用,M11 的 Eino 抽象有助于理解 ADK-Go。
配套练习
使用 ADK-Go 定义并暴露一个物流查询 Agent,再编写 A2A 客户端发现并调用它,最后把远程 Agent 包装为本地主 Agent 的工具。
12.1 相关概念
两个 ADK
Eino 和 Google 的 Agent 框架都使用了 ADK 这个缩写,但是这两者没什么关系。
- Eino ADK:Eino 框架中的 Agent Development Kit 子模块,提供
ChatModelAgent、Runner 和协作能力; - Google ADK for Go:Google 独立维护的 Agent Development Kit,Go 模块路径是
google.golang.org/adk。
┌─────────────────────┐ ┌─────────────────────────┐
│ Eino (字节) │ │ Google ADK for Go (谷歌) │
│ ├── components │ │ 独立框架 │
│ ├── compose │ │ A2A 官方参考实现 │
│ └── adk ←─────────┼─不是同一个─┼─► 这才是本章的 ADK │
│ (Eino 的子模块) │ │ │
└─────────────────────┘ └─────────────────────────┘本文提到“Eino ADK”时,专指 M11 11.7的 Eino 子模块;提到“ADK-Go”或“Google ADK-Go”时,专指 Google 官方 Go 项目。社区中还存在名称相近的项目,依赖时应认准 google.golang.org/adk。
MCP、A2A 与 ADK
这三个概念位于不同层级:
- MCP 解决工具与数据连接,方向通常是 Agent → 外部能力;
- A2A 解决 Agent 之间的协作,方向是 Agent ↔ Agent;
- ADK 是构建 Agent 的框架,Eino 和 Google ADK 都属于这一层。
┌──────────────────────────────────────────────┐
│ 你的主 Agent(用某个 ADK 框架构建) │
│ ── Eino 或 Google ADK 都行 ── │
└───────┬───────────────────────────┬──────────┘
│ MCP(连工具/数据) │ A2A(连别人的 Agent)
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ 数据库 MCP Server │ │ 外部物流 Agent │
│ 文件系统 MCP │ │ (别家、Java写、另一台机)│
└──────────────────┘ └──────────────────────┘
这三个概念分属完全不同的维度,也可以同时使用。例如一个 Agent 可以用 Eino 或 Google ADK 构建,通过 MCP 接数据库和文件系统,再通过 A2A 把部分任务委托给其他 Agent。
12.2 ADK-Go 框架
ADK-Go 介绍
Google ADK for Go 是 Google 官方维护的开源 Go Agent 开发框架。官方将 ADK 定位为 code-first、模块化的 Agent 工具包,用于构建、评估和部署 Agent。它支持 Gemini,也允许接入其他模型。
ADK-Go 提供 Google 官方的 A2A 集成、A2A launcher、远程 Agent 和协议适配能力,是本章理解 A2A 的主要实现载体。A2A 规范本身由 Linux Foundation 托管的 A2A 项目维护,协议与独立 SDK 不从属于某一个 Agent 框架。
ADK-Go 已进入 v1 稳定版本线。
v1.0.0的 GitHub release 发布于 2026 年 3 月 23 日,截至 2026 年 6 月 19 日最新稳定版本为v1.4.0。版本进入 v1 不表示所有子能力都已稳定,A2A launcher、Agent Config 和部分工具确认扩展仍需要按官方文档管理升级风险。
ADK-Go 的设计重点包括:
- code-first:主要使用 Go 代码定义 Agent;
- Agent Config:可选的 YAML 声明式配置,当前 Go 支持仍为实验状态;
- A2A 集成:可通过 launcher 暴露 Agent,也可把远程 A2A Agent 接入本地系统;
- OpenTelemetry:
telemetry包提供 OTel 配置与导出; - 云平台集成:支持 Gemini 与 Google Cloud 相关部署能力,但不要求所有项目运行在 GCP。
ADK-Go 有着清晰的能力边界:
- 不是通用 RPC 框架,只关注 Agent 应用;
- 不替代模型 API,而是组织模型、工具、会话和运行时;
- 不参与模型训练,主要处理推理阶段应用;
- 不强制使用 GCP,本地和其他基础设施也可以运行。
google.golang.org/adk。社区中还存在名称相近的 fork,依赖、代码和版本体系并不相同,课程示例统一以 Google 官方模块为准。Agent、Model、Tool 与 Runner
ADK-Go 的基本结构可以用四个核心抽象理解:
┌────────────────────────────┐
│ Runner │ ← 跑 Agent 的运行时
│ (Query / Iterator / 事件) │
└─────────────┬──────────────┘
│ 跑
▼
┌────────────────────────────┐
│ Agent │ ← 编排核心
│ llmagent.Config{...} │
│ - Model │
│ - Tools │
│ - Instruction(系统提示) │
│ - Description(给 A2A 看) │
└─────────┬─────────┬────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Model │ │ Tool │
│ (LLM 接口) │ │ (能力接口) │
│ gemini │ │ trackingTool│
│ openai │ │ 搜索 / API │
│ ollama │ │ ... │
└──────────────┘ └──────────────┘
- Agent 负责描述指令、模型、工具和子 Agent;
- Model 负责调用模型;
- Tool 封装外部能力;
- Runner 负责会话与执行事件。
它们与 Eino 的大致对应关系如下:
| Eino | ADK-Go |
|---|---|
| Components:ChatModel / Tool / Retriever | Model / Tool |
| Compose:Chain / Graph | Agent 与 workflow agents |
| Eino ADK:ChatModelAgent / Runner | LLM Agent / Runner |
两者的抽象层次相近,但 API、类型系统、编排能力和生态重点不同。
核心能力
A2A 集成
ADK-Go 可以将已有 Agent 通过 A2A launcher 暴露为远程服务,并动态生成 AgentCard。
下面的示意代码表达 Agent 定义与 AgentCard 字段的关系:
ag, _ := llmagent.New(llmagent.Config{
Name: "logistics_agent", // → AgentCard.name
Description: "查询订单物流状态", // → AgentCard.description
Tools: []tool.Tool{...}, // → AgentCard.skills(每个 tool 抽出一条 skill)
})
// ADK-Go 提供一个 Server 把 Agent 一键暴露为 A2A 服务
// 自动:
// - GET /.well-known/agent-card.json → 返回 AgentCard
// - POST /a2a (SendMessage 等) → 驱动 Agent 处理并返回 Task这段代码用于说明字段映射,不代表 v1.4.0 的完整服务端 API。实际接入中,server/adka2a 负责把 ADK 的 Agent 事件转换为 A2A Task、Artifact 和状态更新,A2A Go SDK 负责 AgentCard、HTTP handler 与具体 binding。AgentCard 的 skills、capabilities 和 security 字段需要按服务配置明确生成,不能假设仅凭工具列表就能完整推导。
OpenTelemetry
ADK-Go 的 google.golang.org/adk/telemetry 包用于配置 OpenTelemetry。官方文档说明 ADK 可为 Agent run、工具调用和模型请求产生 OTel 数据,并通过标准 OTLP 环境变量导出。采集完整提示词内容涉及隐私和成本,必须显式评估。
工具确认
ADK-Go 可以在函数工具上声明执行前需要确认。当前 v1.4.0 的 functiontool.Config 提供 RequireConfirmation 和 RequireConfirmationProvider,框架会通过 Tool Context 发起确认请求并暂停工具执行。
// 在工具上要求"执行前需确认"(API 示意,以官方 pkg.go.dev 为准):
// 注意:tool.Tool 是接口;函数工具用 functiontool.New 构造,
// 通过 functiontool.Config.RequireConfirmation 开启确认(不是 tool.New/Sensitive)。
refundTool, _ := functiontool.New(functiontool.Config{
Name: "refund",
Description: "发起退款",
RequireConfirmation: true, // ★ 关键标记:调用前需人工确认
}, refundHandler)
// Agent 调到该工具时(ADK-Go 的 Human-in-the-Loop 机制):
// 1. 暂停执行,通过 tool.Context.RequestConfirmation 产出"等待确认"事件
// (向客户端/UI 发一个 adk_request_confirmation 事件)
// 2. 状态可 checkpoint 落库
// 3. 收到 confirm 信号后,从 checkpoint 恢复执行
// 相关:tool/toolconfirmation 包;toolset 级的 tool.WithConfirmation 目前仍标 EXPERIMENTAL。RequireConfirmation: true 适合所有调用都要审批的工具;RequireConfirmationProvider 可以根据工具参数动态判断,例如只在退款金额超过阈值时要求确认。工具确认被拒绝或尚未确认时,框架不会直接执行副作用操作。
这一能力与 M04 的状态持久化、M06 的副作用工具安全和 A2A 的 TASK_STATE_INPUT_REQUIRED 形成完整链路:框架内部暂停等待确认,协议层则把远程任务表达为等待输入。
YAML Agent Config
ADK 支持使用 YAML 定义 Agent:
# logistics_agent.yaml
name: logistics_agent
model:
type: gemini
name: gemini-flash-latest
instruction: |
你是物流查询助手,根据运单号返回物流状态。
description: 查询订单物流状态
tools:
- track_shipmentYAML 有利于统一配置、审查和生成工具链。ADK 的目标是让 Go、Java、Python 的 Agent Config 保持相近语义,但各语言实现的支持版本、字段覆盖和限制并不总是同步。当前 Go Agent Config 仍需按官方文档确认实验状态、模型支持和可用字段。
Vertex AI 集成
ADK 与 Google Cloud、Gemini 和 Vertex AI 生态结合紧密,适合已经采用 GCP 的项目。
定义一个 LLM Agent
下面是一个 ADK-Go Agent 定义。它与 M11 11.7的 Eino ChatModelAgent 使用相似的配置结构:
import (
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/tool"
"google.golang.org/genai"
)
model, _ := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
ag, err := llmagent.New(llmagent.Config{
Name: "logistics_agent",
Model: model,
Description: "查询订单物流状态", // 给别的 Agent 看的能力简介(A2A 会用到)
Instruction: "你是物流查询助手,根据运单号返回物流状态。", // 系统提示词
Tools: []tool.Tool{trackingTool},
})Name 标识 Agent,Model 指定模型,Instruction 提供系统指令,Tools 注册能力,Description 可用于生成对外说明。当前模型构造函数、Gemini 模型名和 AgentCard 生成规则应以项目锁定版本为准,代码中也必须处理模型和 Agent 初始化错误。
ADK-Go 与 Eino
下面的表格列出了 ADK-Go 与 Eino 框架在不同维度的对比。
| 维度 | Eino | ADK-Go |
|---|---|---|
| 设计定位 | Go LLM / Agent 框架 | Google code-first Agent 工具包 |
| 类型与编排 | 泛型 + Compose,复杂图能力较强 | Agent / workflow agents,运行时组合 |
| 流式 | Compose 统一多种运行范式 | Model、Runner 和 Live 能力按接口提供 |
| A2A | 需要集成独立 SDK 或适配层 | 官方提供实验性的 A2A launcher / remote agent |
| OTel | 通过 callback 或集成接入 | 提供 telemetry 包 |
| 人工确认 | Interrupt / Checkpoint | RequestConfirmation |
| YAML | 非主线 | Agent Config,Go 当前为实验能力 |
| 跨语言生态 | Go 为主 | Python、Java、Go 等 ADK 生态 |
| Google Cloud | 无特定绑定 | Gemini / Google Cloud 集成较深 |
一个可行的选型树如下:
要做一个 Agent 项目
│
├─ 内部复杂编排(多步骤、循环、并行)?
│ │是 → Eino(编排能力最强)
│
├─ 要把 Agent 对外发布给别人调用(A2A)?
│ │是 → ADK-Go(原生支持,开箱即用)
│
├─ 跨语言团队共用 Agent 定义?
│ │是 → ADK-Go(YAML 跨语言对齐)
│
├─ 已部署在 GCP / Vertex AI?
│ │是 → ADK-Go(深度集成)
│
└─ 默认选项
→ Eino(国内生态最完整、生产级最成熟)这棵树适合建立初步判断,但不应作为绝对结论。A2A 也可以通过独立 SDK 接入 Eino,ADK-Go 本身也能构建复杂工作流。项目选型还要考虑团队经验、现有基础设施、版本稳定性、模型供应商和维护成本。
本套课程仍采用 Eino 作为项目内部编排主线,把 ADK-Go 作为理解和实践 A2A 的载体。两者可以组合,但也可能只选其中之一。
12.3 A2A 协议
为什么需要 A2A
假设主 Agent 需要查询物流,而物流能力由另一个团队的独立 Agent 提供。直接调用私有 HTTP API 虽然可行,但每个调用方都要理解对方专用接口,形成 N×M 的适配成本。
把远程 Agent 当成 MCP 工具也会丢失一部分语义。MCP 适合输入明确、调用边界稳定的工具和数据源;远程 Agent 则可能:
- 发起多轮澄清;
- 中途请求补充信息或认证;
- 执行数分钟乃至数小时;
- 持续推送状态与中间产物;
- 根据上下文自主决定下一步。
A2A 为这类有状态、长周期、可能多轮的 Agent 协作提供统一标准。它建立在 HTTP、JSON、SSE、gRPC 等成熟技术之上,但增加了 AgentCard、Task、Artifact 和 Push Notification 等 Agent 语义。
A2A 的定位
A2A 是开放、厂商中立的 Agent 互操作协议,用于让不同框架、语言和供应商构建的 Agent 发现彼此、交换消息并管理协作任务。
Google 于 2025 年 4 月公开 A2A。2025 年 6 月,协议项目移交 Linux Foundation 治理,由多家企业参与技术方向与生态建设。A2A v1.0.0 于 2026 年 3 月 12 日发布,v1.0.1 于 2026 年 5 月发布并成为截至 2026 年 6 月 19 日的最新稳定补丁版本。
移交基金会治理意味着 A2A 不再只是 Google 私有协议,而是面向不同厂商、框架和语言共同演进的开放标准。生态支持数量会随时间变化,选型时应查看 A2A 官方社区与当前 SDK 列表。
A2A 不负责以下工作:
- 不规定 Agent 内部使用 Eino、ADK 还是其他框架;
- 不替代模型 API;
- 不替代通用 RPC,而是在 binding 之上定义 Agent 协作语义;
- 不规定 Agent 的实现语言、部署环境和推理引擎。
架构与 AgentCard
A2A 的角色是 Client Agent 与 Remote Agent。一次调用中,Client Agent 代表用户发起请求,Remote Agent 暴露 A2A 服务。不同交互中,同一个系统可能同时扮演调用方与服务方。
┌──── Agent A ────┐ ┌──── Agent B ────┐
│ │ ──[SendMessage]─►│ │
│ Client Agent │ │ Server Agent │
│ (发起方) │ ◄──[Task/状态]────│ (接收方) │
└─────────────────┘ └─────────────────┘
↑↓ ↑↓
也可能是别人的 也可能反过来
Server Agent Client AgentMCP 的 Host、Client、Server 角色通常更不对称,Host 主导工具调用;A2A 更强调独立 Agent 之间的委托与协作。不过在具体一次网络请求中,仍然存在明确的 A2A Client 和 A2A Server。
| 组件 | 作用 | MCP 中的近似概念 |
|---|---|---|
| AgentCard | 声明身份、接口、能力与安全要求 | Server Card 提案 |
| AgentInterface | 端点、binding、协议版本和可选 tenant | Server 地址与传输配置 |
| Task | 有状态协作对象 | 无直接对应 |
| Capabilities | streaming、push 等支持能力 | capabilities 协商 |
| Skills | 对外能力描述 | 与 tools 相近,但不是直接 RPC 方法 |
AgentCard 示例:
{
"name": "物流查询助手",
"description": "根据运单号查询订单的实时物流状态",
"version": "1.0.0",
"capabilities": { "streaming": true, "pushNotifications": false },
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"supportedInterfaces": [
{
"url": "https://logistics.example.com/a2a",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"skills": [
{
"id": "track_shipment",
"name": "物流跟踪",
"description": "输入运单号,返回当前物流状态与轨迹",
"tags": ["logistics", "tracking"]
}
],
"signatures": [
{
"protected": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9",
"signature": "..."
}
]
}
supportedInterfaces 是 A2A 1.0 的重要结构,每一项包含 url、protocolBinding 和 protocolVersion,还可以包含多租户路由字段。它替代了 0.3 时代顶层 url、preferredTransport 和 additionalInterfaces 的组合。
capabilities 用于声明 streaming、push notification 和扩展 AgentCard 等能力。客户端必须先检查能力再选择操作。
skills 只是能力声明,不等于可直接调用的工具方法。它也不同于 M06 6.9中以 Markdown 和资源组织能力的 Skills 概念。
signatures 使用 JWS 与 JSON Canonicalization 为 AgentCard 提供签名。支持多签名可以适配多个签发者或信任链。客户端还需要结合 TLS、认证、授权和可信密钥来源验证服务身份。
A2A 1.0 的标准发现路径是 /.well-known/agent-card.json。兼容早期实现时可能遇到 /.well-known/agent.json 等其他历史路径,但新服务应使用标准路径。
核心原语
Message
Message 表示一次通信中的单条消息。SendMessage 用于普通请求,SendStreamingMessage 用于通过流持续接收结果。
Message 的 parts 是内容块数组。A2A 1.0 的 Part 使用成员存在性区分类型,可包含 text、raw、url 或 data,不再使用 0.3 的 kind 字段。role 使用 ProtoJSON 枚举名 ROLE_USER 或 ROLE_AGENT,messageId 是必填唯一标识。可选 contextId 和 taskId 分别用于关联上下文与现有任务。
Task
Task 是 A2A 区别于普通 RPC 和 MCP 工具调用的核心抽象,用于表示有状态、长周期、可多轮的协作。
与 Task 相关的操作包括:
| 操作 | 作用 |
|---|---|
GetTask | 查询任务状态与产物 |
ListTasks | 按条件列出任务 |
CancelTask | 取消可取消的任务 |
SubscribeToTask | 订阅已有任务的事件 |
CreateTaskPushNotificationConfig | 创建推送回调配置 |
GetTaskPushNotificationConfig | 查询推送配置 |
ListTaskPushNotificationConfigs | 列出推送配置 |
DeleteTaskPushNotificationConfig | 删除推送配置 |
Task 生命周期如下:
┌───────────────► TASK_STATE_CANCELED
│ (被取消)
│
TASK_STATE_SUBMITTED ──► TASK_STATE_WORKING ──┬──► TASK_STATE_COMPLETED (顺利完成,有 artifacts)
(已提交) (处理中) │
├──► TASK_STATE_FAILED (失败,有 error 信息)
│
├──► TASK_STATE_REJECTED (Agent 拒绝执行)
│
├──► TASK_STATE_AUTH_REQUIRED (需要鉴权)
│
└──► TASK_STATE_INPUT_REQUIRED (需要发起方补充信息)
│
└──► (发起方补充后) 回到 TASK_STATE_WORKING
TaskState | 含义 |
|---|---|
TASK_STATE_UNSPECIFIED | 未指定 |
TASK_STATE_SUBMITTED | 已提交 |
TASK_STATE_WORKING | 正在处理 |
TASK_STATE_INPUT_REQUIRED | 需要补充信息 |
TASK_STATE_AUTH_REQUIRED | 需要额外认证 |
TASK_STATE_COMPLETED | 成功完成 |
TASK_STATE_FAILED | 执行失败 |
TASK_STATE_CANCELED | 已取消 |
TASK_STATE_REJECTED | 服务方拒绝执行 |
A2A 1.0 的枚举遵循 ProtoJSON,使用 SCREAMING_SNAKE_CASE。0.3 中的小写 working、input-required、user 和 agent 已被替换。
TASK_STATE_INPUT_REQUIRED 用于跨进程 Human-in-the-Loop。例如,物流 Agent 发现一个模糊运单号对应三个包裹,可以暂停任务并返回询问消息。调用方获取用户选择后,带同一个 taskId 再次发送 Message,使任务继续执行。
框架内部的暂停确认与协议状态可以对应起来:
Agent 内部"暂停等确认"
⟷
协议上"Task 处于 input-required"
⟷
发起方"看到这个状态,去拿到人工输入,再回传"这条链路把 M04 的检查点与状态持久化、M06 的副作用审批和跨进程协议状态连接起来。
Artifact
Artifact 是 Task 的结构化输出。一个 Task 可以返回多个 Artifact,每个 Artifact 包含名称、parts[] 和可选 metadata。
数据分析 Agent 可以返回 CSV、图表 URL 和总结文本;文档 Agent 可以返回 Markdown、PDF 与引用列表。Artifact 和 Message 共用 Part 内容模型,调用方可以根据媒体类型和 metadata 消费不同产物。
Streaming
SendStreamingMessage 用于创建请求并接收流,SubscribeToTask 用于订阅已有 Task。JSON-RPC 与 HTTP+JSON binding 通常使用 SSE,gRPC binding 使用服务端流。
调用前应确认 AgentCard 声明 streaming 能力。A2A 流推送的是 Task 状态与 Artifact 增量,不等同于 MCP 的诊断日志。
Push Notification
对于运行数小时或跨天的任务,调用方不适合长期保持连接。Push Notification 允许调用方注册 webhook,服务端在状态变化时主动通知,再由调用方查询任务详情。
它是可选能力,需要 AgentCard 声明支持。生产系统必须对回调 URL 做 SSRF 防护,验证回调认证,并避免在通知载荷中泄露敏感结果。
Discovery
调用方通过 GET /.well-known/agent-card.json 获取 AgentCard,再从 supportedInterfaces 选择双方支持的 binding,检查 capabilities 和 security,并在需要时验证签名。
企业注册中心可以索引 AgentCard URL,但不替代 AgentCard 本身的标准定义与发现流程。
请求与响应
A2A 1.0 的规范以 Protobuf 为通用定义源,再为 JSON-RPC、gRPC 和 HTTP+JSON 提供等价 binding。使用 JSON-RPC 时,核心操作名统一为 PascalCase。
| 操作 | 关联原语 | 作用 |
|---|---|---|
SendMessage | Message / Task | 发送消息,返回 Message 或 Task |
SendStreamingMessage | Streaming | 发送消息并流式接收事件 |
SubscribeToTask | Streaming | 订阅已有 Task |
GetTask | Task | 查询任务 |
ListTasks | Task | 列出任务 |
CancelTask | Task | 取消任务 |
CreateTaskPushNotificationConfig | Push | 创建推送配置 |
GetTaskPushNotificationConfig | Push | 查询推送配置 |
ListTaskPushNotificationConfigs | Push | 列出推送配置 |
DeleteTaskPushNotificationConfig | Push | 删除推送配置 |
前六个属于消息和任务主流程相关原语,后面四个是 Push Notification 配置操作。
SendMessage 报文
请求:
{
"jsonrpc": "2.0",
"id": "1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-req-01",
"role": "ROLE_USER",
"parts": [{ "text": "查一下运单 SF1234567890 的物流" }]
}
}
}短交互可以直接返回 Message:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"message": {
"messageId": "msg-aa11",
"role": "ROLE_AGENT",
"parts": [{ "text": "运单 SF1234567890 已签收,签收人:张三,2026-05-30 14:22" }]
}
}
}长交互返回 Task:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"task": {
"id": "task-789",
"contextId": "ctx-456",
"status": { "state": "TASK_STATE_WORKING", "timestamp": "2026-05-30T14:22:00Z" },
"history": [{"role": "ROLE_USER", "parts": [{"text": "查一下运单..."}]}]
}
}
}A2A 1.0 的 SendMessageResponse 使用 oneof 语义,result 中出现 message 或 task。客户端应按成员存在性分支,不能继续依赖 0.3 的 kind 字段或“是否存在 status”之类的探测逻辑。
input-required 报文
// 客户端 GetTask
// 响应:Task 处于 input-required 状态,带询问消息
// 注意:GetTask 的 result 直接是 Task 对象(没有 SendMessage 那种 oneof 包装)
{
"jsonrpc": "2.0",
"id": "2",
"result": {
"id": "task-789",
"status": {
"state": "TASK_STATE_INPUT_REQUIRED",
"message": {
"messageId": "msg-ask-01",
"role": "ROLE_AGENT",
"parts": [{"text": "找到 3 个匹配,请确认查哪个:[A] / [B] / [C]"}]
}
}
}
}
// 客户端拿到用户选择,继续 SendMessage 同一个 task
{
"jsonrpc": "2.0",
"id": "3",
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-reply-01",
"role": "ROLE_USER",
"parts": [{"text": "查 [A]"}],
"taskId": "task-789" // 关联到现有 task
}
}
}调用方不能把 TASK_STATE_INPUT_REQUIRED 当成失败。它需要把 status.message 转交给用户或上层 Agent,获得输入后再续接原 Task。
错误
JSON-RPC 继续使用标准错误码,A2A 还定义协议错误类型。
下面是常见的错误码映射:
| Code | 含义 |
|---|---|
-32700 | Parse error |
-32600 | Invalid Request |
-32601 | Method not found |
-32602 | Invalid params |
-32603 | Internal error |
-32001 | Task not found |
-32002 | Task cannot be canceled |
A2A 1.0 进一步使用 google.rpc.Status 与 google.rpc.ErrorInfo 表达错误详情。具体 code、reason 和 HTTP 映射应直接使用当前 SDK 或规范,不能只靠这张简化表。
协议层错误和业务失败需要分开处理。JSON 无法解析、方法不存在属于协议错误;运单号无效、业务拒绝或执行失败通常应进入 Task 状态与消息,而不是伪装成 JSON-RPC 解析错误。
传输绑定 (transport binding)
传输绑定:将上层协议的协作语义(也就是交互规则、消息格式、业务逻辑),映射适配到具体底层传输协议上的规范。
A2A 的协作语义与传输绑定 分离。AgentCard 的 supportedInterfaces 可以声明 JSON-RPC、gRPC 和 HTTP+JSON,客户端选择双方都支持的接口。
JSON-RPC
- 使用 HTTP/HTTPS + JSON;
- 方法使用
SendMessage、GetTask、CancelTask等 PascalCase 名称; - role 和 state 使用 ProtoJSON 枚举名;
- 请求使用
jsonrpc、id、method、params信封; - 流式操作通过 SSE 返回事件;
- A2A 1.0 请求需要发送
A2A-Version: 1.0版本头。
A2A 0.3 使用过 message/send、tasks/get、小写 role/state 和 kind 字段。对接旧系统时应根据 AgentInterface 的 protocolVersion 做明确兼容,不能在同一请求中混用两套格式。
gRPC
- 使用 HTTP/2 + Protobuf;
- 方法与抽象操作同名;
- 枚举和消息直接来自规范的
a2a.proto; - 流式使用 gRPC server streaming;
- 适合内部高并发、低延迟或希望生成强类型客户端的场景。
HTTP+JSON
HTTP+JSON 使用 Google AIP 风格的资源路径:
POST /message:send:SendMessage;POST /message:stream:SendStreamingMessage;GET /tasks/{id}:GetTask;GET /tasks:ListTasks;POST /tasks/{id}:cancel:CancelTask;POST /tasks/{id}:subscribe:SubscribeToTask;POST /tasks/{id}/pushNotificationConfigs:创建推送配置;GET /tasks/{id}/pushNotificationConfigs/{configId}:查询推送配置;GET /tasks/{id}/pushNotificationConfigs:列出推送配置。
HTTP+JSON 同样使用 ProtoJSON 字段与枚举,并携带 A2A 版本信息。它适合已有 OpenAPI、网关和 REST 运维工具链的团队。
| 场景 | 常见选择 |
|---|---|
| 通用 Web 与跨语言接入 | JSON-RPC |
| 内部高性能与强类型 | gRPC |
| 既有 REST / OpenAPI 工具链 | HTTP+JSON |
| 希望与 MCP 客户端共享 JSON-RPC 基础设施 | JSON-RPC |
本章实战采用 JSON-RPC。M06 6.6 MCP stdio 客户端已经处理过 JSON-RPC 信封与错误,本章只需把 stdio 传输替换为 HTTP,并实现 A2A 的方法、版本和任务模型。
设计特点
独立 Agent 间协作
A2A 面向独立 Agent 系统。Client Agent 不需要获得 Remote Agent 的内部记忆、工具或实现,只依赖对方公开的 AgentCard 与协议接口。
Task 是一等公民
Task 把一次协作建模为有状态对象,支持工作中、补充输入、认证、取消和终态。它是 A2A 相比普通同步 RPC 的核心差异。
标准发现
AgentCard 和 well-known 路径提供标准化发现。企业注册中心可以进一步建立索引、权限和治理,但不会改变 AgentCard 的接口契约。
Streaming 与 Push
Streaming 适合连接持续存在时的实时更新,Push Notification 适合真正异步的长任务。两种方式分别解决“保持连接”和“稍后回调”。
多种 binding
统一语义可以映射为 JSON-RPC、gRPC 和 HTTP+JSON,便于不同基础设施选型。代价是客户端与服务端必须正确协商版本和 binding。
中立治理
A2A 项目由 Linux Foundation 托管,技术方向由社区治理。中立治理有助于企业避免依赖单一厂商的私有 Agent 协议。
安全是协议边界的一部分
AgentCard 可以声明安全方案并提供签名;认证使用 TLS、OAuth 2.0、OpenID Connect、API Key 或 mTLS 等 Web 标准。协议载荷不应直接携带用户身份凭据,授权仍由服务端结合业务策略完成。
| 维度 | A2A | MCP | OpenAPI | gRPC |
|---|---|---|---|---|
| 目标 | Agent 间协作 | AI 应用连接工具与数据 | 通用 Web API | 高性能 RPC |
| 关系 | Client Agent → Remote Agent,可双向部署 | Host / Client / Server | client → server | RPC client / server |
| 状态 | Task 生命周期 | 主要按请求调用 | 通常无状态 | 通常无状态 |
| 形态 | JSON-RPC、gRPC、HTTP+JSON | JSON-RPC,多种传输 | HTTP + JSON | HTTP/2 + Protobuf |
| 发现 | AgentCard | list 方法与配置 | OpenAPI 文档 | proto / reflection |
| 异步 | Streaming + Push | notification / streaming 能力 | 不原生规定 | streaming |
| 中途输入 | TASK_STATE_INPUT_REQUIRED | 无同等 Task 状态 | 无 | 无 |
| 治理 | Linux Foundation A2A 项目 | MCP 社区与规范维护方 | OpenAPI Initiative | gRPC 社区 |
A2A 与 MCP 不是替代关系。企业 Agent 可以通过 MCP 使用数据库和文件系统,再通过 A2A 与物流、风控或客服 Agent 协作。
12.4 暴露并调用 A2A Agent
本节将实现服务端使用 ADK-Go 暴露 Agent,另外再手写一个客户端完整实现“发现 → 选择端点 → 发送消息 → 解析 Message 或 Task”的流程。

服务端职责
使用下面的示意代码概括服务端职责:
// 服务端:把 12.3 定义的 ag 暴露为 A2A 服务(API 示意,以官方为准)
// ADK-Go 会据 ag 的 Name/Description/Tools 自动生成 AgentCard,
// 并起一个 HTTP 服务处理 A2A 的 JSON-RPC 方法。
// - GET /.well-known/agent-card.json → 返回 AgentCard
// - POST /a2a (SendMessage 等) → 驱动 ag 处理并返回 TaskADK-Go 的 A2A 服务端由两层组成:
server/adka2a负责把 ADK 事件翻译为 A2A Task、Artifact 和状态更新;- 外部 A2A Go SDK 负责 HTTP 服务、AgentCard 发布和 JSON-RPC binding。
v1.4.0已同时依赖旧版 SDK 与github.com/a2aproject/a2a-go/v2,实际项目应按官方 quickstart 和锁定版本选择对应包路径。
拉取 AgentCard
客户端先从 well-known 路径获取 AgentCard,再选择 JSON-RPC interface:
package a2a
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/yourname/llmagent/internal/transport" // 复用 M01 的 HTTP 客户端
)
type Skill struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
// AgentInterface 是 v1.0 supportedInterfaces[] 里的一项:一个可连端点 + 它的协议绑定。
type AgentInterface struct {
URL string `json:"url"`
ProtocolBinding string `json:"protocolBinding"` // JSONRPC / GRPC / HTTP+JSON
ProtocolVersion string `json:"protocolVersion"`
}
type AgentCard struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
SupportedInterfaces []AgentInterface `json:"supportedInterfaces"`
URL string `json:"url"` // v0.x 兼容:老卡片把端点放顶层
Skills []Skill `json:"skills"`
}
// JSONRPCEndpoint 从名片里挑出 JSON-RPC 绑定的端点(挑不到就回退到老的顶层 url)。
// 这正是"先取卡、再按绑定选端点、最后才发消息"流程里的一步。
func (c *AgentCard) JSONRPCEndpoint() string {
for _, it := range c.SupportedInterfaces {
if it.ProtocolBinding == "JSONRPC" {
return it.URL
}
}
return c.URL // 兼容 v0.x 单 url
}
// FetchAgentCard 从对方的 well-known 路径拉取名片。
func FetchAgentCard(ctx context.Context, baseURL string) (*AgentCard, error) {
url := baseURL + "/.well-known/agent-card.json"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := transport.NewClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("拉取 AgentCard 失败: 状态码 %d", resp.StatusCode)
}
var card AgentCard
if err := json.NewDecoder(resp.Body).Decode(&card); err != nil {
return nil, err
}
return &card, nil
}生产客户端还需要校验 URL、HTTPS、响应大小、Content-Type、协议版本、安全方案和 AgentCard 签名。JSONRPCEndpoint 也应在找不到可用接口时返回错误,而不是无条件接受空 URL。
发送消息
SendMessage 的结果可能是 Task,也可能是即时 Message。客户端需要同时支持两种结果:
package a2a
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/yourname/llmagent/internal/transport"
)
type TaskStatus struct {
// PROTO 枚举名:TASK_STATE_SUBMITTED / TASK_STATE_WORKING /
// TASK_STATE_INPUT_REQUIRED / TASK_STATE_COMPLETED / TASK_STATE_FAILED / ...
State string `json:"state"`
}
type Task struct {
ID string `json:"id"`
Status TaskStatus `json:"status"`
// 实际还有 artifacts、history 等,按需补充
}
// Message 是即时回复形态:短问题服务端可能直接回一条消息,而不建 Task。
type Message struct {
MessageID string `json:"messageId"` // A2A 1.0 必填
Role string `json:"role"` // 通常是 "ROLE_AGENT"
Parts []struct {
Text string `json:"text"`
} `json:"parts"`
}
// SendResult:SendMessage 的结果可能是 Task(长任务,要跟踪状态机)或 Message(即时回复)。
// A2A 1.0 不保证永远返回 Task——两种都得能接,否则客户端只适配你假想的那种服务端。
type SendResult struct {
Task *Task
Message *Message
}
// uuidNew 生成一个新 messageId(业务代码中可换成 google/uuid)。
func uuidNew() string { /* ... 略 ... */ return "msg-uuid-placeholder" }
// SendMessage 向某个 A2A Agent 的端点发送一条消息,返回结果(Task 或 Message)。
// 严格按 A2A 1.0 JSON-RPC binding 实现:messageId 必填、role 用 PROTO 枚举名、
// 必带 A2A-Version 头、result 内 oneof task/message。
func SendMessage(ctx context.Context, endpoint, text string) (*SendResult, error) {
reqBody := map[string]any{
"jsonrpc": "2.0",
"id": "1",
"method": "SendMessage",
"params": map[string]any{
"message": map[string]any{
"messageId": uuidNew(), // A2A 1.0:必填
"role": "ROLE_USER", // 1.0 PROTO 枚举名
"parts": []map[string]any{{"text": text}}, // 1.0:靠成员存在区分类型,无 kind
},
},
}
data, _ := json.Marshal(reqBody)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("A2A-Version", "1.0") // 1.0 规范 §3.6.1:Client MUST 带该头
resp, err := transport.NewClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// A2A 1.0 §9.4.1:result 是 SendMessageResponse,其内 oneof:task 或 message。
var rpcResp struct {
Result struct {
Task *Task `json:"task,omitempty"`
Message *Message `json:"message,omitempty"`
} `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
return nil, err
}
if rpcResp.Error != nil {
return nil, fmt.Errorf("A2A 错误 %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
}
switch {
case rpcResp.Result.Task != nil:
return &SendResult{Task: rpcResp.Result.Task}, nil
case rpcResp.Result.Message != nil:
return &SendResult{Message: rpcResp.Result.Message}, nil
default:
return nil, fmt.Errorf("A2A 响应 result 既无 task 也无 message")
}
}
// 注:v0.3.x JSON-RPC binding 曾用 result 顶层 kind="task"|"message" 字段或靠
// "有没有 status" 这种结构差异区分;A2A 1.0 已废弃,统一为 result.task / result.message
// oneof 包装。新代码按上面 switch 写,不要再做 kind / status 探测。这段代码完整表达了 1.0 JSON-RPC 主流程,但还不是生产客户端:
json.Marshal错误被忽略;id固定为"1";uuidNew只是占位;- 未检查 HTTP 状态与 Content-Type;
- Task 结构没有 Artifact、history、状态消息和时间戳;
- 未实现 GetTask、SubscribeToTask、重试、认证和签名验证;
- 未限制响应体大小和重定向目标。
包装远程 Agent
把 A2A 调用包装为 M06 的 tool.Tool,主 Agent 就可以通过统一工具接口委托远程 Agent:
func RemoteAgentTool(name, endpoint, desc string) tool.Tool {
return tool.NewTypedTool(name, desc,
func(ctx context.Context, a struct {
Request string `json:"request" desc:"要委托给远程 Agent 处理的请求"`
}) (string, error) {
res, err := SendMessage(ctx, endpoint, a.Request)
if err != nil {
return "", err
}
switch {
case res.Message != nil: // 即时回复:拼出文本直接返回
text := ""
for _, p := range res.Message.Parts {
text += p.Text
}
return text, nil
case res.Task != nil:
// 简化:真实实现要按 res.Task.Status.State 处理
// TASK_STATE_INPUT_REQUIRED / 轮询 TASK_STATE_WORKING / 终态 TASK_STATE_COMPLETED 等
return fmt.Sprintf("远程任务 %s 状态: %s", res.Task.ID, res.Task.Status.State), nil
default:
return "", fmt.Errorf("A2A: 空结果")
}
})
}这和 M06 6.7 MCPToolBridge使用相同的适配器思路:在边界处把远程协议转换为本地工具接口。
真实生产环境下的实现不能只返回 Task 状态,还需要做以下工作:
- 对
TASK_STATE_WORKING轮询或订阅; - 对
TASK_STATE_INPUT_REQUIRED向用户或上层 Agent 请求输入; - 对
TASK_STATE_AUTH_REQUIRED启动协议外认证流程; - 对
TASK_STATE_COMPLETED提取 Artifact 和最终消息; - 对失败、取消、拒绝和超时进行分类;
- 保存
taskId、contextId和审计信息。
12.5 A2A 与 MCP 决策
判断外部依赖应该使用 MCP 还是 A2A,关键在于它是被动能力还是自主 Agent:
要对接一个外部依赖
│
├─ 它是"被动的能力"吗?(给输入→拿输出,无自主性、无多轮状态)
│ │是
│ └─► 用 MCP(当工具)。例:数据库、文件系统、搜索 API
│
└─ 它是"主动的 Agent"吗?(有自主决策、可能多轮/中途要信息/长时异步)
│是
└─► 用 A2A(当对等 Agent)。例:外部物流 Agent、风控 Agent| 特征 | 倾向 MCP | 倾向 A2A |
|---|---|---|
| 交互 | 一次调用获取结果 | 多轮、有状态协作 |
| 对方 | 被动工具或数据源 | 自主 Agent |
| 补充信息 | 通常不需要 | 可能进入 input-required |
| 长时异步 | 较少 | 常见 |
| 流程控制 | 主 Agent 主导 | 双方按任务状态协作 |
“像 API 还是像同事”可以作为快速判断,但工程上还要考虑协议成本。简单确定性的物流查询可能只需要 MCP 或普通 API;只有当物流服务具有自主决策、多轮澄清或长任务能力时,A2A 的 Task 模型才真正有价值。
配套练习:暴露并互调 A2A Agent
需求
使用 ADK-Go 定义并暴露一个物流查询 Agent logistics_agent;再使用 Eino 或 M04 的手写 Agent作为调用方,通过本章客户端把物流问题委托给远程 Agent。
验收点
- 使用
llmagent.New定义物流 Agent; - 按锁定版本使用 ADK-Go A2A launcher 暴露服务;
- 能访问
GET /.well-known/agent-card.json; - AgentCard 至少包含 name、description、skills 和可用 interface;
- 使用
FetchAgentCard获取并打印名片; - 从
supportedInterfaces选择 JSON-RPC 端点; - 使用
SendMessage发送一次查询,并处理 Message 或 Task; - 使用
RemoteAgentTool包装远程 Agent; - 在调用方 Agent 中自动委托物流问题;
- 构造
TASK_STATE_INPUT_REQUIRED场景,补充信息后继续原 Task; - 说明该物流能力为何使用 A2A,而不是 MCP;
- 为 HTTP 客户端补充超时、认证、响应大小限制与错误检查;
- 执行
go build ./...与go test ./...。
练习中应记录完整调用链:发现 AgentCard、选择 interface、发送 Message、获得 Task、补充输入、得到 Artifact。这样才能验证理解的是协议生命周期,而不只是一次 HTTP 请求。
本章小结
| 内容 | 在系统中的作用 |
|---|---|
| Eino ADK 与 Google ADK-Go | 避免同名概念混淆 |
| MCP / A2A / ADK | 区分工具连接、Agent 连接和构建框架 |
| ADK-Go 四个抽象 | Agent、Model、Tool、Runner |
| ADK-Go 能力 | A2A、OTel、确认、YAML 和云平台集成 |
| AgentCard | 发现远程 Agent 并声明接口、能力和安全要求 |
| Message / Task / Artifact | 表达即时消息、长任务和结构化产物 |
| input-required | 表达跨进程人工介入 |
| 多种 binding | JSON-RPC、gRPC 与 HTTP+JSON |
| 手写客户端 | 理解发现、调用和状态解析 |
| RemoteAgentTool | 把远程 Agent 接入本地工具系统 |
| A2A 与 MCP 决策 | 根据外部依赖的自主性和状态选择协议 |
思考题
- 当主 Agent 同时连接本地工具、多个 MCP Server 和多个 A2A Agent 时,仅能力定义就可能占用大量上下文。如何使用 M09 9.5 动态工具暴露、分层检索和按需加载治理这类膨胀?
- A2A Task 可以跨越多个请求并长时间运行。Task 状态如何持久化、恢复和清理?它与 M04 状态持久化、M09 上下文治理和 M11 检查点有什么关系?
- 跨 Agent 调用会让一次请求穿过多个进程和工具。如何传播 trace context,并使用 ADK-Go 的 OTel 与 M10 10.2 可观测体系定位问题?
下一步
下一章进入项目篇。前十二章已经完成知识篇与框架篇的核心铺垫:先手写原理,再使用 Eino 组织内部编排,最后使用 A2A 建立外部 Agent 协作边界。