跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaAIjava

SpringAI Agent 开发实战:基于 Skills 实现代码评审

SpringAI Agent 结合 Skills 机制实现代码评审功能。通过配置 SKILL.md 定义技能元数据与指令,利用 ChatClient 加载技能资源并调用大模型。示例展示了从项目创建、依赖引入到核心 Bean 配置的完整流程,验证了 Agent 在发现、语义匹配及执行阶段的交互逻辑,为 Java 开发者提供 AI 工程化落地方案。

imJackJia发布于 2026/3/22更新于 2026/10/24105 浏览
SpringAI Agent 开发实战:基于 Skills 实现代码评审

SpringAI Agent 开发实战:基于 Skills 实现代码评审

一、项目创建

1. 基础环境要求

要体验 SpringAI & Skills,目前需要升级到 SpringAI 2.x 版本,同时 SpringBoot 也可以升级到 4.x。

  • SpringAI: 2.0.0-M2
  • JDK21
  • SpringBoot: 4.0.1

除了这几个基本依赖之外,我们可以选择一个支持 Function Tool 的大模型来作为这个实现的大脑中枢。这里选择智谱的大模型 GLM-4.5-Flash(免费且效果尚可)。

2. 项目创建

创建一个 SpringAI 应用,在 pom.xml 配置中指定基础版本。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.1</version>
    <relativePath/>
</parent>
<properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-ai.version>2.0.0-M2</spring-ai.version>
</properties>
<dependencyManagement>
    <dependencies>
        
            org.springframework.ai
            spring-ai-bom
            ${spring-ai.version}
            pom
            import
        
    


    
        
            org.springframework.boot
            spring-boot-maven-plugin
        
    


    
        spring-snapshots
        Spring Snapshots
        https://repo.spring.io/snapshot
        false
    
    
        Central Portal Snapshots
        central-portal-snapshots
        https://central.sonatype.com/repository/maven-snapshots/
        false
        true
    

<dependency>
<groupId>
</groupId>
<artifactId>
</artifactId>
<version>
</version>
<type>
</type>
<scope>
</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>
</groupId>
<artifactId>
</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>
</id>
<name>
</name>
<url>
</url>
<releases>
<enabled>
</enabled>
</releases>
</repository>
<repository>
<name>
</name>
<id>
</id>
<url>
</url>
<releases>
<enabled>
</enabled>
</releases>
<snapshots>
<enabled>
</enabled>
</snapshots>
</repository>
</repositories>

核心依赖如下:

<dependencies>
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>spring-ai-agent-utils</artifactId>
        <version>0.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-zhipuai</artifactId>
    </dependency>
</dependencies>
  • spring-ai-agent-utils: SpringAI 进行 agent 开发的关键依赖包。
  • spring-ai-starter-model-zhipuai: 智谱大模型交互的依赖包。

3. 项目配置

在 resources/application.yml 中配置 LLM 访问信息及 Agent 参数。

spring:
  ai:
    zhipuai:
      api-key: ${zhipuai-api-key}
    chat:
      options:
        model: GLM-4.5-Flash
    agent:
      skills:
        dirs: classpath:/.claude/skills
        model: GLM-4.5-Flash

其中 agent.skills.dirs 设置技能存放路径。将 skills 信息放在 resources/.claude/skills 目录下。

新增目录 code-reviewer,文件为 SKILL.md。

.claude/skills/code-reviewer/
└── 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
在审查代码时:
1. 检查是否存在安全漏洞(如 SQL 注入、XSS 等)
2. 验证是否遵循了 Spring Boot 的最佳实践(如正确使用@Service、@Repository 等注解)
3. 查找潜在的空指针异常
4. 提出提高代码可读性和可维护性的建议
5. 提供具体的逐行反馈,并附上代码示例
6. 以中文的方式返回代码评审结果

4. Skills 简要说明

Skills 结构通常包含 SKILL.md(元数据与指令)、脚本、模板和参考资料。

my-skill/
├── SKILL.md # Required: instructions + metadata
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation
└── assets/ # Optional: templates, resources

二、核心实现

2.1 交互日志打印 MyLoggingAdvisor

为了清晰查看系统与大模型的交互,自定义 Advisor 打印日志。

public class MyLoggingAdvisor implements BaseAdvisor {
    private final int order;
    public final boolean showSystemMessage;
    public final boolean showAvailableTools;
    private AtomicInteger cnt = new AtomicInteger(1);

