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
源码
https://gitee.com/kcnf_open/spring-ai-sample/tree/master/spring-ai/spring-ai-v2
部分源码
依赖
<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;
}
}
演示

