第1861篇:Spring AI 0.x到1.x的迁移指南——破坏性变更与平滑升级策略
第1861篇:Spring AI 0.x到1.x的迁移指南——破坏性变更与平滑升级策略
去年底有个做金融风控的朋友找我求助,说他们团队花了两个月基于 Spring AI 0.8 搭的对话系统,听说 1.0 正式版出来了想升级,结果一升级项目直接跑不起来,报错一堆,改了三天还是一塌糊涂,最后只好又回退回去。
这种情况我见过太多了。Spring AI 在 0.x 到 1.x 的这次版本跨越,不是普通的小版本更新,是真正意义上的架构级重构。很多 API 接口改了,包名变了,配置方式也不一样了。官方文档虽然有迁移说明,但写得比较零散,实际踩坑的细节基本靠自己摸索。
今天这篇文章,就把这次迁移的核心变更点、踩坑记录、以及平滑升级策略系统梳理一遍,给打算升级或者正在升级中的同学一个参考。
一、为什么这次升级这么"伤"
先说背景。Spring AI 0.x 系列是探索期版本,框架设计上有不少妥协和实验性质的东西。到 1.0,团队做了一次比较彻底的清理:
- 核心接口重新命名和语义对齐
- 模型调用链路做了抽象层重构
- 配置体系统一迁移到 Spring Boot AutoConfiguration
ChatClient的 API 设计被完全重写
官方把这些统称为 "breaking changes",但实际项目中影响远不止表面上那几个接口名字。很多隐式的约定和行为也变了,而这些才是最难排查的。
我自己经历过两个项目的迁移,一个是内部的知识库问答系统,一个是帮客户做的客服机器人,花的时间都比预期多。这里总结出来的东西,都是真实踩过的坑。
二、版本差异核心对比
先建立一个整体认知,然后再逐个击破。
主要的破坏性变更集中在以下几个维度:
| 变更维度 | 0.x 做法 | 1.x 做法 |
|---|---|---|
| 核心调用接口 | AiClient.generate() | ChatClient.prompt().call() |
| 模型抽象 | ChatClient 既是抽象又是实现 | ChatModel 负责底层,ChatClient 是高级封装 |
| Streaming | 单独方法 generateStream() | .stream() 流式链路 |
| 配置前缀 | spring.ai.openai.api-key (部分保留) | 统一规范,部分 key 变更 |
| Embedding | EmbeddingClient | EmbeddingModel |
| 向量存储 | VectorStore 接口基本一致,但实现类有变 | 接口稳定,部分实现类包名变 |
三、依赖变更:先把 pom.xml 改对
升级第一步是改依赖,这里有几个坑。
0.x 时代的依赖写法:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>0.8.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>1.x 的写法:
<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>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>表面上看只是版本号变了,但实际上 1.x 的 starter artifact id 也做了调整。部分 0.x 的 starter 在 1.x 中被合并或重命名。比如:
spring-ai-azure-openai-spring-boot-starter→ 名字保留但内部实现变spring-ai-vertex-ai-palm2-spring-boot-starter在 1.x 中已经废弃,要改用spring-ai-vertex-ai-gemini-spring-boot-starter
另一个坑:如果你的项目里同时依赖了多个 AI provider,在 0.x 里每个 provider 会各自注册一个 ChatClient bean,而 1.x 里注册的是 ChatModel,需要用 qualifier 区分。
四、最核心的变更:ChatClient API 重写
这是迁移工作量最大的地方。
0.x 的用法:
@Service
public class ChatService {
private final AiClient aiClient;
public ChatService(AiClient aiClient) {
this.aiClient = aiClient;
}
public String chat(String userMessage) {
Prompt prompt = new Prompt(userMessage);
AiResponse response = aiClient.generate(prompt);
return response.getGeneration().getText();
}
public String chatWithSystem(String systemPrompt, String userMessage) {
List<Message> messages = List.of(
new SystemMessage(systemPrompt),
new UserMessage(userMessage)
);
Prompt prompt = new Prompt(messages);
return aiClient.generate(prompt).getGeneration().getText();
}
}1.x 的对应写法:
@Service
public class ChatService {
private final ChatClient chatClient;
// 注意:1.x 里 ChatClient 要通过 Builder 来创建
public ChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
public String chatWithSystem(String systemPrompt, String userMessage) {
return chatClient.prompt()
.system(systemPrompt)
.user(userMessage)
.call()
.content();
}
}光看代码差异,可能觉得还好,改改就行。但麻烦的是 ChatClient 的注入方式变了。
在 0.x 里,AiClient 是一个 Spring bean,直接 @Autowired 就行。 在 1.x 里,你需要注入 ChatClient.Builder,然后调用 .build() 得到实例。或者在配置类里手动定义 ChatClient bean。
我见过最常见的迁移错误就是:把 0.x 的 AiClient 直接替换成 1.x 的 ChatClient,然后发现注入报错——因为 1.x 默认不直接把 ChatClient 注册为 bean,注册的是 ChatClient.Builder。
配置类里正确定义 ChatClient bean 的方式:
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("你是一个专业的AI助手,请用中文回答问题。")
.build();
}
// 如果有多个模型,用 qualifier 区分
@Bean
@Qualifier("gptClient")
public ChatClient openAiChatClient(
@Qualifier("openAiChatModel") ChatModel openAiChatModel) {
return ChatClient.builder(openAiChatModel)
.defaultSystem("你是一个专业的AI助手。")
.build();
}
}五、EmbeddingClient 到 EmbeddingModel 的迁移
这个改动比较直接,但容易漏掉。
0.x:
@Autowired
private EmbeddingClient embeddingClient;
public List<Double> embed(String text) {
EmbeddingResponse response = embeddingClient.embedForResponse(List.of(text));
return response.getResults().get(0).getOutput();
}1.x:
@Autowired
private EmbeddingModel embeddingModel;
public float[] embed(String text) {
// 注意返回类型也变了,从 List<Double> 变成了 float[]
return embeddingModel.embed(text);
}
// 批量 embed
public EmbeddingResponse embedBatch(List<String> texts) {
EmbeddingRequest request = new EmbeddingRequest(texts, EmbeddingOptions.EMPTY);
return embeddingModel.call(request);
}返回类型从 List<Double> 变成了 float[],这个变化如果涉及到向量存储的代码,需要一并修改。
六、Streaming 流式调用的迁移
0.x 里 streaming 是个独立方法,1.x 整合进了 prompt 调用链。
0.x 流式:
public Flux<String> streamChat(String message) {
Prompt prompt = new Prompt(message);
return aiClient.generateStream(prompt)
.map(response -> response.getGeneration().getText());
}1.x 流式:
public Flux<String> streamChat(String message) {
return chatClient.prompt()
.user(message)
.stream()
.content(); // 直接返回 Flux<String>
}
// 如果需要完整 response 对象
public Flux<ChatResponse> streamChatFull(String message) {
return chatClient.prompt()
.user(message)
.stream()
.chatResponse(); // 返回 Flux<ChatResponse>
}七、配置文件的变更
这块相对简单,但有几个属性名改了。
0.x 配置(application.yml):
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
model: gpt-3.5-turbo
temperature: 0.7
# 0.x 有些 embedding 配置在根级别
embedding:
model: text-embedding-ada-0021.x 配置:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.7
max-tokens: 2000
embedding:
options:
model: text-embedding-3-small注意两个变化:
- chat 相关配置移到了
chat.options下 - model 名字也可以顺手升级一下,
gpt-3.5-turbo换成gpt-4o,embedding 从ada-002换成3-small
如果用国内的通义千问或者智谱,配置前缀会有差异,要看各自 starter 的文档。
八、PromptTemplate 的变化
0.x 里 PromptTemplate 是个独立的工具类,1.x 里它还在,但推荐的用法有所不同。
0.x 做法:
PromptTemplate template = new PromptTemplate(
"请分析以下文本的情感倾向:\n{text}\n请给出积极/消极/中性的判断。"
);
Prompt prompt = template.create(Map.of("text", inputText));
String result = aiClient.generate(prompt).getGeneration().getText();1.x 推荐做法(两种都可以):
// 方式1:直接在 user() 里用占位符
String result = chatClient.prompt()
.user(u -> u.text("请分析以下文本的情感倾向:\n{text}\n请给出积极/消极/中性的判断。")
.param("text", inputText))
.call()
.content();
// 方式2:保留 PromptTemplate 用法(依然有效)
PromptTemplate template = new PromptTemplate(
"请分析以下文本的情感倾向:\n{text}\n请给出积极/消极/中性的判断。"
);
Prompt prompt = template.create(Map.of("text", inputText));
// 但 1.x 里要用 ChatModel 来调用
ChatResponse response = chatModel.call(prompt);
String result = response.getResult().getOutput().getContent();第二种方式里,getGeneration().getText() 也变了,变成了 getResult().getOutput().getContent(),这个改动非常隐蔽,编译不报错但运行报空指针。
九、Advisor 机制是 1.x 新增的核心特性
这不是迁移内容,但值得一提——1.x 引入了 Advisor 机制,这是 0.x 完全没有的东西。
Advisor 是一种 AOP 式的拦截器,可以在请求前后插入逻辑,比如:
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultAdvisors(
// 对话历史记忆
new MessageChatMemoryAdvisor(new InMemoryChatMemory()),
// 日志记录
new SimpleLoggerAdvisor()
)
.build();
}如果你在 0.x 里是手动管理对话历史(把之前的消息列表追加到 prompt 里),迁移到 1.x 后可以直接用 MessageChatMemoryAdvisor 替代,代码会简洁很多。
十、平滑升级策略:分阶段迁移
如果你的项目比较大,建议不要一次性全部迁移,分三个阶段来做:
阶段1:依赖升级 + 写适配层(1-2天)
先把依赖版本改到 1.x,然后写一个适配层,用新 API 包装出老接口的样子:
/**
* 迁移适配器:用 1.x API 模拟 0.x 的 AiClient 接口
* 迁移完成后删除这个类
*/
@Component
@Deprecated
public class LegacyAiClientAdapter {
private final ChatClient chatClient;
public LegacyAiClientAdapter(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 模拟 0.x 的 generate 方法签名
*/
public String generate(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
public Flux<String> generateStream(String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
}这样可以让项目先跑起来,再逐步把业务层的调用改成直接使用 ChatClient。
阶段2:逐模块迁移(视规模 3-7天)
按模块逐个改,改一个模块测一个模块,不要大范围改完再测试。重点关注:
AiClient→ChatClient的调用改写EmbeddingClient→EmbeddingModel的改写- response 获取方式的变更(
getText()→getContent())
阶段3:新特性接入 + 清理(1-2天)
把适配层删掉,同时考虑是否引入 Advisor 机制来简化之前手写的一些逻辑(比如对话历史管理)。
十一、常见报错与解决方案速查
整理了迁移中最频繁出现的几类错误:
报错1:No qualifying bean of type 'org.springframework.ai.chat.ChatClient'
原因:1.x 默认注册的是 ChatClient.Builder,不是 ChatClient。
解决:在配置类里定义 ChatClient bean,或者改成注入 ChatClient.Builder。
报错2:Cannot call getGeneration() - method not found
原因:1.x 的 ChatResponse 的方法名改了。
解决:把 .getGeneration().getText() 改成 .getResult().getOutput().getContent()。
报错3:多个 ChatModel bean 导致的歧义错误
原因:同时引入了多个 AI provider 的 starter,每个都注册了一个 ChatModel bean。
解决:使用 @Primary 标注主要使用的,其他的用 @Qualifier 区分注入。
@Bean
@Primary
public ChatClient primaryChatClient(
@Qualifier("openAiChatModel") ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}报错4:Streaming 时 generateStream 方法找不到
原因:1.x 的 ChatClient 没有 generateStream 方法,改用 .stream() 链路。
解决:按前面的流式迁移方式重写。
十二、一些坑后的感悟
说实话,这次迁移让我有点理解了 Spring 团队的决策逻辑。0.x 确实有些设计问题:ChatClient 这个名字承担了太多职责,既是高级 API 又是低层抽象,导致扩展性差。1.x 把 ChatModel 和 ChatClient 分层,是更合理的设计。
但对业务团队来说,框架的每次破坏性变更都是有成本的。我的建议是:新项目直接用 1.x,不要碰 0.x 了;老项目如果运行稳定,可以等有新需求再升级,趁机做一次系统性重构;如果 0.x 项目本身就有设计问题想重构,那升级到 1.x 是个好时机,两件事一起做。
最后提醒一点:升级之前一定要先写好测试,特别是核心的对话流程和 embedding 相关的逻辑,不然升级之后很难验证行为是否一致。这是我做第一次迁移时忽略的,吃了亏。
