使用 Java、Spring Boot 和 Spring AI 开发符合 A2A 标准的 AI 智能体

AI 智能体指的是一种软件实体,它能够利用自然语言处理、机器学习或推理系统等人工智能技术,自主感知、推理和行动,以实现特定目标。

我为 Telex 开发了一个 AI 智能体,该智能体接收一个正则表达式模式,并就该模式所匹配的字符串类型提供易于理解的解释。开发此智能体的灵感源于我在此之前开发的一个 API(您可以在此处查看该项目),在该 API 中我必须使用正则表达式进行一些自然语言处理。尽管我之前学习过正则表达式,但感觉像是第一次见到它。正则表达式就是这样。因此,当 Telex 为其平台寻求更多 AI 智能体时,我决定开发这个智能体。

以下是我使用 Java、Spring AI 和 Spring Boot 实现它的过程。

初始设置

1. Spring Boot 项目初始化

我使用 Spring 提供的初始化工具来初始化项目。请注意,我在依赖项中包含了 Spring Web 和 Open AI。

2. 设置 API 凭证

在我的 application.properties 文件中,我设置了 Spring AI 以使用我的 API 凭证(我的 API 密钥)。我通过 Google AI Studio 获得了一个免费的 Google Gemini API 密钥。我的 application.properties 文件设置如下:

properties 复制代码
spring.config.import=classpath:AI.properties

  


spring.application.name=regexplain

spring.ai.openai.api-key = ${GEMINI_API_KEY}

spring.ai.openai.base-url https://generativelanguage.googleapis.com/v1beta/openai

spring.ai.openai.chat.completions-path = /chat/completions

spring.ai.openai.chat.options.model = gemini-2.5-pro

第一行导入了包含我 API 密钥的文件。重要的是不要将您的 API 密钥暴露给公众。该文件与 application.properties 位于同一文件夹中。

3. 首次项目运行

使用我的包管理器(Maven),我安装了所需的依赖项。然后我运行了我的主类,以确保一切正常。如果您到目前为止一切都做对了,您的项目应该可以无错误运行。如果遇到任何错误,请在 Google 上查找解决方法。

A2A 请求和响应模型

在深入实现之前,让我们先谈谈符合 A2A 标准的请求和响应的结构。A2A 协议遵循标准的 JSON-RPC 2.0 结构来处理请求和响应。

所有方法调用都封装在一个请求对象中,其结构如下:

json 复制代码
{

"jsonrpc": "2.0",

"method": "String",

"id": "String | Integer",

"params": "Message"

}

响应对象有些类似:

json 复制代码
{

"jsonrpc": "2.0",

"id": "String | Integer | null",

"result?": "Task | Message | null",

"error?": "JSONRPCError"

}

响应中的 ID 必须与请求中的 ID 相同。

有关 A2A 协议的更多信息,请查阅 A2A 协议文档

以上就是请求和响应的通用结构。我开发这个智能体是为了在 Telex 平台上使用,因此我的部分实现可能特定于 Telex。

现在进入实现部分。我创建了一个名为 model 的文件夹,用于存储我的模型。请求模型类 A2ARequest 如下所示:

java 复制代码
public class A2ARequest {

private String id;

private RequestParamsProperty params;

  


public A2ARequest(String id, RequestParamsProperty params) {

this.id = id;

this.params = params;

}

  


// getters and setters

}

RequestParamsProperty 类代表了 params 中包含信息的结构。它如下所示:

java 复制代码
public class RequestParamsProperty {

private HistoryMessage message;

private String messageId;

  


public RequestParamsProperty(HistoryMessage message, String messageId) {

this.message = message;

this.messageId = messageId;

}

  


// getters and setter

}

HistoryMessage 类如下所示:

java 复制代码
@JsonIgnoreProperties(ignoreUnknown = true)

@JsonInclude(JsonInclude.Include.NON_NULL)

public class HistoryMessage {

private String kind;

private String role;

private List<MessagePart> parts;

private String messageId;

private String taskId;

  


public HistoryMessage() {}

  


public HistoryMessage(String role, List<MessagePart> parts, String messageId, String taskId) {

this.kind = "message";

this.role = role;

this.parts = parts;

this.messageId = messageId;

this.taskId = taskId;

}

  


// getters and setters

}

注解的作用是让 Spring 知道在请求和响应的 JSON 表示中包含什么。如果请求中不存在某个属性,它应该忽略它并在类中将其设置为 null。如果某个属性设置为 null,则不应将其包含在响应中。

MessagePart 类如下所示:

java 复制代码
@JsonIgnoreProperties(ignoreUnknown = true)

@JsonInclude(JsonInclude.Include.NON_NULL)

public class MessagePart {

private String kind;

private String text;

private List<MessagePart> data;

  


public MessagePart(String kind, String text, List<MessagePart> data) {

this.kind = kind;

this.text = text;

this.data = data;

}

  


// getters and setters

}

