LangChain4j Java AI 应用开发实战(二十五):A2A 协议 —— Agent 之间的通信与协作

目录

  • 前言
  • [一、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 完整客户端代码)
  • 五、安全性设计
    • [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/sendmessage/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 步):

  1. 提取任务 ID:客户端可指定 ID,也可由服务端自动生成
  2. 解析消息内容 :从 message.parts[] 中提取 text 类型的内容
  3. 版本兼容处理 :同时支持 kind(A2A 0.3.x)和 type(1.0.x)字段
  4. 调用 AI 服务storyWriter.writeStory(userText) ------ 这就是 Agent 真正干活的地方
  5. 构建 Task 响应 :将生成的故事包装为 Artifact(产出物),设置状态为 COMPLETED
  6. 存储任务 :存入 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 网络


延伸阅读


🎯 更多专栏系列文章:LangChain4j Java AI应用开发实战、🔥 其他专栏可以查看博客主页

🔔 关于作者 :资深程序老猿,10年+架构经验,现专注 AIGC 探索与实践。

👍 若文章对你有所触动,恳请点赞 ⭐ 关注 ⭐ 收藏!AI 浪潮已至,愿与你同行。