Agent 间如何对话:A2A 协议深度解析与 Java 实战

系列文章

  1. 理解 AI Agent 的大脑:ReAct 模式从入门到实战
  2. AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
  3. AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
  4. AI Agent 的规划大脑:从任务分解到自适应执行策略
  5. MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
  6. AI Agent 的工作流编排:从顺序链到自适应 DAG 的 Java 实战
  7. AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
  8. 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,自动了解:

  1. 它有哪些能力(skills)
  2. 它接受什么格式的输入(defaultInputModes)
  3. 它返回什么格式的输出(defaultOutputModes)
  4. 它是否支持流式响应(capabilities.streaming)
  5. 它需要什么认证方式(authentication)

这就像微服务的服务发现,但描述粒度更细——它描述的不仅是 API 端点,更是 Agent 的语义能力

Agent Card 与 MCP Tool 的区别

你可能会问: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
// 1. 创建任务
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tasks/send",
"params": {
"id": "task-001",
"message": {
"role": "user",
"parts": [
{
"type": "text",
"text": "帮我确认运单号 SF1234567890 是否已签收"
}
]
}
}
}

// 2. 查询任务状态
{
"jsonrpc": "2.0",
"id": "req-002",
"method": "tasks/get",
"params": {
"id": "task-001"
}
}

// 3. 取消任务
{
"jsonrpc": "2.0",
"id": "req-003",
"method": "tasks/cancel",
"params": {
"id": "task-001"
}
}

为什么用 JSON-RPC 而不是 REST?

这是一个有意思的设计选择。REST 是资源导向的(对 /tasks 做 CRUD),而 JSON-RPC 是动作导向的(调用 tasks/sendtasks/cancel 方法)。

A2A 选择 JSON-RPC 的原因在于:

  1. 语义更清晰tasks/sendPOST /tasks 更能表达意图
  2. 批量操作原生支持:JSON-RPC 2.0 天然支持 batch request
  3. 与 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
// Agent Card
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) {}
}

// Task
public record Task(
String id,
TaskStatus status,
Message message,
List<Artifact> artifacts,
Instant createdAt,
Instant updatedAt
) {
public enum TaskStatus {
SUBMITTED, WORKING, COMPLETED, FAILED, CANCELED
}
}

// Message
public record Message(String role, List<Part> parts) {}

// Part - sealed interface for type safety
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 {}
}

// Artifact
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("提取这篇文章的关键词")
)
)
);
}

// JSON-RPC 端点
@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"));

// 创建 Task(状态:SUBMITTED)
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 {
// 更新状态为 WORKING
updateTaskStatus(taskId, Task.TaskStatus.WORKING);

// 执行分析
var result = analysisService.analyze(parts);

// 更新为 COMPLETED,附加 Artifact
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"));

// 使用 Spring AI 进行分析
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;

// 1. 发现目标 Agent 的能力
public AgentCard discoverAgent(String agentUrl) {
return restTemplate.getForObject(
agentUrl + "/.well-known/agent.json", AgentCard.class);
}

// 2. 发送任务
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();
}

// 3. 轮询任务状态
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();
}

// 4. 完整的委托流程
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) {
// Step 1: 用 LLM 理解用户意图
var intent = understandIntent(request.message());

// Step 2: 根据意图决定是否需要委托给其他 Agent
if (intent.needsDocumentAnalysis()) {
// 通过 A2A 委托给文档分析 Agent
var analysisResult = a2aClient.delegateToAgent(
documentAnalysisAgentUrl,
request.message()
);

// Step 3: 基于分析结果生成回复
return generateResponse(request, analysisResult);
}

return generateDirectResponse(request);
}
}

生产环境的挑战与最佳实践

1. 服务发现与负载均衡

在生产环境中,你不会只有一个文档分析 Agent 实例。你需要:

  • 服务注册:Agent 启动时向注册中心注册自己的 Agent Card URL
  • 负载均衡:多个同类型 Agent 实例之间做负载分配
  • 健康检查:定期探测 Agent 是否可用

Spring Cloud 的服务发现机制可以直接复用:

1
2
3
4
5
6
7
# application.yml
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)); // Agent 任务可能很慢
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 之间可以互相发现、互相调用的互联网

想象一下这个场景:

  1. 你有一个个人助理 Agent
  2. 你告诉它:”帮我规划下个月的日本旅行”
  3. 助理 Agent 通过 A2A 调用旅行规划 Agent(制定行程)
  4. 旅行规划 Agent 又通过 A2A 调用酒店预订 Agent(预订住宿)
  5. 酒店预订 Agent 通过 MCP 调用 Booking API(实际预订)
  6. 结果层层返回,你得到一份完整的旅行方案

这就是 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 协作。

保持关注,提前布局。