Spring AI 2.0.0 接 Skill:不是多写一段 Prompt,而是让 Agent 按流程干活

把 Skill 接进 Java 项目,最容易写成:
在 system prompt 里多塞几条规则。
比如代码审查。
你可以直接写:
text
你是一个 Java 代码审查专家,请帮我看看下面这段代码。有没有空指针,异常等
模型通常也能回答。
但这种写法有一个问题:它更像临场发挥。
这一次它可能先看空指针,下一次可能先讲代码风格。再换一个人写 Prompt,又可能把安全、异常、测试全漏掉。
真正适合沉淀成 Skill 的,不是一句“你很专业”,而是一套稳定流程:
text
先判断代码场景
再按风险优先级检查
发现问题后给出原因和修改建议
最后补充测试建议
下面用 Spring AI 2.0.0 跑一个最小 Demo:
text
本地 SKILL.md
→ SkillsTool 加载技能说明
→ ChatClient.tools(…)
→ 模型根据说明调用 Java Tool
→ 返回代码审查报告
先说清楚边界。
这里不是接入某个c厂商的原生 Skills Runtime,也不是让 Spring AI 自动执行完整 Skill 包。
这里用的是 spring-ai-agent-utils 里的 SkillsTool,把本地 SKILL.md 接进 Spring AI 的 Tool Calling 链路。
对应的官方参考是 Spring 博客《Spring AI Agentic Patterns: Agent Skills - Modular, Reusable Capabilities》:
text
https://spring.io/blog/2026/01/13/spring-ai-generic-agent-skills
一、这套方案到底接了什么
Spring 官方文章里,Agent Skills 的核心不是“更长的 Prompt”。
它更像一个目录:
text
my-skill/
├── SKILL.md
├── scripts/
├── references/
└── assets/
其中 SKILL.md 至少包含技能名称、描述和执行说明,也可以再配脚本、参考资料、模板和资源文件。
官方文章还强调了 progressive disclosure,也就是渐进式披露:
text
Discovery:启动时只暴露 Skill 的 name 和 description
Activation:任务匹配时,再加载完整 SKILL.md
Execution:执行时,再按需要读取参考文件或执行脚本
这样做的好处是:
不需要一上来把所有 Skill 的完整内容都塞进上下文。
模型先知道“有哪些 Skill”,等任务真的匹配,再加载对应说明。
放到 Spring AI 里,这套方式是 tool-based integration。
官方文章提到的核心工具包括:
text
SkillsTool:发现和加载 Skill
FileSystemTools:读取参考文件
ShellTools:执行辅助脚本
这个最小版本先只接两个东西:
text
SkillsTool:加载本地 SKILL.md
CodeReviewTools:执行基础代码检查
所以这条链路的分工是:
text
SKILL.md:定义代码审查流程
SkillsTool:让模型按需加载这本技能书
CodeReviewTools:真正执行基础检查
ChatClient:把模型、Skill、Tool 串起来
这也是本文最重要的边界:
Skill 负责给流程,Tool 负责做动作,模型负责把它们串起来。
二、准备依赖和配置
示例环境:
text
Java 17
Spring Boot 4.1.0
Spring AI 2.0.0
deepseek-v4-flash
spring-ai-agent-utils 0.10.0
pom.xml 保留三个核心依赖:
xml
<java.version>17</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agent-utils</artifactId>
<version>0.10.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
配置文件分两块。
一块是模型配置。
一块是代码审查 Prompt。
Prompt 不建议硬编码在 Controller 里,放到配置文件更容易改。
application.yaml:
yaml
spring:
application:
name: springai-deepseek-demo
ai:
deepseek:
api-key: ${DEEPSEEK\_API\_KEY}
chat:
model: deepseek-v4-flash
temperature: 0.3
app:
code-review:
prompt:
system: |
你是一个 Java 代码审查助手。
长期规则:
- 如果用户提交 Java 代码并要求审查,先调用 Skill 工具加载 code-review-skill。
- 加载技能书后,再按照技能书里的审查顺序调用 reviewJavaCode 工具。
- 优先指出 bug、安全风险、边界条件、异常处理和缺失测试。
- 如果信息不足,要说明缺少哪些上下文,不要编造项目背景。
- 不要输出与代码审查无关的泛泛建议。
输出要求:
- 用中文回答。
- 使用 Markdown。
- 先给总体结论,再列主要问题,最后给测试建议和下一步。
user-template: |
请审查下面这段 Java 代码。
代码内容:
{code}
如果用 IDEA 启动,在这里配置 Key:
text
Run/Debug Configurations
→ SpringaiDeepseekDemoApplication
→ Environment variables
→ DEEPSEEK_API_KEY=你的 DeepSeek API Key
命令行启动:
bash
export DEEPSEEK_API_KEY=你的 DeepSeek API Key
./mvnw spring-boot:run
三、写一本代码审查 Skill
在项目里新建:
text
src/main/resources/skills/code-review-skill/SKILL.md
内容如下:
markdown
name: code-review-skill
description: 按固定流程审查 Java 代码,优先发现 bug、安全风险、边界条件、可维护性问题和缺失测试。当用户要求 review、审查、检查 Java 代码或判断代码有没有风险时使用。
Java Code Review Skill
这个 Skill 用来审查 Java 代码。
它不是简单评价“代码好不好”,而是要求 Agent 按固定顺序检查:
-
明显 bug
-
空值、空集合、非法输入等边界条件
-
安全风险,例如硬编码密钥、敏感信息泄露、权限绕过
-
异常处理和日志
-
可维护性问题
-
缺失的测试场景
工作流程
Step 1:先判断代码场景
先识别代码属于哪一类:
-
Controller / API 入参处理
-
Service 业务逻辑
-
Repository / 数据访问
-
工具类
-
配置类
-
测试代码
不同类型代码的审查重点不同。
Step 2:按风险优先级审查
优先输出会导致线上问题的内容。
输出顺序:
-
高风险问题
-
中风险问题
-
低风险建议
-
建议补充的测试
不要把格式问题放在 bug 前面。
Step 3:调用代码审查工具
当用户提供 Java 代码时,调用 reviewJavaCode 工具。
工具返回的是基础审查报告。你可以在报告基础上补充解释,但不要改写成空泛建议,也不要删除审查路径。
Step 4:输出格式
输出结构:
-
代码审查报告
-
审查路径
-
总体结论
-
主要问题
-
建议补充的测试
-
下一步
边界
-
不确定的问题要说明“不确定”,不要编造上下文。
-
没有看到完整工程时,不要断言一定会出问题。
-
涉及安全、权限、数据删除、支付、订单状态流转时,要提高风险级别。
-
如果代码片段太短,要指出还需要哪些上下文。
参考资料
如果需要更细的检查项,读取 references/checklist.md。
再放一个参考清单:
text
src/main/resources/skills/code-review-skill/references/checklist.md
markdown
Java 代码审查检查清单
Bug 和边界条件
-
参数是否可能为 null
-
集合是否可能为空
-
字符串是否可能为空白
-
下标、分页、金额、数量是否有边界
安全风险
-
是否硬编码密码、Token、API Key
-
是否缺少权限校验
-
是否暴露敏感字段
异常和日志
-
是否吞掉异常
-
日志是否包含必要上下文
-
日志是否泄露敏感信息
这一步才是 Skill 的核心。
它不是让模型“更努力一点”,而是把团队希望长期复用的审查顺序写下来。
四、用 SkillsTool 加载 Skill
新建配置类:
java
package com.example.springaideepseekdemo.config;
import org.springaicommunity.agent.tools.SkillsTool;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
@Configuration
public class SkillToolConfig {
@Bean
public ToolCallback skillTool() {
return SkillsTool.builder()
.addSkillsResource(new ClassPathResource("skills"))
.build();
}
}
这段代码会扫描:
text
src/main/resources/skills/
然后把里面的 Skill 注册成一个 ToolCallback。
模型需要时,可以通过这个工具加载 code-review-skill 的完整 SKILL.md。
注意,它加载的是 Skill 说明,不是直接执行代码审查。
五、准备真正做检查的 Tool
SkillsTool 负责加载说明。
真正检查代码的,是应用侧 Tool。
java
package com.example.springaideepseekdemo.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class CodeReviewTools {
@Tool(description = "按照 code-review-skill 的审查流程审查 Java 代码。参数 code 是待审查代码,返回 Markdown 格式审查报告。")
public String reviewJavaCode(String code) {
String source = code == null ? "" : code.strip();
if (source.isBlank()) {
return "请提供需要审查的 Java 代码。";
}
List<Finding> findings = new ArrayList<>();
checkNullPointerRisk(source, findings);
checkSwallowedException(source, findings);
checkSystemOut(source, findings);
checkHardcodedSecret(source, findings);
checkMissingValidation(source, findings);
StringBuilder report = new StringBuilder();
report.append("""
# 代码审查报告
审查依据:code-review-skill / references/checklist.md
## 审查路径
- Step 1:识别代码场景
- Step 2:按风险优先级检查
- Step 3:调用 reviewJavaCode 工具执行基础检查
- Step 4:按固定结构输出报告
## 总体结论
""");
if (findings.isEmpty()) {
report.append("这段代码没有命中当前内置的高风险规则。仍建议补充单元测试,并结合业务上下文继续人工确认。\n\n");
} else {
report.append("这段代码发现 ").append(findings.size()).append(" 个需要关注的问题,建议先处理高风险项。\n\n");
}
report.append("## 主要问题\n\n");
if (findings.isEmpty()) {
report.append("- 暂无明确问题。\n\n");
} else {
for (int i = 0; i < findings.size(); i++) {
Finding finding = findings.get(i);
report.append(i + 1).append(". \*\*").append(finding.title()).append("\*\*\n\n")
.append(" 风险级别:").append(finding.level()).append("\n\n")
.append(" 问题说明:").append(finding.detail()).append("\n\n")
.append(" 修改建议:").append(finding.suggestion()).append("\n\n");
}
}
report.append("""
## 建议补充的测试
- 正常输入场景
- 空值或非法输入场景
- 异常分支场景
- 关键业务规则的回归测试
## 下一步
先修复高风险问题,再补测试,最后重新发起一次审查。
""");
return report.toString();
}
private void checkNullPointerRisk(String source, List<Finding> findings) {
if (source.contains(".getName()") && !source.contains("null")) {
findings.add(new Finding(
"存在空指针风险",
"高",
"代码直接调用对象方法,但没有看到空值保护。参数为 null 时可能触发 NullPointerException。",
"在进入业务逻辑前做参数校验,或者使用明确的异常提示,让调用方知道问题出在哪里。"
));
}
}
private void checkSwallowedException(String source, List<Finding> findings) {
if (source.contains("catch") && (source.contains("catch (Exception") || source.contains("catch(Exception"))
&& !source.contains("throw") && !source.contains("log.")) {
findings.add(new Finding(
"异常被吞掉",
"高",
"代码捕获了通用异常,但没有重新抛出,也没有记录日志。线上排查时会丢失关键上下文。",
"不要静默吞异常。至少记录错误上下文,必要时转换成业务异常继续抛出。"
));
}
}
private void checkSystemOut(String source, List<Finding> findings) {
if (source.contains("System.out.println")) {
findings.add(new Finding(
"使用 System.out.println 输出日志",
"中",
"业务代码里直接使用标准输出,不利于日志级别控制、链路追踪和线上检索。",
"改用项目统一日志框架,并补充必要的业务字段。"
));
}
}
private void checkHardcodedSecret(String source, List<Finding> findings) {
String lower = source.toLowerCase();
if (lower.contains("password") || lower.contains("secret") || lower.contains("apikey") || lower.contains("api\_key")) {
findings.add(new Finding(
"疑似硬编码敏感信息",
"高",
"代码中出现 password、secret 或 api key 相关字段,可能存在敏感信息硬编码风险。",
"敏感信息应放到环境变量、配置中心或密钥管理系统,不要写死在代码里。"
));
}
}
private void checkMissingValidation(String source, List<Finding> findings) {
if ((source.contains("@RequestBody") || source.contains("@RequestParam")) && !source.contains("@Valid") && !source.contains("Assert.")) {
findings.add(new Finding(
"缺少入参校验",
"中",
"接口层接收外部输入,但没有看到 Bean Validation 或显式参数校验。",
"为请求对象补充校验注解,或者在方法入口明确校验必填字段和取值范围。"
));
}
}
private record Finding(String title, String level, String detail, String suggestion) {
}
}
这里的 CodeReviewTools 不是完整的静态代码扫描器,只做几个简单规则检查。这样做是为了让 Demo 每次都能稳定返回同一种结果,方便我们验证 Skill、Tool 和模型调用链路有没有串起来。
六、用 Controller 串起来
Controller 提供三个接口:
text
POST /skills/code-review/run
POST /skills/code-review/run/stream
POST /skills/code-review/review
其中 /run 是主链路:
text
模型看到用户请求
→ 调用 SkillsTool 加载 code-review-skill
→ 根据 SKILL.md 的流程调用 reviewJavaCode
→ 返回报告
/run/stream 是同一条链路的流式版本。
/review 不经过模型,只直接调用 Java Tool。
保留它是为了做对比:如果 /review 能返回报告,说明本地 Tool 没问题;如果 /run 返回了带审查路径的报告,才说明模型链路也串起来了。
先写配置绑定类:
java
package com.example.springaideepseekdemo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = “app.code-review.prompt”)
public class CodeReviewPromptProperties {
private String system = "";
private String userTemplate = "";
public String system() {
return system;
}
public void setSystem(String system) {
this.system = system;
}
public String userTemplate() {
return userTemplate;
}
public void setUserTemplate(String userTemplate) {
this.userTemplate = userTemplate;
}
}
再写 Controller:
java
package com.example.springaideepseekdemo.controller;
import com.example.springaideepseekdemo.config.CodeReviewPromptProperties;
import com.example.springaideepseekdemo.tool.CodeReviewTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping(“/skills/code-review”)
public class CodeReviewSkillController {
private static final Logger log = LoggerFactory.getLogger(CodeReviewSkillController.class);
private final ChatClient chatClient;
private final CodeReviewPromptProperties promptProperties;
private final ToolCallback skillTool;
private final CodeReviewTools codeReviewTools;
public CodeReviewSkillController(ChatClient.Builder builder,
CodeReviewPromptProperties promptProperties,
ToolCallback skillTool,
CodeReviewTools codeReviewTools) {
this.chatClient = builder
.defaultSystem(promptProperties.system())
.build();
this.promptProperties = promptProperties;
this.skillTool = skillTool;
this.codeReviewTools = codeReviewTools;
}
@PostMapping("/run")
public String run(@RequestBody String code) {
String source = normalizeCode(code);
try {
String content = chatClient.prompt()
.user(user -> user.text(promptProperties.userTemplate())
.param("code", source))
.tools(skillTool, codeReviewTools)
.call()
.content();
if (content != null && content.contains("代码审查报告")) {
return content;
}
} catch (Exception ex) {
log.warn("Code review model call failed, fallback to local tool. reason={}", ex.getMessage());
}
return """
模型没有在本轮稳定触发工具调用,已走应用侧兜底审查。
%s
""".formatted(codeReviewTools.reviewJavaCode(source));
}
@PostMapping(value = "/run/stream", produces = MediaType.TEXT\_EVENT\_STREAM\_VALUE)
public Flux<String> runStream(@RequestBody String code) {
String source = normalizeCode(code);
return chatClient.prompt()
.user(user -> user.text(promptProperties.userTemplate())
.param("code", source))
.tools(skillTool, codeReviewTools)
.stream()
.content()
.onErrorResume(ex -> {
log.warn("Code review stream call failed, fallback to local tool. reason={}", ex.getMessage());
return Flux.just("""
模型没有在本轮稳定触发流式工具调用,已走应用侧兜底审查。
%s
""".formatted(codeReviewTools.reviewJavaCode(source)));
});
}
@PostMapping("/review")
public String review(@RequestBody String code) {
return codeReviewTools.reviewJavaCode(normalizeCode(code));
}
private String normalizeCode(String code) {
return code == null ? "" : code.strip();
}
}
这里加兜底,不是为了掩盖模型调用失败。
真实项目里反而应该这么做。
模型是否触发工具,受模型能力、Prompt、工具描述和上下文影响。应用侧要保证:即使模型这轮没有稳定触发工具,接口也能给出一个可解释的结果。
七、运行测试
先编译:
bash
./mvnw -DskipTests compile
启动项目:
bash
export DEEPSEEK_API_KEY=你的 DeepSeek API Key
./mvnw spring-boot:run
测试主链路:
bash
curl -X POST “http://localhost:8080/skills/code-review/run” \
-H “Content-Type: text/plain” \
–data-binary ‘public String getName(User user) { return user.getName(); }’
如果模型稳定触发工具,会返回代码审查报告,可以在工具方法加个日志观察下。
如果模型没有触发工具,接口会走应用侧兜底,也会返回类似结果:
markdown
代码审查报告
审查依据:code-review-skill / references/checklist.md
审查路径
-
Step 1:识别代码场景
-
Step 2:按风险优先级检查
-
Step 3:调用 reviewJavaCode 工具执行基础检查
-
Step 4:按固定结构输出报告
总体结论
这段代码发现 1 个需要关注的问题,建议先处理高风险项。
主要问题
-
**存在空指针风险**
风险级别:高
问题说明:代码直接调用对象方法,但没有看到空值保护。参数为 null 时可能触发 NullPointerException。
修改建议:在进入业务逻辑前做参数校验,或者使用明确的异常提示,让调用方知道问题出在哪里。
再测流式接口:
bash
curl -N -X POST “http://localhost:8080/skills/code-review/run/stream” \
-H “Content-Type: text/plain” \
–data-binary ‘public String getName(User user) { return user.getName(); }’
这里的 -N 很关键。
它会关闭 curl 的输出缓冲,更容易看到流式返回。
最后测本地 Tool:
bash
curl -X POST “http://localhost:8080/skills/code-review/review” \
-H “Content-Type: text/plain” \
–data-binary ‘public String getName(User user) { return user.getName(); }’
这条接口不经过模型,只用来验证本地检查逻辑。
对比这三条接口,重点不是多写一个 endpoint,而是看清楚分工:
text
/review:验证本地 Tool 能不能产出确定报告
/run:验证模型能不能按 Skill 说明调用 Tool
/run/stream:验证同一条链路能不能流式返回
八、后面可以怎么扩展
这个 Demo 只做了代码片段审查。
真实项目里,可以继续往研发流程里扩展:
text
读取 Git Diff
加载团队代码规范
扫描 Pull Request
生成 Review Comment
补充测试建议
也可以把 FileSystemTools 接进来,让模型按Skill 里的说明读取 references/checklist.md。
再往后,还可以接 ShellTools,执行一些确定性的脚本,比如跑单元测试、生成依赖树、检查格式。
但只要涉及读文件、执行命令、访问外部系统,就要补安全边界:
text
限制目录
限制命令
高风险操作二次确认
记录工具调用日志
必要时放进容器执行
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

更多推荐

所有评论(0)