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

Java:关于哈希表

Java:关于哈希表

目录 哈希表 概念 冲突 负载因子调节 冲突的解决 哈希桶的实现 完整代码 哈希表 概念 哈希表是一种理想的从顺序表以及平衡树中查找元素的方式,它可以不经过任何比较,一次直接从表中得到想要搜索的元素。如果构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。 * 插入元素:根据待插入元素的关键码,根据函数计算出存储位置并进行存放 * 搜索元素:对元素的关键码进行计算,把求得的数据当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功 该种方法即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表 例如:数据集合{1,7,6,4,5,9}; 哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。 上面这种存放元素的方式,不用多次进行关键码的比较,搜索速度比较快,但是上面所取的集合只是一个普通情况,

By Ne0inhk
Java计算机毕设之基于Springboot的就业管理系统的设计与实现基于springboot的龙岗区在线就业推荐平台的设计与实现(完整前后端代码+说明文档+LW,调试定制等)

Java计算机毕设之基于Springboot的就业管理系统的设计与实现基于springboot的龙岗区在线就业推荐平台的设计与实现(完整前后端代码+说明文档+LW,调试定制等)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围::小程序、SpringBoot、SSM、JSP、Vue、PHP、Java、python、爬虫、数据可视化、大数据、物联网、机器学习等设计与开发。 主要内容:免费开题报告、任务书、全bao定制+中期检查PPT、代码编写、🚢文编写和辅导、🚢文降重、长期答辩答疑辅导、一对一专业代码讲解辅导答辩、模拟答辩演练、和理解代码逻辑思路。 特色服务内容:答辩必过班 (全程一对一技术交流,帮助大家顺利完成答辩,小白必选) 全网粉丝50W+,累计帮助2000+完成优秀毕设 🍅文末获取源码🍅 感兴趣的可以先收藏起来,还有大家在毕设选题,

By Ne0inhk
飞算JavaAI赋能企业级电商管理系统开发实践——一位资深开发者的技术选型与落地总结

飞算JavaAI赋能企业级电商管理系统开发实践——一位资深开发者的技术选型与落地总结

目录 * 一、背景与选型考量 * 二、开发环境与工具适配 * 1. 基础环境搭建 * 2. 飞算JavaAI插件配置 * 3. 版本控制与协作配置 * 三、核心模块设计与实现 * 1. 需求分析与模块拆分 * 2. 核心代码实现与技术亮点 * (1)实体类设计(带审计字段与枚举约束) * (2)服务层实现(带事务控制与业务校验) * (3)控制器实现(带权限控制与参数校验) * (4)网页端 * 四、系统架构与扩展性设计 * 1. 分层架构设计 * 2. 接口设计规范 * 3. 扩展性保障 * 五、资深开发者视角的工具评价 * 1. 代码规范性与可维护性 * 2. 对企业级业务的理解深度 * 3. 与资深开发者工作流的适配性 * 六、项目成果与经验总结 一、背景与选型考量 作为一名从业20余年的开发者,我亲历了从JSP+

By Ne0inhk