基于A2A协议的生产应用实践指南(Java)

基于 A2A 协议的生产应用实践指南(Java)

一、A2A 协议概述

1.1 什么是 A2A

Agent2Agent (A2A) 是由 Google 发起并已捐赠给 Linux Foundation 的开放协议标准,旨在让不同框架、不同厂商构建的 AI Agent 能够互相发现、通信和协作。

核心设计原则:

  • 互操作性:连接用不同平台(LangGraph、CrewAI、自定义方案等)构建的 Agent
  • 不透明执行:Agent 协作时无需暴露内部状态、记忆或工具实现,保护知识产权
  • 异步优先:原生支持长时间运行的任务和人机协作场景
  • 模态无关:支持文本、音视频、结构化 JSON 数据等多种内容类型

1.2 A2A vs MCP 的关系

维度 A2A MCP
定位 Agent-to-Agent 通信 Agent-to-Tool 通信
核心功能 Agent 之间发现、协商、任务委派 Agent 连接工具、API、数据源
类比 Agent 之间的"公共互联网" Agent 的"工具箱"
互补关系 A2A Client 通过 A2A 请求远程 Agent 工作 远程 Agent 内部通过 MCP 调用工具完成工作

1.3 协议技术栈

复制代码
┌─────────────────────────────────────────────┐
│           Layer 3: Protocol Bindings        │
│  JSON-RPC 2.0 / gRPC / HTTP+JSON (REST)    │
├─────────────────────────────────────────────┤
│           Layer 2: Abstract Operations      │
│  SendMessage / GetTask / CancelTask / ...   │
├─────────────────────────────────────────────┤
│           Layer 1: Canonical Data Model     │
│  Task / Message / AgentCard / Part / ...    │
└─────────────────────────────────────────────┘

通信机制:JSON-RPC 2.0 over HTTP(S),使用 SSE 做流式推送

Agent 发现 :通过 /.well-known/agent-card.json 获取 Agent Card(类似"名片")


二、核心数据模型

2.1 AgentCard(Agent 名片)

Agent Card 是 A2A 协议中 Agent 自描述的元数据文档,描述 Agent 的身份、能力、技能和通信要求。

json 复制代码
{
  "name": "Order Query Agent",
  "description": "电商订单查询与处理智能体,支持订单查询、退款申请、物流追踪",
  "version": "1.0.0",
  "url": "https://agent.example.com",
  "provider": {
    "organization": "Example Corp",
    "url": "https://www.example.com"
  },
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["text/plain", "application/json"],
  "skills": [
    {
      "id": "order-query",
      "name": "订单查询",
      "description": "根据订单号查询订单详情",
      "tags": ["order", "query", "search"],
      "examples": ["查询订单 ORD-20250417-001 的详情"]
    },
    {
      "id": "refund-apply",
      "name": "退款申请",
      "description": "发起退款申请流程",
      "tags": ["refund", "return"],
      "examples": ["对订单 ORD-20250417-001 申请退款"]
    }
  ]
}

2.2 Task(任务)

Task 是 A2A 的核心工作单元,有唯一 ID 和状态生命周期。

json 复制代码
{
  "id": "task-uuid-001",
  "contextId": "context-uuid-001",
  "status": {
    "state": "TASK_STATE_WORKING",
    "message": {
      "role": "ROLE_AGENT",
      "parts": [{"text": "正在查询订单信息..."}],
      "messageId": "msg-001"
    },
    "timestamp": "2025-04-17T10:30:00.000Z"
  },
  "artifacts": [],
  "history": []
}

TaskState 生命周期:

复制代码
SUBMITTED → WORKING → COMPLETED
                     → FAILED
                     → CANCELED
                     → INPUT_REQUIRED(需要用户额外输入)
                     → AUTH_REQUIRED(需要认证授权)
                     → REJECTED

2.3 Message(消息)

Message 是客户端与服务端之间的一次通信单元:

json 复制代码
{
  "messageId": "msg-uuid-001",
  "contextId": "context-uuid-001",
  "taskId": "task-uuid-001",
  "role": "ROLE_USER",
  "parts": [
    {"text": "帮我查询订单 ORD-20250417-001 的物流状态"}
  ]
}

Part 类型(消息内容的最小单元):

类型 字段 说明
TextPart text 纯文本内容
FilePart raw(base64)或 url 文件(图片、文档等)
DataPart data 结构化 JSON 数据

三、协议绑定与 API

