把 Skill 接进 Java 项目,最容易写成:

在 system prompt 里多塞几条规则。

比如代码审查。

你可以直接写:

text

你是一个 Java 代码审查专家,请帮我看看下面这段代码。有没有空指针,异常等

模型通常也能回答。

但这种写法有一个问题:它更像临场发挥。

这一次它可能先看空指针,下一次可能先讲代码风格。再换一个人写 Prompt,又可能把安全、异常、测试全漏掉。

真正适合沉淀成 Skill 的,不是一句“你很专业”,而是一套稳定流程:

text

先判断代码场景

再按风险优先级检查

发现问题后给出原因和修改建议

最后补充测试建议

下面用 Spring AI 2.0.0 跑一个最小 Demo:

text

本地 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》:

text

https://spring.io/blog/2026/01/13/spring-ai-generic-agent-skills

一、这套方案到底接了什么

Spring 官方文章里,Agent Skills 的核心不是“更长的 Prompt”。

它更像一个目录:

text

my-skill/

├── SKILL.md

├── scripts/

├── references/

└── assets/

其中 SKILL.md 至少包含技能名称、描述和执行说明,也可以再配脚本、参考资料、模板和资源文件。

官方文章还强调了 progressive disclosure,也就是渐进式披露:

text

Discovery:启动时只暴露 Skill 的 name 和 description

Activation:任务匹配时,再加载完整 SKILL.md

Execution:执行时,再按需要读取参考文件或执行脚本

这样做的好处是:

不需要一上来把所有 Skill 的完整内容都塞进上下文。

模型先知道“有哪些 Skill”,等任务真的匹配,再加载对应说明。

放到 Spring AI 里,这套方式是 tool-based integration。

官方文章提到的核心工具包括:

text

SkillsTool:发现和加载 Skill

FileSystemTools:读取参考文件

ShellTools:执行辅助脚本

这个最小版本先只接两个东西:

text

SkillsTool:加载本地 SKILL.md

CodeReviewTools:执行基础代码检查

所以这条链路的分工是:

text

SKILL.md:定义代码审查流程

SkillsTool:让模型按需加载这本技能书

CodeReviewTools:真正执行基础检查

ChatClient:把模型、Skill、Tool 串起来

这也是本文最重要的边界:

Skill 负责给流程,Tool 负责做动作,模型负责把它们串起来。

二、准备依赖和配置

示例环境:

text

Java 17

Spring Boot 4.1.0

Spring AI 2.0.0

deepseek-v4-flash

spring-ai-agent-utils 0.10.0

pom.xml 保留三个核心依赖:

xml

<java.version>17</java.version>

<spring-ai.version>2.0.0</spring-ai.version>
<dependency>

    <groupId>org.springframework.ai</groupId>

    <artifactId>spring-ai-starter-model-deepseek</artifactId>

</dependency>

<dependency>

    <groupId>org.springaicommunity</groupId>

    <artifactId>spring-ai-agent-utils</artifactId>

    <version>0.10.0</version>

</dependency>

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

</dependency>
<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>

配置文件分两块。

一块是模型配置。

一块是代码审查 Prompt。

Prompt 不建议硬编码在 Controller 里,放到配置文件更容易改。

application.yaml

yaml

spring:

application:

name: springai-deepseek-demo

ai:

deepseek:

  api-key: ${DEEPSEEK\_API\_KEY}

  chat:

    model: deepseek-v4-flash

    temperature: 0.3

app:

code-review:

prompt:

  system: |

    你是一个 Java 代码审查助手。

    长期规则:

    - 如果用户提交 Java 代码并要求审查,先调用 Skill 工具加载 code-review-skill。

    - 加载技能书后,再按照技能书里的审查顺序调用 reviewJavaCode 工具。

    - 优先指出 bug、安全风险、边界条件、异常处理和缺失测试。

    - 如果信息不足,要说明缺少哪些上下文,不要编造项目背景。

    - 不要输出与代码审查无关的泛泛建议。

    输出要求:

    - 用中文回答。

    - 使用 Markdown。

    - 先给总体结论,再列主要问题,最后给测试建议和下一步。

  user-template: |

    请审查下面这段 Java 代码。

    代码内容:

    {code}

如果用 IDEA 启动,在这里配置 Key:

text

Run/Debug Configurations

→ SpringaiDeepseekDemoApplication

→ Environment variables

→ DEEPSEEK_API_KEY=你的 DeepSeek API Key

命令行启动:

bash

export DEEPSEEK_API_KEY=你的 DeepSeek API Key

./mvnw spring-boot:run

三、写一本代码审查 Skill

在项目里新建:

text

src/main/resources/skills/code-review-skill/SKILL.md

内容如下:

markdown


name: code-review-skill

description: 按固定流程审查 Java 代码,优先发现 bug、安全风险、边界条件、可维护性问题和缺失测试。当用户要求 review、审查、检查 Java 代码或判断代码有没有风险时使用。


Java Code Review Skill

这个 Skill 用来审查 Java 代码。

它不是简单评价“代码好不好”,而是要求 Agent 按固定顺序检查:

  1. 明显 bug

  2. 空值、空集合、非法输入等边界条件

  3. 安全风险,例如硬编码密钥、敏感信息泄露、权限绕过

  4. 异常处理和日志

  5. 可维护性问题

  6. 缺失的测试场景

工作流程

