A2A 概述
A2A到底是什么?
A2A(Agent2Agent)是一种开放协议,旨在实现不透明Agent之间的通信和互操作。其主要作用就是为不同的Agent提供一种共同的“语言”标准,在它们需要“交流”的时候可以更加简单、高效与安全,实现信息共享与任务分配,从而更快的完成任务,即使它们的底层平台完全不同:
A2A架构是怎样的?
也就是“基于A2A规范的Agent集成架构”,大致描述如下:
A2A与MCP什么关系?
A2A在诞生动机、架构甚至协议方面都与MCP非常相似,但它们之间的关系与区别还是很清楚的:
MCP解决的是Agent与外部工具/数据之间的集成,是Agent的“内部事务”;A2A解决的是Agent与Agent之间的集成,属于更高层次的集成关系。
它们之间是可以共存与协作的,比如:
在这样的架构中Agent通过MCP使用工具,Agent与Agent之间则通过A2A产生互动与协作。
技术原理
基于HTTP+JSON的AI代理通信协议,主要包含以下模块:
Agent Card
- 位置:/.well-known/agent.json
- 作用:JSON格式的代理能力说明书(含API端点、技能、认证方式)
角色模型
- 服务端:提供API接口(必须实现tasks/send等标准方法)
- 客户端:通过HTTP调用其他代理
任务流程
-
生命周期:submitted → working → (input-required) → completed/failed
-
交互单元:
- 消息:由多类型Part组成(文本/文件/结构化数据)
- 工件:任务产出的结构化结果
通信机制
-
基础模式:HTTP + JSON-RPC风格接口
-
高级功能:
- 实时推送:SSE协议(tasks/sendSubscribe)
- 异步通知:Webhook回调
- 多模态支持:通过Part类型实现
UX协商
- 代理间预先协商交互形式(如文本/语音/表单)
示例demo
server端controller:
package com.example.spring_ai.a2a.server.controller;
import com.example.spring_ai.a2a.server.bean.TaskRequest;
import com.example.spring_ai.a2a.server.bean.TaskResponse;
import com.example.spring_ai.a2a.server.service.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* TaskController
* 实现任务提交与异步处理逻辑
*
**/
@RestController
@Slf4j
public class AgentController {
@Autowired
private TaskService taskService;
@GetMapping("/.well-known/agent.json")
public Map<String, Object> getAgentCard() {
return Map.of(
"name", "CalculateAgent",
"version", "1.0",
"description", "提供计算服务",
"endpoints", Map.of(
"task_send", "/api/tasks/submit",
"task_get", "/api/tasks/subscribe_sse"
),
"input_schema", Map.of(
"type", "object",
"properties", Map.of(
"tasks", Map.of("type", "string", "enum", List.of("task1", "task2")),
"required", List.of("tasks")
),
"authentication", Map.of("methods", List.of("API_Key"))
));
}
@PostMapping("/api/tasks/submit")
public ResponseEntity<TaskResponse> submitTask(
@RequestHeader("Authorization") String apiKey,
@RequestBody TaskRequest request
) {
// 认证校验(示例简化)
if (!"SECRET_KEY".equals(apiKey)) {
return ResponseEntity.status(401).build();
}
// 异步处理任务
CompletableFuture.runAsync(() ->
taskService.processTask(request.getTaskId(), request.getParams())
);
return ResponseEntity.accepted().body(
new TaskResponse(request.getTaskId(), "submitted", "", "")
);
}
@GetMapping(value = "/api/tasks/subscribe_sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<TaskResponse> streamTaskStatus(@RequestParam("taskId") String taskId) {
log.info("[server] updates task {} with streamTaskStatus", taskId);
return taskService.streamTaskStatus(taskId);
}
}
service:
package com.example.spring_ai.a2a.server.service;
import com.example.spring_ai.a2a.server.bean.TaskResponse;
import com.example.spring_ai.a2a.server.bean.TaskStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
@Slf4j
public class TaskService {
private final Map<String, TaskStatus> taskStore = new ConcurrentHashMap<>();
public void processTask(String taskId, Map<String, String> params) {
log.info("[server] Processing task {} with params {}", taskId, params);
List<String> tasks = Arrays.asList(params.get("tasks").split(","));
String progress = "0/" + tasks.size();
String finalRes = "";
taskStore.put(taskId, new TaskStatus("working", progress, ""));
try {
// 模拟任务执行
for (int i = 0; i < tasks.size(); i++) {
progress = (i + 1) + "/" + tasks.size();
//随机返回success或者failed
String res = Math.random() < 0.5 ? "success" : "failed";
finalRes = finalRes + "{" + tasks.get(i) + ":" + res + "}, ";
taskStore.put(taskId, new TaskStatus("working", progress, finalRes));
Thread.sleep(10000);
}
taskStore.put(taskId, new TaskStatus("completed", progress, finalRes));
} catch (InterruptedException e) {
taskStore.put(taskId, new TaskStatus("failed", progress,
Map.of("error", "任务被中断"))
);
Thread.currentThread().interrupt(); // 恢复中断状态
} catch (Exception e) {
// 处理其他异常
taskStore.put(taskId, new TaskStatus("failed", progress,
Map.of("error", e.getMessage()))
);
}
log.info("[server] Processing task {} end", taskId);
}
public Flux<TaskResponse> streamTaskStatus(String taskId) {
return Flux.interval(Duration.ofSeconds(1))
.map(tick -> {
TaskStatus taskStatus = taskStore.get(taskId);
if (taskStatus == null) {
return new TaskResponse(taskId, "not_found", null, null);
} else {
return new TaskResponse(
taskId,
taskStatus.getStatus(),
taskStatus.getProgress(),
taskStatus.getResult()
);
}
})
.takeUntil(response ->
"not_found".equals(response.getStatus()) ||
"completed".equals(response.getStatus()) ||
"failed".equals(response.getStatus())
)
.doOnNext(response ->
log.info("Sending SSE: {}", response))
.doOnComplete(() ->
log.info("SSE stream completed for task {}", taskId))
.onErrorResume(e -> {
log.error("Error in SSE stream for task {}", taskId, e);
return Flux.empty();
});
}
}
测试client:
package com.example.spring_ai.a2a.client.service;
import com.example.spring_ai.a2a.server.bean.TaskRequest;
import com.example.spring_ai.a2a.server.bean.TaskResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
import reactor.util.retry.Retry;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
@Service
@Slf4j
public class AgentClient {
private final RestTemplate restTemplate;
private final WebClient webClient;
String baseUrl = "http://localhost:8181";
public AgentClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
public SseEmitter tasksPlan(String tasks) throws InterruptedException {
// 获取WeatherAgent的AgentCard
log.info("[client] Start to get AgentCard");
Map<String, Object> agentCard = restTemplate.getForObject(
"http://localhost:8181/.well-known/agent.json",
Map.class
);
if (agentCard == null) {
throw new RuntimeException("Failed to fetch agent card");
}
log.info("[client] Get AgentCard: %s".formatted(agentCard));
// 构造A2A标准任务请求
Map<String, Object> endpoints = (Map<String, Object>) agentCard.get("endpoints");
String taskSubmitUrl = baseUrl + endpoints.get("task_send");
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "SECRET_KEY");
String taskId = UUID.randomUUID().toString();
Map<String, String> params = Map.of("tasks", tasks);
ResponseEntity<TaskResponse> response = restTemplate.exchange(
taskSubmitUrl,
HttpMethod.POST,
new HttpEntity<>(new TaskRequest(taskId, params), headers),
TaskResponse.class
);
log.info("[client] Step task_send end, {}", response);
// SSE订阅
SseEmitter sseEmitter = new SseEmitter(60_000L);
Flux<TaskResponse> eventStream = webClient.get()
.uri(uriBuilder -> uriBuilder
.path(endpoints.get("task_get").toString())
.queryParam("taskId", taskId)
.build())
.header("Authorization", "SECRET_KEY")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(TaskResponse.class)
.filter(resp -> taskId.equals(resp.getTaskId()))
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)));
eventStream.subscribe(
resp -> {
try {
sseEmitter.send(resp);
if ("completed".equals(resp.getStatus())) {
sseEmitter.complete(); // 服务端标记完成时关闭
}
} catch (IOException e) {
throw new RuntimeException(e);
}
},
error -> System.err.println("SSE连接异常: " + error.getMessage())
);
log.info("[client] Step sse_subscribe end ");
return sseEmitter;
}
}
完整示例:https://gitee.com/jackyzhang888/spring-ai-demo/tree/master/src
测试:
参考:https://zhuanlan.zhihu.com/p/1894801654202748934