A2A 支持三种协议绑定,以下是 HTTP+JSON (REST) 绑定的端点映射:

3.1 核心 REST 端点

操作 方法 端点
发送消息 POST /message:send
流式发送消息 POST /message:stream
获取任务状态 GET /tasks/{id}
列出任务 GET /tasks
取消任务 POST /tasks/{id}:cancel
订阅任务更新 POST /tasks/{id}:subscribe
获取 Agent Card GET /.well-known/agent-card.json

3.2 请求/响应示例

发送消息:

http 复制代码
POST /message:send HTTP/1.1
Content-Type: application/json
A2A-Version: 1.0

{
  "message": {
    "role": "ROLE_USER",
    "parts": [{"text": "查询订单 ORD-001"}],
    "messageId": "msg-001"
  }
}

响应:

json 复制代码
{
  "task": {
    "id": "task-001",
    "contextId": "ctx-001",
    "status": {
      "state": "TASK_STATE_COMPLETED",
      "timestamp": "2025-04-17T10:30:00Z"
    },
    "artifacts": [
      {
        "artifactId": "art-001",
        "name": "订单查询结果",
        "parts": [{"text": "订单 ORD-001 已发货,预计明天送达"}]
      }
    ]
  }
}

四、Java 生产实践

4.1 项目结构

使用 Spring Boot 3 + Java 21 构建:

复制代码
a2a-demo/
├── pom.xml
├── src/main/java/com/example/a2a/
│   ├── A2aDemoApplication.java
│   ├── model/
│   │   ├── AgentCard.java
│   │   ├── AgentSkill.java
│   │   ├── AgentCapabilities.java
│   │   ├── Task.java
│   │   ├── TaskStatus.java
│   │   ├── TaskState.java
│   │   ├── Message.java
│   │   ├── Part.java
│   │   ├── Artifact.java
│   │   ├── SendMessageRequest.java
│   │   ├── SendMessageResponse.java
│   │   └── JsonRpcRequest.java
│   ├── client/
│   │   └── A2AClient.java
│   ├── server/
│   │   ├── A2AAgentController.java
│   │   ├── AgentCardController.java
│   │   └── TaskManager.java
│   └── config/
│       └── A2aProperties.java

4.2 Maven 依赖

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>a2a-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

4.3 核心数据模型定义

java 复制代码
package com.example.a2a.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
import java.util.Map;

/**
 * A2A Agent Card - Agent 的自描述元数据
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AgentCard {
    private String name;
    private String description;
    private String version;
    private String url;
    private Provider provider;
    private AgentCapabilities capabilities;
    private List<String> defaultInputModes;
    private List<String> defaultOutputModes;
    private List<AgentSkill> skills;
    private Map<String, SecurityScheme> securitySchemes;

    @Data
    public static class Provider {
        private String organization;
        private String url;
    }

    @Data
    public static class SecurityScheme {
        private String type;       // "http", "apiKey", "oauth2"
        private String scheme;     // "Bearer"
        private String description;
    }
}
java 复制代码
package com.example.a2a.model;

import lombok.Data;
import java.util.List;

/**
 * Agent 能力声明
 */
@Data
public class AgentCapabilities {
    private boolean streaming = false;
    private boolean pushNotifications = false;
}
java 复制代码
package com.example.a2a.model;

import lombok.Data;
import java.util.List;

/**
 * Agent 技能描述
 */
@Data
public class AgentSkill {
    private String id;
    private String name;
    private String description;
    private List<String> tags;
    private List<String> examples;
}
java 复制代码
package com.example.a2a.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.time.Instant;
import java.util.List;

/**
 * A2A Task - 核心工作单元
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Task {
    private String id;
    private String contextId;
    private TaskStatus status;
    private List<Artifact> artifacts;
    private List<Message> history;
}
java 复制代码
package com.example.a2a.model;

import lombok.Data;
import java.time.Instant;

/**
 * Task 状态
 */
@Data
public class TaskStatus {
    private TaskState state;
    private Message message;
    private Instant timestamp;

    public static TaskStatus of(TaskState state) {
        TaskStatus status = new TaskStatus();
        status.setState(state);
        status.setTimestamp(Instant.now());
        return status;
    }

    public static TaskStatus of(TaskState state, Message message) {
        TaskStatus status = of(state);
        status.setMessage(message);
        return status;
    }
}
java 复制代码
package com.example.a2a.model;

