Spring AI 框架下接入 agent skill 手把手教程

AI2周前发布 beixibaobao
15 0 0

参考文档:Spring AI Agentic Patterns (Part 1): Agent Skills – Modular, Reusable Capabilities

引言

点进来的读者应该都了解了 agent skills 是什么,为什么会出现这种工程手段等等,此处不在多说,本篇博客聚焦于在 Spring-AI 下如何快速接入 Skills,并且探究背后实现的原理。
项目示例代码可以在 https://github.com/MimicHunterZ/PocketMind/tree/master/backend/src/main/java/com/doublez/pocketmindserver/demo 下查看,如果觉得项目不错,欢迎给我star~

环境准备

maven依赖

根据官方手册,skill 需要 Spring-AI 2.0.0-M2 版本以上,所以根据这个配置,项目demo的依赖如下:

<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>  
   <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、jdk17、Spring AI 1.1.2 也可以跑通demo,不过不知道有没有更多的坑

yml配置

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

在根目录下添加对应的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();  
    }  
}

此时运行程序,访问对应的端口即可查看返回内容

代码解释

  1. 先声明一个 ChatClient ,并且通过 DI 进行注入
  2. 通过 chatClientBuilder 进行 builder 策略构建
    • .defaultToolCallbacks(...):给 ChatClient 一个“已经组装好”的工具包(包含代码逻辑 + JSON Schema 描述),此处即为注册 skill 功能
    • .defaultTools(): 注册对应的系统工具名称,用于动态发现skill来进行使用
    • .defaultToolContext(Map.of("foo", "bar")) 添加工具上下文,防止报错
    • .defaultToolContext(Map.of("foo", "bar")) 这个是为了框架报错,需要添加一个map传入作为ToolContext,否则无法正常build,为框架缺陷
  3. 通过链条进行构建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;  
	    }
	    //...
	}
//...
}
  • toolDescriptionTemplate: 添加 skill 描述说明

    在这里插入图片描述

  • addSkillsResourceaddSkillsDirectory 添加 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. 提取摘要…”)。
  1. t添加 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: %snn%s".formatted(skillBaseDirectory, skill.content());  
       }  
       return "Skill not found: " + input.command();  
    }  
}
  • 此时返回的是“路径”和“正文内容”,于是 AI 读到返回的文字后,会发现这是一份“Code Review 的操作指南”。

至此 skill 的机制已经完整实现了,ai 只需要根据返回的 Skill.md 就可以调用对应的说明或者reference/scripts 下面的技能。

如果读者对于spring ai 框架下 ai 怎么进行多次工具调用循环好奇,可以查看Spring ai下的工具调用以及循环调用。

© 版权声明

相关文章