目录
- 前言
- [一、A2A 协议核心概念](#一、A2A 协议核心概念)
-
- [1.1 什么是 A2A 协议?](#1.1 什么是 A2A 协议?)
- [1.2 三大核心概念](#1.2 三大核心概念)
-
- [① AgentCard ------ Agent 的"名片"](#① AgentCard —— Agent 的"名片")
- [② JSON-RPC 2.0 ------ 通信标准](#② JSON-RPC 2.0 —— 通信标准)
- [③ Task ------ 任务生命周期管理](#③ Task —— 任务生命周期管理)
- [1.3 A2A 支持的核心方法](#1.3 A2A 支持的核心方法)
- [二、实战案例:跨 Agent 故事创作系统](#二、实战案例:跨 Agent 故事创作系统)
-
- [2.1 业务场景](#2.1 业务场景)
- [2.2 项目结构](#2.2 项目结构)
- [三、服务端实现:搭建 A2A Agent 服务器](#三、服务端实现:搭建 A2A Agent 服务器)
-
- [3.1 AgentCard 配置 ------ 声明"我是谁、我能做什么"](#3.1 AgentCard 配置 —— 声明"我是谁、我能做什么")
- [3.2 AI 写作服务 ------ Agent 的核心能力](#3.2 AI 写作服务 —— Agent 的核心能力)
- [3.3 JSON-RPC 控制器 ------ A2A 协议的入口](#3.3 JSON-RPC 控制器 —— A2A 协议的入口)
-
- [端点 ①:AgentCard 发现](#端点 ①:AgentCard 发现)
- [端点 ②:JSON-RPC 请求路由](#端点 ②:JSON-RPC 请求路由)
- 核心处理器:tasks/send
- [tasks/get 和 tasks/cancel 处理器](#tasks/get 和 tasks/cancel 处理器)
- [JSON-RPC 响应构建](#JSON-RPC 响应构建)
- [3.4 依赖配置](#3.4 依赖配置)
- [四、客户端实现:像调用本地 Agent 一样调用远程 Agent](#四、客户端实现:像调用本地 Agent 一样调用远程 Agent)
-
- [4.1 A2A 客户端接口定义](#4.1 A2A 客户端接口定义)
- [4.2 本地版 Agent(开发/回退用)](#4.2 本地版 Agent(开发/回退用))
- [4.3 风格润色 Agent(本地)](#4.3 风格润色 Agent(本地))
- [4.4 完整客户端代码](#4.4 完整客户端代码)
-
- 模式一:本地版工作流(可直接运行)
- [模式二:A2A 远程版工作流(需要启动远程服务器)](#模式二:A2A 远程版工作流(需要启动远程服务器))
- 五、安全性设计
-
- [5.1 认证与授权](#5.1 认证与授权)
- [5.2 输入校验](#5.2 输入校验)
- [5.3 速率限制](#5.3 速率限制)
- 六、常见问题与避坑指南
-
- [6.1 问题:AgentCard 获取失败](#6.1 问题:AgentCard 获取失败)
- [6.2 问题:tasks/send 返回 Method not found](#6.2 问题:tasks/send 返回 Method not found)
- [6.3 问题:Task 状态一直是 working](#6.3 问题:Task 状态一直是 working)
- [6.4 问题:版本兼容性](#6.4 问题:版本兼容性)
- 七、进阶技巧与最佳实践
-
- [7.1 本地优先开发策略](#7.1 本地优先开发策略)
- [7.2 Supervisor + A2A 组合](#7.2 Supervisor + A2A 组合)
- [7.3 生产环境的任务持久化](#7.3 生产环境的任务持久化)
- [7.4 多 AgentCard 注册(一个服务提供多种能力)](#7.4 多 AgentCard 注册(一个服务提供多种能力))
- 结语
- 延伸阅读
前言
在前面七篇文章中,我们已经掌握了 Agentic 工作流编排的完整技能树:
- ✅ 第18篇:Agentic AI 入门 --- 单步 Agent 与 ReAct 模式
- ✅ 第19篇:顺序工作流 --- 多阶段流水线串联
- ✅ 第20篇:循环工作流 --- 自动迭代直到质量达标
- ✅ 第21篇:并行工作流 --- 多 Agent 并发协同
- ✅ 第22篇:条件分支 --- 基于规则的智能路由
- ✅ 第23篇:主管编排与组合工作流 --- Supervisor 自主调度与模式嵌套
- ✅ 第24篇:人机协同与非 AI Agent --- 混合执行系统
但所有这些文章都有一个隐含的前提 :所有 Agent 都运行在同一个 JVM 进程中。
现在请思考一个真实生产场景:
场景:跨团队、跨语言的 Agent 协作
你的团队(Java):
构建了一个"故事风格润色 Agent",擅长优化中文文风
另一个团队(Python):
维护了一个"创意写作 Agent",基于 Llama 模型,擅长生成故事初稿
需求:
先调用 Python 团队的写作 Agent 生成初稿
→ 再用你们的 Java 润色 Agent 优化文风
→ 输出最终成品
传统方案的问题:
❌ 需要将 Python Agent 用 Java 重写一遍(费力不讨好)
❌ 或者通过 REST API 手动集成(需要自己处理序列化、错误、重试)
❌ 两个 Agent 无法被统一编排(各自的接口、协议都不同)
A2A 协议正是为解决这个问题而生的!
A2A(Agent-to-Agent)是 Google 提出的开放标准协议 ,它定义了 Agent 之间如何相互发现、通信和协作。通过 A2A,一个 Java Agent 可以像调用本地方法一样调用远程的 Python、TypeScript 或 Go Agent,实现真正的跨语言、跨平台分布式 Agent 系统。
一、A2A 协议核心概念
1.1 什么是 A2A 协议?
A2A 协议的核心设计理念是:让 Agent 能够像 Web Service 一样被发布和发现。每个 A2A Agent 都暴露出一个标准化的接口,任何语言的客户端都可以通过统一的方式与之交互。
┌─────────────────────────────────────────────────────────┐
│ A2A 生态全景 │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Java Agent │ │ Python Agent │ │
│ │ (LangChain4j) │◄──►│ (LangGraph) │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ A2A Protocol │ │
│ │ ┌─────────────────┐ │ │
│ └──┤ JSON-RPC 2.0 ├──┘ │
│ │ • AgentCard │ │
│ │ • tasks/send │ │
│ │ • tasks/get │ │
│ │ • tasks/cancel │ │
│ └─────────────────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ TS Agent │ │ Go Agent │ │ Rust Agent │ ... │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────┘
1.2 三大核心概念
① AgentCard ------ Agent 的"名片"
AgentCard 是 A2A 协议中最核心的概念。它是一个自描述的元数据文档,告诉潜在调用者这个 Agent 能做什么、怎么通信。
json
{
"name": "创意写作助手",
"description": "根据主题创作富有想象力的短篇故事",
"url": "http://localhost:11000",
"version": "1.0.0",
"capabilities": {
"streaming": false,
"pushNotifications": false
},
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"skills": [
{
"id": "creative_writing",
"name": "创意写作",
"description": "根据给定主题创作短篇故事,约300字",
"tags": ["写作", "创意", "故事"],
"examples": [
"写一个关于龙与魔法师的故事",
"创作一个关于太空探险的短篇故事"
]
}
]
}
AgentCard 的作用:
- 🔍 服务发现 :客户端通过
GET /.well-known/agent-card.json获取 AgentCard,自动了解 Agent 的能力 - 📋 能力声明 :通过
skills列表声明自己擅长什么任务 - 📝 示例引导 :
examples字段帮助调用者理解如何与 Agent 交互 - 🔌 协议协商:声明支持的传输协议(gRPC/HTTP)和通信模式(流式/推送)
② JSON-RPC 2.0 ------ 通信标准
A2A 使用 JSON-RPC 2.0 作为通信协议。这是一个轻量级的远程调用协议,所有请求和响应都是标准的 JSON 格式。
请求格式:
json
{
"jsonrpc": "2.0",
"method": "tasks/send",
"params": {
"id": "task-001",
"message": {
"parts": [
{ "text": "请写一个关于龙与魔法师的故事" }
]
}
},
"id": "1"
}
响应格式:
json
{
"jsonrpc": "2.0",
"result": {
"id": "task-001",
"status": { "state": "completed" },
"artifacts": [
{
"artifactId": "artifact-001",
"name": "创作的故事",
"parts": [{ "text": "在古老的魔法学院..." }]
}
]
},
"id": "1"
}
③ Task ------ 任务生命周期管理
A2A 将每次 Agent 调用抽象为一个 Task,具有完整的生命周期:
┌──────────┐
创建 ──────►│ PENDING │
└─────┬────┘
│
┌─────▼────┐
│ WORKING │
└─────┬────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼────┐ ┌───▼────┐ ┌───▼──────┐
│COMPLETED │ │ FAILED │ │ CANCELED │
└──────────┘ └────────┘ └──────────┘
Task 的核心字段:
| 字段 | 说明 |
|---|---|
id |
任务唯一标识 |
contextId |
会话上下文 ID(同一对话的多个 Task 共享) |
status.state |
当前状态(pending/working/completed/failed/canceled) |
artifacts |
任务产出物列表(如生成的故事文本) |
history |
任务历史记录 |
1.3 A2A 支持的核心方法
| JSON-RPC 方法 | 作用 | 说明 |
|---|---|---|
tasks/send |
发送任务给 Agent | 携带用户消息,触发 Agent 执行 |
tasks/get |
查询任务状态 | 获取任务进度和结果 |
tasks/cancel |
取消任务 | 中止正在执行的任务 |
agent/getCard |
获取 AgentCard | 通过 JSON-RPC 方式获取元数据 |
二、实战案例:跨 Agent 故事创作系统
2.1 业务场景
我们要构建一个分布式的故事创作与润色系统:
用户输入:"请创作一个关于龙与魔法师的故事"
│
▼
┌──────────────────────────────────────────────────┐
│ Supervisor Agent(本地,Java) │
│ │
│ 步骤 1:调用远程创意写作 Agent(A2A 协议) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ 远程 A2A 服务器(可 Python 实现)│ │
│ │ 创意写作 Agent │ │
│ │ 生成故事初稿 │ │
│ └──────────────┬──────────────────┘ │
│ │ A2A 协议返回 │
│ ▼ │
│ 步骤 2:调用本地风格润色 Agent(直接调用) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ 本地 AI Agent(Java) │ │
│ │ 故事风格润色 │ │
│ └──────────────┬──────────────────┘ │
│ │ │
└──────────────────┼────────────────────────────────┘
▼
最终交付:润色后的精美故事
架构特点:
- 远程 Agent 可以用任何语言实现(Python、TypeScript、Go 等)
- 本地 Agent 通过 A2A 协议与远程 Agent 通信,完全不需要知道远程的实现细节
- 所有 Agent 被 Supervisor 统一编排在同一个工作流中
2.2 项目结构
本项目分为两个子模块:
langchain4j-spring-boot-12-agentic-a2aService-provider/ ← A2A 服务端
├── src/main/java/com/langchain4j/a2a/
│ ├── A2AJsonRpcController.java # JSON-RPC 控制器
│ ├── StoryWriterService.java # AI 写作服务
│ └── StoryAgentCardProducer.java # AgentCard 配置
└── pom.xml
langchain4j-spring-boot-12-agentic/ ← A2A 客户端
├── src/main/java/com/langchain4j/agentic/_09_A2A/
│ ├── A2ACreativeWriter.java # A2A 远程 Agent 客户端接口
│ ├── StoryCreator.java # 本地 Agent(开发/回退用)
│ └── StoryStyleEditor.java # 本地风格润色 Agent
└── src/test/java/com/langchain4j/
└── _09_A2ATest.java # 测试类
三、服务端实现:搭建 A2A Agent 服务器
3.1 AgentCard 配置 ------ 声明"我是谁、我能做什么"
服务端的第一步是定义 AgentCard,告诉潜在调用者这个 Agent 的能力:
java
package com.langchain4j.a2a;
import io.a2a.spec.AgentCapabilities;
import io.a2a.spec.AgentCard;
import io.a2a.spec.AgentSkill;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class StoryAgentCardProducer {
@Value("${server.port}")
private int port;
@Bean
public AgentCard agentCard() {
return new AgentCard.Builder()
.name("创意写作助手")
.description("根据主题创作富有想象力的短篇故事。" +
"支持中文和英文创作,故事结构完整,语言生动。")
.url("http://localhost:" + port)
.version("1.0.0")
// 能力声明
.capabilities(new AgentCapabilities.Builder()
.streaming(false) // 不支持流式输出
.pushNotifications(false) // 不支持推送通知
.stateTransitionHistory(false)
.build())
// 支持的输入/输出格式
.defaultInputModes(List.of("text"))
.defaultOutputModes(List.of("text"))
// 技能列表
.skills(List.of(
new AgentSkill.Builder()
.id("creative_writing")
.name("创意写作")
.description("根据给定主题创作富有想象力的" +
"短篇故事,约300字")
.tags(List.of("写作", "创意", "故事"))
.examples(List.of(
"写一个关于龙与魔法师的故事",
"创作一个关于太空探险的短篇故事",
"写一个关于友情的故事"
))
.build()
))
.build();
}
}
关键设计点:
| 配置项 | 作用 | 生产建议 |
|---|---|---|
name |
Agent 名称,用于 Supervisor 调度识别 | 使用业务名称,如"创意写作助手 v2" |
description |
功能描述,客户端通过此了解 Agent 用途 | 详细描述能力边界 |
url |
服务端点地址 | 生产环境使用域名或服务发现地址 |
skills |
技能列表,最重要的元数据 | 每个 skill 附带示例,帮助调用者理解用法 |
capabilities |
能力标志位(流式、推送等) | 如实声明,避免客户端误用 |
3.2 AI 写作服务 ------ Agent 的核心能力
AgentCard 只是"名片",真正的 AI 能力由 StoryWriterService 提供:
java
package com.langchain4j.a2a;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface StoryWriterService {
@SystemMessage("""
你是一位才华横溢的创意作家,擅长根据简单主题创作引人入胜的短篇故事。
故事应富有想象力,情节紧凑,语言生动。字数控制在 300 字左右。
""")
@UserMessage("""
请根据以下主题创作一个精彩的短篇故事。
主题:{{topic}}
要求:
- 故事有完整的起承转合
- 语言生动有趣
- 300 字左右
""")
String writeStory(@V("topic") String topic);
}
💡 这是 LangChain4j 的标准
@AiService,与前面任何一篇文章中的 AI Service 没有任何区别。A2A 的魔力在于:这个普通的 Java 接口被 A2A 协议包装后,任何语言的客户端都能调用它。
3.3 JSON-RPC 控制器 ------ A2A 协议的入口
这是整个 A2A 服务端的核心组件,负责处理所有来自 A2A 客户端的请求:
java
package com.langchain4j.a2a;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.a2a.spec.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@RestController
public class A2AJsonRpcController {
private static final Logger log =
LoggerFactory.getLogger(A2AJsonRpcController.class);
private final io.a2a.spec.AgentCard agentCard;
private final StoryWriterService storyWriter;
private final ObjectMapper objectMapper;
// 任务存储(生产环境应使用数据库或 Redis)
private final Map<String, Task> taskStore = new ConcurrentHashMap<>();
private final AtomicLong taskIdCounter = new AtomicLong(1);
public A2AJsonRpcController(io.a2a.spec.AgentCard agentCard,
StoryWriterService storyWriter,
ObjectMapper objectMapper) {
this.agentCard = agentCard;
this.storyWriter = storyWriter;
this.objectMapper = objectMapper;
}
两个核心端点:
端点 ①:AgentCard 发现
java
/**
* AgentCard 发现端点
*
* A2A 客户端首次连接时,通过 GET 请求获取 Agent 的元数据。
* 路径是 A2A 规范定义的固定路径。
*/
@GetMapping("/.well-known/agent-card.json")
public io.a2a.spec.AgentCard getAgentCard() {
return agentCard;
}
为什么用 /.well-known/ 路径? 这是 RFC 8615 定义的"Well-Known URIs"标准。就像 /.well-known/openid-configuration 用于 OIDC 发现一样,A2A 使用 /.well-known/agent-card.json 作为 Agent 发现的固定入口。客户端连接任何 A2A 服务器时,都知道去这个路径获取 AgentCard。
端点 ②:JSON-RPC 请求路由
java
@PostMapping("/")
public ResponseEntity<?> handleJsonRpc(@RequestBody JsonNode body) {
String method = body.has("method") ?
body.get("method").asText() : null;
JsonNode idNode = body.get("id");
if (method == null) {
return jsonRpcError(idNode, -32600,
"Missing 'method' field");
}
try {
return switch (method) {
case "tasks/send", "message/send"
-> handleTaskSend(body, idNode);
case "tasks/get", "tasks/pushNotificationConfig/get"
-> handleTaskGet(body, idNode);
case "tasks/cancel"
-> handleTaskCancel(body, idNode);
case "agent/getCard"
-> handleAgentGetCard(body, idNode);
default -> jsonRpcError(idNode, -32601,
"Method not found: " + method);
};
} catch (Exception e) {
log.error("Error handling A2A request: {}", method, e);
return jsonRpcError(idNode, -32603,
"Internal error: " + e.getMessage());
}
}
关键设计:
- 🎯 统一入口 :所有 JSON-RPC 方法都通过
POST /进入,由method字段路由到不同的处理器 - 🛡️ 错误处理 :未知方法返回
-32601,内部错误返回-32603 - 📦 向前兼容 :同时支持
tasks/send和message/send(不同版本 A2A 规范的方法名)
核心处理器:tasks/send
java
private ResponseEntity<Map<String, Object>> handleTaskSend(
JsonNode body, JsonNode idNode) {
JsonNode params = body.get("params");
if (params == null) {
return jsonRpcError(idNode, -32602,
"Missing 'params' field");
}
// 提取或生成任务 ID
String taskId = params.has("id") ?
params.get("id").asText() :
"task-" + taskIdCounter.getAndIncrement();
// 从消息中提取文本内容
JsonNode message = params.get("message");
String userText = "";
if (message != null && message.has("parts")) {
StringBuilder sb = new StringBuilder();
for (JsonNode part : message.get("parts")) {
// 兼容 A2A 0.3.x (kind) 和 1.0.x (type)
String partType = part.has("kind") ?
part.get("kind").asText() :
part.has("type") ? part.get("type").asText() : "";
if (("text".equals(partType) || part.has("text"))
&& part.has("text")) {
sb.append(part.get("text").asText());
}
}
userText = sb.toString();
}
log.info("A2A tasks/send --- taskId: {}, topic: {}", taskId, userText);
// ⭐ 调用 AI 服务(这就是 Agent 的核心能力)
String story = storyWriter.writeStory(userText);
// 构建 Task 响应(包含产出物)
Task task = new Task.Builder()
.id(taskId)
.contextId(UUID.randomUUID().toString())
.status(new TaskStatus(TaskState.COMPLETED))
.artifacts(List.of(
new Artifact.Builder()
.artifactId(UUID.randomUUID().toString())
.name("创作的故事")
.parts(new TextPart(story))
.build()
))
.history(new ArrayList<>())
.build();
taskStore.put(taskId, task);
return jsonRpcResponse(idNode, task);
}
核心流程(6 步):
- 提取任务 ID:客户端可指定 ID,也可由服务端自动生成
- 解析消息内容 :从
message.parts[]中提取text类型的内容 - 版本兼容处理 :同时支持
kind(A2A 0.3.x)和type(1.0.x)字段 - 调用 AI 服务 :
storyWriter.writeStory(userText)------ 这就是 Agent 真正干活的地方 - 构建 Task 响应 :将生成的故事包装为
Artifact(产出物),设置状态为COMPLETED - 存储任务 :存入
taskStore,供后续tasks/get查询
tasks/get 和 tasks/cancel 处理器
java
private ResponseEntity<Map<String, Object>> handleTaskGet(
JsonNode body, JsonNode idNode) {
JsonNode params = body.get("params");
String taskId = params.get("id").asText();
Task task = taskStore.get(taskId);
if (task == null) {
return jsonRpcError(idNode, -32000,
"Task not found: " + taskId);
}
return jsonRpcResponse(idNode, task);
}
private ResponseEntity<Map<String, Object>> handleTaskCancel(
JsonNode body, JsonNode idNode) {
// ... 检查任务是否存在、是否可取消
Task canceledTask = new Task.Builder(existingTask)
.status(new TaskStatus(TaskState.CANCELED))
.build();
taskStore.put(taskId, canceledTask);
return jsonRpcResponse(idNode, canceledTask);
}
JSON-RPC 响应构建
java
// 成功响应
private ResponseEntity<Map<String, Object>> jsonRpcResponse(
JsonNode idNode, Object result) {
String jsonrpcId = idNode != null ? idNode.asText() : "0";
return ResponseEntity.ok(Map.of(
"jsonrpc", "2.0",
"result", result,
"id", jsonrpcId
));
}
// 错误响应
private ResponseEntity<Map<String, Object>> jsonRpcError(
JsonNode idNode, int code, String message) {
String jsonrpcId = idNode != null ? idNode.asText() : "0";
log.warn("A2A JSON-RPC error --- code: {}, message: {}", code, message);
return ResponseEntity.ok(Map.of(
"jsonrpc", "2.0",
"error", Map.of("code", code, "message", message),
"id", jsonrpcId
));
}
💡 注意 :即使返回错误,HTTP 状态码仍然是
200 OK------这是 JSON-RPC 2.0 规范的要求,错误信息通过响应体中的error字段表达。
3.4 依赖配置
xml
<!-- A2A Java SDK - 提供 AgentCard、Task 等类型定义 -->
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-client</artifactId>
<version>0.3.2.Final</version>
</dependency>
<!-- LangChain4j Agentic - 提供 a2aBuilder() 等 API -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-agentic</artifactId>
</dependency>
服务端启动:
bash
# 启动 A2A 服务端(默认端口 11000)
cd langchain4j-spring-boot-12-agentic-a2aService-provider
mvn spring-boot:run
# 验证 AgentCard 可访问
curl http://localhost:11000/.well-known/agent-card.json
四、客户端实现:像调用本地 Agent 一样调用远程 Agent
4.1 A2A 客户端接口定义
在客户端,你只需要定义一个普通 Java 接口 ,然后通过 a2aBuilder() 构建代理。框架会自动处理 A2A 协议的所有细节:
java
package com.langchain4j.agentic._09_A2A;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.V;
/**
* A2A 远程创意写作 Agent 客户端接口
*
* 这个接口本身不包含任何 AI 逻辑------它只是一个代理(Proxy),
* 所有方法调用都会被转发到远程的 A2A 服务器执行。
*/
public interface A2ACreativeWriter {
@Agent("根据给定主题创作一个富有想象力的短篇故事" +
"(部署在远程 A2A 服务器上)")
String writeStory(@V("topic") String topic);
}
关键点:
- ❌ 没有
@SystemMessage、@UserMessage(这些在远程服务器上定义) - ❌ 没有
chatModel配置(使用远程服务器的模型) - ✅ 只有
@Agent注解(描述功能,供 Supervisor 理解) - ✅ 只有
@V参数绑定(参数会被序列化到 A2A 消息中)
4.2 本地版 Agent(开发/回退用)
在开发和测试阶段,你可以先用本地 Agent 代替远程 Agent。当远程服务器就绪后,只需一行代码切换:
java
/**
* 本地创意写作 Agent ------ 功能与远程 A2A 服务器上的完全相同
* 在开发阶段作为 A2A 远程 Agent 的本地替代方案
*/
public interface StoryCreator {
@SystemMessage("""
你是一位才华横溢的创意作家,擅长根据简单主题创作引人入胜的短篇故事。
故事应富有想象力,情节紧凑,语言生动。字数控制在 300 字左右。
""")
@UserMessage("""
请根据以下主题创作一个精彩的短篇故事。
主题:{{topic}}
""")
@Agent("根据给定主题,创作一个富有想象力的短篇故事")
String writeStory(@V("topic") String topic);
}
本地 vs A2A 远程对比:
| 本地 Agent | A2A 远程 Agent | |
|---|---|---|
| 实现方式 | @Agent 接口 + LLM |
A2A 客户端代理 |
| 运行位置 | 当前 JVM | 远程服务器 |
| 通信方式 | 直接方法调用 | A2A 协议(JSON-RPC over HTTP) |
| 语言限制 | Java only | 无限制(Python/TS/Go 等) |
| 适用阶段 | 开发/测试/简单场景 | 生产/跨团队协作 |
| 切换方式 | --- | 只需改一行 builder 代码 |
4.3 风格润色 Agent(本地)
java
public interface StoryStyleEditor {
@SystemMessage("""
你是一位资深文学编辑,擅长润色故事文本,提升文学品质。
你的润色不应改变故事的核心情节,只优化措辞、节奏和文采。
""")
@UserMessage("""
请对以下故事进行风格润色,提升其文学品质。
原始故事:{{story}}
要求:
- 保持原故事的核心情节和结构不变
- 优化措辞,使语言更加优美流畅
- 直接输出润色后的故事,不要附带修改说明
""")
@Agent("对故事初稿进行风格润色,提升文学品质但不改变核心情节")
String polish(@V("story") String story);
}
4.4 完整客户端代码
模式一:本地版工作流(可直接运行)
当 A2A 远程服务器尚未部署时,使用本地 Agent 完成完整流程:
java
@Test
public void testLocalStoryWorkflow() {
// 1. 创建本地创意写作 Agent
StoryCreator storyCreator = AgenticServices
.agentBuilder(StoryCreator.class)
.chatModel(chatModel)
.outputKey("story")
.build();
// 2. 创建本地风格润色 Agent
StoryStyleEditor styleEditor = AgenticServices
.agentBuilder(StoryStyleEditor.class)
.chatModel(chatModel)
.outputKey("finalStory")
.build();
// 3. 构建顺序工作流:先创作 → 再润色
UntypedAgent storyPipeline = AgenticServices
.sequenceBuilder()
.subAgents(storyCreator, styleEditor)
.outputKey("finalStory")
.build();
// 4. 用户提供主题,一键生成润色后的故事
Map<String, Object> input = Map.of(
"topic", "一只会说话的猫在深夜的图书馆里冒险"
);
String finalStory = (String) storyPipeline.invoke(input);
System.out.println("=== 最终交付 ===");
System.out.println(finalStory);
}
模式二:A2A 远程版工作流(需要启动远程服务器)
只需一行代码,即可将本地 Agent 替换为远程 A2A Agent:
java
@Test
public void testA2ARemoteWorkflow() {
// ⭐ 关键差异:使用 a2aBuilder() 替代 agentBuilder()
// 框架会自动从 http://localhost:11000 获取 AgentCard,
// 解析其能力描述和技能列表
A2ACreativeWriter a2aWriter = AgenticServices
.a2aBuilder("http://localhost:11000", A2ACreativeWriter.class)
.outputKey("story")
.build();
// 本地润色 Agent ------ 与本地版完全相同
StoryStyleEditor styleEditor = AgenticServices
.agentBuilder(StoryStyleEditor.class)
.chatModel(chatModel)
.outputKey("finalStory")
.build();
// 使用 Supervisor Agent 编排远程 + 本地 Agent
SupervisorAgent storySupervisor = AgenticServices
.supervisorBuilder()
.chatModel(chatModel)
.subAgents(a2aWriter, styleEditor)
.build();
// 自然语言描述需求,Supervisor 自主编排
String result = storySupervisor.invoke(
"请创作并润色一个故事,主题是龙与魔法师");
System.out.println("=== 远程协作结果 ===");
System.out.println(result);
}
a2aBuilder() 做了什么(后台自动完成):
1. 连接远程服务器 → GET http://localhost:11000/.well-known/agent-card.json
2. 解析 AgentCard → 提取名称、能力、技能列表
3. 创建动态代理 → 拦截所有接口方法调用
4. 方法调用时 → 将参数序列化为 JSON-RPC 请求
5. 发送请求 → POST http://localhost:11000/ (tasks/send)
6. 解析响应 → 从 Task.artifacts 中提取文本结果
7. 返回结果 → 就像调用本地方法一样!
本地版 vs A2A 远程版切换对比:
java
// 本地版
StoryCreator writer = AgenticServices
.agentBuilder(StoryCreator.class) // ← 本地构建器
.chatModel(chatModel) // ← 需要配置模型
.outputKey("story")
.build();
// A2A 远程版(只改了两行!)
A2ACreativeWriter writer = AgenticServices
.a2aBuilder("http://localhost:11000", // ← A2A 构建器 + 服务器地址
A2ACreativeWriter.class)
.outputKey("story") // ← 不需要 chatModel!
.build();
// 其余工作流代码完全不变!
五、安全性设计
5.1 认证与授权
在生产环境中,A2A 服务器不应该对公网裸奔。常见的防护方案:
java
// 方案一:API Key 认证
@PostMapping("/")
public ResponseEntity<?> handleJsonRpc(
@RequestBody JsonNode body,
@RequestHeader("X-A2A-API-Key") String apiKey) {
if (!apiKeyService.validate(apiKey)) {
return jsonRpcError(body.get("id"), -32001,
"Unauthorized: invalid API key");
}
// 继续处理请求...
}
// 方案二:mTLS 双向认证
// 在 Spring Boot 中配置 server.ssl.client-auth=need
// 只允许持有合法证书的客户端连接
// 方案三:OAuth2 / JWT
// 在网关层验证 JWT Token,A2A 服务只接收已认证的请求
5.2 输入校验
永远不要信任来自 A2A 客户端的输入:
java
private ResponseEntity<Map<String, Object>> handleTaskSend(
JsonNode body, JsonNode idNode) {
// ... 提取消息文本
// ✅ 输入校验
if (userText == null || userText.isBlank()) {
return jsonRpcError(idNode, -32602,
"Message text cannot be empty");
}
if (userText.length() > 5000) {
return jsonRpcError(idNode, -32602,
"Message text too long (max 5000 characters)");
}
// ✅ 防止提示词注入(Prompt Injection)
userText = sanitizeInput(userText);
// 继续处理...
}
5.3 速率限制
java
// 使用令牌桶算法限制请求频率
private final RateLimiter rateLimiter =
RateLimiter.create(10.0); // 每秒最多 10 个请求
@PostMapping("/")
public ResponseEntity<?> handleJsonRpc(@RequestBody JsonNode body) {
if (!rateLimiter.tryAcquire()) {
return jsonRpcError(body.get("id"), -32002,
"Rate limit exceeded. Try again later.");
}
// 继续处理...
}
六、常见问题与避坑指南
6.1 问题:AgentCard 获取失败
症状:
Connection refused: http://localhost:11000/.well-known/agent-card.json
原因:A2A 服务器未启动,或端口配置不匹配。
排查步骤:
bash
# 1. 确认服务已启动
curl http://localhost:11000/.well-known/agent-card.json
# 2. 检查服务器端口配置
# application.properties:
server.port=11000
6.2 问题:tasks/send 返回 Method not found
症状:
json
{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found: tasks/send"},"id":"1"}
原因 :A2A 服务端的 switch 语句中没有处理 tasks/send 方法。
解决方案 :确保服务端支持客户端使用的所有方法名(包括 message/send 等别名)。
6.3 问题:Task 状态一直是 working
症状 :客户端调用 tasks/get 一直返回 state: "working"。
原因:异步任务尚未完成,或服务端忘记更新任务状态。
解决方案:
- 客户端实现轮询 + 超时机制
- 服务端确保任务完成后调用
taskStore.put(taskId, completedTask)更新状态
6.4 问题:版本兼容性
症状:服务端使用 A2A 0.3.x,客户端使用 1.0.x,消息格式不匹配。
解决方案:在消息解析时同时兼容两个版本:
java
// 兼容 A2A 0.3.x (kind) 和 1.0.x (type)
String partType = part.has("kind") ? part.get("kind").asText()
: part.has("type") ? part.get("type").asText() : "";
七、进阶技巧与最佳实践
7.1 本地优先开发策略
在 Agent 开发中,推荐先在本地调通,再拆分为 A2A 服务:
开发阶段:
StoryCreator(本地 AI Agent) → StoryStyleEditor(本地)
优势:调试方便,无需网络,快速迭代
上线阶段:
A2ACreativeWriter(远程) → StoryStyleEditor(本地)
优势:跨团队复用,独立部署,独立扩缩容
7.2 Supervisor + A2A 组合
A2A 远程 Agent 可以作为 Supervisor 的子 Agent,与其他本地 Agent 一同被编排:
java
SupervisorAgent supervisor = AgenticServices
.supervisorBuilder()
.chatModel(chatModel)
.subAgents(
a2aWriter, // A2A 远程 Agent
styleEditor, // 本地 AI Agent
humanApprover // HumanInTheLoop Agent
)
.build();
// Supervisor 根据上下文自主决定调用顺序:
// 可能先调远程 Writer → 再调本地 Editor → 最后调人工审批
7.3 生产环境的任务持久化
演示代码中使用 ConcurrentHashMap 存储任务------这在生产环境中会丢失数据。生产方案:
java
// 方案一:Redis
private final RedisTemplate<String, Task> redisTemplate;
public Task getTask(String taskId) {
return redisTemplate.opsForValue().get("a2a:task:" + taskId);
}
// 方案二:数据库
@Repository
public interface TaskRepository extends JpaRepository<TaskEntity, String> {
}
7.4 多 AgentCard 注册(一个服务提供多种能力)
一个 A2A 服务器可以发布多个 AgentCard,对应不同的能力:
java
@Bean
public AgentCard writerAgentCard() { /* 写作 AgentCard */ }
@Bean
public AgentCard translatorAgentCard() { /* 翻译 AgentCard */ }
@Bean
public AgentCard reviewerAgentCard() { /* 评审 AgentCard */ }
// 客户端按需选择:
// A2ACreativeWriter → 调用 writerAgentCard
// A2ATranslator → 调用 translatorAgentCard
结语
本文深入讲解了 A2A(Agent-to-Agent)协议的完整技术栈------从 AgentCard 服务发现、JSON-RPC 2.0 通信标准到 Task 生命周期管理。通过"创意写作 + 风格润色"的跨 Agent 协作实战,你学会了如何搭建 A2A 服务端(Spring Boot + JSON-RPC Controller)、如何用 a2aBuilder() 像调用本地方法一样调用远程 Agent、以及如何将 A2A 远程 Agent 与 Supervisor 编排无缝结合。A2A 协议的精髓在于让 Agent 成为网络中的一等公民 ------它们自描述能力、标准化通信、独立部署,最终构建出跨语言、跨平台的分布式 Agent 网络。
延伸阅读
- A2A 协议官方规范:https://github.com/google/A2A
- LangChain4j Agentic 框架文档:Agentic Services
- JSON-RPC 2.0 规范:https://www.jsonrpc.org/specification
- RFC 8615 Well-Known URIs:https://tools.ietf.org/html/rfc8615

🎯 更多专栏系列文章:LangChain4j Java AI应用开发实战、🔥 其他专栏可以查看博客主页
🔔 关于作者 :资深程序老猿,10年+架构经验,现专注 AIGC 探索与实践。
👍 若文章对你有所触动,恳请点赞 ⭐ 关注 ⭐ 收藏!AI 浪潮已至,愿与你同行。