public enum TaskState {
    TASK_STATE_SUBMITTED,
    TASK_STATE_WORKING,
    TASK_STATE_COMPLETED,
    TASK_STATE_FAILED,
    TASK_STATE_CANCELED,
    TASK_STATE_INPUT_REQUIRED,
    TASK_STATE_AUTH_REQUIRED,
    TASK_STATE_REJECTED
}
java 复制代码
package com.example.a2a.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;

/**
 * A2A Message - 通信单元
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Message {
    private String messageId;
    private String contextId;
    private String taskId;
    private String role;    // "ROLE_USER" or "ROLE_AGENT"
    private List<Part> parts;
}
java 复制代码
package com.example.a2a.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.Map;

/**
 * A2A Part - 消息内容的最小单元
 * 只能包含 text / raw / url / data 其中之一
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Part {
    /** 纯文本内容 */
    private String text;
    /** 文件 base64 字节 */
    private String raw;
    /** 文件 URL */
    private String url;
    /** 结构化 JSON 数据 */
    private Object data;
    /** 文件名 */
    private String filename;
    /** MIME 类型 */
    private String mediaType;
    /** 元数据 */
    private Map<String, Object> metadata;

    /** 便捷:创建文本 Part */
    public static Part text(String text) {
        Part part = new Part();
        part.setText(text);
        return part;
    }
}
java 复制代码
package com.example.a2a.model;

import lombok.Data;
import java.util.List;

/**
 * Artifact - 任务输出产物
 */
@Data
public class Artifact {
    private String artifactId;
    private String name;
    private String description;
    private List<Part> parts;
}

4.4 A2A Server 端实现

Agent Card 端点
java 复制代码
package com.example.a2a.server;

import com.example.a2a.model.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * Agent Card 控制器
 * 对应 A2A 协议的 Agent 发现端点:GET /.well-known/agent-card.json
 */
@RestController
public class AgentCardController {

    private final AgentCard agentCard;

    public AgentCardController() {
        // 构建当前 Agent 的 Card
        AgentCard.Provider provider = new AgentCard.Provider();
        provider.setOrganization("Example Corp");
        provider.setUrl("https://www.example.com");

        AgentCapabilities caps = new AgentCapabilities();
        caps.setStreaming(true);

        AgentSkill orderQuery = new AgentSkill();
        orderQuery.setId("order-query");
        orderQuery.setName("订单查询");
        orderQuery.setDescription("根据订单号查询订单详情、物流状态");
        orderQuery.setTags(List.of("order", "query", "logistics"));

        AgentSkill refund = new AgentSkill();
        refund.setId("refund-apply");
        refund.setName("退款申请");
        refund.setDescription("发起退款申请并查询退款进度");
        refund.setTags(List.of("refund", "return"));

        this.agentCard = new AgentCard();
        agentCard.setName("Order Service Agent");
        agentCard.setDescription("电商订单查询与处理智能体");
        agentCard.setVersion("1.0.0");
        agentCard.setUrl("http://localhost:8081");
        agentCard.setProvider(provider);
        agentCard.setCapabilities(caps);
        agentCard.setDefaultInputModes(List.of("text/plain"));
        agentCard.setDefaultOutputModes(List.of("text/plain", "application/json"));
        agentCard.setSkills(List.of(orderQuery, refund));
    }

    @GetMapping("/.well-known/agent-card.json")
    public AgentCard getAgentCard() {
        return agentCard;
    }
}
任务管理器
java 复制代码
package com.example.a2a.server;

import com.example.a2a.model.*;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 内存任务管理器(生产环境应替换为数据库持久化)
 */
@Component
public class TaskManager {

    private final Map<String, Task> taskStore = new ConcurrentHashMap<>();

    /**
     * 创建新任务
     */
    public Task createTask(String contextId, Message userMessage) {
        Task task = new Task();
        task.setId(UUID.randomUUID().toString());
        task.setContextId(contextId != null ? contextId : UUID.randomUUID().toString());
        task.setStatus(TaskStatus.of(TaskState.TASK_STATE_SUBMITTED));
        task.setHistory(List.of(userMessage));
        taskStore.put(task.getId(), task);
        return task;
    }

    /**
     * 获取任务
     */
    public Task getTask(String taskId) {
        return taskStore.get(taskId);
    }

    /**
     * 更新任务状态
     */
    public void updateStatus(String taskId, TaskState state) {
        Task task = taskStore.get(taskId);
        if (task != null) {
            task.setStatus(TaskStatus.of(state));
        }
    }

