spring-ai|Spring‑AI 2.0.0 新特性 + 入门教程
Spring AI 2.0.0 GA 最低依赖 Spring Boot 4.x、Spring Framework 7、Java21 不再兼容 Spring Boot3 底层切换厂商官方 SDK,大量 API 重构稳定性提升,面向 Agent、RAG 生产环境
一、核心新特性Spring
1. 底层架构重大升级
- 全链路 JSpecify 空安全注解,大量返回
Optional,减少 NPE,Kotlin 友好;Options 配置全部改为不可变 Builder 模式,移除 setter 方法Spring。 - Jackson3 序列化,新增
JsonHelper工具类,自定义 JSON 更方便。 - 模型厂商剥离到独立仓库:OpenAI、Anthropic 不再内置 http 封装,直接使用官方 Java SDK;OpenAI 兼容接口依然可用Microsoft ...。
- 部分向量存储移除:SAP HANA、Infinispan 被移除,需要更换其他向量库。
2. Tool‑Calling(工具调用)Agent 能力大幅增强
- 术语统一:
FunctionCallback→ToolCallback,``functions()→tools(),行业对齐 Agent 术语Spring。 ToolSearchToolCallingAdvisor:工具数量多(>30)时,不会一次性把全部工具定义传给大模型,按需检索工具,减少 token 消耗,解决上下文膨胀问题,支持 regex/lucene/vector 三种索引模式Spring。ToolExecutionEligibilityChecker:自定义工具调用循环终止条件,灵活控制 Agent 执行逻辑Spring。
3. 结构化输出增强(Self‑Correcting)
.entity()结构化输出新增自校验纠错机制:模型输出 JSON 不符合 Java Record schema,框架自动把错误返回给模型,让模型自我修正,提升结构化返回成功率;同时支持厂商原生结构化输出模式Spring。
4. ChatMemory 对话内存改进
PromptChatMemoryAdvisor必须显式传入conversationId会话 ID,不再隐式自动生成,消除会话状态混乱问题,生产环境更可控GitHub。MessageWindowChatMemory支持按对话轮次边界截断,避免工具消息重复注入内存。
5. RAG 与向量存储
- 只读向量存储接口
ReadOnlyVectorStore,纯检索场景不需要写权限。 - Advisor 模块包名重构:
spring‑ai‑advisors‑vector‑store→spring‑ai‑vector‑store‑advisorSpring。
6. 可观测性 & 评估 API
- Micrometer 完整可观测:自动埋点 token 消耗、调用耗时、模型调用链路,对接 Prometheus/Grafana。
- Evaluation 评估框架:自动化评估 RAG 回答质量,内置相关性评估、事实校验评估器,输出 0‑1 分数,用于自动化测试 RAG 系统效果。
7. API 变更重点(迁移注意)
- Options 类全部移除 setter,只能用 Builder 构建;
- OpenAI 属性类重命名
OpenAiConnectionProperties→OpenAiCommonProperties; - 旧
OpenAiApi自定义 API 层全部删除,替换为官方 SDK Client 对象GitHub
源码
spring-ai-sample: spring ai 和 spring ai alibaba 相关项目学习 demo - Gitee.com
部分源码
依赖
<properties> <spring-boot.version>4.0.0</spring-boot.version> <spring-ai.version>2.0.0</spring-ai.version> </properties> <dependencyManagement> <dependencies> <!-- 1. Spring Boot 依赖管理(替代 parent 的作用) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 2. Spring AI 依赖管理 --> <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>
package com.kcnf.ai.controller; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.Map; import java.util.UUID; /** * Spring‑AI 2.0.0 演示Controller * 两个ChatClientBean做隔离: * 2. memoryChatClient:装配了MessageChatMemoryAdvisor,需要传入ConversationId实现多轮上下文记忆 * */ @RestController @RequestMapping("/api/ai") public class AiMemoryController { /** * 带记忆Advisor的ChatClient,用于多轮对话,必须传入会话ID */ private final ChatClient memoryChatClient; public AiMemoryController( @Qualifier("memoryChatClient") ChatClient memoryChatClient) { this.memoryChatClient = memoryChatClient; } /** * ④多轮记忆对话(阻塞版) * POST /api/ai/chat/memory * Header:Conversation‑Id,不传自动生成UUID * advisors():给MemoryAdvisor传入会话ID,绑定ChatMemory存储的历史消息 */ @PostMapping("/chat/memory") public Map<String,String> chatWithMemory( @RequestParam(value = "message") String message, @RequestHeader(value = "Conversation-Id",required = false) String conversationId ){ // 没有传入会话ID,生成全新会话 if(conversationId == null || conversationId.isBlank()){ conversationId = UUID.randomUUID().toString(); } String finalConversationId = conversationId; String reply = memoryChatClient.prompt() .user(message) // 给记忆Advisor设置会话ID,读取/写入该会话历史 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, finalConversationId)) .call() .content(); return Map.of("conversationId",conversationId,"reply",reply); } /** * ⑥SSE流式 + 上下文记忆 * GET /api/ai/chat/memory/stream * Header Conversation‑Id:同一个ID,历史上下文会被读取 */ @GetMapping(value = "/chat/memory/stream",produces = "text/event-stream") public SseEmitter chatMemoryStream( @RequestParam(value = "message") String message, @RequestHeader(value = "Conversation-Id",required = false) String conversationId ){ SseEmitter emitter = new SseEmitter(60_000L); if(conversationId == null || conversationId.isBlank()){ conversationId = UUID.randomUUID().toString(); } String convId = conversationId; memoryChatClient.prompt() .user(message) // 流式请求同样必须指定CONVERSATION_ID .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, convId)) .stream() .content() .subscribe( token -> { try { emitter.send(SseEmitter.event().data(token)); }catch (Exception e){ emitter.completeWithError(e); } }, emitter::completeWithError, ()->{ try { emitter.send(SseEmitter.event().data("\n会话ID:"+convId)); }catch (Exception ignored){} emitter.complete(); } ); return emitter; } }
演示