以上就是表示从 Telex 接收的请求结构所需的所有类。现在需要为我的响应创建一个模型,以及表示响应所需的所有支持类。

A2AResponse 类:

java 复制代码
@JsonInclude(JsonInclude.Include.NON_NULL)

public class A2AResponse {

private final String jsonrpc;

@JsonInclude(JsonInclude.Include.ALWAYS)

private String id;

private Result result;

private CustomError error;

  


public A2AResponse() {

this.jsonrpc = "2.0";

}

  


public A2AResponse(String id, Result result, CustomError error) {

this.jsonrpc = "2.0";

this.id = id;

this.result = result;

this.error = error;

}

  


//getters and setters

}

Result 类:

java 复制代码
public class Result {

private String id;

private String contextId;

private TaskStatus status;

private List<Artifact> artifacts;

private List<HistoryMessage> history;

private String kind;

  


public Result() {}

  


public Result(String id, String contextId, TaskStatus status, List<Artifact> artifacts, List<HistoryMessage> history, String task) {

this.id = id;

this.contextId = contextId;

this.status = status;

this.artifacts = artifacts;

this.history = history;

this.kind = task;

}

  


// getters and setters

}

CustomError 类:

java 复制代码
public class CustomError {

private int code;

private String message;

private Map<String, String> data;

  


public CustomError(int code, String message, Map<String, String> data) {

this.code = code;

this.message = message;

this.data = data;

}

  


// getters and setters

}

TaskStatus 类:

java 复制代码
@JsonInclude(JsonInclude.Include.NON_NULL)

public class TaskStatus {

private String state;

private Instant timestamp;

private HistoryMessage message;

  


public TaskStatus() {}

  


public TaskStatus(String state, Instant timestamp, HistoryMessage message) {

this.state = state;

this.timestamp = timestamp;

this.message = message;

}

  


// getters and setters

}

Artifact 类:

java 复制代码
public class Artifact {

private String artifactId;

private String name;

private List<MessagePart> parts; // 稍后复查此类型

  


public Artifact() {}

  


public Artifact(String artifactId, String name, List<MessagePart> parts) {

this.artifactId = artifactId;

this.name = name;

this.parts = parts;

}

  


// getters and setters

}

A2A 协议还包含一个称为"智能体卡片"的东西。我也为它创建了一个模型。

java 复制代码
public class AgentCard {

private String name;

private String description;

private String url;

private Map<String, String> provider;

private String version;

private Map<String, Boolean> capabilities;

private List<String> defaultInputModes;

private List<String> defaultOutputModes;

private List<Map<String, Object>> skills;

  


public AgentCard() {

this.provider = new HashMap<>();

this.capabilities = new HashMap<>();

this.skills = new ArrayList<>();

}

  


// getters and setters

}

模型部分就这些了。继续...

服务类

我的智能体的作用是获取一个正则表达式字符串,然后使用预定义的提示词将其发送到 OpenAI 的 API。服务类负责与 OpenAI 通信,发送提示词并接收响应。我创建了另一个名为 service 的文件夹,我的服务类就放在这里。我是这样编写我的服务类的:

java 复制代码
@Service

public class RegExPlainService {

private ChatClient chatClient;

  


RegExPlainService(ChatClient.Builder chatClientBuilder) {

this.chatClient = chatClientBuilder.build();

}

  


@Tool(name = "regexplain", description = "An agent that explains what type of string a regex pattern matches")

public String generateResponse(String regex) {

return chatClient

.prompt("Give me a simple explanation of the type of string matched by this regex pattern: %s. No validating statements from you. Just straight to the point".formatted(regex))

.call()

.content();

}

}

@Service 注解允许 Spring Boot 将服务注入到您的控制器中。@Tool 注解将该方法标记为一个智能体工具,如果将来要扩展该智能体以包含该功能,它可以被自主调用。不过目前并不需要它。

控制器

控制器通过 REST API 暴露该智能体。在这个案例中,我有两个端点,一个 GET 端点和一个 POST 端点。我在一个名为 controller 的文件夹中创建了我的控制器。实现如下:

java 复制代码
@RestController

