跳到主要内容SpringAI Agent 开发实战:基于 Skills 实现代码评审 | 极客日志JavaAIjava
SpringAI Agent 开发实战:基于 Skills 实现代码评审
综述由AI生成SpringAI Agent 结合 Skills 机制实现代码评审功能。通过配置 SKILL.md 定义技能元数据与指令,利用 ChatClient 加载技能资源并调用大模型。示例展示了从项目创建、依赖引入到核心 Bean 配置的完整流程,验证了 Agent 在发现、语义匹配及执行阶段的交互逻辑,为 Java 开发者提供 AI 工程化落地方案。
imJackJia20 浏览 SpringAI Agent 开发实战:基于 Skills 实现代码评审
一、项目创建
1. 基础环境要求
要体验 SpringAI & Skills,目前需要升级到 SpringAI 2.x 版本,同时 SpringBoot 也可以升级到 4.x。
- SpringAI: 2.0.0-M2
- JDK21
- 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>
<dependencies>
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
<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: SpringAI 进行 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.dirs 设置技能存放路径。将 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 简要说明
Skills 结构通常包含 SKILL.md(元数据与指令)、脚本、模板和参考资料。
my-skill/
├── SKILL.md
├── scripts/
├── references/
└── assets/
二、核心实现
2.1 交互日志打印 MyLoggingAdvisor
为了清晰查看系统与大模型的交互,自定义 Advisor 打印日志。
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 准备用于评审的代码
使用文档分块工具类 DocumentChunker.java 作为待评审内容。
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).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(): 自定义日志记录
@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 执行演示
启动项目,验证效果。系统与大模型之间进行了三轮对话:
- 第一轮:用户原始诉求 -> 大模型识别到需要进行代码评审,响应
{"command":"code-reviewer"}。
- 第二轮:系统接收响应,读取 skills 约束,大模型返回需要读取代码路径
{"filePath":"..."}。
- 第三轮:系统读取代码内容发送给大模型,返回评审结果。
基于对代码的详细分析,我为您提供以下代码评审报告:
## 📋 代码评审报告 - DocumentChunker.java
### ✅ **优点**
1. **功能完整性强**:代码实现了完整的文档分块功能...
### ⚠️ **需要改进的问题**
#### 1. **线程安全问题**
// 问题:静态实例不是线程安全的
public static DocumentChunker DEFAULT_CHUNKER = new DocumentChunker();
**建议**:使用 `volatile` 或 `AtomicReference`,或者移除静态实例让用户自己管理。
...
三、小结
SpringAI 的 Agent 开发范式配合 Skills 机制,简单实现了 AI 应用的工程化。整个过程门槛较低。
背后的设计哲学值得参考——在 AI 时代,如何将现有的技能(如可复用、模块化设计等)转变到大模型应用开发。
Spring AI 采用基于工具的集成方法,通过实现各种工具,使任何 LLM 都能回调执行。Skills 的运行过程通常是下面三步:
- 发现(启动阶段):通过
SKILL.md 文件中的元数据,快速实现技能的安装注册。
- 语义匹配(对话过程中):当用户发出请求时,LLM 会检查工具定义中嵌入的技能描述。如果 LLM 判断用户请求在语义上与某个技能的描述匹配,则会调用该技能工具。
- 执行(技能调用时):当调用技能工具时,SkillsTool 会从磁盘加载完整的 SKILL.md 内容,并将其与技能的基础目录路径一起返回给大型语言模型(LLM)。然后,LLM 会按照技能内容中的指令执行。
相关免费在线工具
- 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