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 等实战内容。

相关推荐
小Ti客栈4 小时前
Spring Boot 集成 Springdoc-OpenAPI 与 Knife4j实现接口文档与可视化调试
java·spring boot·后端
Ai拆代码的曹操5 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
IT_陈寒7 小时前
SpringBoot这个分页坑,我踩了三天才爬出来
前端·人工智能·后端
颜酱7 小时前
05 | 召回前置准备:根据业务数据库生成各数据库(读取配置阶段)
前端·人工智能·后端
Wang's Blog7 小时前
Go-Zero项目开发24: 基于Bitmap实现群聊消息已读未读
开发语言·后端·golang
Conan在掘金8 小时前
鸿蒙报错速查:struct 里嵌 namespace 声明就炸,根因 + 真解法
后端
东方小月8 小时前
从零开发一个 Coding Agent(三):EventStream 事件流通道设计与实现
前端·人工智能·后端
卫子miao8 小时前
如何评价当前大语言模型的记忆机制?
后端·架构
神奇小汤圆8 小时前
为什么 AQS 成为 Java 并发的基石?
后端
星栈9 小时前
MCP 从 stdio 迁到 SSE,踩了 5 个传输层坑
人工智能·后端·架构