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

Spring AI 框架下接入 agent skill 手把手教程
参考文档: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:8080spring:application:name: pocketmind-server ai:chat:client:observations:log-prompt:truelog-completion:trueopenai: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

importorg.springaicommunity.agent.tools.FileSystemTools;importorg.springaicommunity.agent.tools.ShellTools;importorg.springaicommunity.agent.tools.SkillsTool;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.web.bind.annotation.*;importjava.util.Map;@RestController@RequestMapping("/demo")publicclassSkillController{privatefinalChatClient chatClient;publicSkillController(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")publicStringchat(@RequestBodyString 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. 设置目录:

publicclassSkillsTool{//...publicstaticclassBuilder{privateList<Skill> skills =newArrayList<>();privateString toolDescriptionTemplate = TOOL_DESCRIPTION_TEMPLATE;protectedBuilder(){}publicBuildertoolDescriptionTemplate(String template){this.toolDescriptionTemplate = template;returnthis;}publicBuilderaddSkillsResources(List<Resource> skillsRootPaths){for(Resource skillsRootPath : skillsRootPaths){this.addSkillsResource(skillsRootPath);}returnthis;}publicBuilderaddSkillsResource(Resource skillsRootPath){try{String path = skillsRootPath.getFile().toPath().toAbsolutePath().toString();this.addSkillsDirectory(path);}catch(IOException ex){thrownewRuntimeException("Failed to load skills from directory: "+ skillsRootPath, ex);}returnthis;}publicBuilderaddSkillsDirectory(String skillsRootDirectory){this.addSkillsDirectories(List.of(skillsRootDirectory));returnthis;}publicBuilderaddSkillsDirectories(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;}//...}//...}
  • addSkillsResourceaddSkillsDirectory 添加 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 */privatestaticList<Skill>skills(String rootDirectory)throwsIOException{Path rootPath =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{// 解析文件:分为 FrontMatter (元数据) 和 Content (正文)String markdown =Files.readString(path,StandardCharsets.UTF_8);MarkdownParser parser =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. 提取摘要…”)。
  1. t添加 skill 技能
publicToolCallbackbuild(){Assert.notEmpty(this.skills,"At least one skill must be configured");String skillsXml =this.skills.stream().map(s -> s.toXml()).collect(Collectors.joining("\n"));returnFunctionToolCallback.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>

3. 调用skill

当 AI 决定调用 Skill("pdf") 时,实际上触发了这段逻辑:
publicstaticclassSkillsFunctionimplementsFunction<SkillsInput,String>{privateMap<String,Skill> skillsMap;publicSkillsFunction(Map<String,Skill> skillsMap){this.skillsMap = skillsMap;}@OverridepublicStringapply(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 下面的技能。

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

Read more

我用Openclaw + Claude搭了一套自动写作系统,每天省3小时

我用Openclaw + Claude搭了一套自动写作系统,每天省3小时

这是我目前最重要的一套AI工作流。从信息获取到发布,几乎不用手动完成。 一、为什么我要搭建这套系统? 信息过载的困境 如果你也在持续关注AI,应该会有同样的感受: 信息太多了。 每天打开 X、公众号、GitHub、技术社区,都会冒出大量新内容。 AI模型更新、工具更新、Agent框架、自动化方案…… 想跟上这些信息,本身就已经是一项工作。 手动写作的低效循环 更别说: * 整理信息 * 找选题 * 写文章 * 配图 * 发布到各个平台 如果全部手动完成,写作就会变成一件非常消耗精力的事。 我一度也在这种状态里: 想持续输出,但写作本身占用了太多时间。 一个关键问题 后来我开始思考一个问题: 如果写作这件事可以被"系统化",会发生什么? 于是,我不再把AI当成写作工具。 而是开始搭一套完整的 AI写作工作流。 二、思路转变:从优化写作到优化流程 大多数人的AI写作方式 大多数人使用AI写作,是这样:

为什么“虚拟现实“和“增强现实“不同?——从虚拟到混合的视觉革命

🕶️ 为什么"虚拟现实"和"增强现实"不同?——从虚拟到混合的视觉革命 🌈 大家好,我是无限大,欢迎收看十万个为什么系列文章 希望今天的内容能对大家有所帮助 今天咱们来聊聊VR和AR这个"视觉科技的双生子"!想象一下,你戴着头显在虚拟世界里打游戏,仿佛身临其境;你用手机对着桌子,屏幕上出现一个3D模型,仿佛它真的在桌子上——这些炫酷的体验,都是VR和AR带来的!但你知道它们的区别吗? 🤔 核心问题:VR和AR的区别是什么?它们的技术原理和应用场景有何不同? 很多人觉得VR和AR是"一回事",其实它们差别很大!VR就像"完全进入另一个世界",而AR是"在现实世界里加东西"。今天咱们就来揭开它们的神秘面纱! VR和AR的本质 * 🎮 VR(Virtual Reality):虚拟现实,通过头显完全沉浸在虚拟世界中,

FPGA原理和应用

FPGA原理和应用

大家好,我是良许。 说到 FPGA,可能很多做嵌入式的朋友都听说过,但真正深入了解的可能不多。 作为一名嵌入式程序员,我在工作中虽然主要接触的是单片机和嵌入式 Linux,但在汽车电子领域,FPGA 也是一个非常重要的技术方向。 今天就来和大家聊聊 FPGA 的原理和应用,希望能帮助大家对这个"神秘"的器件有更清晰的认识。 1. FPGA 是什么 1.1 FPGA 的基本概念 FPGA 的全称是 Field Programmable Gate Array,翻译过来就是"现场可编程门阵列"。 这个名字听起来有点拗口,但其实很好理解。 我们可以把 FPGA 想象成一块"电子积木",你可以根据自己的需求,把这些积木搭建成不同的电路结构。 与我们常用的单片机(如 STM32)

基于腾讯云云服务器搭建一个Clawdbot,实现Telegram机器人自动回复

基于腾讯云云服务器搭建一个Clawdbot,实现Telegram机器人自动回复

哈咯大家好,这里依然是码农的搬运工!! 从25年开始,全球都开始走向AI,拥抱AI。 最近博主,也就是我,发现一个国外作者,【Peter Steinberger】在本月推出了一个新的智能体【Clawdbot】,首先我们可以先去官网看一下这个东西是什么:Clawdbot  那么我也是研究了一把,但是这个文档实在是差点把我这个大专生劝退,纯英文,废了九牛二虎之力,我才差不多看懂了。肯定有小伙伴比较好奇,那么文档给你们放出来你们也可以看看:https://docs.molt.bot/start/getting-started OK!话不多说,那我们开始实操一下: 首先呢,看了一下这个文档,安装环境还是不错的,macOS/Linux、Windows【Powershell/CMD】 而且作者还贴心的给了安装命令,这样就省了好大一部分精力。不需要费劲去git拉取代码编译了。【这里需要注意一点,macos系统得14+,作者只有13的系统,所以是没有办法弄mac的】 当然,如果有小伙伴就是头铁,还是想从git上拉代码,那我也给你贴一下这个文档,你来安装: