SpringAI Agent开发秘籍:让javaer也可以用上Agent Skills

SpringAI Agent开发秘籍:让javaer也可以用上Agent Skills
告别传统AI开发!SpringAI Agent + Skills重新定义智能应用

要说最近AI相关话题中什么最火,毫无疑问是Claude Skills,让我感到震惊的倒不是它为什么火爆,而是SpringAI居然已经迅速支持上Skills了,这效率真的是堪比🚀了。

谁说AI时代java开发者要掉队了? 肉虽然不一定吃得上,但是喝口汤还是妥妥的

接下来我们通过构建一个code reviewer, 来实际体验一把,如何将SpringAI和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应用,对于一个标准的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><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><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build><repositories><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></repository><repository><name>Central Portal Snapshots</name><id>central-portal-snapshots</id><url>https://central.sonatype.com/repository/maven-snapshots/</url><releases><enabled>false</enabled></releases><snapshots><enabled>true</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. 项目配置

依赖搞定之后,接下来就是在配置文件中,配置LLM访问的相关信息、以及agent相关配置参数,对应的配置文件 resources/application.yml

spring:ai:zhipuai:# api-key 使用你自己申请的进行替换;如果为了安全考虑,可以通过启动参数进行设置api-key: ${zhipuai-api-key}chat:# 聊天模型options:model: GLM-4.5-Flash ## Agent Configurationagent:skills:dirs: classpath:/.claude/skills model: GLM-4.5-Flash 

这几个配置看起来和之前SpringAI相关的并没有太多的区别,其中 agent 相关的配置中,主要设置了skills的存放路径,使用的model

根据上面的定义,我们将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简要说明

我们上面的Skill比较简单,就是一个markdown文档,SpringAI支持的Skills中,除了包含基本的SKILL.md文件(包含元数据(名称和描述)以及指导代理如何执行特定任务的说明)之外,还可以有相关的脚本、模板和参考资料

一个常见的skills结构如下

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

二、核心实现

现在前置准备已经完成,接下来开始正式的体验吧

2.1 交互日志打印 MyLoggingAdvisor

为了让系统与大模型之间的交互更清晰,我们将双方交互的日志进行更友好的打印(也顺便看一下,一次用户感知的问答过程中,实际上有几次交互)

