Spring AI 2.0.0 接 Skill 最小 Demo:SkillsTool 加载 SKILL.md 一次跑通

Spring AI 2.0.0 接 Skill 最小 Demo:SkillsTool 加载 SKILL.md 一次跑通

把 Skill 接进 Java 项目,最容易写偏成一件事:

在 system prompt 里多塞几条规则。

比如代码审查。

你可以直接写:

text 复制代码
你是一个 Java 代码审查专家,请帮我看看下面这段代码。

模型通常也能回答。

但这种写法有一个问题:它更像临场发挥。

这一次它可能先看空指针,下一次可能先讲代码风格。再换一个人写 Prompt,又可能把安全、异常、测试全漏掉。

真正适合沉淀成 Skill 的,不是一句"你很专业",而是一套稳定流程:

text 复制代码
先判断代码场景
再按风险优先级检查
发现问题后给出原因和修改建议
最后补充测试建议

这篇用 Spring AI 2.0.0 跑一个最小 Demo:

text 复制代码
本地 SKILL.md
→ SkillsTool 加载技能说明
→ ChatClient.tools(...)
→ 模型根据说明调用 Java Tool
→ 返回代码审查报告

先说清楚边界。

这里不是接入某个厂商的原生 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 复制代码
<properties>
    <java.version>17</java.version>
    <spring-ai.version>2.0.0</spring-ai.version>
</properties>

<dependencies>
    <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>

<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>

配置文件分两块。

一块是模型配置。

一块是代码审查 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 按固定顺序检查:

1. 明显 bug
2. 空值、空集合、非法输入等边界条件
3. 安全风险,例如硬编码密钥、敏感信息泄露、权限绕过
4. 异常处理和日志
5. 可维护性问题
6. 缺失的测试场景

## 工作流程

### Step 1:先判断代码场景

先识别代码属于哪一类:

- Controller / API 入参处理
- Service 业务逻辑
- Repository / 数据访问
- 工具类
- 配置类
- 测试代码

不同类型代码的审查重点不同。

### Step 2:按风险优先级审查

优先输出会导致线上问题的内容。

输出顺序:

1. 高风险问题
2. 中风险问题
3. 低风险建议
4. 建议补充的测试

不要把格式问题放在 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。

这里可以先写一个朴素但确定的 CodeReviewTools:检查空指针、吞异常、System.out.println、疑似硬编码密钥、接口入参缺少校验。

它不是为了替代完整静态扫描,而是为了让 Demo 有一个确定结果:读者不需要猜模型有没有真的发现问题。

六、用 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 复制代码
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 codeReviewTools.reviewJavaCode(source);

这里加兜底,不是为了掩盖模型调用失败。

真实项目里反而应该这么做。

模型是否触发工具,受模型能力、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 个需要关注的问题,建议先处理高风险项。

## 主要问题

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 复制代码
限制目录
限制命令
高风险操作二次确认
记录工具调用日志
必要时放进容器执行

Skill 的价值不是"让 Agent 什么都能自动做"。

更实际的价值是:

把一套可复用的工作方法,变成 Agent 能按需加载的流程说明。

写在最后

Spring AI 2.0.0 接 Skill,不是为了追一个新名词。

更关键的是把三件事拆开:

text 复制代码
SKILL.md 定流程
SkillsTool 加载流程
Java Tool 执行动作

当这三件事分清楚以后,Skill 就不只是概念里的"技能书"。

它会变成项目里可以版本管理、可以复用、可以逐步增强的工程能力。

后续会继续更新 Spring AI、RAG、Memory、Tool Calling、MCP 等实战内容。

相关推荐
zoulee241 小时前
doris-python:让 SQLAlchemy 玩转 Apache Doris 多驱动生态
后端
RainCity1 小时前
Java Swing 自定义组件库分享(十二)
java·笔记·后端
Csvn2 小时前
Linux 系统性能监控与瓶颈排查
后端
铁皮饭盒2 小时前
Rust版Bun1.4之前, 盘点Bun1.3新特性
前端·javascript·后端
kfaino9 小时前
码农的AI翻身(五)你好,我叫 Transformer
后端·aigc
Oneslide15 小时前
机械革命 单系统纯净重装Ubuntu(全盘覆盖,清空原有Windows)
后端
GetcharZp15 小时前
告别OOM!用Go+libvips实现30000×50000超大图片的流式瓦片服务
后端·go