Spring AI 升级的血泪史——从 M1 到 1.0 GA 的迁移记录
Spring AI 升级的血泪史——从 M1 到 1.0 GA 的迁移记录
如果你现在还在用 Spring AI 的某个 milestone 版本,这篇文章可能会让你推迟升级计划,也可能让你下定决心尽快升——取决于你的项目有多深。
我是在 Spring AI 还是 0.8.x 的时候开始用的,一路跟到 1.0 GA。每次大版本迭代,都有或多或少的 breaking change。1.0 GA 那次最猛,基本上把几个核心 API 都重构了一遍。
这篇文章不是官方文档,是我踩坑总结。
先说结论:值不值得升到 1.0 GA
如果你还在 0.8.x 或更早版本:值得,而且建议尽快。理由:
- 1.0 GA 的 API 稳定了,不会再有大的 breaking change
- BOM 管理完善,依赖冲突问题少了很多
- 流式输出的稳定性大幅提升(早期版本流式经常丢最后几个 token)
- Advisor 体系完整了,扩展性好很多
- 社区活跃,文档质量提升明显
如果你在 1.0.0-SNAPSHOT 或某个 RC 版本:也建议升,RC 和 SNAPSHOT 的 bug 还不少,GA 修了很多。
代价是:迁移过程会有痛苦。我预估中等规模的项目(5~10 个 service 使用 Spring AI)大约需要 1~2 天的迁移时间。
最大的变化:ChatClient API 的重构
这是影响面最大的一个变化。
Before(0.x 时代)
老版本里,大家直接用 ChatModel(或者叫 ChatClient,命名混乱是那个时期的问题之一):
// 0.x 老写法
@Autowired
private ChatClient chatClient; // 0.x 里 ChatClient 就是模型调用入口
public String chat(String message) {
// 直接调用,没有 builder 模式
return chatClient.call(message);
}
// 带 Prompt 的调用
public String chatWithPrompt(String message) {
Prompt prompt = new Prompt(
List.of(
new SystemMessage("你是助手"),
new UserMessage(message)
)
);
ChatResponse response = chatClient.call(prompt);
return response.getResult().getOutput().getContent();
}After(1.0 GA)
1.0 里,ChatModel 和 ChatClient 彻底分离了:
ChatModel:低层接口,直接发 Prompt,返回ChatResponse。面向框架开发者。ChatClient:高层接口,Builder 模式,支持 Advisor 链,面向业务开发者。
// 1.0 新写法 - ChatClient(推荐,日常业务开发)
@Autowired
private ChatClient chatClient; // 注意:这里的 ChatClient 和老版本不是同一个接口!
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
// 带系统 Prompt
public String chatWithSystem(String message) {
return chatClient.prompt()
.system("你是一个 Java 技术专家")
.user(message)
.call()
.content();
}
// 需要完整 ChatResponse(比如要拿 usage 信息)
public ChatResponse chatFull(String message) {
return chatClient.prompt()
.user(message)
.call()
.chatResponse();
}ChatClient 的 Bean 不是自动注入的,需要你自己用 Builder 创建:
@Configuration
public class ChatConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是一个专业的技术助手")
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}迁移要点:
把所有 chatClient.call(message) 替换成 chatClient.prompt().user(message).call().content()。
如果你原来代码里有大量 new Prompt(List.of(...)) 的写法,要重构成 Builder 链式调用。
PromptTemplate 的变化
Before
// 0.x
PromptTemplate promptTemplate = new PromptTemplate(
"帮我用 {language} 写一个 {task}",
Map.of("language", "Java", "task", "排序算法")
);
Prompt prompt = promptTemplate.create();After
// 1.0 - PromptTemplate 接口和用法变了
// 方式一:直接用 String.format 或 MessageTemplate
String userText = "帮我用 {language} 写一个 {task}";
Map<String, Object> params = Map.of("language", "Java", "task", "排序算法");
chatClient.prompt()
.user(u -> u.text(userText).params(params))
.call()
.content();
// 方式二:还是可以用 PromptTemplate,但构造方式不同
PromptTemplate template = PromptTemplate.create("帮我用 {language} 写一个 {task}");
String rendered = template.render(Map.of("language", "Java", "task", "排序算法"));
chatClient.prompt().user(rendered).call().content();OutputConverter 的变化
Before(0.x)
// 0.x 里 OutputParser 是旧名字
BeanOutputParser<MyBean> parser = new BeanOutputParser<>(MyBean.class);
String format = parser.getFormat();
// 手动把 format 加到 prompt 里...
String raw = chatClient.call(message + "\n" + format);
MyBean result = parser.parse(raw);After(1.0 GA)
名字从 OutputParser 改成了 OutputConverter:
// 1.0 - BeanOutputConverter(注意改了名字)
BeanOutputConverter<MyBean> converter = new BeanOutputConverter<>(MyBean.class);
// 直接用 ChatClient 集成,无需手动处理 format
MyBean result = chatClient.prompt()
.user(u -> u.text("提取以下信息:{text}").param("text", inputText))
.call()
.entity(MyBean.class); // 1.0 新增的便捷方法entity(Class) 这个方法是 1.0 新加的,内部自动处理了 format instruction 的注入和结果的反序列化,简洁了很多。
AutoConfiguration 的变化
这部分变化是影响「配置方式」的。
Before
# 0.x 配置方式
spring:
ai:
openai:
api-key: xxx
model: gpt-4 # 直接在这里配置 model
temperature: 0.7After(1.0 GA)
# 1.0 配置方式
spring:
ai:
openai:
api-key: xxx
chat:
options:
model: gpt-4o # 注意层级变了,放在 chat.options 下
temperature: 0.7
embedding:
options:
model: text-embedding-3-small层级加深了,model 相关的配置都在 chat.options 或 embedding.options 下面。
另一个变化是 BOM 的使用方式:
<!-- 1.0 GA 的 BOM 引入方式 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 之后各依赖不需要指定版本 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>VectorStore 接口的变化
Before
// 0.x
vectorStore.add(documents);
List<Document> results = vectorStore.similaritySearch("查询文本");After
// 1.0 - similaritySearch 支持 SearchRequest
vectorStore.add(documents);
// 简单查询(还是能用字符串,有默认 topK=4)
List<Document> results = vectorStore.similaritySearch("查询文本");
// 带参数查询
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("查询文本")
.topK(5)
.similarityThreshold(0.7) // 新增:过滤相似度太低的结果
.filterExpression(fb.eq("department", "tech").build()) // 新增:元数据过滤
.build()
);
// 删除操作(1.0 新增)
vectorStore.delete(List.of("docId1", "docId2")); // 按 ID 删除similarityThreshold 这个参数很有用,我用 0.x 时候一直手动过滤低质量的检索结果,1.0 直接支持了。
流式 API 的变化
Before
// 0.x 的流式,比较 raw
Flux<ChatResponse> stream = chatModel.stream(prompt);
stream.subscribe(response -> {
// 处理每个 chunk
});After
// 1.0 的流式,通过 ChatClient
Flux<String> contentStream = chatClient.prompt()
.user(message)
.stream()
.content(); // 直接拿 content 的 Flux
// 或者拿完整的 ChatResponse flux
Flux<ChatResponse> responseStream = chatClient.prompt()
.user(message)
.stream()
.chatResponse();
// 拼接全部内容
String fullContent = chatClient.prompt()
.user(message)
.stream()
.content()
.collectList()
.map(chunks -> String.join("", chunks))
.block();一些不那么明显的 Breaking Change
1. Message 类的包路径变了
// Before (0.x)
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.SystemMessage;
// After (1.0) - 包路径基本没变,但有些类移动了
// 主要检查:如果有 import 报错,一般是类移动了,用 IDE 自动修复2. Document 的 metadata 获取方式
// Before
String value = document.getMetadata().get("key").toString();
// After - 没变,但注意 1.0 里 metadata 值的类型可能变化
// 特别是数值类型,原来可能是 Integer,现在可能是 String
Object raw = document.getMetadata().get("key");
String value = raw != null ? raw.toString() : null;3. ChatOptions 的构建方式
// Before
OpenAiChatOptions options = new OpenAiChatOptions();
options.setModel("gpt-4");
options.setTemperature(0.7f);
// After - 统一用 Builder
OpenAiChatOptions options = OpenAiChatOptions.builder()
.withModel("gpt-4o")
.withTemperature(0.7) // 注意:从 float 改成了 Double
.build();完整的迁移 Checklist
我把自己迁移时用的 checklist 整理出来,供参考:
[ ] 升级 spring-ai BOM 版本到 1.0.0
[ ] 检查所有 ChatClient 注入点(老版本里 ChatClient 就是模型,新版本是 Builder 产出)
[ ] 把 chatClient.call(message) 改成 chatClient.prompt().user().call().content()
[ ] 把 new Prompt(List.of(...)) 改成 ChatClient Builder 链式调用
[ ] 检查 application.yml,model 配置移到 spring.ai.[provider].chat.options.model
[ ] BeanOutputParser 改名为 BeanOutputConverter
[ ] similaritySearch(String) 改为 similaritySearch(SearchRequest)(推荐,字符串方法还在)
[ ] 检查 @Async 配置,确保 Advisor 链的异步行为符合预期
[ ] 流式接口:chatModel.stream() 换成 chatClient.prompt().stream()
[ ] 运行集成测试,验证核心 AI 功能流程
[ ] 检查日志,看是否有 WARN 级别的 deprecated API 警告我的升级教训
有一次升级到某个 RC 版本,发现 PromptChatMemoryAdvisor 的构造参数顺序变了(chatMemory 和 conversationId 的参数顺序对调了),编译不报错(两个都是 Object 或 String),运行时行为完全错了,排查了两个小时才发现。
教训:RC 和 SNAPSHOT 版本升级后,一定要跑集成测试,不能只看编译通过。API 参数类型相同但顺序不同的 breaking change,编译器检查不出来。
另一个教训:不要一次性从 0.8 直接跳到 1.0,中间最好经过一个 M 版本。如果跨度太大,出了问题不知道是哪个版本引入的,排查成本很高。