Step 1:先判断代码场景

先识别代码属于哪一类:

  • Controller / API 入参处理

  • Service 业务逻辑

  • Repository / 数据访问

  • 工具类

  • 配置类

  • 测试代码

不同类型代码的审查重点不同。

Step 2:按风险优先级审查

优先输出会导致线上问题的内容。

输出顺序:

  1. 高风险问题

  2. 中风险问题

  3. 低风险建议

  4. 建议补充的测试

不要把格式问题放在 bug 前面。

Step 3:调用代码审查工具

当用户提供 Java 代码时,调用 reviewJavaCode 工具。

工具返回的是基础审查报告。你可以在报告基础上补充解释,但不要改写成空泛建议,也不要删除审查路径。

Step 4:输出格式

输出结构:

  • 代码审查报告

  • 审查路径

  • 总体结论

  • 主要问题

  • 建议补充的测试

  • 下一步

边界

  • 不确定的问题要说明“不确定”,不要编造上下文。

  • 没有看到完整工程时,不要断言一定会出问题。

  • 涉及安全、权限、数据删除、支付、订单状态流转时,要提高风险级别。

  • 如果代码片段太短,要指出还需要哪些上下文。

参考资料

如果需要更细的检查项,读取 references/checklist.md

再放一个参考清单:

text

src/main/resources/skills/code-review-skill/references/checklist.md

markdown

Java 代码审查检查清单

Bug 和边界条件

  • 参数是否可能为 null

  • 集合是否可能为空

  • 字符串是否可能为空白

  • 下标、分页、金额、数量是否有边界

安全风险

  • 是否硬编码密码、Token、API Key

  • 是否缺少权限校验

  • 是否暴露敏感字段

异常和日志

  • 是否吞掉异常

  • 日志是否包含必要上下文

  • 日志是否泄露敏感信息

这一步才是 Skill 的核心。

它不是让模型“更努力一点”,而是把团队希望长期复用的审查顺序写下来。

四、用 SkillsTool 加载 Skill

新建配置类:

java

package 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;

@Configuration

public class SkillToolConfig {

@Bean

public ToolCallback skillTool() {

    return SkillsTool.builder()

            .addSkillsResource(new ClassPathResource("skills"))

            .build();

}

}

这段代码会扫描:

text

src/main/resources/skills/

然后把里面的 Skill 注册成一个 ToolCallback

模型需要时,可以通过这个工具加载 code-review-skill 的完整 SKILL.md

注意,它加载的是 Skill 说明,不是直接执行代码审查。

五、准备真正做检查的 Tool

SkillsTool 负责加载说明。

真正检查代码的,是应用侧 Tool。

java

package com.example.springaideepseekdemo.tool;

import org.springframework.ai.tool.annotation.Tool;

import org.springframework.stereotype.Component;

import java.util.ArrayList;

import java.util.List;

@Component

public 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 代码。";

    }

    List<Finding> 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, List<Finding> findings) {

    if (source.contains(".getName()") && !source.contains("null")) {

        findings.add(new Finding(

                "存在空指针风险",

                "高",

                "代码直接调用对象方法,但没有看到空值保护。参数为 null 时可能触发 NullPointerException。",

                "在进入业务逻辑前做参数校验,或者使用明确的异常提示,让调用方知道问题出在哪里。"

        ));

    }

}

private void checkSwallowedException(String source, List<Finding> 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, List<Finding> findings) {

    if (source.contains("System.out.println")) {

        findings.add(new Finding(

                "使用 System.out.println 输出日志",

                "中",

                "业务代码里直接使用标准输出,不利于日志级别控制、链路追踪和线上检索。",

                "改用项目统一日志框架,并补充必要的业务字段。"

        ));

    }

}

private void checkHardcodedSecret(String source, List<Finding> 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, List<Finding> 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 提供三个接口:

text

POST /skills/code-review/run

POST /skills/code-review/run/stream

POST /skills/code-review/review

其中 /run 是主链路:

text

模型看到用户请求

→ 调用 SkillsTool 加载 code-review-skill

→ 根据 SKILL.md 的流程调用 reviewJavaCode

→ 返回报告

/run/stream 是同一条链路的流式版本。

/review 不经过模型,只直接调用 Java Tool。

保留它是为了做对比:如果 /review 能返回报告,说明本地 Tool 没问题;如果 /run 返回了带审查路径的报告,才说明模型链路也串起来了。

先写配置绑定类:

java

package com.example.springaideepseekdemo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

import org.springframework.stereotype.Component;

@Component

@ConfigurationProperties(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;

}

}

再写 Controller:

java

package 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;

@RestController

@RequestMapping(“/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 Flux<String> 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

启动项目:

bash

export DEEPSEEK_API_KEY=你的 DeepSeek API Key

./mvnw spring-boot:run

测试主链路:

bash

curl -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 个需要关注的问题,建议先处理高风险项。

主要问题

  1. **存在空指针风险**

    风险级别:高

    问题说明:代码直接调用对象方法,但没有看到空值保护。参数为 null 时可能触发 NullPointerException。

    修改建议:在进入业务逻辑前做参数校验,或者使用明确的异常提示,让调用方知道问题出在哪里。

再测流式接口:

bash

curl -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 的输出缓冲,更容易看到流式返回。

最后测本地 Tool:

bash

curl -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%免费

在这里插入图片描述

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