
前置文章JAVA25Spring Boot 4.1.0Spring AI 2.0.1 调用 DeepSeek 模型JAVA25Spring Boot 4.1.0Spring AI 2.0.1 调用模型之工具调用前言Skill 调用其实也是工具调用的一种只是需要额外配置 Skill 文件。从功能上看它似乎与 Spring AI 2.0.1 的工具调用有些重复但 Skill 在 Spring AI 2.0.1 中可以被当作一份复杂流程的说明书来使用用于约束模型的行为边界减少模型调用工具时的不确定性。举个简单的例子假设项目中已经写好两个工具一个用于获取当前服务器时间另一个用于获取天气。在不加入 Skill 的情况下当你发送「获取今天天气」时模型通常会先调用获取当前服务器时间的工具再调用获取天气的工具但众所周知模型时不时会产生幻觉这就增加了工具调用的不确定性。如果添加一个「今天天气」的 Skill并在其中写明先调用获取当前服务器时间的工具、再调用获取天气的工具就能大幅减少模型幻觉提升程序的稳定性。这只是一个简单的例子如果实际业务中一次操作需要多个工具协同完成添加 Skill 是一个很好的选择。本文在前置文章的基础上进一步扩展讲解如何在项目中让模型使用 Skill。一、Maven 引入properties!-- spring-ai-agent-utils 版本 --spring-ai-agent-utils.version0.10.0/spring-ai-agent-utils.version/properties!-- Agent Skills 核心依赖 --dependencygroupIdorg.springaicommunity/groupIdartifactIdspring-ai-agent-utils/artifactIdversion${spring-ai-agent-utils.version}/version/dependency二、修改前置文章中访问模型请求参数对象 AiChatRequestpackagecom.lh.dto;importio.swagger.v3.oas.annotations.media.Schema;importjakarta.validation.constraints.NotBlank;importlombok.Builder;importjava.io.Serial;importjava.io.Serializable;/** * description: 业务接口请求参数对象 * author: LIHAO * createDate: 2026-10-27 14:48:25 */BuilderSchema(title业务接口请求参数对象)publicrecordAiChatRequest(NotBlank(message用户消息不能为空)Schema(description用户消息,requiredModeSchema.RequiredMode.REQUIRED)Stringmessage,Schema(description对话标识 可为空由业务传递保存记忆和保存链路日志时使用)StringconversationId,Schema(description是否启用jdbc长记忆保存启用后会保存问题到jdbc数据库并时带上上下文访问模型)BooleanisAddJDBCMemoryAdvisor,Schema(description是否启用向量长记忆保存启用后会保存问题到向量数据库并带上问题相关的上下文访问模型)BooleanisAddVectorMemoryAdvisor,Schema(description是否启用关联业务工具启用后会调用关联问题的工具)BooleanisAddTool,Schema(description是否启用关联业务技能启用后会调用关联问题的技能如果技能涉及到工具调用必须开启isAddTool)BooleanisAddSkills)implementsSerializable{SerialprivatestaticfinallongserialVersionUID1L;}三、修改前置文章中 Service添加 Skill 工具注册方法java/**description: 添加技能author: LIHAOcreateDate: 2026-10-30 10:44:31*/private void addSkills(AiChatRequest request,ChatClient.ChatClientRequestSpec chatClientRequestSpec) {if(ObjectUtils.isNotEmpty(request.isAddSkills()) request.isAddSkills ()) {// 使用相对路径开发时指向项目根目录/skills生产时指向jar同目录/skills// ★ 使用相对路径开发时指向项目根目录/skills生产时指向jar同目录/skillsString skillsDir “E:\skill”;// 确保目录存在如果不存在则创建避免加载失败 var path Paths.get(skillsDir); if (!Files.exists(path)) { try { Files.createDirectories(path); } catch (IOException e) { throw new RuntimeException(e); } System.out.println(✅ 已创建技能目录: path.toAbsolutePath()); } // 1. 构建 SkillsTool指定技能文件所在的根目录 ToolCallback skillsTool SkillsTool.builder() .addSkillsDirectory(skillsDir) .build(); chatClientRequestSpec .tools(skillsTool);}}- 在所有调用模型的方法中添加 Skill 注册方法 addSkills 的调用java /** * description: 单次模型访问一次性返回文本结果 * author: LIHAO * createDate: 2026-10-27 15:05:11 * param: [request] * return: java.lang.String */ public String generateText(AiChatRequest request) { Assert.notNull(request, SysAiChatService#generateText参数request不能为空); //添加文件信息并返回用户消息 String message request.message(); //创建 配置单次请求的参数对象 ChatClient.ChatClientRequestSpec chatClientRequestSpec chatClient.prompt().user(message); //添加记忆顾问 addMemoryAdvisor( request, chatClientRequestSpec); //添加工具 addTool( request, chatClientRequestSpec); //添加技能 addSkills( request, chatClientRequestSpec); //访问模型并返回 return chatClientRequestSpec.call().content(); } /** * description: 单次模型访问一次性返回实体对象结果 * author: LIHAO * createDate: 2026-10-27 16:20:00 */ public ActorsFilms generateEntity(AiChatRequest request) { Assert.notNull(request, SysAiChatService#generateEntity参数request不能为空); //添加文件信息并返回用户消息 String message request.message(); //创建 配置单次请求的参数对象 ChatClient.ChatClientRequestSpec chatClientRequestSpec chatClient.prompt().user(message); //添加记忆顾问 addMemoryAdvisor( request, chatClientRequestSpec); //添加工具 addTool( request, chatClientRequestSpec); //添加技能 addSkills( request, chatClientRequestSpec); //访问模型并返回 return chatClientRequestSpec.call() // 调用大模型 // 指定目标类型并开启自修正 .entity(ActorsFilms.class, spec - spec.validateSchema()//验证模型返回数据格式是否符合实体对象类型 ); } /** * description: 单次模型访问流式返回文本结果 * author: LIHAO * createDate: 2026-10-27 16:25:11 * param: [request] * return: reactor.core.publisher.Fluxjava.lang.String */ public FluxString streamText(AiChatRequest request) { Assert.notNull(request, SysAiChatService#streamText参数request不能为空); //添加文件信息并返回用户消息 String message request.message(); //创建 配置单次请求的参数对象 ChatClient.ChatClientRequestSpec chatClientRequestSpec chatClient.prompt().user(message); //添加记忆顾问 addMemoryAdvisor( request, chatClientRequestSpec); //添加工具 addTool( request, chatClientRequestSpec); //添加技能 addSkills( request, chatClientRequestSpec); //访问 return chatClientRequestSpec .stream() .content(); } /** * description: 单次模型访问流式返回结构化数据对象结果 * author: LIHAO * createDate: 2026-10-27 16:58:14 * param: [request] * return: reactor.core.publisher.Fluxcom.lh.dto.StreamEvent */ public FluxStreamEvent streamEvents(AiChatRequest request) { Assert.notNull(request, SysAiChatService#streamText参数request不能为空); //添加文件信息并返回用户消息 String message request.message(); //创建 配置单次请求的参数对象 ChatClient.ChatClientRequestSpec chatClientRequestSpec chatClient.prompt().user(message); //添加记忆顾问 addMemoryAdvisor( request, chatClientRequestSpec); //添加工具 addTool( request, chatClientRequestSpec); //添加技能 addSkills( request, chatClientRequestSpec); //AtomicReference 多线程 原子性toEvents 跑在 concatMap 的 mapper 里和 supplier 很可能不在同一个线程 AtomicReferenceString finishReason new AtomicReference(); AtomicReferenceMapString, Object usage new AtomicReference(); return chatClientRequestSpec.stream() // .chatResponse() 拿到完整 ChatResponse可取 finishReason / usage / metadata .chatResponse() //流每次返回数据触发 .concatMap(resp - toEvents(resp, finishReason, usage)) // concatWith 拦截的是主流的 onComplete事件不让它提前传给下游 // 待上游完成后再订阅第二个流把 finish 帧追加到尾部 // 等第二个流也完成后才把 onComplete 传给下游。 .concatWith( // Mono.fromSupplier惰性求值 —— concatWith 等上游 complete 之后才订阅第二个源 Mono.fromSupplier( () - StreamEvent.finish( finishReason.get() null ? stop : finishReason.get(), // 拿不到就兜底 stop别让前端猜 usage.get() ) ) ); }注意 addMemoryAdvisor( request, chatClientRequestSpec); 在JAVA25Spring Boot 4.1.0 Spring AI 2.0.1 模型调用保存长记忆中添加的四、skill文件E:\skill└── test-skill -- 子目录└── SKILL.md -- 必须存在SKILL.md 内容--- name: test-skill description: 查询昆明今天天气需要先获取当前时间以及获取当前系统时间 --- # 测试技能 ## 能力 - 查询昆明天气返回当前日期时间 固定的昆明 14~24°C 天气。 - 获取当前时间调用 getCurrentDateTime 工具获取真实的系统时间。 ## 使用规则 1. **当用户询问“今天天气”、“昆明天气”或类似意图时** - **第一步**你【必须】先调用 getCurrentDateTime 工具获取当前的系统日期时间。 - **第二步**基于获取到的时间结合固定的天气数据返回给用户。 - **回答示例**“今天是 2026 年 10 月 30 日昆明今天天气多云气温 14~24°C。” 2. **当用户只询问“现在几点”或“今天几号”时** - 直接调用 getCurrentDateTime 工具并返回结果即可。 3. **绝对禁止** - 禁止在用户问天气时不调用 getCurrentDateTime 就直接编造时间。 - 禁止修改昆明的天气数据固定为 14~24°C。注意编辑 SKILL.md 文件时最好使用 VS Code 打开若使用 Windows 文本编辑器编辑格式容易出问题可能导致程序无法加载文件。五、接口测试启动项目访问 http://localhost:8001/doc.html。注意如果测试 skill 的时候有需要调用程序里面的工具时不要忘了加载工具否则会调用失败。六、总结本文在前置文章的基础上完整演示了如何在 Spring AI 2.0.1 项目中接入 Agent Skills核心步骤可以归纳为四点引入依赖添加spring-ai-agent-utils本文使用 0.10.0它是 Agent Skills 的核心支撑。扩展请求参数在AiChatRequest中新增isAddSkills开关让技能是否启用由业务侧按需控制。注册技能在 Service 中新增addSkills方法通过SkillsTool.builder().addSkillsDirectory(...)指定技能根目录并在所有调用模型的方法中统一调用。编写技能文件在技能根目录下按「子目录 SKILL.md」的约定组织文件用name、description描述技能用「能力」「使用规则」约束模型的行为边界。从实践角度看Skill 的价值不在于替代工具调用而在于把多工具协同的流程固化成一份说明书模型不再需要自行猜测调用顺序而是按SKILL.md中写明的步骤执行从而显著降低幻觉带来的不确定性。当一次业务操作需要串联多个工具时优先考虑用 Skill 编排会比在提示词里反复强调更稳定、也更易维护。最后提醒几个容易踩坑的地方技能目录路径要写对且程序对目录有读写权限否则会加载失败SKILL.md建议用 VS Code 等编辑器编写避免 Windows 文本编辑器引入格式问题技能中若涉及工具调用务必同时开启isAddTool否则工具不会被加载调用必然失败。至此从模型调用、工具调用到 Skill 编排的完整链路已经打通读者可以基于本文的示例把test-skill替换成自己业务中的真实技能逐步沉淀出一套可复用的技能库。