    /**
     * 向任务追加 Agent 回复并标记完成
     */
    public void completeWithArtifact(String taskId, Artifact artifact, Message agentReply) {
        Task task = taskStore.get(taskId);
        if (task == null) return;
        task.setStatus(TaskStatus.of(TaskState.TASK_STATE_COMPLETED, agentReply));
        task.setArtifacts(List.of(artifact));
        // 追加历史
        if (task.getHistory() != null) {
            task.getHistory().add(agentReply);
        } else {
            task.setHistory(List.of(agentReply));
        }
    }

    /**
     * 取消任务
     */
    public Task cancelTask(String taskId) {
        Task task = taskStore.get(taskId);
        if (task == null) return null;
        task.setStatus(TaskStatus.of(TaskState.TASK_STATE_CANCELED));
        return task;
    }
}
A2A 消息处理控制器
java 复制代码
package com.example.a2a.server;

import com.example.a2a.model.*;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.UUID;

/**
 * A2A Server 控制器
 * 实现 A2A HTTP+JSON 协议绑定
 */
@RestController
@RequestMapping
public class A2AAgentController {

    private final TaskManager taskManager;

    public A2AAgentController(TaskManager taskManager) {
        this.taskManager = taskManager;
    }

    /**
     * A2A 核心操作:发送消息
     * POST /message:send
     */
    @PostMapping(value = "/message:send",
                 consumes = MediaType.APPLICATION_JSON_VALUE,
                 produces = MediaType.APPLICATION_JSON_VALUE)
    public SendMessageResponse sendMessage(@RequestBody SendMessageRequest request) {
        Message userMessage = request.getMessage();

        // 1. 创建任务
        Task task = taskManager.createTask(
                userMessage.getContextId(),
                userMessage
        );

        // 2. 异步处理任务(生产环境应提交到线程池)
        String taskText = extractText(userMessage);
        processTaskAsync(task.getId(), taskText);

        // 3. 立即返回任务信息
        SendMessageResponse response = new SendMessageResponse();
        response.setTask(task);
        return response;
    }

    /**
     * 获取任务状态
     * GET /tasks/{id}
     */
    @GetMapping(value = "/tasks/{id}",
                produces = MediaType.APPLICATION_JSON_VALUE)
    public Task getTask(@PathVariable String id) {
        Task task = taskManager.getTask(id);
        if (task == null) {
            throw new A2ATaskNotFoundException(id);
        }
        return task;
    }

    /**
     * 取消任务
     * POST /tasks/{id}:cancel
     */
    @PostMapping(value = "/tasks/{id}:cancel",
                 produces = MediaType.APPLICATION_JSON_VALUE)
    public Task cancelTask(@PathVariable String id) {
        Task task = taskManager.cancelTask(id);
        if (task == null) {
            throw new A2ATaskNotFoundException(id);
        }
        return task;
    }

    // ===================== 业务处理逻辑 =====================

    /**
     * 模拟异步处理任务
     * 生产环境中应调用 LLM / 业务服务 / MCP 工具
     */
    private void processTaskAsync(String taskId, String userText) {
        // 更新为处理中
        taskManager.updateStatus(taskId, TaskState.TASK_STATE_WORKING);

        try {
            // 模拟业务处理(这里替换为实际的 LLM 调用或 MCP 工具调用)
            Thread.sleep(100);

            // 构建结果
            String resultText = handleBusinessLogic(userText);

            Artifact artifact = new Artifact();
            artifact.setArtifactId(UUID.randomUUID().toString());
            artifact.setName("处理结果");
            artifact.setParts(List.of(Part.text(resultText)));

            Message agentReply = new Message();
            agentReply.setMessageId(UUID.randomUUID().toString());
            agentReply.setTaskId(taskId);
            agentReply.setRole("ROLE_AGENT");
            agentReply.setParts(List.of(Part.text("已为您完成处理,结果如下:")));

            taskManager.completeWithArtifact(taskId, artifact, agentReply);

        } catch (Exception e) {
            taskManager.updateStatus(taskId, TaskState.TASK_STATE_FAILED);
        }
    }

