系列文章
- 理解 AI Agent 的大脑:ReAct 模式从入门到实战
- AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
- AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
- AI Agent 的规划大脑:从任务分解到自适应执行策略
- MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
- AI Agent 的工作流编排:从顺序链到自适应 DAG 的 Java 实战
- AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
- Agent 间如何对话:A2A 协议深度解析与 Java 实战(本文)
一个真实的困境
假设你正在构建一个企业级 AI 系统。你的客服 Agent 能处理售前咨询,你的物流 Agent 能追踪包裹,你的财务 Agent 能处理退款。三个 Agent 各自独立运行,都很优秀。
但用户问了一个问题:”我上周买的那双鞋,物流显示签收了但我没收到,帮我退款。”
这个问题横跨了三个领域。你怎么办?
方案一:把所有能力塞进一个巨大的 Agent。问题是——这个 Agent 的 System Prompt 会膨胀到几千行,Tool 列表越来越长,模型的注意力被稀释,每个子任务的质量都在下降。
方案二:让 Agent 之间互相调用。客服 Agent 接到请求后,先问物流 Agent 确认签收状态,再问财务 Agent 发起退款。这就是多 Agent 协作。
方案二显然更合理。但马上面临一个现实问题:这些 Agent 可能用不同的框架开发——客服 Agent 用 LangChain4j,物流 Agent 用 Python 的 LangGraph,财务 Agent 是一个独立的微服务。它们之间怎么通信?
这就是 Google 在 2025 年 4 月提出的 A2A(Agent-to-Agent)协议要解决的问题。
如果说 MCP 是 Agent 与工具之间的「万能接口」,那么 A2A 就是 Agent 与 Agent 之间的「通用语言」。
为什么需要 A2A?从协议缺失说起
在我们之前的 MCP 文章 中,我们详细讲过 MCP 如何让 Agent 统一地连接外部工具和数据源。MCP 解决了 Agent ↔ Tool 的标准化问题。
但在多 Agent 场景下,还有一个更关键的问题没有解决:Agent ↔ Agent 的通信。
你可能会说,Agent 之间通信有什么难的?写个 REST API 不就行了?
确实,你可以手写 API。但想想这些挑战:
异构 Agent 的互操作
每个 Agent 框架都有自己的消息格式、状态管理方式和通信协议。LangChain4j 的 Agent 返回 ChatMemory,Spring AI 的 Agent 返回 ChatResponse,Python Agent 可能返回完全不同的 JSON 结构。你要为每对 Agent 的组合写适配代码,组合数是 N×(N-1)。
长时间任务的生命周期管理
Agent 执行的任务可能不是瞬间完成的。一个数据分析 Agent 处理一个复杂查询可能需要几分钟。你需要任务状态追踪、进度回调、取消机制。REST API 的简单 request/response 模型不够用。
能力发现
Agent A 怎么知道 Agent B 能做什么?在微服务架构中我们有服务注册中心和 API 网关,但 Agent 的能力描述比 API 端点复杂得多——它涉及模型能力、支持的输入格式、上下文窗口大小等。
安全与授权
Agent 之间的调用需要身份验证和授权。你不能让任意 Agent 都能调用财务 Agent 发起退款。
A2A 协议正是为了解决这些问题而设计的。
A2A 核心概念:一张全景图
在深入细节之前,我们先建立一个整体认知。A2A 协议基于 HTTP + JSON-RPC 2.0,定义了以下几个核心概念:
1 2 3 4 5 6 7 8 9 10
| ┌─────────────┐ A2A Protocol ┌─────────────┐ │ Client Agent│ ──────────────────────────── → │ Server Agent│ │ (发起方) │ ← ──────────────────────────── │ (执行方) │ └─────────────┘ └─────────────┘ │ │ │ 1. Discovery (Agent Card) │ │ 2. Task lifecycle (send/cancel) │ │ 3. Message exchange │ │ 4. Streaming (SSE) │ │ 5. Artifact return │
|
- Agent Card:Agent 的「名片」,描述它能做什么、怎么调用
- Task:一次完整的交互单元,有完整的生命周期
- Message:Agent 之间传递的消息,包含多个 Part
- Part:消息的内容单元,可以是文本、文件、结构化数据
- Artifact:任务的输出产物,比如生成的文件、分析报告
这些概念之间的关系很清晰:
1 2 3 4 5
| 一个 Agent Card 描述一个 Agent 的能力 一次调用创建一个 Task 一个 Task 包含多轮 Message 一个 Message 包含多个 Part 一个 Task 产生多个 Artifact
|
深入 Agent Card:Agent 的自我介绍
Agent Card 是 A2A 的起点。它是一个 JSON 文档,通常托管在 /.well-known/agent.json 路径下,类似于 OpenAPI Spec 对 REST API 的作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| { "name": "Logistics Agent", "description": "物流查询与追踪 Agent,支持包裹查询、签收确认、异常处理", "url": "https://logistics-agent.example.com", "version": "1.0.0", "capabilities": { "streaming": true, "pushNotifications": false, "stateTransitionHistory": true }, "authentication": { "schemes": ["Bearer"] }, "defaultInputModes": ["text", "file"], "defaultOutputModes": ["text", "file"], "skills": [ { "id": "track-package", "name": "包裹追踪", "description": "根据运单号查询包裹的实时物流状态", "tags": ["logistics", "tracking"], "examples": [ "帮我查一下运单号 SF1234567890 的物流状态", "我的快递到哪了?运单号是 JD9876543210" ] }, { "id": "confirm-delivery", "name": "签收确认", "description": "确认包裹是否已签收,包括签收时间、签收人、签收照片", "tags": ["logistics", "delivery"], "examples": [ "帮我确认一下运单 SF1234567890 是否已签收" ] } ] }
|
为什么要用 Agent Card 而不是直接写文档?
因为 Agent Card 是机器可读的。一个编排 Agent(Orchestrator)可以通过读取其他 Agent 的 Agent Card,自动了解:
- 它有哪些能力(skills)
- 它接受什么格式的输入(defaultInputModes)
- 它返回什么格式的输出(defaultOutputModes)
- 它是否支持流式响应(capabilities.streaming)
- 它需要什么认证方式(authentication)
这就像微服务的服务发现,但描述粒度更细——它描述的不仅是 API 端点,更是 Agent 的语义能力。
你可能会问:MCP 也有 Tool 描述,Agent Card 有什么不同?
关键区别在于粒度和语义:
| 维度 |
MCP Tool |
Agent Card |
| 描述对象 |
单个工具函数 |
整个 Agent 的能力集合 |
| 交互模型 |
同步调用,一次请求一次响应 |
异步任务,支持多轮对话 |
| 发现方式 |
客户端连接 MCP Server 后获取 |
HTTP GET /.well-known/agent.json |
| 适用场景 |
Agent 调用外部工具 |
Agent 调用另一个 Agent |
一个有用的类比:MCP 是函数调用,A2A 是远程协作。你用 MCP 调用一个天气查询工具,得到结果就结束了。但你用 A2A 委托另一个 Agent 处理一个复杂任务,可能需要多轮交互,中间有状态变化,最终产出一个复杂的输出。
Task 生命周期:任务是如何流转的
A2A 中的核心交互单元是 Task。一个 Task 有明确的状态机:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ┌──────┐ │submitted│ └──┬───┘ │ ▼ ┌─────────┐ │ working │ ←── Agent 正在处理 └──┬──┬──┘ │ │ ┌─────────┘ └──────────┐ ▼ ▼ ┌──────────┐ ┌──────────┐ │completed │ │ failed │ └──────────┘ └──────────┘ │ ▼ ┌──────────┐ │canceled │ ←── Client 取消 └──────────┘
|
通过 JSON-RPC 调用来驱动状态转移:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| { "jsonrpc": "2.0", "id": "req-001", "method": "tasks/send", "params": { "id": "task-001", "message": { "role": "user", "parts": [ { "type": "text", "text": "帮我确认运单号 SF1234567890 是否已签收" } ] } } }
{ "jsonrpc": "2.0", "id": "req-002", "method": "tasks/get", "params": { "id": "task-001" } }
{ "jsonrpc": "2.0", "id": "req-003", "method": "tasks/cancel", "params": { "id": "task-001" } }
|
为什么用 JSON-RPC 而不是 REST?
这是一个有意思的设计选择。REST 是资源导向的(对 /tasks 做 CRUD),而 JSON-RPC 是动作导向的(调用 tasks/send、tasks/cancel 方法)。
A2A 选择 JSON-RPC 的原因在于:
- 语义更清晰:
tasks/send 比 POST /tasks 更能表达意图
- 批量操作原生支持:JSON-RPC 2.0 天然支持 batch request
- 与 SSE 流式传输兼容:JSON-RPC 的 notification 机制(无 id 字段)可以自然地映射到 SSE 事件
Message 与 Part:消息的结构化表达
A2A 的消息设计非常灵活。一个 Message 包含多个 Part,每个 Part 可以是不同类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "role": "agent", "parts": [ { "type": "text", "text": "查询结果显示运单 SF1234567890 已于 2026-06-18 14:30 签收。" }, { "type": "file", "file": { "name": "签收照片.jpg", "mimeType": "image/jpeg", "bytes": "base64编码的图片数据..." } }, { "type": "data", "data": { "trackingNumber": "SF1234567890", "status": "delivered", "signedAt": "2026-06-18T14:30:00+08:00", "signedBy": "本人签收" } } ] }
|
这种设计的妙处在于多模态消息的原生支持。Agent 返回的结果可以同时包含人类可读的文本描述、机器可解析的结构化数据、以及附件(图片、文件等)。
Part 类型详解
| Part 类型 |
用途 |
典型场景 |
text |
纯文本内容 |
自然语言回复 |
file |
文件附件 |
图片、PDF、代码文件 |
data |
结构化 JSON 数据 |
API 返回值、分析结果 |
data 类型特别重要——它让 Agent 之间可以传递结构化的中间结果,而不只是自然语言。比如客服 Agent 调用物流 Agent 后,可以直接拿到一个结构化的物流状态对象,不需要再从自然语言中解析。
流式传输与推送通知
A2A 支持两种异步通信模式:
SSE(Server-Sent Events)流式传输
对于需要实时反馈的场景,Client Agent 可以使用 tasks/sendSubscribe 方法,通过 SSE 接收实时更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| POST /a2a HTTP/1.1 Content-Type: application/json
{ "jsonrpc": "2.0", "id": "req-001", "method": "tasks/sendSubscribe", "params": { "id": "task-001", "message": { "role": "user", "parts": [{"type": "text", "text": "分析最近一个月的销售数据"}] } } }
// SSE 响应 event: task-status data: {"id":"task-001","status":"working","message":{"role":"agent","parts":[{"type":"text","text":"正在连接数据源..."}]}}
event: task-status data: {"id":"task-001","status":"working","message":{"role":"agent","parts":[{"type":"text","text":"已获取数据,正在分析..."}]}}
event: task-artifact data: {"id":"task-001","artifact":{"name":"销售报告","parts":[{"type":"text","text":"# 月度销售分析报告\n..."}]}}
event: task-status data: {"id":"task-001","status":"completed"}
|
推送通知(Push Notifications)
对于长时间运行的任务(比如跑一个复杂的 ETL 流程),Client 可以注册一个 webhook URL,Server Agent 在任务状态变化时主动推送:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "jsonrpc": "2.0", "method": "tasks/send", "params": { "id": "task-002", "message": { "role": "user", "parts": [{"type": "text", "text": "生成年度财务报告"}] }, "configuration": { "pushNotificationConfig": { "url": "https://client-agent.example.com/webhook", "token": "secure-callback-token" } } } }
|
这种设计让 A2A 能适应从毫秒级的简单查询到小时级的复杂分析任务。
A2A 与 MCP:互补而非竞争
这是很多开发者困惑的问题:有了 MCP,为什么还需要 A2A?它们是什么关系?
让我用一个类比来解释:
MCP 是 USB 接口,A2A 是网络协议。
USB 让你的电脑连接各种外设(键盘、鼠标、U 盘),就像 MCP 让 Agent 连接各种工具(搜索引擎、数据库、文件系统)。但如果你想让两台电脑协作完成一个任务,你需要网络协议(TCP/IP),这就是 A2A。
在实际架构中,它们是层级互补的:
1 2 3 4 5 6 7 8 9 10 11
| ┌──────────────────────────────────────────┐ │ 应用层:Agent 编排 │ │ ┌─────────┐ A2A ┌─────────┐ │ │ │ Agent A │ ←───────→ │ Agent B │ │ │ └────┬────┘ └────┬────┘ │ │ │ MCP │ MCP │ │ ┌────┴────┐ ┌────┴────┐ │ │ │ Tool 1 │ │ Tool 3 │ │ │ │ Tool 2 │ │ Tool 4 │ │ │ └─────────┘ └─────────┘ │ └──────────────────────────────────────────┘
|
- Agent 与工具之间:用 MCP(标准化的工具调用)
- Agent 与 Agent 之间:用 A2A(标准化的协作通信)
一个 Server Agent 内部可能通过 MCP 调用了多个工具来完成 A2A 任务。Client Agent 不需要关心 Server Agent 内部用了什么工具,就像你访问一个网站不需要关心它后端用了什么数据库。
对比表
| 维度 |
MCP |
A2A |
| 定位 |
Agent ↔ Tool |
Agent ↔ Agent |
| 通信模型 |
请求-响应(同步) |
任务(支持异步、流式) |
| 状态管理 |
无状态 |
有状态(Task 生命周期) |
| 发现机制 |
连接时获取 Tool List |
HTTP GET Agent Card |
| 传输协议 |
stdio / HTTP+SSE |
HTTP + JSON-RPC 2.0 |
| 多模态支持 |
有限 |
原生(text/file/data Part) |
| 标准化组织 |
Anthropic |
Google |
Java 实战:用 Spring Boot 构建 A2A Server Agent
理论讲够了,让我们写代码。我们将构建一个文档分析 Agent,它接受文档文件,返回分析结果。
项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| a2a-server-agent/ ├── src/main/java/com/example/a2a/ │ ├── A2aServerApplication.java │ ├── config/ │ │ └── A2aConfig.java │ ├── controller/ │ │ └── A2aController.java │ ├── model/ │ │ ├── AgentCard.java │ │ ├── Task.java │ │ ├── Message.java │ │ ├── Part.java │ │ └── Artifact.java │ └── service/ │ └── DocumentAnalysisService.java ├── src/main/resources/ │ └── application.yml └── pom.xml
|
核心数据模型
先定义 A2A 协议的核心数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public record AgentCard( String name, String description, String url, String version, Capabilities capabilities, Authentication authentication, List<String> defaultInputModes, List<String> defaultOutputModes, List<Skill> skills ) { public record Capabilities(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory) {} public record Authentication(List<String> schemes) {} public record Skill(String id, String name, String description, List<String> tags, List<String> examples) {} }
public record Task( String id, TaskStatus status, Message message, List<Artifact> artifacts, Instant createdAt, Instant updatedAt ) { public enum TaskStatus { SUBMITTED, WORKING, COMPLETED, FAILED, CANCELED } }
public record Message(String role, List<Part> parts) {}
public sealed interface Part { record TextPart(String type, String text) implements Part {} record FilePart(String type, FileInfo file) implements Part { public record FileInfo(String name, String mimeType, String bytes) {} } record DataPart(String type, Map<String, Object> data) implements Part {} }
public record Artifact(String name, List<Part> parts) {}
|
Agent Card 端点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @RestController public class A2aController {
private final DocumentAnalysisService analysisService; private final Map<String, Task> taskStore = new ConcurrentHashMap<>();
@GetMapping("/.well-known/agent.json") public AgentCard getAgentCard() { return new AgentCard( "Document Analysis Agent", "文档分析 Agent,支持文本摘要、关键词提取、情感分析", "http://localhost:8080", "1.0.0", new AgentCard.Capabilities(true, false, true), new AgentCard.Authentication(List.of("Bearer")), List.of("text", "file"), List.of("text", "data"), List.of( new AgentCard.Skill( "summarize", "文档摘要", "对长文档进行智能摘要,提取核心信息", List.of("nlp", "summary"), List.of("帮我总结一下这份报告的核心内容") ), new AgentCard.Skill( "extract-keywords", "关键词提取", "从文档中提取关键词和主题", List.of("nlp", "keywords"), List.of("提取这篇文章的关键词") ) ) ); }
@PostMapping("/a2a") public Object handleJsonRpc(@RequestBody JsonRpcRequest request) { return switch (request.method()) { case "tasks/send" -> handleSend(request); case "tasks/get" -> handleGet(request); case "tasks/cancel" -> handleCancel(request); default -> new JsonRpcError(request.id(), -32601, "Method not found"); }; } }
|
Task 处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private JsonRpcResponse handleSend(JsonRpcRequest request) { var params = (Map<String, Object>) request.params(); var taskId = (String) params.get("id"); var messageMap = (Map<String, Object>) params.get("message"); var parts = parseParts((List<Map<String, Object>>) messageMap.get("parts"));
var task = new Task(taskId, Task.TaskStatus.SUBMITTED, new Message("user", parts), new ArrayList<>(), Instant.now(), Instant.now()); taskStore.put(taskId, task);
CompletableFuture.runAsync(() -> { try { updateTaskStatus(taskId, Task.TaskStatus.WORKING);
var result = analysisService.analyze(parts);
var taskResult = taskStore.get(taskId); taskResult.artifacts().add(result); updateTaskStatus(taskId, Task.TaskStatus.COMPLETED); } catch (Exception e) { updateTaskStatus(taskId, Task.TaskStatus.FAILED); } });
return new JsonRpcResponse(request.id(), task); }
|
文档分析 Service(集成 Spring AI)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| @Service public class DocumentAnalysisService {
private final ChatModel chatModel;
public DocumentAnalysisService(ChatModel chatModel) { this.chatModel = chatModel; }
public Artifact analyze(List<Part> parts) { String textContent = parts.stream() .filter(p -> p instanceof Part.TextPart) .map(p -> ((Part.TextPart) p).text()) .collect(Collectors.joining("\n"));
var systemPrompt = """ 你是一个专业的文档分析助手。请对以下文档进行分析,返回: 1. 核心摘要(100字以内) 2. 关键词列表(5-10个) 3. 情感倾向(正面/中性/负面) 4. 文档类型判断
以 JSON 格式返回结果。 """;
var response = chatModel.call( new Prompt(List.of( new SystemMessage(systemPrompt), new UserMessage(textContent) )) );
var analysisResult = parseAnalysisResult(response.getResult().getOutput().getText());
return new Artifact("文档分析结果", List.of( new Part.DataPart("data", analysisResult) )); } }
|
SSE 流式传输实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @GetMapping(value = "/a2a/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ServerSentEvent<Object>> streamTask(@RequestParam String taskId) { return Flux.create(sink -> { var listener = new TaskEventListener() { @Override public void onStatusChange(Task task) { sink.next(ServerSentEvent.builder() .event("task-status") .data(task) .build());
if (task.status() == Task.TaskStatus.COMPLETED || task.status() == Task.TaskStatus.FAILED) { sink.complete(); } }
@Override public void onArtifact(Task task, Artifact artifact) { sink.next(ServerSentEvent.builder() .event("task-artifact") .data(artifact) .build()); } };
taskEventBus.register(taskId, listener); sink.onDispose(() -> taskEventBus.unregister(taskId, listener)); }); }
|
Java 实战:A2A Client Agent(调用方)
构建一个 客服 Agent,它通过 A2A 协议调用文档分析 Agent:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| @Service public class A2aClientAgent {
private final RestTemplate restTemplate; private final ChatModel chatModel;
public AgentCard discoverAgent(String agentUrl) { return restTemplate.getForObject( agentUrl + "/.well-known/agent.json", AgentCard.class); }
public Task sendTask(String agentUrl, String taskId, String message) { var request = Map.of( "jsonrpc", "2.0", "id", UUID.randomUUID().toString(), "method", "tasks/send", "params", Map.of( "id", taskId, "message", Map.of( "role", "user", "parts", List.of(Map.of("type", "text", "text", message)) ) ) );
var response = restTemplate.postForObject( agentUrl + "/a2a", request, JsonRpcResponse.class);
return (Task) response.result(); }
public Task pollTask(String agentUrl, String taskId) { var request = Map.of( "jsonrpc", "2.0", "id", UUID.randomUUID().toString(), "method", "tasks/get", "params", Map.of("id", taskId) );
var response = restTemplate.postForObject( agentUrl + "/a2a", request, JsonRpcResponse.class);
return (Task) response.result(); }
public String delegateToAgent(String agentUrl, String userRequest) { var card = discoverAgent(agentUrl); log.info("发现 Agent: {} - {}", card.name(), card.description());
String taskId = UUID.randomUUID().toString(); var task = sendTask(agentUrl, taskId, userRequest);
Instant deadline = Instant.now().plus(Duration.ofMinutes(5)); while (Instant.now().isBefore(deadline)) { task = pollTask(agentUrl, taskId);
if (task.status() == Task.TaskStatus.COMPLETED) { return extractResult(task); } else if (task.status() == Task.TaskStatus.FAILED) { throw new RuntimeException("Agent task failed: " + taskId); }
Thread.sleep(Duration.ofSeconds(2)); }
throw new RuntimeException("Agent task timeout: " + taskId); } }
|
客服 Agent 的实际使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @RestController @RequestMapping("/customer-service") public class CustomerServiceController {
private final A2aClientAgent a2aClient; private final ChatModel chatModel;
@Value("${agents.document-analysis.url}") private String documentAnalysisAgentUrl;
@PostMapping("/handle") public String handleCustomerRequest(@RequestBody CustomerRequest request) { var intent = understandIntent(request.message());
if (intent.needsDocumentAnalysis()) { var analysisResult = a2aClient.delegateToAgent( documentAnalysisAgentUrl, request.message() );
return generateResponse(request, analysisResult); }
return generateDirectResponse(request); } }
|
生产环境的挑战与最佳实践
1. 服务发现与负载均衡
在生产环境中,你不会只有一个文档分析 Agent 实例。你需要:
- 服务注册:Agent 启动时向注册中心注册自己的 Agent Card URL
- 负载均衡:多个同类型 Agent 实例之间做负载分配
- 健康检查:定期探测 Agent 是否可用
Spring Cloud 的服务发现机制可以直接复用:
1 2 3 4 5 6 7
| spring: cloud: consul: discovery: instance-id: ${spring.application.name}:${server.port} health-check-interval: 10s
|
2. 安全性
Agent 之间的通信必须安全:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Configuration public class A2aSecurityConfig {
@Bean public SecurityFilterChain a2aSecurityChain(HttpSecurity http) throws Exception { return http .securityMatcher("/a2a/**", "/.well-known/agent.json") .oauth2ResourceServer(oauth2 -> oauth2 .jwt(Customizer.withDefaults())) .authorizeHttpRequests(auth -> auth .requestMatchers("/.well-known/agent.json").permitAll() .requestMatchers("/a2a/**").authenticated() .anyRequest().denyAll()) .build(); } }
|
3. 超时与重试
Agent 调用可能失败,需要合理的超时和重试策略:
1 2 3 4 5 6 7 8 9 10 11
| @Configuration public class A2aClientConfig {
@Bean public RestTemplate a2aRestTemplate() { var factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(Duration.ofSeconds(5)); factory.setReadTimeout(Duration.ofMinutes(5)); return new RestTemplate(factory); } }
|
4. 可观测性
Agent 间的调用链需要追踪。在 可观测性文章 中我们讲过如何用 Micrometer + OpenTelemetry 追踪 Agent 行为,A2A 调用同样适用:
1 2 3 4 5
| @Bean public ObservationRegistryCustomizer<ObservationRegistry> a2aObservation() { return registry -> registry.observationConfig() .observationHandler(new A2aObservationHandler()); }
|
5. 幂等性
由于网络不稳定,同一个任务可能被发送多次。Server Agent 必须保证幂等性:
1 2 3 4 5 6 7 8 9 10 11
| public JsonRpcResponse handleSend(JsonRpcRequest request) { String taskId = extractTaskId(request);
Task existing = taskStore.get(taskId); if (existing != null && !isTerminal(existing.status())) { return new JsonRpcResponse(request.id(), existing); }
}
|
A2A 的局限性与适用边界
A2A 不是银弹。以下场景不建议使用 A2A:
简单工具调用
如果你只是需要查询天气、调用计算器,用 MCP 就够了。A2A 的任务生命周期管理对于简单调用来说是过度设计。
延迟敏感的场景
A2A 的 HTTP + JSON-RPC 通信有网络开销。对于毫秒级延迟要求的场景(比如实时翻译),直接的 API 调用更合适。
同一框架内的 Agent
如果你的所有 Agent 都用同一个框架(比如都是 LangChain4j),框架内部的消息传递机制通常比 A2A 更高效。A2A 的价值在于异构系统间的互操作。
需要共享大量上下文
A2A 的消息传递适合传递任务指令和结果,不适合传递大量上下文数据(比如几 MB 的文档内容)。大数据量场景应该先通过存储服务共享数据,再通过 A2A 传递引用。
未来展望:Agent 互联网
A2A 协议目前还处于早期阶段(v1.0 规范刚发布不久),但它描绘的愿景很宏大:一个 Agent 之间可以互相发现、互相调用的互联网。
想象一下这个场景:
- 你有一个个人助理 Agent
- 你告诉它:”帮我规划下个月的日本旅行”
- 助理 Agent 通过 A2A 调用旅行规划 Agent(制定行程)
- 旅行规划 Agent 又通过 A2A 调用酒店预订 Agent(预订住宿)
- 酒店预订 Agent 通过 MCP 调用 Booking API(实际预订)
- 结果层层返回,你得到一份完整的旅行方案
这就是 Agent 互联网 的雏形。每个 Agent 都是一个节点,A2A 是节点之间的通信协议,MCP 是节点连接外部世界的接口。
要实现这个愿景,还需要解决几个关键问题:
- 信任机制:Agent 之间如何建立信任?是否需要去中心化的身份验证?
- 计费模型:调用其他 Agent 如何计费?是否需要微支付系统?
- 标准化:A2A 能否成为事实标准?还是会出现多个竞争协议?
- Agent 质量:如何评估一个 Agent 的可靠性?是否需要类似 App Store 的评价机制?
这些问题的答案,将在未来一两年内逐渐清晰。
总结
A2A 协议填补了 Agent 生态中一个关键的空白:Agent 之间的标准化通信。
核心要点回顾:
| 概念 |
说明 |
| Agent Card |
Agent 的机器可读名片,描述能力和接口 |
| Task |
交互的核心单元,有完整的生命周期(submitted → working → completed/failed) |
| Message + Part |
灵活的消息结构,支持多模态内容 |
| SSE / Push |
两种异步通信模式,适应不同场景 |
| A2A vs MCP |
互补关系:A2A 管 Agent 间通信,MCP 管 Agent-Tool 调用 |
如果你正在构建多 Agent 系统,A2A 值得关注。即使协议本身还在演进,它所解决的问题——异构 Agent 互操作、长时间任务管理、能力发现——是任何多 Agent 架构都会遇到的。
从架构设计的角度看,A2A 给我们的启示是:标准化的通信协议比定制化的集成方案更有生命力。就像 HTTP 统一了 Web,MCP 正在统一 Agent-Tool 连接,A2A 可能会统一 Agent-Agent 协作。
保持关注,提前布局。