public class RegExPlainController {

private final RegExPlainService regexplainService;

  


@Autowired

RegExPlainController (RegExPlainService regexplainService) {

this.regexplainService = regexplainService;

}

  


@GetMapping("/a2a/agent/regexplain/.well-known/agent.json")

public ResponseEntity<AgentCard> getAgentCard () {

AgentCard agentCard = new AgentCard();

agentCard.setName("regexplain");

agentCard.setDescription("An agent that provides a simple explanation of the type of string a regex pattern matches");

agentCard.setUrl("regexplain-production.up.railway.app/api");

agentCard.setProvider("Bituan", null); // 假设 setProvider 处理 Map 的填充

agentCard.setVersion("1.0");

agentCard.setCapabilities(false, false, false); // 假设 setCapabilities 处理 Map 的填充

agentCard.setDefaultInputModes(List.of("text/plain"));

agentCard.setDefaultOutputModes(List.of("application/json", "text/plain"));

agentCard.setSkill("skill-001", "Explain Regex", "Provides a simple explanation of the type of string a regex pattern matches",

List.of("text/plain"), List.of("text/plain"), List.of());

  


return ResponseEntity.ok(agentCard);

}

  


@PostMapping("/a2a/agent/regexplain")

public ResponseEntity<A2AResponse> explainRegex (@RequestBody A2ARequest request) {

String regexRequest;

String responseText;

  


// 如果参数无效,返回 403

try {

regexRequest = request.getParams().getMessage().getParts().get(0).getText();

} catch (Exception e) {

CustomError error = new CustomError(-32603, "Invalid Parameter", Map.of("details", e.getMessage()));

A2AResponse errorResponse = new A2AResponse(null, null, error);

return ResponseEntity.status(HttpStatusCode.valueOf(403)).body(errorResponse);

}

  


// 如果调用服务失败,返回错误 500

try {

responseText = regexplainService.generateResponse(regexRequest);

} catch (Exception e) {

CustomError error = new CustomError(-32603, "Internal Error", Map.of("details", e.getMessage()));

A2AResponse errorResponse = new A2AResponse(null, null, error);

return ResponseEntity.internalServerError().body(errorResponse);

}

  


// 构建响应

A2AResponse response = new A2AResponse();

response.setId(request.getId());

  


// 构建响应 -> 构建结果

Result result = new Result();

result.setId(UUID.randomUUID().toString());

result.setContextId(UUID.randomUUID().toString());

result.setKind("task");

  


// 构建响应 -> 构建结果 -> 构建状态

TaskStatus status = new TaskStatus();

status.setState("completed");

status.setTimestamp(Instant.now());

  


// 构建响应 -> 构建结果 -> 构建状态 -> 构建消息

HistoryMessage message = new HistoryMessage();

message.setRole("agent");

message.setParts(List.of(new MessagePart("text", responseText, null)));

message.setKind("message");

message.setMessageId(UUID.randomUUID().toString());

  


// 构建响应 -> 构建结果 -> 构建状态 (续)

status.setMessage(message);

  


// 构建响应 -> 构建结果 -> 构建工件

List<Artifact> artifacts = new ArrayList<>();

Artifact artifact = new Artifact();

artifact.setArtifactId(UUID.randomUUID().toString());

artifact.setName("regexplainerResponse");

artifact.setParts(List.of(new MessagePart("text", responseText, null)));

artifacts.add(artifact);

  


// 构建响应 -> 构建结果 -> 构建历史记录

List<HistoryMessage> history = new ArrayList<>();

  


// 构建响应 -> 构建结果 (续)

result.setStatus(status);

result.setArtifacts(artifacts);

result.setHistory(history);

  


// 构建响应 (续)

response.setResult(result);

  


return ResponseEntity.ok(response);

}

}
  • GET 端点使用的路由路径是 A2A 协议标准中用于获取智能体卡片的部分。智能体卡片是对智能体及其功能的描述。

  • POST 端点接收一个符合 A2A 标准的请求,执行智能体,然后返回适当的响应。

结论

就是这样。这就是我编写 Regexplain 的过程。

通过这个示例,您可以从头开始构建您的 AI 智能体并使其符合 A2A 标准。或者,至少我希望这能让您对如何使用 Java 开发符合 A2A 标准的 AI 智能体有所了解。


【注】本文译自:Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI - DEV Community

相关推荐
飞哥数智坊6 小时前
TRAE CN + K2 Thinking,我试着生成了一个简版的在线 PS
人工智能·ai编程·trae
YoungHong199217 小时前
MiniMax-M2 全方位配置手册:覆盖 Claude Code, Cursor, Cline 等工具
ai编程
人工智能训练17 小时前
如何在 Ubuntu 22.04 中安装 Docker 引擎和 Linux 版 Docker Desktop 桌面软件
linux·运维·服务器·数据库·ubuntu·docker·ai编程
数据智能老司机21 小时前
Spring AI 实战——提交用于生成的提示词
spring·llm·ai编程
数据智能老司机1 天前
Spring AI 实战——评估生成结果
spring·llm·ai编程
该用户已不存在1 天前
免费的 Vibe Coding 助手?你想要的Gemini CLI 都有
人工智能·后端·ai编程
一只柠檬新1 天前
当AI开始读源码,调Bug这件事彻底变了
android·人工智能·ai编程
用户4099322502121 天前
Vue 3中watch侦听器的正确使用姿势你掌握了吗?深度监听、与watchEffect的差异及常见报错解析
前端·ai编程·trae
yaocheng的ai分身1 天前
【转载】我如何用Superpowers MCP强制Claude Code在编码前进行规划
ai编程·claude