    private MyLoggingAdvisor(int order, boolean showSystemMessage, boolean showAvailableTools) {
        this.order = order;
        this.showSystemMessage = showSystemMessage;
        this.showAvailableTools = showAvailableTools;
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    @Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        System.out.println("======================= 第 " + cnt.getAndAdd(1) + " 轮 ====================================");
        StringBuilder sb = new StringBuilder("\nUSER: ");
        if (this.showSystemMessage && chatClientRequest.prompt().getSystemMessage() != null) {
            sb.append("\n - SYSTEM: ").append(first(chatClientRequest.prompt().getSystemMessage().getText(), 300));
        }
        if (this.showAvailableTools) {
            Object tools = "No Tools";
            if (chatClientRequest.prompt().getOptions() instanceof ToolCallingChatOptions toolOptions) {
                tools = toolOptions.getToolCallbacks().stream()
                        .map(tc -> tc.getToolDefinition().name()).toList();
            }
            sb.append("\n - TOOLS: ").append(ModelOptionsUtils.toJsonString(tools));
        }
        Message lastMessage = chatClientRequest.prompt().getLastUserOrToolResponseMessage();
        if (lastMessage.getMessageType() == MessageType.TOOL) {
            ToolResponseMessage toolResponseMessage = (ToolResponseMessage) lastMessage;
            for (var toolResponse : toolResponseMessage.getResponses()) {
                var tr = toolResponse.name() + ": " + first(toolResponse.responseData(), 1000);
                sb.append("\n - TOOL-RESPONSE: ").append(tr);
            }
        } else if (lastMessage.getMessageType() == MessageType.USER) {
            if (StringUtils.hasText(lastMessage.getText())) {
                sb.append("\n - TEXT: ").append(first(lastMessage.getText(), 1000));
            }
        }
        System.out.println("before: " + sb);
        return chatClientRequest;
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
        StringBuilder sb = new StringBuilder("\nASSISTANT: ");
        if (chatClientResponse.chatResponse() == null || chatClientResponse.chatResponse().getResults() == null) {
            sb.append(" No chat response ");
            System.out.println("after: " + sb);
            return chatClientResponse;
        }
        for (var generation : chatClientResponse.chatResponse().getResults()) {
            var message = generation.getOutput();
            if (message.getToolCalls() != null) {
                for (var toolCall : message.getToolCalls()) {
                    sb.append("\n - TOOL-CALL: ").append(toolCall.name()).append(" (").append(toolCall.arguments()).append(")");
                }
            }
            if (message.getText() != null) {
                if (StringUtils.hasText(message.getText())) {
                    sb.append("\n - TEXT: ").append(first(message.getText(), 1200));
                }
            }
        }
        System.out.println("after: " + sb);
        return chatClientResponse;
    }

    private String first(String text, int n) {
        if (text.length() <= n) return text;
        return text.substring(0, n) + "...";
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private int order = 0;
        private boolean showSystemMessage = true;
        private boolean showAvailableTools = true;

        public Builder order(int order) {
            this.order = order;
            return this;
        }

        public Builder showSystemMessage(boolean showSystemMessage) {
            this.showSystemMessage = showSystemMessage;
            return this;
        }

        public Builder showAvailableTools(boolean showAvailableTools) {
            this.showAvailableTools = showAvailableTools;
            return this;
        }

        public MyLoggingAdvisor build() {
            MyLoggingAdvisor advisor = new MyLoggingAdvisor(this.order, this.showSystemMessage, this.showAvailableTools);
            return advisor;
        }
    }
}

2.2 准备用于评审的代码

使用文档分块工具类 DocumentChunker.java 作为待评审内容。

package com.git.hui.springai.app.demo;

import org.springframework.ai.document.Document;
import java.util.ArrayList;
import java.util.List;

/**
 * 文档分块工具类
 * 将长文档分割成较小的块,以便更好地进行向量化和检索
 */
public class DocumentChunker {
    private final int maxChunkSize;
    private final int overlapSize;

    public static DocumentChunker DEFAULT_CHUNKER = new DocumentChunker();

    public DocumentChunker() {
        this(500, 50); // 默认值:最大块大小 500 个字符,重叠 50 个字符
    }

    public DocumentChunker(int maxChunkSize, int overlapSize) {
        this.maxChunkSize = maxChunkSize;
        this.overlapSize = overlapSize;
    }