    /**
     * 实际业务逻辑处理
     * 生产环境此处对接 LLM 或业务 API
     */
    private String handleBusinessLogic(String userText) {
        // 简单示例:关键词匹配
        if (userText.contains("订单") && userText.contains("查询")) {
            return "已查询到订单信息:\n" +
                   "订单号:ORD-20250417-001\n" +
                   "状态:已发货\n" +
                   "快递:顺丰速运 SF1234567890\n" +
                   "预计送达:2025-04-18";
        } else if (userText.contains("退款")) {
            return "退款申请已提交:\n" +
                   "退款单号:REF-" + UUID.randomUUID().toString().substring(0, 8) + "\n" +
                   "预计 1-3 个工作日处理完成";
        }
        return "已收到您的请求:" + userText + ",正在处理中...";
    }

    private String extractText(Message message) {
        if (message.getParts() == null || message.getParts().isEmpty()) {
            return "";
        }
        return message.getParts().stream()
                .filter(p -> p.getText() != null)
                .map(Part::getText)
                .findFirst()
                .orElse("");
    }

    /**
     * A2A 自定义异常 - 任务未找到
     */
    @ResponseStatus(org.springframework.http.HttpStatus.NOT_FOUND)
    public static class A2ATaskNotFoundException extends RuntimeException {
        public A2ATaskNotFoundException(String taskId) {
            super("Task not found: " + taskId);
        }
    }
}
SendMessageRequest / Response
java 复制代码
package com.example.a2a.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

/**
 * SendMessage 请求
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SendMessageRequest {
    private Message message;
    private SendMessageConfiguration configuration;
}

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SendMessageConfiguration {
    private boolean returnImmediately = false;
    private int historyLength = 10;
}

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SendMessageResponse {
    private Task task;
    private Message message;
}

4.5 A2A Client 端实现

java 复制代码
package com.example.a2a.client;

import com.example.a2a.model.*;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.UUID;

/**
 * A2A 协议客户端
 * 支持发现 Agent Card、发送消息、轮询任务状态
 */
@Slf4j
public class A2AClient {

    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final String baseUrl;
    private AgentCard cachedAgentCard;

    public A2AClient(String baseUrl) {
        this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();
        this.objectMapper = new ObjectMapper();
        this.objectMapper.registerModule(new JavaTimeModule());
    }

    // ==================== Agent 发现 ====================

    /**
     * 获取远程 Agent 的 Agent Card
     * GET /.well-known/agent-card.json
     */
    public AgentCard fetchAgentCard() {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(baseUrl + "/.well-known/agent-card.json"))
                    .header("A2A-Version", "1.0")
                    .timeout(Duration.ofSeconds(10))
                    .GET()
                    .build();

            HttpResponse<String> response = httpClient.send(request,
                    HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() != 200) {
                throw new RuntimeException(
                    "Failed to fetch agent card, status: " + response.statusCode());
            }

