注:实测 Spring Boot 3.5.10、JDK 17、Spring AI 1.1.2 亦可运行,但建议以官方推荐版本为准。
配置文件
server:port:8080spring:application:name:pocketmind-serverai:chat:client:observations:log-prompt:truelog-completion:trueopenai:api-key:xxxx# 替换为你的 API Keybase-url:xxxx# 替换为你的 Base URL,不需要 /v1options:model:deepseek-chat# 替换为你使用的模型名称
---
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
publicstaticclassBuilder {
private List<Skill> skills = newArrayList<>();
privateStringtoolDescriptionTemplate= TOOL_DESCRIPTION_TEMPLATE;
protectedBuilder() {}
public Builder toolDescriptionTemplate(String template) {
this.toolDescriptionTemplate = template;
returnthis;
}
public Builder addSkillsResources(List<Resource> skillsRootPaths) {
for (Resource skillsRootPath : skillsRootPaths) {
this.addSkillsResource(skillsRootPath);
}
returnthis;
}
public Builder addSkillsResource(Resource skillsRootPath) {
try {
Stringpath= skillsRootPath.getFile().toPath().toAbsolutePath().toString();
this.addSkillsDirectory(path);
} catch (IOException ex) {
thrownewRuntimeException("Failed to load skills from directory: " + skillsRootPath, ex);
}
returnthis;
}
public Builder addSkillsDirectory(String skillsRootDirectory) {
this.addSkillsDirectories(List.of(skillsRootDirectory));
returnthis;
}
public Builder addSkillsDirectories(List<String> skillsRootDirectories) {
for (String skillsRootDirectory : skillsRootDirectories) {
try {
this.skills.addAll(skills(skillsRootDirectory));
} catch (IOException ex) {
thrownewRuntimeException("Failed to load skills from directory: " + skillsRootDirectory, ex);
}
}
returnthis;
}
}
toolDescriptionTemplate 用于添加 skill 的描述说明。
2. 加载 skill 元数据
这是加载器的入口。它会去你指定的文件夹里找 SKILL.md 文件。
privatestatic List<Skill> skills(String rootDirectory)throws IOException {
PathrootPath= Paths.get(rootDirectory);
if (!Files.exists(rootPath)) {
thrownewIOException("Root directory does not exist: " + rootDirectory);
}
if (!Files.isDirectory(rootPath)) {
thrownewIOException("Path is not a directory: " + rootDirectory);
}
List<Skill> skillFiles = newArrayList<>();
try (Stream<Path> paths = Files.walk(rootPath)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().equals("SKILL.md"))
.forEach(path -> {
try {
Stringmarkdown= Files.readString(path, StandardCharsets.UTF_8);
MarkdownParserparser=newMarkdownParser(markdown);
skillFiles.add(newSkill(path, parser.getFrontMatter(), parser.getContent()));
} catch (IOException e) {
thrownewRuntimeException("Failed to read SKILL.md file: " + path, e);
}
});
}
return skillFiles;
}
FrontMatter (YAML 头):包含技能的名字(如 name: pdf)和描述。这部分会被提取出来,告诉 AI '我有这个技能'。
Content (正文):这是具体的 Prompt 指令(比如'处理 PDF 的步骤是:1. 转换文本… 2. 提取摘要…')。
3. 添加 skill 技能
public ToolCallback build() {
Assert.notEmpty(this.skills, "At least one skill must be configured");
StringskillsXml=this.skills.stream()
.map(s -> s.toXml())
.collect(Collectors.joining("\n"));
return FunctionToolCallback.builder("Skill", newSkillsFunction(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>