    /**
     * 将文档分割成块
     *
     * @param document 输入文档
     * @return 分割后的文档块列表
     */
    public List<Document> chunkDocument(Document document) {
        String content = document.getText();
        if (content == null || content.trim().isEmpty()) {
            return List.of(document);
        }
        List<String> chunks = splitText(content);
        List<Document> chunkedDocuments = new ArrayList<>();
        for (int i = 0; i < chunks.size(); i++) {
            String chunk = chunks.get(i);
            String chunkId = document.getId() + "_chunk_" + i;
            Document chunkDoc = new Document(chunkId, chunk, new java.util.HashMap<>(document.getMetadata()));
            chunkDoc.getMetadata().put("chunk_index", i);
            chunkDoc.getMetadata().put("total_chunks", chunks.size());
            chunkDoc.getMetadata().put("original_document_id", document.getId());
            chunkedDocuments.add(chunkDoc);
        }
        return chunkedDocuments;
    }

    /**
     * 将文本分割成块
     *
     * @param text 输入文本
     * @return 分割后的文本块列表
     */
    private List<String> splitText(String text) {
        List<String> chunks = new ArrayList<>();
        String[] sentences = text.split("(?<=。)|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)");
        StringBuilder currentChunk = new StringBuilder();
        for (String sentence : sentences) {
            if (sentence.trim().isEmpty()) continue;
            if (currentChunk.length() + sentence.length() <= maxChunkSize) {
                if (currentChunk.length() > 0) currentChunk.append(sentence);
                else currentChunk.append(sentence);
            } else {
                if (currentChunk.length() == 0) {
                    List<String> subChunks = forceSplit(sentence, maxChunkSize);
                    for (int i = 0; i < subChunks.size(); i++) {
                        String subChunk = subChunks.get(i);
                        if (i < subChunks.size() - 1) chunks.add(subChunk);
                        else currentChunk.append(subChunk);
                    }
                } else {
                    chunks.add(currentChunk.toString());
                    currentChunk = new StringBuilder();
                    if (sentence.length() > overlapSize) {
                        String overlap = sentence.substring(Math.max(0, sentence.length() - overlapSize));
                        currentChunk.append(overlap).append(sentence);
                    } else {
                        currentChunk.append(sentence);
                    }
                }
            }
        }
        if (currentChunk.length() > 0) chunks.add(currentChunk.toString());
        return chunks;
    }

    /**
     * 强制将长文本分割成指定大小的块
     *
     * @param text 输入文本
     * @param maxSize 最大块大小
     * @return 分割后的文本块列表
     */
    private List<String> forceSplit(String text, int maxSize) {
        List<String> chunks = new ArrayList<>();
        int start = 0;
        while (start < text.length()) {
            int end = Math.min(start + maxSize, text.length());
            String chunk = text.substring(start, end);
            chunks.add(chunk);
            start = end;
        }
        return chunks;
    }

    /**
     * 将多个文档分别分割成块
     *
     * @param documents 输入文档列表
     * @return 分割后的文档块列表
     */
    public List<Document> chunkDocuments(List<Document> documents) {
        List<Document> allChunks = new ArrayList<>();
        for (Document document : documents) allChunks.addAll(chunkDocument(document));
        return allChunks;
    }
}

2.3 核心实现

配置 Agent 实现代码评审。

Bean 定义与依赖注入

  • CommandLineRunner: Spring 启动后自动执行的接口
  • ChatClient.Builder: 用于构建聊天客户端
  • @Value("${agent.skills.dirs:Unknown}"): 注入配置属性,获取技能目录资源列表

ChatClient 配置链

  • 系统提示词配置
  • 技能工具配置:
    • SkillsTool.builder().addSkillsResources(agentSkillsDirs).build(): 动态加载预定义的技能资源
    • FileSystemTools.builder().build(): 提供文件系统访问能力
    • ShellTools.builder().build(): 提供命令行执行能力
  • Advisor 配置:
    • ToolCallAdvisor.builder().build(): 处理工具调用逻辑
    • MyLoggingAdvisor.builder().showAvailableTools(false).showSystemMessage(false).build(): 自定义日志记录

代码评审执行流程

@Bean
CommandLineRunner commandLineRunner(ChatClient.Builder chatClientBuilder,
                                    @Value("${agent.skills.dirs:Unknown}") List<Resource> agentSkillsDirs) throws IOException {
    return args -> {
        ChatClient chatClient = chatClientBuilder
                .defaultSystem("始终运用现有技能协助用户满足其要求.")
                .defaultToolCallbacks(SkillsTool.builder().addSkillsResources(agentSkillsDirs).build())
                .defaultTools(FileSystemTools.builder().build())
                .defaultTools(ShellTools.builder().build())
                .defaultAdvisors(
                        ToolCallAdvisor.builder().build(),
                        MyLoggingAdvisor.builder().showAvailableTools(false).showSystemMessage(false).build())
                .build();

        var answer = chatClient
                .prompt("按照最佳实际的方式,评审下面的代码实现:D:\\Workspace\\hui\\project\\spring-ai-demo\\v2\\T01-agentic-skills-simple-design\\src\\main\\java\\com\\git\\hui\\springai\\app\\demo\\DocumentChunker.java ")
                .call()
                .content();
        System.out.println("The Answer: " + answer);
    };
}

