跳到主要内容
Spring AI Agent 开发:用 Skills 构建代码评审助手 | 极客日志
Java AI java
Spring AI Agent 开发:用 Skills 构建代码评审助手 围绕 Spring AI 2.x 与 Claude Skills/Agent 能力,文章用一个代码评审助手演示了如何在 Java 项目中加载 Skills、配置模型与工具、通过 Tool Calling 触发技能并读取目标文件完成评审。内容还展示了日志 Advisor、Skills 目录结构和 `SKILL.md` 的编写方式,最终说明了 Spring AI 如何把模型推理、工具执行和可复用技能组合成工程化的 Agent 流程。
2267279241 发布于 2026/3/27 0 浏览Spring AI Agent 开发:用 Skills 构建代码评审助手
最近 AI 圈里讨论得最热的一个话题,少不了 Claude Skills。更让人惊喜的是,Spring AI 已经很快跟进了这套能力。对 Java 开发者来说,这意味着我们不必重头切换技术栈,也能比较顺手地把 Agent 和 Skills 这套机制用起来。
这篇文章就拿一个代码评审助手 来做演示,看看 Spring AI 和 Skills 结合之后,实际开发会是什么样子。
一、项目准备
1. 基础环境
要体验 Spring AI 的 Agent 与 Skills,目前建议使用较新的版本组合:
Spring AI:2.0.0-M2
JDK:21
Spring Boot:4.0.1
模型方面,我们选择智谱的 GLM-4.5-Flash,主要原因很简单:免费,效果也足够应付这次演示,试起来没有额外成本。
2. 依赖配置
标准的 Spring AI 项目,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 >
org.springframework.ai
spring-ai-bom
${spring-ai.version}
pom
import
<dependencyManagement >
<dependencies >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
<version >
</version >
<type >
</type >
<scope >
</scope >
</dependency >
</dependencies >
</dependencyManagement >
<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,核心就是把模型访问参数和 Skills 目录配置好:
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 技能,目录结构可以这样组织:
.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 的基本形态 Spring AI 支持的 Skills 不只是一个 SKILL.md。除了这个必需文件,还可以附带脚本、参考资料和模板,方便把一个技能组织得更完整一些。
my-skill/
├── SKILL.md # 必需:包含元数据和执行指令
├── scripts/ # 可选:可执行脚本
├── references/ # 可选:说明文档
└── assets/ # 可选:模板或资源
二、核心实现 前面的准备完成后,就可以进入真正的 Agent 逻辑了。这个示例里,我们先把交互过程打印清楚,再把待评审代码喂给模型,看看 Skills 能把什么样的反馈带回来。
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 () {
return new MyLoggingAdvisor (this .order, this .showSystemMessage, this .showAvailableTools);
}
}
}
这段代码的作用不复杂,但非常实用:每一轮请求、模型调用的工具、工具返回值都会被打印出来。实际调 Agent 时,有了这层日志,很多'模型为什么没按预期工作'的问题都能更快定位。
2. 准备用于评审的代码 这里直接拿前面一篇知识库问答实战里的 DocumentChunker 作为评审对象。它是一个文本分块工具,逻辑不算长,但足够用来观察 Skills 的评审效果。
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) {
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;
}
}
3. 核心实现:把 Agent 跑起来 前面的思路已经清楚了:启动后构建一个 ChatClient,挂上 Skills、文件系统工具、Shell 工具,再加上 Tool Calling 的 Advisor 和日志 Advisor。真正发起请求时,只需要给出评审目标,剩下的交给模型和工具链去完成。
@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);
};
}
这段代码里最值得注意的,是 SkillsTool 并不是单纯把一段说明塞给模型,而是会结合技能元数据、技能目录和后续工具调用,把整个技能流程串起来。也就是说,模型先判断要不要调用技能,再按技能里的说明决定下一步读取什么文件、执行什么脚本,最后才形成完整回答。
4. 启动验证 启动类本身很简单,只要让 Spring Boot 跑起来即可:
@Slf4j
@SpringBootApplication
public class T01Application {
public static void main (String[] args) {
SpringApplication.run(T01Application.class, args);
}
}
启动时记得把模型的 api-key 配好。实际运行后,日志里可以看到系统和模型之间经历了几轮来回:
用户提出'评审代码'的请求,模型先识别出需要调用 code-reviewer 技能。
技能被触发后,模型要求系统读取指定的代码文件。
系统把代码内容返回给模型,模型再基于技能约束生成评审结果。
这个流程和传统'把一大段 Prompt 塞进去'的做法很不一样。这里更像是在构建一个可以自我组织能力的 Agent:模型负责判断,工具负责执行,Skills 负责给出任务边界。
下面是模型返回的评审结果,整体上已经能比较完整地指出问题和改进方向:
基于对代码的详细分析,我为您提供以下代码评审报告:
代码评审报告 - DocumentChunker.java
优点
功能完整,覆盖了单文档分块、批量分块、语义边界切分、重叠机制和长文本强制分割。
注释清楚,代码意图容易理解。
默认参数比较合理。
元数据保留得比较完整。
需要改进的问题
静态实例存在线程安全和可维护性上的隐患。
构造函数没有做参数校验。
正则表达式可读性一般,建议抽成常量。
StringBuilder 的部分逻辑可以进一步简化。
缺少异常处理和边界情况兜底。
总体评价
这是一个功能完整、设计合理的文档分块工具类,主要可以从参数校验、线程安全和可维护性三方面继续打磨。
三、小结 Spring AI 的 Agent 开发方式,配合 Skills 机制,确实把'把 AI 能力工程化'这件事往前推了一步。它并不复杂,但思路很值得借鉴:把可复用的能力拆成技能,把执行交给工具,把推理交给模型,三者分工明确,组合起来就能形成一个比较完整的智能应用。
对 Java 开发者来说,这种方式很友好。我们不需要改变熟悉的开发习惯,只要把现有工程能力重新组织一遍,就能进入 Agent 开发的语境。
Spring AI 的这套设计,本质上是在告诉我们一件事:到了 AI 时代,真正有价值的,不只是'会不会调模型',而是你能不能把原有的软件工程方法,稳稳地迁移到大模型应用里去。
运行机制的理解 Spring AI 采用的是基于工具的集成方式。Skills 的执行大致会经历三步:
发现 :启动时通过 SKILL.md 的元数据完成技能注册。
语义匹配 :用户提问后,模型根据技能描述判断是否需要调用对应技能。
执行 :当技能被调用后,SkillsTool 会加载完整的 SKILL.md,并结合技能目录让模型继续按说明执行;如果技能里还引用了其他文件或脚本,则再通过 FileSystemTools 或 ShellTools 按需访问。
https://github.com/liuyueyi/spring-ai-demo/tree/master/v2/T01-agentic-skills-simple-design
参考资料 相关免费在线工具 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