把 Skill 接进 Java 项目最容易写成在 system prompt 里多塞几条规则。比如代码审查。你可以直接写text你是一个 Java 代码审查专家请帮我看看下面这段代码。有没有空指针异常等模型通常也能回答。但这种写法有一个问题它更像临场发挥。这一次它可能先看空指针下一次可能先讲代码风格。再换一个人写 Prompt又可能把安全、异常、测试全漏掉。真正适合沉淀成 Skill 的不是一句“你很专业”而是一套稳定流程text先判断代码场景再按风险优先级检查发现问题后给出原因和修改建议最后补充测试建议下面用 Spring AI 2.0.0 跑一个最小 Demotext本地 SKILL.md→ SkillsTool 加载技能说明→ ChatClient.tools(…)→ 模型根据说明调用 Java Tool→ 返回代码审查报告先说清楚边界。这里不是接入某个c厂商的原生 Skills Runtime也不是让 Spring AI 自动执行完整 Skill 包。这里用的是spring-ai-agent-utils里的SkillsTool把本地SKILL.md接进 Spring AI 的 Tool Calling 链路。对应的官方参考是 Spring 博客《Spring AI Agentic Patterns: Agent Skills - Modular, Reusable Capabilities》texthttps://spring.io/blog/2026/01/13/spring-ai-generic-agent-skills一、这套方案到底接了什么Spring 官方文章里Agent Skills 的核心不是“更长的 Prompt”。它更像一个目录textmy-skill/├── SKILL.md├── scripts/├── references/└── assets/其中SKILL.md至少包含技能名称、描述和执行说明也可以再配脚本、参考资料、模板和资源文件。官方文章还强调了progressive disclosure也就是渐进式披露textDiscovery启动时只暴露 Skill 的 name 和 descriptionActivation任务匹配时再加载完整 SKILL.mdExecution执行时再按需要读取参考文件或执行脚本这样做的好处是不需要一上来把所有 Skill 的完整内容都塞进上下文。模型先知道“有哪些 Skill”等任务真的匹配再加载对应说明。放到 Spring AI 里这套方式是 tool-based integration。官方文章提到的核心工具包括textSkillsTool发现和加载 SkillFileSystemTools读取参考文件ShellTools执行辅助脚本这个最小版本先只接两个东西textSkillsTool加载本地 SKILL.mdCodeReviewTools执行基础代码检查所以这条链路的分工是textSKILL.md定义代码审查流程SkillsTool让模型按需加载这本技能书CodeReviewTools真正执行基础检查ChatClient把模型、Skill、Tool 串起来这也是本文最重要的边界Skill 负责给流程Tool 负责做动作模型负责把它们串起来。二、准备依赖和配置示例环境textJava 17Spring Boot 4.1.0Spring AI 2.0.0deepseek-v4-flashspring-ai-agent-utils 0.10.0pom.xml保留三个核心依赖xmljava.version17/java.version spring-ai.version2.0.0/spring-ai.versiondependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-deepseek/artifactId /dependency dependency groupIdorg.springaicommunity/groupId artifactIdspring-ai-agent-utils/artifactId version0.10.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencydependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies配置文件分两块。一块是模型配置。一块是代码审查 Prompt。Prompt 不建议硬编码在 Controller 里放到配置文件更容易改。application.yamlyamlspring:application:name: springai-deepseek-demoai:deepseek: api-key: ${DEEPSEEK\_API\_KEY} chat: model: deepseek-v4-flash temperature: 0.3app:code-review:prompt: system: | 你是一个 Java 代码审查助手。 长期规则 - 如果用户提交 Java 代码并要求审查先调用 Skill 工具加载 code-review-skill。 - 加载技能书后再按照技能书里的审查顺序调用 reviewJavaCode 工具。 - 优先指出 bug、安全风险、边界条件、异常处理和缺失测试。 - 如果信息不足要说明缺少哪些上下文不要编造项目背景。 - 不要输出与代码审查无关的泛泛建议。 输出要求 - 用中文回答。 - 使用 Markdown。 - 先给总体结论再列主要问题最后给测试建议和下一步。 user-template: | 请审查下面这段 Java 代码。 代码内容 {code}如果用 IDEA 启动在这里配置 KeytextRun/Debug Configurations→ SpringaiDeepseekDemoApplication→ Environment variables→ DEEPSEEK_API_KEY你的 DeepSeek API Key命令行启动bashexport DEEPSEEK_API_KEY你的 DeepSeek API Key./mvnw spring-boot:run三、写一本代码审查 Skill在项目里新建textsrc/main/resources/skills/code-review-skill/SKILL.md内容如下markdownname: code-review-skilldescription: 按固定流程审查 Java 代码优先发现 bug、安全风险、边界条件、可维护性问题和缺失测试。当用户要求 review、审查、检查 Java 代码或判断代码有没有风险时使用。Java Code Review Skill这个 Skill 用来审查 Java 代码。它不是简单评价“代码好不好”而是要求 Agent 按固定顺序检查明显 bug空值、空集合、非法输入等边界条件安全风险例如硬编码密钥、敏感信息泄露、权限绕过异常处理和日志可维护性问题缺失的测试场景工作流程Step 1先判断代码场景先识别代码属于哪一类Controller / API 入参处理Service 业务逻辑Repository / 数据访问工具类配置类测试代码不同类型代码的审查重点不同。Step 2按风险优先级审查优先输出会导致线上问题的内容。输出顺序高风险问题中风险问题低风险建议建议补充的测试不要把格式问题放在 bug 前面。Step 3调用代码审查工具当用户提供 Java 代码时调用reviewJavaCode工具。工具返回的是基础审查报告。你可以在报告基础上补充解释但不要改写成空泛建议也不要删除审查路径。Step 4输出格式输出结构代码审查报告审查路径总体结论主要问题建议补充的测试下一步边界不确定的问题要说明“不确定”不要编造上下文。没有看到完整工程时不要断言一定会出问题。涉及安全、权限、数据删除、支付、订单状态流转时要提高风险级别。如果代码片段太短要指出还需要哪些上下文。参考资料如果需要更细的检查项读取references/checklist.md。再放一个参考清单textsrc/main/resources/skills/code-review-skill/references/checklist.mdmarkdownJava 代码审查检查清单Bug 和边界条件参数是否可能为 null集合是否可能为空字符串是否可能为空白下标、分页、金额、数量是否有边界安全风险是否硬编码密码、Token、API Key是否缺少权限校验是否暴露敏感字段异常和日志是否吞掉异常日志是否包含必要上下文日志是否泄露敏感信息这一步才是 Skill 的核心。它不是让模型“更努力一点”而是把团队希望长期复用的审查顺序写下来。四、用 SkillsTool 加载 Skill新建配置类javapackage com.example.springaideepseekdemo.config;import org.springaicommunity.agent.tools.SkillsTool;import org.springframework.ai.tool.ToolCallback;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;Configurationpublic class SkillToolConfig {Bean public ToolCallback skillTool() { return SkillsTool.builder() .addSkillsResource(new ClassPathResource(skills)) .build(); }}这段代码会扫描textsrc/main/resources/skills/然后把里面的 Skill 注册成一个ToolCallback。模型需要时可以通过这个工具加载code-review-skill的完整SKILL.md。注意它加载的是 Skill 说明不是直接执行代码审查。五、准备真正做检查的 ToolSkillsTool负责加载说明。真正检查代码的是应用侧 Tool。javapackage com.example.springaideepseekdemo.tool;import org.springframework.ai.tool.annotation.Tool;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;Componentpublic class CodeReviewTools {Tool(description 按照 code-review-skill 的审查流程审查 Java 代码。参数 code 是待审查代码返回 Markdown 格式审查报告。) public String reviewJavaCode(String code) { String source code null ? : code.strip(); if (source.isBlank()) { return 请提供需要审查的 Java 代码。; } ListFinding findings new ArrayList(); checkNullPointerRisk(source, findings); checkSwallowedException(source, findings); checkSystemOut(source, findings); checkHardcodedSecret(source, findings); checkMissingValidation(source, findings); StringBuilder report new StringBuilder(); report.append( # 代码审查报告 审查依据code-review-skill / references/checklist.md ## 审查路径 - Step 1识别代码场景 - Step 2按风险优先级检查 - Step 3调用 reviewJavaCode 工具执行基础检查 - Step 4按固定结构输出报告 ## 总体结论 ); if (findings.isEmpty()) { report.append(这段代码没有命中当前内置的高风险规则。仍建议补充单元测试并结合业务上下文继续人工确认。\n\n); } else { report.append(这段代码发现 ).append(findings.size()).append( 个需要关注的问题建议先处理高风险项。\n\n); } report.append(## 主要问题\n\n); if (findings.isEmpty()) { report.append(- 暂无明确问题。\n\n); } else { for (int i 0; i findings.size(); i) { Finding finding findings.get(i); report.append(i 1).append(. \*\*).append(finding.title()).append(\*\*\n\n) .append( 风险级别).append(finding.level()).append(\n\n) .append( 问题说明).append(finding.detail()).append(\n\n) .append( 修改建议).append(finding.suggestion()).append(\n\n); } } report.append( ## 建议补充的测试 - 正常输入场景 - 空值或非法输入场景 - 异常分支场景 - 关键业务规则的回归测试 ## 下一步 先修复高风险问题再补测试最后重新发起一次审查。 ); return report.toString(); } private void checkNullPointerRisk(String source, ListFinding findings) { if (source.contains(.getName()) !source.contains(null)) { findings.add(new Finding( 存在空指针风险, 高, 代码直接调用对象方法但没有看到空值保护。参数为 null 时可能触发 NullPointerException。, 在进入业务逻辑前做参数校验或者使用明确的异常提示让调用方知道问题出在哪里。 )); } } private void checkSwallowedException(String source, ListFinding findings) { if (source.contains(catch) (source.contains(catch (Exception) || source.contains(catch(Exception)) !source.contains(throw) !source.contains(log.)) { findings.add(new Finding( 异常被吞掉, 高, 代码捕获了通用异常但没有重新抛出也没有记录日志。线上排查时会丢失关键上下文。, 不要静默吞异常。至少记录错误上下文必要时转换成业务异常继续抛出。 )); } } private void checkSystemOut(String source, ListFinding findings) { if (source.contains(System.out.println)) { findings.add(new Finding( 使用 System.out.println 输出日志, 中, 业务代码里直接使用标准输出不利于日志级别控制、链路追踪和线上检索。, 改用项目统一日志框架并补充必要的业务字段。 )); } } private void checkHardcodedSecret(String source, ListFinding findings) { String lower source.toLowerCase(); if (lower.contains(password) || lower.contains(secret) || lower.contains(apikey) || lower.contains(api\_key)) { findings.add(new Finding( 疑似硬编码敏感信息, 高, 代码中出现 password、secret 或 api key 相关字段可能存在敏感信息硬编码风险。, 敏感信息应放到环境变量、配置中心或密钥管理系统不要写死在代码里。 )); } } private void checkMissingValidation(String source, ListFinding findings) { if ((source.contains(RequestBody) || source.contains(RequestParam)) !source.contains(Valid) !source.contains(Assert.)) { findings.add(new Finding( 缺少入参校验, 中, 接口层接收外部输入但没有看到 Bean Validation 或显式参数校验。, 为请求对象补充校验注解或者在方法入口明确校验必填字段和取值范围。 )); } } private record Finding(String title, String level, String detail, String suggestion) { }}这里的 CodeReviewTools 不是完整的静态代码扫描器只做几个简单规则检查。这样做是为了让 Demo 每次都能稳定返回同一种结果方便我们验证 Skill、Tool 和模型调用链路有没有串起来。六、用 Controller 串起来Controller 提供三个接口textPOST /skills/code-review/runPOST /skills/code-review/run/streamPOST /skills/code-review/review其中/run是主链路text模型看到用户请求→ 调用 SkillsTool 加载 code-review-skill→ 根据 SKILL.md 的流程调用 reviewJavaCode→ 返回报告/run/stream是同一条链路的流式版本。/review不经过模型只直接调用 Java Tool。保留它是为了做对比如果/review能返回报告说明本地 Tool 没问题如果/run返回了带审查路径的报告才说明模型链路也串起来了。先写配置绑定类javapackage com.example.springaideepseekdemo.config;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;ComponentConfigurationProperties(prefix “app.code-review.prompt”)public class CodeReviewPromptProperties {private String system ; private String userTemplate ; public String system() { return system; } public void setSystem(String system) { this.system system; } public String userTemplate() { return userTemplate; } public void setUserTemplate(String userTemplate) { this.userTemplate userTemplate; }}再写 Controllerjavapackage com.example.springaideepseekdemo.controller;import com.example.springaideepseekdemo.config.CodeReviewPromptProperties;import com.example.springaideepseekdemo.tool.CodeReviewTools;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.tool.ToolCallback;import org.springframework.http.MediaType;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;RestControllerRequestMapping(“/skills/code-review”)public class CodeReviewSkillController {private static final Logger log LoggerFactory.getLogger(CodeReviewSkillController.class); private final ChatClient chatClient; private final CodeReviewPromptProperties promptProperties; private final ToolCallback skillTool; private final CodeReviewTools codeReviewTools; public CodeReviewSkillController(ChatClient.Builder builder, CodeReviewPromptProperties promptProperties, ToolCallback skillTool, CodeReviewTools codeReviewTools) { this.chatClient builder .defaultSystem(promptProperties.system()) .build(); this.promptProperties promptProperties; this.skillTool skillTool; this.codeReviewTools codeReviewTools; } PostMapping(/run) public String run(RequestBody String code) { String source normalizeCode(code); try { String content chatClient.prompt() .user(user - user.text(promptProperties.userTemplate()) .param(code, source)) .tools(skillTool, codeReviewTools) .call() .content(); if (content ! null content.contains(代码审查报告)) { return content; } } catch (Exception ex) { log.warn(Code review model call failed, fallback to local tool. reason{}, ex.getMessage()); } return 模型没有在本轮稳定触发工具调用已走应用侧兜底审查。 %s .formatted(codeReviewTools.reviewJavaCode(source)); } PostMapping(value /run/stream, produces MediaType.TEXT\_EVENT\_STREAM\_VALUE) public FluxString runStream(RequestBody String code) { String source normalizeCode(code); return chatClient.prompt() .user(user - user.text(promptProperties.userTemplate()) .param(code, source)) .tools(skillTool, codeReviewTools) .stream() .content() .onErrorResume(ex - { log.warn(Code review stream call failed, fallback to local tool. reason{}, ex.getMessage()); return Flux.just( 模型没有在本轮稳定触发流式工具调用已走应用侧兜底审查。 %s .formatted(codeReviewTools.reviewJavaCode(source))); }); } PostMapping(/review) public String review(RequestBody String code) { return codeReviewTools.reviewJavaCode(normalizeCode(code)); } private String normalizeCode(String code) { return code null ? : code.strip(); }}这里加兜底不是为了掩盖模型调用失败。真实项目里反而应该这么做。模型是否触发工具受模型能力、Prompt、工具描述和上下文影响。应用侧要保证即使模型这轮没有稳定触发工具接口也能给出一个可解释的结果。七、运行测试先编译bash./mvnw -DskipTests compile启动项目bashexport DEEPSEEK_API_KEY你的 DeepSeek API Key./mvnw spring-boot:run测试主链路bashcurl -X POST “http://localhost:8080/skills/code-review/run” \-H “Content-Type: text/plain” \–data-binary ‘public String getName(User user) { return user.getName(); }’如果模型稳定触发工具会返回代码审查报告可以在工具方法加个日志观察下。如果模型没有触发工具接口会走应用侧兜底也会返回类似结果markdown代码审查报告审查依据code-review-skill / references/checklist.md审查路径Step 1识别代码场景Step 2按风险优先级检查Step 3调用 reviewJavaCode 工具执行基础检查Step 4按固定结构输出报告总体结论这段代码发现 1 个需要关注的问题建议先处理高风险项。主要问题**存在空指针风险**风险级别高问题说明代码直接调用对象方法但没有看到空值保护。参数为 null 时可能触发 NullPointerException。修改建议在进入业务逻辑前做参数校验或者使用明确的异常提示让调用方知道问题出在哪里。再测流式接口bashcurl -N -X POST “http://localhost:8080/skills/code-review/run/stream” \-H “Content-Type: text/plain” \–data-binary ‘public String getName(User user) { return user.getName(); }’这里的-N很关键。它会关闭 curl 的输出缓冲更容易看到流式返回。最后测本地 Toolbashcurl -X POST “http://localhost:8080/skills/code-review/review” \-H “Content-Type: text/plain” \–data-binary ‘public String getName(User user) { return user.getName(); }’这条接口不经过模型只用来验证本地检查逻辑。对比这三条接口重点不是多写一个 endpoint而是看清楚分工text/review验证本地 Tool 能不能产出确定报告/run验证模型能不能按 Skill 说明调用 Tool/run/stream验证同一条链路能不能流式返回八、后面可以怎么扩展这个 Demo 只做了代码片段审查。真实项目里可以继续往研发流程里扩展text读取 Git Diff加载团队代码规范扫描 Pull Request生成 Review Comment补充测试建议也可以把FileSystemTools接进来让模型按Skill 里的说明读取references/checklist.md。再往后还可以接ShellTools执行一些确定性的脚本比如跑单元测试、生成依赖树、检查格式。但只要涉及读文件、执行命令、访问外部系统就要补安全边界text限制目录限制命令高风险操作二次确认记录工具调用日志必要时放进容器执行学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】