引言
本文聚焦于在 Spring AI 下如何快速接入 Skills,并探究背后实现的原理。
环境准备
Maven 依赖
根据官方手册,Skill 需要 Spring AI 2.0.0-M2 版本以上。项目依赖配置如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.2</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0-M2</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- 引入社区实现的 skills 工具 -->
<dependency>
<groupId>org.springaicommunity</groupId>
<artifactId>spring-ai-agent-utils</artifactId>
<version>0.4.2</version>
</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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
实测,Spring Boot 3.5.10、JDK 17、Spring AI 1.1.2 也可以跑通 Demo,不过可能存在其他兼容性问题。
配置文件
server:
port: 8080
spring:
application:
name: pocketmind-server
ai:
chat:
client:
observations:
log-prompt: true
log-completion: true
openai:
api-key: xxxx # 替换为你的 API Key
base-url: xxxx # 替换为你的 Base URL,不需要 /v1
chat:
options:
model: deepseek-chat # 替换为你使用的模型名称
示例 Demo 采用 OpenAI 兼容的 API,如需兼容 Anthropic,请根据对应文档进行切换。
示例代码
Skill 文件结构
在根目录下添加对应的 Skill,格式如下:
my-skill/
├── SKILL.md # Required: instructions + metadata
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation
└── assets/ # Optional: templates, resources
在 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
When reviewing code:
1. Check **for** security vulnerabilities (SQL injection, XSS, etc.)
2. Verify Spring Boot best practices (proper use of @Service, @Repository, etc.)
3. Look **for** potential null pointer exceptions
4. Suggest improvements **for** readability and maintainability
5. Provide specific line-by-line feedback with code examples
Controller 实现
import org.springaicommunity.agent.tools.FileSystemTools;
import org.springaicommunity.agent.tools.ShellTools;
import org.springaicommunity.agent.tools.SkillsTool;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/demo")
public class SkillController {
private final ChatClient chatClient;
public SkillController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultToolCallbacks(SkillsTool.builder()
.addSkillsDirectory(".claude/skills")
// 也可以使用下面这个
// .addSkillsResource(resourceLoader.getResource("classpath:.claude/skills"))
.build())
.defaultTools(FileSystemTools.builder().build())
.defaultTools(ShellTools.builder().build())
.defaultToolContext(Map.of("foo", "bar"))
.build();
}
/**
* 测试 Skill 流程
*
* @param message 用户的输入
* @return
*/
@PostMapping("/skill")
public String chat(@RequestBody String message) {
return chatClient.prompt().user(message).call().content();
}
}
此时运行程序,访问对应的端口即可查看返回内容。
代码解释
- 先声明一个
ChatClient,并通过DI进行注入。 - 通过
chatClientBuilder进行Builder策略构建:.defaultToolCallbacks(...):给ChatClient一个'已经组装好'的工具包(包含代码逻辑 + JSON Schema 描述),此处即为注册 Skill 功能。.defaultTools():注册对应的系统工具名称,用于动态发现 Skill 来进行使用。.defaultToolContext(Map.of("foo", "bar")):添加工具上下文,防止报错(为框架缺陷,需添加一个 Map 传入作为 ToolContext,否则无法正常 build)。
- 通过链条进行构建 LLM 的 Request:
.user(message):加载用户提示词。.call():由框架内部发送请求。.content():获取大模型返回的内容。
源码分析
0. 设置目录
public class SkillsTool {
// ...
public static class Builder {
private List<Skill> skills = new ArrayList<>();
private String toolDescriptionTemplate = TOOL_DESCRIPTION_TEMPLATE;
protected Builder() {}
public Builder toolDescriptionTemplate(String template) {
this.toolDescriptionTemplate = template;
return this;
}
public Builder addSkillsResources(List<Resource> skillsRootPaths) {
for (Resource skillsRootPath : skillsRootPaths) {
this.addSkillsResource(skillsRootPath);
}
return this;
}
public Builder addSkillsResource(Resource skillsRootPath) {
try {
String path = skillsRootPath.getFile().toPath().toAbsolutePath().toString();
this.addSkillsDirectory(path);
} catch (IOException ex) {
throw new RuntimeException("Failed to load skills from directory: " + skillsRootPath, ex);
}
return this;
}
public Builder addSkillsDirectory(String skillsRootDirectory) {
this.addSkillsDirectories(List.of(skillsRootDirectory));
return this;
}
public Builder addSkillsDirectories(List<String> skillsRootDirectories) {
for (String skillsRootDirectory : skillsRootDirectories) {
try {
this.skills.addAll(skills(skillsRootDirectory));
} catch (IOException ex) {
throw new RuntimeException("Failed to load skills from directory: " + skillsRootDirectory, ex);
}
}
return this;
}
// ...
}
// ...
}
addSkillsResource、addSkillsDirectory:添加 Skill 的路径,支持多个。toolDescriptionTemplate:添加 Skill 描述说明。
1. 加载 Skill 元数据
这是加载器的入口。它会去你指定的文件夹里找
SKILL.md文件。
/**
* Recursively finds all SKILL.md files in the given root directory and returns their
* parsed contents.
* @param rootDirectory the root directory to search for SKILL.md files
* @return a list of SkillFile objects containing the path, front-matter, and content
* of each SKILL.md file
* @throws IOException if an I/O error occurs while reading the directory or files
*/
private static List<Skill> skills(String rootDirectory) throws IOException {
Path rootPath = Paths.get(rootDirectory);
if (!Files.exists(rootPath)) {
throw new IOException("Root directory does not exist: " + rootDirectory);
}
if (!Files.isDirectory(rootPath)) {
throw new IOException("Path is not a directory: " + rootDirectory);
}
List<Skill> skillFiles = new ArrayList<>();
try (Stream<Path> paths = Files.walk(rootPath)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().equals("SKILL.md"))
.forEach(path -> {
try {
// 解析文件:分为 FrontMatter (元数据) 和 Content (正文)
String markdown = Files.readString(path, StandardCharsets.UTF_8);
MarkdownParser parser = new MarkdownParser(markdown);
skillFiles.add(new Skill(path, parser.getFrontMatter(), parser.getContent()));
} catch (IOException e) {
throw new RuntimeException("Failed to read SKILL.md file: " + path, e);
}
});
}
return skillFiles;
}
- FrontMatter (YAML 头):包含技能的名字(如
name: pdf)和描述。这部分会被提取出来,告诉 AI'我有这个技能'。 - Content (正文):这是具体的 Prompt 指令(比如'处理 PDF 的步骤是:1. 转换文本… 2. 提取摘要…')。
2. 添加 Skill 技能
public ToolCallback build() {
Assert.notEmpty(this.skills, "At least one skill must be configured");
String skillsXml = this.skills.stream().map(s -> s.toXml()).collect(Collectors.joining("\n"));
return FunctionToolCallback.builder("Skill", new SkillsFunction(toSkillsMap(this.skills)))
.description(this.toolDescriptionTemplate.formatted(skillsXml))
.inputType(SkillsInput.class)
.build();
}
- 此步骤会把扫描到的技能列表编织进工具的描述里。
- 当 AI 看到这个工具时,它的 Prompt 里会出现你定义过的 Skill 列表,例如:
<skill><name>pdf</name><description>Extract text from PDF</description></skill><skill><name>git</name><description>Git version control</description></skill>
3. 调用 Skill
当 AI 决定调用
Skill("pdf")时,实际上触发了这段逻辑:
public static class SkillsFunction implements Function<SkillsInput, String> {
private Map<String, Skill> skillsMap;
public SkillsFunction(Map<String, Skill> skillsMap) {
this.skillsMap = skillsMap;
}
@Override
public String apply(SkillsInput input) {
Skill skill = this.skillsMap.get(input.command());
if (skill != null) {
var skillBaseDirectory = skill.path().getParent().toString();
return "Base directory for this skill: %s\n\n%s".formatted(skillBaseDirectory, skill.content());
}
return "Skill not found: " + input.command();
}
}
- 此时返回的是'路径'和'正文内容',于是 AI 读到返回的文字后,会发现这是一份'Code Review 的操作指南'。
至此 Skill 的机制已经完整实现了,AI 只需要根据返回的 Skill.md 就可以调用对应的说明或者 reference/scripts 下面的技能。


