跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaAIjava

Spring AI 接入 Agent Skill 实战指南

引言 聚焦于在 Spring AI 下如何快速接入 Skills,并探究背后实现的原理。 环境准备 Maven 依赖 根据官方手册,Skill 需要 Spring AI 2.0.0-M2 版本以上。项目依赖配置如下: > 实测,Spring Boot 3.5.10、JDK 17、Spring AI 1.1.2 也可以跑通 Demo,不过可能存在其他兼容性问题。 配置文件 > 示例 Demo 采…

星星泡饭发布于 2026/4/6更新于 2026/10/2447K 浏览
Spring AI 接入 Agent Skill 实战指南

引言

本文聚焦于在 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();
    }
}

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

代码解释

  1. 先声明一个 ChatClient,并通过 DI 进行注入。
  2. 通过 chatClientBuilder 进行 Builder 策略构建:
    • .defaultToolCallbacks(...):给 ChatClient 一个'已经组装好'的工具包(包含代码逻辑 + JSON Schema 描述),此处即为注册 Skill 功能。
    • .defaultTools():注册对应的系统工具名称,用于动态发现 Skill 来进行使用。
    • .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;
        }
        // ...
    }
    // ...
}
  • 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 下面的技能。

目录

  1. 引言
  2. 环境准备
  3. Maven 依赖
  4. 配置文件
  5. 示例代码
  6. Skill 文件结构
  7. Code Reviewer
  8. Instructions
  9. Controller 实现
  10. 代码解释
  11. 源码分析
  12. 0. 设置目录
  13. 1. 加载 Skill 元数据
  14. 2. 添加 Skill 技能
  15. 3. 调用 Skill

更多推荐文章

查看全部
  • Agent Skills 完全教程:AI 智能体技能开发指南
  • 贪心算法实战:摆动序列与最长递增子序列
  • llama-cpp-python 完整安装与配置指南
  • 前端缓存策略详解:从 localStorage 到 Service Worker
  • MySQL 表约束详解:非空、主键与外键的作用
  • LangChain 中基于图论实现多 Agent 的利器:LangGraph
  • 前端营销实战:业务理解与技术架构解析
  • Python 打造 AI 三剑客:文档总结、代码生成与智能检索
  • Quartus 18.0 安装及 ModelSim 环境配置指南
  • MCP 插件配置实战:browser-tools-mcp 集成指南
  • 前端微前端架构:避免应用变成巨石单体
  • 普通人利用 AI 赚钱:从入门到变现全攻略
  • Stable Diffusion 底模 VAE 推荐与配置指南
  • ControlNet-v1-1_fp16_safetensors 使用指南:AI 绘画控制
  • Visual Studio 2022 无法使用 GitHub Copilot 的排查与解决
  • SkyWalking 多语言探针现状:.NET / C++ / Lua 与社区支持
  • Stable Diffusion 3.5 FP8 镜像在 Mac M 系列芯片上的 Rosetta 转译实测
  • 基于 Java 与 Leaflet 构建湖南省道路长度 WebGIS 系统
  • 低代码 AI 架构助力灵活智能架构落地
  • Electron 快速上手教程:前端桌面应用开发入门指南

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online