跳到主要内容SpringAI Agent 开发实战:基于 Skills 的代码评审实践 | 极客日志JavaAIjava
SpringAI Agent 开发实战:基于 Skills 的代码评审实践
SpringAI 通过 Skills 机制实现了 Agent 能力的模块化复用。本文演示了如何基于 SpringAI 2.x 版本和智谱大模型,构建一个具备代码审查能力的 Agent。通过定义 SKILL.md 规范技能元数据,结合 FileSystemTools 和 ShellTools,Agent 能够自动读取本地代码文件并进行分析。实际运行显示,系统经过三轮交互完成从意图识别到代码反馈的全过程,有效提升了 Java 代码的安全性及规范性。
SpringAI Agent 开发实战:基于 Skills 的代码评审实践
SpringAI 近期迅速支持了 Skills 概念,这让 Java 开发者也能快速接入 Agent 能力。我们通过构建一个 Code Reviewer,实际体验如何将 SpringAI 和 Skills 结合起来使用。
一、项目创建
1. 基础环境要求
要体验 SpringAI & Skills,目前需要升级到 SpringAI 2.x 版本,同时 SpringBoot 也可以升级到 4.x。
- SpringAI: 2.0.0-M2
- JDK 21
- SpringBoot: 4.0.1
除了这几个基本依赖之外,我们可以选择一个支持 Function Tool 的大模型来作为这个实现的大脑中枢。这里选择智谱的大模型 GLM-4.5-Flash(免费且效果尚可,适合体验)。
2. 项目创建
创建一个标准的 SpringAI 应用,在 pom.xml 配置中指定基础版本。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.1</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai.version>2.0.0-M2</spring-ai.version>
</properties>
<dependencyManagement>
org.springframework.ai
spring-ai-bom
${spring-ai.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
spring-snapshots
Spring Snapshots
https://repo.spring.io/snapshot
false
Central Portal Snapshots
central-portal-snapshots
https://central.sonatype.com/repository/maven-snapshots/
false
true
<dependencies>
<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
<version>
</version>
<type>
</type>
<scope>
</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>
</groupId>
<artifactId>
</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>
</id>
<name>
</name>
<url>
</url>
<releases>
<enabled>
</enabled>
</releases>
</repository>
<repository>
<name>
</name>
<id>
</id>
<url>
</url>
<releases>
<enabled>
</enabled>
</releases>
<snapshots>
<enabled>
</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agent-utils</artifactId>
<version>0.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>
</dependencies>
spring-ai-agent-utils: Agent 开发的关键依赖包。
spring-ai-starter-model-zhipuai: 智谱大模型交互的依赖包。
3. 项目配置
依赖搞定之后,在 resources/application.yml 配置 LLM 访问信息及 Agent 参数。
spring:
ai:
zhipuai:
api-key: ${zhipuai-api-key}
chat:
options:
model: GLM-4.5-Flash
agent:
skills:
dirs: classpath:/.claude/skills
model: GLM-4.5-Flash
其中 agent 相关配置主要设置了 skills 的存放路径和使用的 model。根据定义,我们将 skills 信息放在 resources/.claude/skills 目录下。
新增一个目录 code-reviewer,目录下的文件为 SKILL.md:
.claude/skills/code-reviewer/
└── SKILL.md
--- name: code-reviewer description: Reviews Java code for best practices, security issues, and Spring Framework conventions. Use when user asks to review, analyze, or audit code. ---
# Code Reviewer
## Instructions
在审查代码时:
1. 检查是否存在安全漏洞(如 SQL 注入、XSS 等)
2. 验证是否遵循了 Spring Boot 的最佳实践(如正确使用@Service、@Repository 等注解)
3. 查找潜在的空指针异常
4. 提出提高代码可读性和可维护性的建议
5. 提供具体的逐行反馈,并附上代码示例
6. 以中文的方式返回代码评审结果
4. Skills 简要说明
上面的 Skill 比较简单,就是一个 Markdown 文档。SpringAI 支持的 Skills 中,除了包含基本的 SKILL.md 文件(包含元数据及指导代理如何执行特定任务的说明)之外,还可以有相关的脚本、模板和参考资料。
my-skill/
├── SKILL.md
├── scripts/
├── references/
└── assets/
二、核心实现
2.1 交互日志打印 MyLoggingAdvisor
为了让系统与大模型之间的交互更清晰,我们将双方交互的日志进行更友好的打印,顺便看一下一次用户感知的问答过程中实际上有几次交互。
public class MyLoggingAdvisor implements BaseAdvisor {
private final int order;
public final boolean showSystemMessage;
public final boolean showAvailableTools;
private AtomicInteger cnt = new AtomicInteger(1);
private MyLoggingAdvisor(int order, boolean showSystemMessage, boolean showAvailableTools) {
this.order = order;
this.showSystemMessage = showSystemMessage;
this.showAvailableTools = showAvailableTools;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
System.out.println("======================= 第 " + cnt.getAndAdd(1) + " 轮 ====================================");
StringBuilder sb = new StringBuilder("\nUSER: ");
if (this.showSystemMessage && chatClientRequest.prompt().getSystemMessage() != null) {
sb.append("\n - SYSTEM: ").append(first(chatClientRequest.prompt().getSystemMessage().getText(), 300));
}
if (this.showAvailableTools) {
Object tools = "No Tools";
if (chatClientRequest.prompt().getOptions() instanceof ToolCallingChatOptions toolOptions) {
tools = toolOptions.getToolCallbacks().stream()
.map(tc -> tc.getToolDefinition().name())
.toList();
}
sb.append("\n - TOOLS: ").append(ModelOptionsUtils.toJsonString(tools));
}
Message lastMessage = chatClientRequest.prompt().getLastUserOrToolResponseMessage();
if (lastMessage.getMessageType() == MessageType.TOOL) {
ToolResponseMessage toolResponseMessage = (ToolResponseMessage) lastMessage;
for (var toolResponse : toolResponseMessage.getResponses()) {
var tr = toolResponse.name() + ": " + first(toolResponse.responseData(), 1000);
sb.append("\n - TOOL-RESPONSE: ").append(tr);
}
} else if (lastMessage.getMessageType() == MessageType.USER) {
if (StringUtils.hasText(lastMessage.getText())) {
sb.append("\n - TEXT: ").append(first(lastMessage.getText(), 1000));
}
}
System.out.println("before: " + sb);
return chatClientRequest;
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
StringBuilder sb = new StringBuilder("\nASSISTANT: ");
if (chatClientResponse.chatResponse() == null || chatClientResponse.chatResponse().getResults() == null) {
sb.append(" No chat response ");
System.out.println("after: " + sb);
return chatClientResponse;
}
for (var generation : chatClientResponse.chatResponse().getResults()) {
var message = generation.getOutput();
if (message.getToolCalls() != null) {
for (var toolCall : message.getToolCalls()) {
sb.append("\n - TOOL-CALL: ").append(toolCall.name()).append(" (").append(toolCall.arguments()).append(")");
}
}
if (message.getText() != null) {
if (StringUtils.hasText(message.getText())) {
sb.append("\n - TEXT: ").append(first(message.getText(), 1200));
}
}
}
System.out.println("after: " + sb);
return chatClientResponse;
}
private String first(String text, int n) {
if (text.length() <= n) {
return text;
}
return text.substring(0, n) + "...";
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private int order = 0;
private boolean showSystemMessage = true;
private boolean showAvailableTools = true;
public Builder order(int order) {
this.order = order;
return this;
}
public Builder showSystemMessage(boolean showSystemMessage) {
this.showSystemMessage = showSystemMessage;
return this;
}
public Builder showAvailableTools(boolean showAvailableTools) {
this.showAvailableTools = showAvailableTools;
return this;
}
public MyLoggingAdvisor build() {
MyLoggingAdvisor advisor = new MyLoggingAdvisor(this.order, this.showSystemMessage, this.showAvailableTools);
return advisor;
}
}
}
2.2 准备用于评审的代码
我们直接使用一段文档分块工具类的代码作为待评审的内容,看下这段简单的文本分块工具会评审出什么内容。
package com.git.hui.springai.app.demo;
import org.springframework.ai.document.Document;
import java.util.ArrayList;
import java.util.List;
public class DocumentChunker {
private final int maxChunkSize;
private final int overlapSize;
public static DocumentChunker DEFAULT_CHUNKER = new DocumentChunker();
public DocumentChunker() {
this(500, 50);
}
public DocumentChunker(int maxChunkSize, int overlapSize) {
this.maxChunkSize = maxChunkSize;
this.overlapSize = overlapSize;
}
public List<Document> chunkDocument(Document document) {
String content = document.getText();
if (content == null || content.trim().isEmpty()) {
return List.of(document);
}
List<String> chunks = splitText(content);
List<Document> chunkedDocuments = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
String chunk = chunks.get(i);
String chunkId = document.getId() + "_chunk_" + i;
Document chunkDoc = new Document(chunkId, chunk, new java.util.HashMap<>(document.getMetadata()));
chunkDoc.getMetadata().put("chunk_index", i);
chunkDoc.getMetadata().put("total_chunks", chunks.size());
chunkDoc.getMetadata().put("original_document_id", document.getId());
chunkedDocuments.add(chunkDoc);
}
return chunkedDocuments;
}
private List<String> splitText(String text) {
List<String> chunks = new ArrayList<>();
String[] sentences = text.split("(?<=。)|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)");
StringBuilder currentChunk = new StringBuilder();
for (String sentence : sentences) {
if (sentence.trim().isEmpty()) {
continue;
}
if (currentChunk.length() + sentence.length() <= maxChunkSize) {
if (currentChunk.length() > 0) {
currentChunk.append(sentence);
} else {
currentChunk.append(sentence);
}
} else {
if (currentChunk.length() == 0) {
List<String> subChunks = forceSplit(sentence, maxChunkSize);
for (int i = 0; i < subChunks.size(); i++) {
String subChunk = subChunks.get(i);
if (i < subChunks.size() - 1) {
chunks.add(subChunk);
} else {
currentChunk.append(subChunk);
}
}
} else {
chunks.add(currentChunk.toString());
currentChunk = new StringBuilder();
if (sentence.length() > overlapSize) {
String overlap = sentence.substring(Math.max(0, sentence.length() - overlapSize));
currentChunk.append(overlap);
currentChunk.append(sentence);
} else {
currentChunk.append(sentence);
}
}
}
}
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString());
}
return chunks;
}
private List<String> forceSplit(String text, int maxSize) {
List<String> chunks = new ArrayList<>();
int start = 0;
while (start < text.length()) {
int end = Math.min(start + maxSize, text.length());
String chunk = text.substring(start, end);
chunks.add(chunk);
start = end;
}
return chunks;
}
public List<Document> chunkDocuments(List<Document> documents) {
List<Document> allChunks = new ArrayList<>();
for (Document document : documents) {
allChunks.addAll(chunkDocument(document));
}
return allChunks;
}
}
2.3 核心实现
- CommandLineRunner: Spring 启动后自动执行的接口
- ChatClient.Builder: 用于构建聊天客户端
@Value("${agent.skills.dirs:Unknown}"): 注入配置属性,获取技能目录资源列表
- 系统提示词配置
- 技能工具配置:
SkillsTool.builder().addSkillsResources(agentSkillsDirs).build(): 动态加载预定义的技能资源
FileSystemTools.builder().build(): 提供文件系统访问能力
ShellTools.builder().build(): 提供命令行执行能力
- Advisor 配置:
ToolCallAdvisor.builder().build(): 处理工具调用逻辑
MyLoggingAdvisor.builder().showAvailableTools(false).showSystemMessage(false).build(): 自定义日志记录,隐藏工具和系统消息详情
请求执行通过 prompt() 构建提示词,.call() 发起 AI 请求,.content() 获取返回结果。
@Bean
CommandLineRunner commandLineRunner(ChatClient.Builder chatClientBuilder,
@Value("${agent.skills.dirs:Unknown}") List<Resource> agentSkillsDirs) throws IOException {
return args -> {
ChatClient chatClient = chatClientBuilder
.defaultSystem("始终运用现有技能协助用户满足其要求.")
.defaultToolCallbacks(SkillsTool.builder().addSkillsResources(agentSkillsDirs).build())
.defaultTools(FileSystemTools.builder().build())
.defaultTools(ShellTools.builder().build())
.defaultAdvisors(
ToolCallAdvisor.builder().build(),
MyLoggingAdvisor.builder().showAvailableTools(false).showSystemMessage(false).build())
.build();
var answer = chatClient
.prompt("""
按照最佳实际的方式,评审下面的代码实现:D:\\Workspace\\hui\\project\\spring-ai-demo\\v2\\T01-agentic-skills-simple-design\\src\\main\\java\\com\\git\\hui\\springai\\app\\demo\\DocumentChunker.java
""")
.call()
.content();
System.out.println("The Answer: " + answer);
};
}
2.4 执行演示
启动项目,验证一下效果如何(在启动命令行参数中,配置上大模型的 api-key,当然也可以直接在 yml 配置文件中进行配置)。
@Slf4j
@SpringBootApplication
public class T01Application {
public static void main(String[] args) {
SpringApplication.run(T01Application.class, args);
}
}
从运行结果可以看出,系统与大模型之间进行了三轮对话:
- 第一轮:用户的原始诉求 -> 大模型
- 大模型识别到需要进行代码评审,给系统响应
{"command":"code-reviewer"}
- 第二轮:系统接收到响应之后,读取 skills 的约束进行响应
- 大模型返回需要读取需要评审的代码内容,对应的响应为
{"filePath":"D:\\Workspace\\hui\\project\\spring-ai-demo\\v2\\T01-agentic-skills-simple-design\\src\\main\\java\\com\\git\\hui\\springai\\app\\demo\\DocumentChunker.java"}
- 第三轮:系统读取代码内容,发送给大模型
基于对代码的详细分析,我为您提供以下代码评审报告:
## 📋 代码评审报告 - DocumentChunker.java
### ✅ **优点**
1. **功能完整性强**:代码实现了完整的文档分块功能...
### ⚠️ **需要改进的问题**
#### 1. **线程安全问题**
public static DocumentChunker DEFAULT_CHUNKER = new DocumentChunker();
**建议**:使用 `volatile` 或 `AtomicReference`,或者移除静态实例让用户自己管理。
#### 2. **参数验证不足**
public DocumentChunker(int maxChunkSize, int overlapSize) { ... }
**建议**:添加参数验证,确保 `maxChunkSize > 0` 且 `overlapSize < maxChunkSize`。
...
### 🎯 **总体评价**
这是一个功能完整、设计合理的文档分块工具类,主要问题集中在线程安全、参数验证和代码可维护性方面。通过上述改进,可以显著提升代码的健壮性和可维护性。
三、小结
SpringAI 的 Agent 开发范式配合 Skills 机制,非常简单就实现了 AI 应用的工程化。整个过程实现下来,门槛还是比较低的。
实现方式虽然简单,但是这个背后的设计哲学、开发思维的转变,还是很值得我们学习参考的——在 AI 时代,如何将我们现有的技能(如可复用、模块化思设计等)转变到大模型应用开发,这可能是我们每一个程序员最大的财富。
Spring AI 采用基于工具的集成方法,通过实现各种工具,使任何 LLM 都能回调执行。Skills 的运行过程,通常是下面三步:
- 发现(启动阶段)
- 通过
SKILL.md 文件中的元数据,快速实现技能的安装注册
- 语义匹配(对话过程中)
- 当用户发出请求时,LLM 会检查工具定义中嵌入的技能描述。如果 LLM 判断用户请求在语义上与某个技能的描述匹配,则会调用该技能工具,并将技能名称作为参数传递给它。
- 执行(技能调用时)
- 当调用技能工具时,SkillsTool 会从磁盘加载完整的 SKILL.md 内容,并将其与技能的基础目录路径一起返回给大型语言模型(LLM)。然后,LLM 会按照技能内容中的指令执行。如果技能引用了其他文件或辅助脚本,LLM 会使用
FileSystemTools 的 Read 函数或 ShellTools 的 Bash 函数来按需访问它们
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online