            this.cachedAgentCard = objectMapper.readValue(response.body(), AgentCard.class);
            log.info("Discovered agent: {} - {}", cachedAgentCard.getName(),
                    cachedAgentCard.getDescription());
            return cachedAgentCard;

        } catch (Exception e) {
            throw new RuntimeException("Failed to discover agent at " + baseUrl, e);
        }
    }

    /**
     * 获取缓存的 Agent Card,如不存在则重新获取
     */
    public AgentCard getAgentCard() {
        if (cachedAgentCard == null) {
            return fetchAgentCard();
        }
        return cachedAgentCard;
    }

    // ==================== 消息操作 ====================

    /**
     * 发送消息给远程 Agent
     * POST /message:send
     */
    public Task sendMessage(String text) {
        return sendMessage(text, null, null);
    }

    /**
     * 发送消息并等待任务完成
     * POST /message:send,然后轮询直到任务到达终态
     */
    public Task sendMessageAndWait(String text) {
        Task task = sendMessage(text, null, false);
        return waitForCompletion(task.getId(), Duration.ofSeconds(30));
    }

    /**
     * 发送消息
     * @param text    消息文本
     * @param contextId 上下文 ID(用于多轮对话)
     * @param returnImmediately 是否立即返回(不等待完成)
     */
    public Task sendMessage(String text, String contextId, Boolean returnImmediately) {
        try {
            Message message = new Message();
            message.setMessageId(UUID.randomUUID().toString());
            message.setRole("ROLE_USER");
            message.setParts(java.util.List.of(Part.text(text)));
            if (contextId != null) {
                message.setContextId(contextId);
            }

            SendMessageRequest request = new SendMessageRequest();
            request.setMessage(message);
            if (returnImmediately != null) {
                SendMessageConfiguration config = new SendMessageConfiguration();
                config.setReturnImmediately(returnImmediately);
                request.setConfiguration(config);
            }

            String body = objectMapper.writeValueAsString(request);

            HttpRequest httpRequest = HttpRequest.newBuilder()
                    .uri(URI.create(baseUrl + "/message:send"))
                    .header("Content-Type", "application/json")
                    .header("A2A-Version", "1.0")
                    .timeout(Duration.ofSeconds(30))
                    .POST(HttpRequest.BodyPublishers.ofString(body))
                    .build();

            HttpResponse<String> response = httpClient.send(httpRequest,
                    HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() != 200) {
                throw new RuntimeException(
                    "Send message failed, status: " + response.statusCode()
                    + ", body: " + response.body());
            }

            JsonNode root = objectMapper.readTree(response.body());
            JsonNode taskNode = root.get("task");
            if (taskNode != null) {
                return objectMapper.treeToValue(taskNode, Task.class);
            }
            // 直接返回 Message 的情况
            JsonNode msgNode = root.get("message");
            if (msgNode != null) {
                Message agentMsg = objectMapper.treeToValue(msgNode, Message.class);
                // 包装成已完成的 Task
                Task task = new Task();
                task.setId(UUID.randomUUID().toString());
                task.setStatus(TaskStatus.of(TaskState.TASK_STATE_COMPLETED, agentMsg));
                return task;
            }

            throw new RuntimeException("Invalid response: no task or message found");

        } catch (Exception e) {
            throw new RuntimeException("Failed to send message", e);
        }
    }

    // ==================== 任务管理 ====================

    /**
     * 获取任务状态
     * GET /tasks/{id}
     */
    public Task getTask(String taskId) {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(baseUrl + "/tasks/" + taskId))
                    .header("A2A-Version", "1.0")
                    .timeout(Duration.ofSeconds(10))
                    .GET()
                    .build();

            HttpResponse<String> response = httpClient.send(request,
                    HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() == 404) {
                throw new RuntimeException("Task not found: " + taskId);
            }
            if (response.statusCode() != 200) {
                throw new RuntimeException(
                    "Get task failed, status: " + response.statusCode());
            }

            return objectMapper.readValue(response.body(), Task.class);

        } catch (Exception e) {
            throw new RuntimeException("Failed to get task: " + taskId, e);
        }
    }

    /**
     * 取消任务
     * POST /tasks/{id}:cancel
     */
    public Task cancelTask(String taskId) {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(baseUrl + "/tasks/" + taskId + ":cancel"))
                    .header("A2A-Version", "1.0")
                    .timeout(Duration.ofSeconds(10))
                    .POST(HttpRequest.BodyPublishers.noBody())
                    .build();

            HttpResponse<String> response = httpClient.send(request,
                    HttpResponse.BodyHandlers.ofString());

            return objectMapper.readValue(response.body(), Task.class);

        } catch (Exception e) {
            throw new RuntimeException("Failed to cancel task: " + taskId, e);
        }
    }

    /**
     * 轮询等待任务完成
     */
    public Task waitForCompletion(String taskId, Duration timeout) {
        long deadline = System.currentTimeMillis() + timeout.toMillis();
        long interval = 500; // 500ms 轮询间隔

        while (System.currentTimeMillis() < deadline) {
            Task task = getTask(taskId);
            TaskState state = task.getStatus().getState();

            switch (state) {
                case TASK_STATE_COMPLETED:
                case TASK_STATE_FAILED:
                case TASK_STATE_CANCELED:
                case TASK_STATE_REJECTED:
                    return task;
                case TASK_STATE_INPUT_REQUIRED:
                case TASK_STATE_AUTH_REQUIRED:
                    log.info("Task {} requires input: {}",
                            taskId, task.getStatus().getMessage());
                    return task;
                default:
                    // 继续轮询
                    break;
            }

            try {
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Task polling interrupted", e);
            }
        }

        throw new RuntimeException("Task " + taskId + " timed out after " + timeout);
    }
}

4.6 多 Agent 编排实践(Host Agent 模式)

在实际生产中,推荐使用 Host Agent 模式:一个路由 Agent 负责发现和调度其他专业 Agent。

java 复制代码
package com.example.a2a.client;

import com.example.a2a.model.*;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Agent 注册中心
 * 管理多个远程 Agent 的连接和发现
 */
@Slf4j
public class AgentRegistry {