2.4 执行演示

启动项目,验证效果。系统与大模型之间进行了三轮对话:

  1. 第一轮:用户原始诉求 -> 大模型识别到需要进行代码评审,响应 {"command":"code-reviewer"}。
  2. 第二轮:系统接收响应,读取 skills 约束,大模型返回需要读取代码路径 {"filePath":"..."}。
  3. 第三轮:系统读取代码内容发送给大模型,返回评审结果。

完整返回文本示例:

基于对代码的详细分析,我为您提供以下代码评审报告:
## 📋 代码评审报告 - DocumentChunker.java
### ✅ **优点**
1. **功能完整性强**:代码实现了完整的文档分块功能...
### ⚠️ **需要改进的问题**
#### 1. **线程安全问题**
// 问题:静态实例不是线程安全的
public static DocumentChunker DEFAULT_CHUNKER = new DocumentChunker();
**建议**:使用 `volatile` 或 `AtomicReference`,或者移除静态实例让用户自己管理。
...

三、小结

SpringAI 的 Agent 开发范式配合 Skills 机制,简单实现了 AI 应用的工程化。整个过程门槛较低。

背后的设计哲学值得参考——在 AI 时代,如何将现有的技能(如可复用、模块化设计等)转变到大模型应用开发。

Spring AI 采用基于工具的集成方法,通过实现各种工具,使任何 LLM 都能回调执行。Skills 的运行过程通常是下面三步:

  1. 发现(启动阶段):通过 SKILL.md 文件中的元数据,快速实现技能的安装注册。
  2. 语义匹配(对话过程中):当用户发出请求时,LLM 会检查工具定义中嵌入的技能描述。如果 LLM 判断用户请求在语义上与某个技能的描述匹配,则会调用该技能工具。
  3. 执行(技能调用时):当调用技能工具时,SkillsTool 会从磁盘加载完整的 SKILL.md 内容,并将其与技能的基础目录路径一起返回给大型语言模型(LLM)。然后,LLM 会按照技能内容中的指令执行。

项目源码:https://github.com/liuyueyi/spring-ai-demo/tree/master/v2/T01-agentic-skills-simple-design

参考:Spring AI Agentic Patterns (Part 1): Agent Skills - Modular, Reusable Capabilities

目录

  1. SpringAI Agent 开发实战:基于 Skills 实现代码评审
  2. 一、项目创建
  3. 1. 基础环境要求
  4. 2. 项目创建
  5. 3. 项目配置
  6. Code Reviewer
  7. Instructions
  8. 4. Skills 简要说明
  9. 二、核心实现
  10. 2.1 交互日志打印 MyLoggingAdvisor
  11. 2.2 准备用于评审的代码
  12. 2.3 核心实现
  13. 2.4 执行演示
  14. 📋 代码评审报告 - DocumentChunker.java
  15. ✅ 优点
  16. ⚠️ 需要改进的问题
  17. 1. 线程安全问题
  18. 三、小结

更多推荐文章

查看全部
  • 本地部署大语言模型实践(2):集成外部知识库详细步骤
  • FPGA 嵌入式块存储器 RAM:原理与实现指南
  • OpenClaw 对接飞书机器人常见问题与 Gateway 稳定性排查
  • Git 下载、安装与配置教程
  • 大模型微调与 RAG 的区别是什么?
  • 二叉树深度优先遍历实战:计算布尔值与路径数字和
  • Flutter inAppWebviewCookieManager 在鸿蒙系统的适配与安全实践
  • SBUS 协议详解:从原理到 STM32 实战
  • Triton-Copilot 人机协同设计哲学:超越传统代码生成器
  • 字符串模拟算法题精选:思维与实现解析
  • 基于 YOLOv13 的无人机电动自行车违规载人检测系统实战
  • 7 款 AI 工具助力产品经理工作效率提升
  • C++ 核心面试题总结:语法、内存与类机制
  • Python 双层独立循环实现文件批量存储案例
  • Java GUI 编程:Swing 与 JavaFX 入门实战
  • ChatGPT-4o 辅助数学建模与代码实现实战指南
  • Haversine 距离算法详解
  • Java 代码性能优化的 11 个实用技巧
  • 机器学习常见分类算法实战:原理与代码示例
  • Chart.js 集成 CosyVoice3 前端可视化监控方案

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online