SpringAI Agent 开发:使用 Agent Skills 进行代码评审
介绍如何使用 SpringAI Agent 结合 Skills 机制实现代码评审功能。通过配置 SpringAI 2.x 版本及智谱大模型,定义 SKILL.md 技能文件,利用 ChatClient 加载技能工具并执行代码分析。示例展示了从环境搭建、依赖配置到核心 Bean 实现的完整流程,验证了 Agent 在工程化应用中的低门槛与高效性,为 Java 开发者提供了 AI 应用开发的实践参考。

介绍如何使用 SpringAI Agent 结合 Skills 机制实现代码评审功能。通过配置 SpringAI 2.x 版本及智谱大模型,定义 SKILL.md 技能文件,利用 ChatClient 加载技能工具并执行代码分析。示例展示了从环境搭建、依赖配置到核心 Bean 实现的完整流程,验证了 Agent 在工程化应用中的低门槛与高效性,为 Java 开发者提供了 AI 应用开发的实践参考。

最近 AI 相关话题中,Claude Skills 非常火爆。SpringAI 迅速支持了 Skills,效率很高。本文将通过构建一个 Code Reviewer,体验如何将 SpringAI 和 Skills 结合起来使用。
要体验 SpringAI & Skills,需要升级到 SpringAI 2.x 版本,SpringBoot 也可以升级到 4.x。
除了基本依赖之外,选择一个支持 Function Tool 的大模型作为大脑中枢。这里选择智谱的大模型 GLM-4.5-Flash。
创建一个 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>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases><enabled>false</enabled></releases>
</repository>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</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: SpringAI 进行 agent 开发的关键依赖包。spring-ai-starter-model-zhipuai: 智谱大模型进行交互的依赖包。在配置文件 resources/application.yml 中配置 LLM 访问信息及 agent 相关参数。
spring:
ai:
zhipuai:
# api-key 使用你自己申请的进行替换;如果为了安全考虑,可以通过启动参数进行设置
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. 以中文的方式返回代码评审结果
Skill 是一个 markdown 文档,SpringAI 支持的 Skills 中,除了包含基本的 SKILL.md 文件(包含元数据以及指导代理如何执行特定任务的说明)之外,还可以有相关的脚本、模板和参考资料。
一个常见的 skills 结构如下:
my-skill/
├── SKILL.md # Required: instructions + metadata
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation
└── assets/ # Optional: templates, resources
为了让系统与大模型之间的交互更清晰,我们将双方交互的日志进行更友好的打印。
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: ");
(.showSystemMessage && chatClientRequest.prompt().getSystemMessage() != ) {
sb.append().append(first(chatClientRequest.prompt().getSystemMessage().getText(), ));
}
(.showAvailableTools) {
;
(chatClientRequest.prompt().getOptions() ToolCallingChatOptions toolOptions) {
tools = toolOptions.getToolCallbacks().stream()
.map(tc -> tc.getToolDefinition().name())
.toList();
}
sb.append().append(ModelOptionsUtils.toJsonString(tools));
}
chatClientRequest.prompt().getLastUserOrToolResponseMessage();
(lastMessage.getMessageType() == MessageType.TOOL) {
(ToolResponseMessage) lastMessage;
( toolResponse : toolResponseMessage.getResponses()) {
toolResponse.name() + + first(toolResponse.responseData(), );
sb.append().append(tr);
}
} (lastMessage.getMessageType() == MessageType.USER) {
(StringUtils.hasText(lastMessage.getText())) {
sb.append().append(first(lastMessage.getText(), ));
}
}
System.out.println( + sb);
chatClientRequest;
}
ChatClientResponse {
();
(chatClientResponse.chatResponse() == || chatClientResponse.chatResponse().getResults() == ) {
sb.append();
System.out.println( + sb);
chatClientResponse;
}
( generation : chatClientResponse.chatResponse().getResults()) {
generation.getOutput();
(message.getToolCalls() != ) {
( toolCall : message.getToolCalls()) {
sb.append().append(toolCall.name()).append().append(toolCall.arguments()).append();
}
}
(message.getText() != ) {
(StringUtils.hasText(message.getText())) {
sb.append().append(first(message.getText(), ));
}
}
}
System.out.println( + sb);
chatClientResponse;
}
String {
(text.length() <= n) {
text;
}
text.substring(, n) + ;
}
Builder {
();
}
{
;
;
;
Builder {
.order = order;
;
}
Builder {
.showSystemMessage = showSystemMessage;
;
}
Builder {
.showAvailableTools = showAvailableTools;
;
}
MyLoggingAdvisor {
(.order, .showSystemMessage, .showAvailableTools);
advisor;
}
}
}
直接使用文档分块工具的内容作为待评审的内容。
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); // 默认值:最大块大小 500 个字符,重叠 50 个字符
}
public DocumentChunker(int maxChunkSize, int overlapSize) {
this.maxChunkSize = maxChunkSize;
this.overlapSize = overlapSize;
}
/**
* 将文档分割成块
*
* @param document 输入文档
* @return 分割后的文档块列表
*/
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 = <>();
( ; i < chunks.size(); i++) {
chunks.get(i);
document.getId() + + i;
(chunkId, chunk, .util.HashMap<>(document.getMetadata()));
chunkDoc.getMetadata().put(, i);
chunkDoc.getMetadata().put(, chunks.size());
chunkDoc.getMetadata().put(, document.getId());
chunkedDocuments.add(chunkDoc);
}
chunkedDocuments;
}
List<String> {
List<String> chunks = <>();
String[] sentences = text.split();
();
(String sentence : sentences) {
(sentence.trim().isEmpty()) {
;
}
(currentChunk.length() + sentence.length() <= maxChunkSize) {
(currentChunk.length() > ) {
currentChunk.append(sentence);
} {
currentChunk.append(sentence);
}
} {
(currentChunk.length() == ) {
List<String> subChunks = forceSplit(sentence, maxChunkSize);
( ; i < subChunks.size(); i++) {
subChunks.get(i);
(i < subChunks.size() - ) {
chunks.add(subChunk);
} {
currentChunk.append(subChunk);
}
}
} {
chunks.add(currentChunk.toString());
currentChunk = ();
(sentence.length() > overlapSize) {
sentence.substring(Math.max(, sentence.length() - overlapSize));
currentChunk.append(overlap);
currentChunk.append(sentence);
} {
currentChunk.append(sentence);
}
}
}
}
(currentChunk.length() > ) {
chunks.add(currentChunk.toString());
}
chunks;
}
List<String> {
List<String> chunks = <>();
;
(start < text.length()) {
Math.min(start + maxSize, text.length());
text.substring(start, end);
chunks.add(chunk);
start = end;
}
chunks;
}
List<Document> {
List<Document> allChunks = <>();
(Document document : documents) {
allChunks.addAll(chunkDocument(document));
}
allChunks;
}
}
配置 Agent 实现代码评审。
Bean 定义与依赖注入
@Value("${agent.skills.dirs:Unknown}"): 注入配置属性,获取技能目录资源列表ChatClient 配置链
SkillsTool.builder().addSkillsResources(agentSkillsDirs).build(): 动态加载预定义的技能资源FileSystemTools.builder().build(): 提供文件系统访问能力ShellTools.builder().build(): 提供命令行执行能力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("始终运用现有技能协助用户满足其要求.")
// Skills tool
.defaultToolCallbacks(SkillsTool.builder().addSkillsResources(agentSkillsDirs).build())
// 支持读取系统文件内容,用于读取我们需要评审的代码
.defaultTools(FileSystemTools.builder().build())
// 支持执行脚本,如果 skills 中存在 script,那么这些脚本的执行,靠的就是它
.defaultTools(ShellTools.builder().build())
.defaultAdvisors(
// Tool Calling advisor
ToolCallAdvisor.builder().build(),
// Custom logging advisor
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);
};
}
启动项目,验证效果(在启动命令行参数中,配置上大模型的 api-key,当然也可以直接在 yml 配置文件中进行配置)。
从上面的运行结果可以看出,系统与大模型之间进行了三轮对话:
{"command":"code-reviewer"}{"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. **功能完整性强**:代码实现了完整的文档分块功能,包括:
- 单文档分块
- 批量文档分块
- 智能语义边界分割
- 重叠机制
- 长文本强制分割
2. **良好的文档注释**:类和方法都有清晰的中文注释,说明了功能用途
3. **合理的默认值**:提供了合理的默认参数(maxChunkSize=500, overlapSize=50)
4. **元数据保留**:正确保留了原始文档的元数据,并添加了块相关的元数据
### ⚠️ **需要改进的问题**
#### 1. **线程安全问题**
// 问题:静态实例不是线程安全的
public static DocumentChunker DEFAULT_CHUNKER = new DocumentChunker();
**建议**:使用 `volatile` 或 `AtomicReference`,或者移除静态实例让用户自己管理。
#### 2. **参数验证不足**
// 问题:构造函数没有参数验证
public DocumentChunker(int maxChunkSize, int overlapSize) {
this.maxChunkSize = maxChunkSize;
this.overlapSize = overlapSize;
}
**建议**:添加参数验证,确保 `maxChunkSize > 0` 且 `overlapSize < maxChunkSize`。
#### 3. **正则表达式复杂度**
// 问题:正则表达式过于复杂,难以维护
String[] sentences = text.split("(?<=。)|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)");
**建议**:提取为常量或使用更清晰的方式处理多语言分隔符。
#### 4. **性能优化空间**
// 问题:StringBuilder 操作可以优化
if (currentChunk.length() > 0) {
currentChunk.append(sentence);
} else {
currentChunk.append(sentence);
}
**建议**:简化为 `currentChunk.append(sentence);`
#### 5. **异常处理缺失**
代码中没有处理可能的异常情况,如:
- 内存不足
- 文档过大导致性能问题
### 🔧 **具体改进建议**
#### 1. **添加参数验证**
public DocumentChunker(int maxChunkSize, int overlapSize) {
if (maxChunkSize <= 0) {
throw new IllegalArgumentException("maxChunkSize must be positive");
}
if (overlapSize >= maxChunkSize) {
throw new IllegalArgumentException("overlapSize must be less than maxChunkSize");
}
this.maxChunkSize = maxChunkSize;
this.overlapSize = overlapSize;
}
#### 2. **线程安全改进**
public static final DocumentChunker = new DocumentChunker()
private static final String =
考虑使用 Builder 模式来更灵活地配置参数。
// 优化 StringBuilder 操作
private List<String> splitText(String text) {
List<String> = new ArrayList<>()
String = text.split(SENTENCE_SEPARATOR_PATTERN)
StringBuilder = new StringBuilder()
for (String sentence : sentences) {
if (sentence.trim().isEmpty()) continue
if (currentChunk.length() + sentence.length() <= maxChunkSize) {
currentChunk.append(sentence)
} else {
// 处理块分割逻辑
}
}
// ...
}
这是一个功能完整、设计合理的文档分块工具类,主要问题集中在线程安全、参数验证和代码可维护性方面。通过上述改进,可以显著提升代码的健壮性和可维护性。
**建议优先级**:
1. 高优先级:参数验证、线程安全
2. 中优先级:性能优化、代码简化
3. 低优先级:Builder 模式、异常处理
SpringAI 的 Agent 开发范式配合 Skills 机制,非常简单就实现了 AI 应用的工程化。整个过程实现下来,门槛还是比较低的。
实现方式虽然简单,但是这个背后的设计哲学、开发思维的转变,还是很值得我们学习参考——在 AI 时代,如何将我们现有的技能(如可复用、模块化思设计等)转变到大模型应用开发,这可能是我们每一个程序员最大的财富。
Spring AI 采用基于工具的集成方法,通过实现各种工具,使任何 LLM 都能回调执行,Skills 的运行过程,通常是下面三步:
SKILL.md 文件中的元数据,快速实现技能的安装注册FileSystemTools 的 Read 函数或 ShellTools 的 Bash 函数来按需访问它们项目源码:
参考:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online