publicclassMyLoggingAdvisorimplementsBaseAdvisor{privatefinalint order;publicfinalboolean showSystemMessage;publicfinalboolean showAvailableTools;privateAtomicInteger cnt =newAtomicInteger(1);privateMyLoggingAdvisor(int order,boolean showSystemMessage,boolean showAvailableTools){this.order = order;this.showSystemMessage = showSystemMessage;this.showAvailableTools = showAvailableTools;}@OverridepublicintgetOrder(){returnthis.order;}@OverridepublicChatClientRequestbefore(ChatClientRequest chatClientRequest,AdvisorChain advisorChain){System.out.println("======================= 第 "+ cnt.getAndAdd(1)+" 轮 ====================================");StringBuilder sb =newStringBuilder("\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()instanceofToolCallingChatOptions 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);}}elseif(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;}@OverridepublicChatClientResponseafter(ChatClientResponse chatClientResponse,AdvisorChain advisorChain){StringBuilder sb =newStringBuilder("\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;}privateStringfirst(String text,int n){if(text.length()<= n){return text;}return text.substring(0, n)+"...";}publicstaticBuilderbuilder(){returnnewBuilder();}publicstaticclassBuilder{privateint order =0;privateboolean showSystemMessage =true;privateboolean showAvailableTools =true;publicBuilderorder(int order){this.order = order;returnthis;}publicBuildershowSystemMessage(boolean showSystemMessage){this.showSystemMessage = showSystemMessage;returnthis;}publicBuildershowAvailableTools(boolean showAvailableTools){this.showAvailableTools = showAvailableTools;returnthis;}publicMyLoggingAdvisorbuild(){MyLoggingAdvisor advisor =newMyLoggingAdvisor(this.order,this.showSystemMessage,this.showAvailableTools);return advisor;}}}

2.2 准备用于评审的代码

我们直接使用 实战 | 零基础搭建知识库问答机器人:基于SpringAI+RAG的完整实现 中的代码分块的内容作为待评审的内容,看下这段简单的文本分块工具会评审出什么内容

packagecom.git.hui.springai.app.demo;importorg.springframework.ai.document.Document;importjava.util.ArrayList;importjava.util.List;/** * 文档分块工具类 * 将长文档分割成较小的块,以便更好地进行向量化和检索 */publicclassDocumentChunker{privatefinalint maxChunkSize;privatefinalint overlapSize;publicstaticDocumentChunker DEFAULT_CHUNKER =newDocumentChunker();publicDocumentChunker(){this(500,50);// 默认值:最大块大小500个字符,重叠50个字符}publicDocumentChunker(int maxChunkSize,int overlapSize){this.maxChunkSize = maxChunkSize;this.overlapSize = overlapSize;}/** * 将文档分割成块 * * @param document 输入文档 * @return 分割后的文档块列表 */publicList<Document>chunkDocument(Document document){String content = document.getText();if(content ==null|| content.trim().isEmpty()){returnList.of(document);}List<String> chunks =splitText(content);List<Document> chunkedDocuments =newArrayList<>();for(int i =0; i < chunks.size(); i++){String chunk = chunks.get(i);String chunkId = document.getId()+"_chunk_"+ i;// 创建新的文档块,保留原始文档的元数据Document chunkDoc =newDocument(chunkId, chunk,newjava.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 分割后的文本块列表 */privateList<String>splitText(String text){List<String> chunks =newArrayList<>();// 按多种分隔符分割,优先在语义边界处分割(包括中文句号、问号、感叹号等)String[] sentences = text.split("(?<=。)|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)");StringBuilder currentChunk =newStringBuilder();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 =newStringBuilder();// 添加重叠部分,如果句子长度大于重叠大小,则只取末尾部分if(sentence.length()> overlapSize){String overlap = sentence.substring(Math.max(0, sentence.length()- overlapSize)); currentChunk.append(overlap); currentChunk.append(sentence);}else{ currentChunk.append(sentence);}}}}// 添加最后一个块if(currentChunk.length()>0){ chunks.add(currentChunk.toString());}return chunks;}/** * 强制将长文本分割成指定大小的块 * * @param text 输入文本 * @param maxSize 最大块大小 * @return 分割后的文本块列表 */privateList<String>forceSplit(String text,int maxSize){List<String> chunks =newArrayList<>();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 分割后的文档块列表 */publicList<Document>chunkDocuments(List<Document> documents){List<Document> allChunks =newArrayList<>();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(): 自定义日志记录,隐藏工具和系统消息详情

代码评审执行流程

请求执行

  • prompt(): 构建提示词
  • .call(): 发起AI请求
  • .content(): 获取返回结果
@BeanCommandLineRunnercommandLineRunner(ChatClient.Builder chatClientBuilder,@Value("${agent.skills.dirs:Unknown}")List<Resource> agentSkillsDirs)throwsIOException{return args ->{ChatClient chatClient = chatClientBuilder // @formatter:off.defaultSystem("始终运用现有技能协助用户满足其要求.")// Skills tool.defaultToolCallbacks(SkillsTool.builder().addSkillsResources(agentSkillsDirs).build())// 支持读取系统文件内容,用于读取我们需要评审的代码.defaultTools(FileSystemTools.builder().build())// 支持执行脚本,如果skills中存在script,那么这些脚本的执行,靠的就是它.defaultTools(ShellTools.builder().build()).defaultAdvisors(// Tool Calling advisorToolCallAdvisor.builder().build(),// Custom logging advisorMyLoggingAdvisor.builder().showAvailableTools(false).showSystemMessage(false).build()).build();// @formatter:onvar 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 执行演示

接下来我们启动项目,验证一下效果如何(在启动命令行参数中,配置上大模型的api-key,当然也可以直接在yml配置文件中进行配置)

@Slf4j@SpringBootApplicationpublicclassT01Application{publicstaticvoidmain(String[] args){SpringApplication.run(T01Application.class, args);}}

从上面的截图也可以看出,系统与大模型之间进行了三轮对话,

  1. 第一轮: 用户的原始诉求 -> 大模型
  • 大模型识别到需要进行代码评审,给系统响应 {"command":"code-reviewer"}
  1. 第二轮:系统接收到响应之后,读取skills的约束进行响应
  • 大模型返回需要读取需要评审的代码内容,对应的响应为 {"filePath":"D:\\Workspace\\hui\\project\\spring-ai-demo\\v2\\T01-agentic-skills-simple-design\\src\\main\\java\\com\\git\\hui\\springai\\app\\demo\\DocumentChunker.java"}
  1. 第三轮:系统读取代码内容,发送给大模型
  • 大模型返回评审结果

下面是完整的返回文本,有兴趣的小伙伴可以看看

基于对代码的详细分析,我为您提供以下代码评审报告: ## 📋 代码评审报告 - DocumentChunker.java ### ✅ **优点** 1. **功能完整性强**:代码实现了完整的文档分块功能,包括: - 单文档分块 - 批量文档分块 - 智能语义边界分割 - 重叠机制 - 长文本强制分割 2. **良好的文档注释**:类和方法都有清晰的中文注释,说明了功能用途 3. **合理的默认值**:提供了合理的默认参数(maxChunkSize=500, overlapSize=50) 4. **元数据保留**:正确保留了原始文档的元数据,并添加了块相关的元数据 ### ⚠️ **需要改进的问题** #### 1. **线程安全问题** // 问题:静态实例不是线程安全的 public static DocumentChunker DEFAULT_CHUNKER = new DocumentChunker(); **建议**:使用 `volatile` 或 `AtomicReference`,或者移除静态实例让用户自己管理。 #### 2. **参数验证不足** // 问题:构造函数没有参数验证 public DocumentChunker(int maxChunkSize, int overlapSize) { this.maxChunkSize = maxChunkSize; this.overlapSize = overlapSize; } **建议**:添加参数验证,确保 `maxChunkSize > 0` 且 `overlapSize < maxChunkSize`。 #### 3. **正则表达式复杂度** // 问题:正则表达式过于复杂,难以维护 String[] sentences = text.split("(?<={。})|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)"); **建议**:提取为常量或使用更清晰的方式处理多语言分隔符。 #### 4. **性能优化空间** // 问题:StringBuilder操作可以优化 if (currentChunk.length() > 0) { currentChunk.append(sentence); } else { currentChunk.append(sentence); } **建议**:简化为 `currentChunk.append(sentence);` #### 5. **异常处理缺失** 代码中没有处理可能的异常情况,如: - 内存不足 - 文档过大导致性能问题 ### 🔧 **具体改进建议** #### 1. **添加参数验证** public DocumentChunker(int maxChunkSize, int overlapSize) { if (maxChunkSize <= 0) { throw new IllegalArgumentException("maxChunkSize must be positive"); } if (overlapSize >= maxChunkSize) { throw new IllegalArgumentException("overlapSize must be less than maxChunkSize"); } this.maxChunkSize = maxChunkSize; this.overlapSize = overlapSize; } #### 2. **线程安全改进** public static final DocumentChunker DEFAULT_CHUNKER = new DocumentChunker(); // 或者移除静态实例,让用户自己管理 #### 3. **提取常量** private static final String SENTENCE_SEPARATOR_PATTERN = "(?<={。})|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)"; #### 4. **添加Builder模式** 考虑使用Builder模式来更灵活地配置参数。 #### 5. **性能优化** // 优化StringBuilder操作 private List<String> splitText(String text) { List<String> chunks = new ArrayList<>(); String[] sentences = text.split(SENTENCE_SEPARATOR_PATTERN); StringBuilder currentChunk = new StringBuilder(); for (String sentence : sentences) { if (sentence.trim().isEmpty()) continue; if (currentChunk.length() + sentence.length() <= maxChunkSize) { currentChunk.append(sentence); } else { // 处理块分割逻辑 } } // ... } ### 🎯 **总体评价** 这是一个功能完整、设计合理的文档分块工具类,主要问题集中在线程安全、参数验证和代码可维护性方面。通过上述改进,可以显著提升代码的健壮性和可维护性。 **建议优先级**: 1. 高优先级:参数验证、线程安全 2. 中优先级:性能优化、代码简化 3. 低优先级:Builder模式、异常处理 

三、小结

SpringAI的Agent开发范式配合Skills机制,非常简单就实现了AI应用的工程化。整个过程实现下来,门槛还是比较低的。 不得不高喊一声:Spring🐂🍺

实现方式虽然简单,但是这个背后的设计哲学、开发思维的转变,还是很值得我们学习参考的——在AI时代,如何将我们现有的技能(如可复用、模块化思设计等)转变到大模型应用开发,这可能是我们每一个旧时代程序员最大的财富。

那么这一套是怎么实现的呢?

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

  1. 发现(启动阶段)
  • 通过SKILL.md文件中的元数据,快速实现技能的安装注册
  1. 语义匹配(对话过程中)
  • 当用户发出请求时,LLM 会检查工具定义中嵌入的技能描述。如果 LLM 判断用户请求在语义上与某个技能的描述匹配,则会调用该技能工具,并将技能名称作为参数传递给它。
  1. 执行(技能调用时)
  • 当调用技能工具时,SkillsTool会从磁盘加载完整的SKILL.md内容,并将其与技能的基础目录路径一起返回给大型语言模型(LLM)。然后,LLM会按照技能内容中的指令执行。如果技能引用了其他文件或辅助脚本,LLM会使用FileSystemToolsRead函数或ShellToolsBash函数来按需访问它们

项目源码:

零基础入门:


实战

参考:

Read more

最新电子电气架构(EEA)调研-3

而新一代的强实时性、高确定性,以及满足CAP定理的同步分布式协同技术(SDCT),可以实现替代TSN、DDS的应用,且此技术已经在无人车辆得到验证,同时其低成本学习曲线、无复杂二次开发工作,将开发人员的劳动强度、学习曲线极大降低,使开发人员更多的去完成算法、执行器功能完善。 五、各大车厂的EEA 我们调研策略是从公开信息中获得各大车厂的EEA信息,并在如下中进行展示。 我们集中了华为、特斯拉、大众、蔚来、小鹏、理想、东风(岚图)等有代表领先性的车辆电子电气架构厂商。        1、华为 图12 华为的CCA电子电气架构              (1)华为“计算+通信”CC架构的三个平台                         1)MDC智能驾驶平台;                         2)CDC智能座舱平台                         3)VDC整车控制平台。        联接指的是华为智能网联解决方案,解决车内、车外网络高速连接问题,云服务则是基于云计算提供的服务,如在线车主服务、娱乐和OTA等。 华

By Ne0inhk
Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践

Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践

Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践 文章目录 * Apache IoTDB 架构特性与 Prometheus+Grafana 监控体系部署实践 * Apache IoTDB 核心特性与价值 * Apache IoTDB 监控面板完整部署方案 * 安装步骤 * 步骤一:IoTDB开启监控指标采集 * 步骤二:安装、配置Prometheus * 步骤三:安装grafana并配置数据源 * 步骤四:导入IoTDB Grafana看板 * TimechoDB(基于 Apache IoTDB)增强特性 * 总结与应用场景建议 Apache IoTDB 核心特性与价值 Apache IoTDB 专为物联网场景打造的高性能轻量级时序数据库,以 “设备 - 测点” 原生数据模型贴合物理设备与传感器关系,通过高压缩算法、百万级并发写入能力和毫秒级查询响应优化海量时序数据存储成本与处理效率,同时支持边缘轻量部署、

By Ne0inhk
SQL Server 2019安装教程(超详细图文)

SQL Server 2019安装教程(超详细图文)

SQL Server 介绍) SQL Server 是由 微软(Microsoft) 开发的一款 关系型数据库管理系统(RDBMS),支持结构化查询语言(SQL)进行数据存储、管理和分析。自1989年首次发布以来,SQL Server 已成为企业级数据管理的核心解决方案,广泛应用于金融、电商、ERP、CRM 等业务系统。它提供高可用性、安全性、事务处理(ACID)和商业智能(BI)支持,并支持 Windows 和 Linux 跨平台部署。 一、获取 SQL Server 2019 安装包 1. 官方下载方式 前往微软官网注册账号后,即可下载 SQL Server Developer 版本(

By Ne0inhk