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

前端文件上传方案:别再只用input type=file了

前端文件上传方案:别再只用input type=file了

前端文件上传方案:别再只用input type=file了 毒舌时刻 这代码写得跟网红滤镜似的——仅供参考。 各位前端同行,咱们今天聊聊前端文件上传。别告诉我你还在用原生的input上传大文件,那感觉就像在用小水管灌满游泳池——慢得让人绝望。 为什么你需要文件上传方案 最近看到一个项目,上传100MB的文件直接卡死浏览器,没有任何进度提示,我差点当场去世。我就想问:你是在做上传还是在做浏览器杀手? 反面教材 <!-- 反面教材:原生文件上传 --> <input type="file" onchange="uploadFile(this.files[0])" /> <script> function uploadFile(file) { const formData = new FormData(

noteDigger:终极前端扒谱工具,让音乐制作变得简单快速

noteDigger:终极前端扒谱工具,让音乐制作变得简单快速 【免费下载链接】noteDigger在线前端频率分析扒谱 front-end music transcription 项目地址: https://gitcode.com/gh_mirrors/no/noteDigger noteDigger是一款创新的前端扒谱工具,专为音乐创作者和制作人设计。这款免费工具采用纯前端技术,无需安装任何软件或依赖库,双击即可使用,让音乐扒谱变得前所未有的简单!🎵 为什么选择noteDigger进行音乐扒谱? 在数字音乐时代,扒谱工具是每位音乐制作人的必备利器。noteDigger以其独特的优势脱颖而出: * 零配置使用:直接打开HTML文件即可开始工作 * 现代UI设计:直观的界面让新手也能快速上手 * 自主技术栈:完全自主研发,不依赖任何框架,项目体积小巧 * 跨平台兼容:支持所有现代浏览器,包括Chrome、Firefox等 快速上手:三步完成音乐扒谱 第一步:导入音频文件 noteDigger支持多种音频格式,包括常见的MP3、WAV文件,甚至视频格式如MP

前端实战:基于Vue3与免费满血版DeepSeek实现无限滚动+懒加载+瀑布流模块及优化策略

前端实战:基于Vue3与免费满血版DeepSeek实现无限滚动+懒加载+瀑布流模块及优化策略

目录 前端实战:基于Vue3与免费满血版DeepSeek实现无限滚动+懒加载+瀑布流模块及优化策略 一、前言 二、如何使用腾讯云免费满血版deepseek 1、腾讯云大模型知识引擎体验中心 2、体验deepseek联网助手 3、人机交互获取AI支持 三、基于DeepSeek实现无限滚动+懒加载+瀑布流模块 1、无限滚动+懒加载+瀑布流模块的底层逻辑 2、人机交互策略与Deepseek的实现过程 ①虚拟列表管理 ②布局容器初始化 ③动态渲染与销毁机制 ④无线滚动实现 ⑤内存优化策略 四、最终代码呈现 1、组件代码 2、组件用法 五、结语         作者:watermelo37         ZEEKLOG万粉博主、华为云云享专家、阿里云专家博主、腾讯云、支付宝合作作者,全平台博客昵称watermelo37。         一个假装是giser的coder,做不只专注于业务逻辑的前端工程师,Java、Docker、

从 “一脸懵” 到 Web 题 “稳拿分”:CTF Web 方向入门到进阶全攻略

从 “一脸懵” 到 Web 题 “稳拿分”:CTF Web 方向入门到进阶全攻略

从 “一脸懵” 到 Web 题 “稳拿分”:CTF Web 方向入门到进阶全攻略 刚接触 CTF 时,很多人都会被 Web 方向 “劝退”—— 打开题目看到一堆代码、抓包改参数、蹦出 “SQL 注入”“XSS” 等黑话,瞬间大脑空白:“这到底在考啥?” 但其实 CTF Web 是入门门槛最低、得分最稳的方向之一 —— 只要掌握核心漏洞原理,配合工具和思路,从 “一题不会” 到 “中等题稳拿”,3 个月就能实现突破。这篇文章从新手视角出发,带你拆解 Web 题的套路,梳理学习路径,帮你快速入门 CTF Web! 一、先搞懂:CTF Web