    private final Map<String, AgentEntry> agents = new ConcurrentHashMap<>();

    /**
     * Agent 注册条目
     */
    public static class AgentEntry {
        public AgentCard card;
        public A2AClient client;
        public String url;
    }

    /**
     * 发现并注册一个远程 Agent
     * 自动获取 Agent Card 并建立连接
     */
    public void discoverAgent(String url) {
        A2AClient client = new A2AClient(url);
        AgentCard card = client.fetchAgentCard();
        agents.put(card.getName(), new AgentEntry() {{
            this.card = card;
            this.client = client;
            this.url = url;
        }});
        log.info("Registered agent: {} at {}", card.getName(), url);
    }

    /**
     * 根据技能标签匹配最合适的 Agent
     */
    public Optional<AgentEntry> findAgentByTag(String tag) {
        return agents.values().stream()
                .filter(entry -> entry.card.getSkills() != null)
                .filter(entry -> entry.card.getSkills().stream()
                        .anyMatch(skill -> skill.getTags() != null
                                && skill.getTags().contains(tag)))
                .findFirst();
    }

    /**
     * 向指定 Agent 发送消息并等待结果
     */
    public Task delegateTo(String agentName, String text) {
        AgentEntry entry = agents.get(agentName);
        if (entry == null) {
            throw new RuntimeException("Agent not found: " + agentName);
        }
        return entry.client.sendMessageAndWait(text);
    }

    /**
     * 获取所有已注册 Agent 摘要
     */
    public String getAgentsSummary() {
        StringBuilder sb = new StringBuilder("可用 Agent 列表:\n");
        agents.forEach((name, entry) -> {
            sb.append("  - ").append(name)
              .append(": ").append(entry.card.getDescription()).append("\n");
            if (entry.card.getSkills() != null) {
                for (AgentSkill skill : entry.card.getSkills()) {
                    sb.append("    技能: ").append(skill.getName())
                      .append(" (").append(skill.getTags()).append(")\n");
                }
            }
        });
        return sb.toString();
    }
}

使用示例:

java 复制代码
package com.example.a2a;

import com.example.a2a.client.AgentRegistry;
import com.example.a2a.model.Task;
import com.example.a2a.model.AgentRegistry.AgentEntry;

import java.util.Optional;

/**
 * Host Agent 编排示例
 * 路由 Agent 根据用户意图将任务委派给专业 Agent
 */
public class HostAgentExample {

    public static void main(String[] args) {
        AgentRegistry registry = new AgentRegistry();

        // 1. 发现并注册专业 Agent
        registry.discoverAgent("http://localhost:8081");  // 订单服务 Agent
        registry.discoverAgent("http://localhost:8082");  // 客服 Agent
        registry.discoverAgent("http://localhost:8083");  // 数据分析 Agent

        // 2. 根据用户意图路由
        String userInput = "帮我查询订单 ORD-001 的物流状态";

        if (userInput.contains("订单") || userInput.contains("退款")) {
            Task result = registry.delegateTo("Order Service Agent", userInput);
            System.out.println("结果: " + result.getArtifacts().get(0).getParts().get(0).getText());
        } else if (userInput.contains("投诉") || userInput.contains("人工")) {
            Task result = registry.delegateTo("Customer Service Agent", userInput);
            System.out.println("结果: " + result.getArtifacts().get(0).getParts().get(0).getText());
        }
    }
}

4.7 生产环境注意事项

线程池与异步处理
java 复制代码
@Configuration
public class A2aAsyncConfig {

    @Bean
    public ThreadPoolTaskExecutor a2aTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("a2a-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}
任务持久化

生产环境应将 TaskManager 替换为数据库实现:

java 复制代码
@Repository
public class JpaTaskRepository implements TaskStore {

    @PersistenceContext
    private EntityManager em;

    @Override
    public Task createTask(Task task) {
        em.persist(task);
        return task;
    }

    @Override
    @Transactional
    public void completeWithArtifact(String taskId, Artifact artifact, Message reply) {
        Task task = em.find(Task.class, taskId);
        task.setStatus(TaskStatus.of(TaskState.TASK_STATE_COMPLETED, reply));
        task.setArtifacts(List.of(artifact));
        em.persist(artifact);
        em.merge(task);
    }
}
安全认证
java 复制代码
@Component
public class A2aAuthFilter extends OncePerRequestFilter {

    @Value("${a2a.auth.api-key}")
    private String expectedApiKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        // Agent Card 端点允许匿名访问
        if (request.getRequestURI().equals("/.well-known/agent-card.json")) {
            filterChain.doFilter(request, response);
            return;
        }

        String auth = request.getHeader("Authorization");
        if (auth == null || !auth.equals("Bearer " + expectedApiKey)) {
            response.setStatus(401);
            response.getWriter().write("{\"error\":{\"code\":401,\"message\":\"Unauthorized\"}}");
            return;
        }

        filterChain.doFilter(request, response);
    }
}
错误处理
java 复制代码
@RestControllerAdvice
public class A2aErrorHandler {

    @ExceptionHandler(A2ATaskNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, Object> handleTaskNotFound(A2ATaskNotFoundException ex) {
        return Map.of(
            "error", Map.of(
                "code", 404,
                "status", "NOT_FOUND",
                "message", ex.getMessage(),
                "details", List.of(Map.of(
                    "@type", "type.googleapis.com/google.rpc.ErrorInfo",
                    "reason", "TASK_NOT_FOUND",
                    "domain", "a2a-protocol.org"
                ))
            )
        );
    }
}

五、架构模式对比

5.1 P2P 模式 vs Host Agent 模式

维度 P2P(对等网络) Host Agent(中心路由)
复杂度 Agent 间 N*(N-1)/2 连接 Agent 只需连接 Host
扩展性 新 Agent 需通知所有其他 Agent 新 Agent 只需注册到 Host
调试 困难(分布式决策) 简单(Host 集中决策)
推荐场景 少量固定 Agent 生产环境首选

5.2 推荐的生产架构

复制代码
                        ┌──────────────┐
                        │   用户/客户端  │
                        └──────┬───────┘
                               │
                        ┌──────▼───────┐
                        │  Host Agent  │  ← 路由 + 编排
                        │  (Gateway)   │
                        └──┬───┬───┬───┘
                           │   │   │
              ┌────────────┘   │   └────────────┐
              ▼                ▼                ▼
        ┌──────────┐   ┌──────────┐   ┌──────────────┐
        │ 订单 Agent │   │ 客服 Agent │   │ 数据分析 Agent │
        │ :8081     │   │ :8082     │   │ :8083         │
        └─────┬─────┘   └─────┬─────┘   └──────┬───────┘
              │               │                 │
              ▼               ▼                 ▼
        ┌──────────┐   ┌──────────┐   ┌──────────────┐
        │ 订单 DB   │   │ 工单系统  │   │ 数据仓库     │
        └──────────┘   └──────────┘   └──────────────┘

六、总结

A2A 协议为构建多 Agent 协作系统提供了标准化的通信方案。本文的核心要点:

  1. Agent Card 是 A2A 的发现机制,每个 Agent 必须在 /.well-known/agent-card.json 提供自描述信息
  2. Task 是 A2A 的核心工作单元,有完整的生命周期状态管理
  3. Host Agent 模式 是生产环境的推荐架构,避免 P2P 的连接爆炸问题
  4. A2A + MCP 互补:A2A 负责 Agent 间通信,MCP 负责 Agent 与工具的交互
  5. Java 实现可直接使用 HttpClient + Jackson,无需额外 SDK 依赖

参考资料

相关推荐
Rust语言中文社区2 小时前
【Rust日报】Clone:像进程一样 fork 虚拟机的 Rust KVM VMM
开发语言·后端·rust
求知也求真佳2 小时前
S02|工具使用:让 Agent 真正会干活,添加工具
开发语言·agent
后端漫漫2 小时前
Redis 配置文件与服务功能
java·redis
Dwzun2 小时前
基于Java+SpringBoot+Vue的校园二手物品置换系统设计与实现【附源码+文档+部署视频+讲解】
java·开发语言·spring boot
charlie1145141912 小时前
嵌入式Linux驱动开发(3)——内核模块机制 - Linux 的插件系统
linux·运维·开发语言·驱动开发·嵌入式硬件·学习
polaris06302 小时前
Spring Boot 项目开发流程全解析
java·spring boot·log4j
谭欣辰2 小时前
AC自动机:多模式匹配的高效利器
数据结构·c++·算法
凌奕2 小时前
Mac 从零部署 Hermes Agent 并接入飞书:一篇就够的保姆级教程
agent
zuowei28892 小时前
spring实例化对象的几种方式(使用XML配置文件)
xml·java·spring