Spring AI 1.0 正式版——从 M1 到正式版,我们的迁移踩坑记录
Spring AI 1.0 正式版——从 M1 到正式版,我们的迁移踩坑记录
适读人群:使用 Spring AI 里程碑版本、需要升级到正式版的 Java 开发者 | 阅读时长:约 15 分钟 | 核心价值:避开迁移中那些让人崩溃的坑
2025 年 5 月,Spring AI 1.0.0 正式发布。群里有人欢呼,我当时只有一个想法:惨了,得改代码了。
我们有两个项目在用 Spring AI,一个用的 1.0.0-M3,一个用的 1.0.0-M6。两个项目的迁移经历完全不同,踩的坑也不一样。
说结论:M3 到正式版,这一路变动太大了,不能指望改几行配置就搞定,得认真读 migration guide,而且 migration guide 里没写清楚的地方还不少,那些才是真正的坑。
先说整体变化
Spring AI 从 M1 走到 1.0 正式版,经历了好几次包名大换血、API 重命名和架构调整。最大的几个变化:
变化一:包名重构
M1-M2 时代:org.springframework.ai.client.* M3-M6 时代:org.springframework.ai.chat.client.* 1.0 正式版:org.springframework.ai.chat.client.*(继续,但内部有调整)
如果你还在用 M1/M2,ChatClient 接口已经完全被重写,不是改包名那么简单。
变化二:ChatClient 从接口变成了 Fluent Builder
这个变化影响最大。
M3 之前的用法:
// 老写法,M3之前
@Autowired
private ChatClient chatClient;
public String chat(String message) {
return chatClient.call(message);
}1.0 正式版的用法:
// 1.0 正式版写法
@Autowired
private ChatClient.Builder chatClientBuilder;
private ChatClient chatClient;
@PostConstruct
public void init() {
this.chatClient = chatClientBuilder.build();
}
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}这不只是改个方法名,ChatClient 的整个使用范式都变了。
M3 到 1.0 的具体迁移
依赖变化
M3 的 pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M3</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>
<!-- M3时期的向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
</dependencies>1.0 正式版的 pom.xml:
<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-starter-model-openai</artifactId>
</dependency>
<!-- 1.0 向量存储 starter 命名改了 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
</dependencies>注意 starter 的命名规则变了:
- 以前:
spring-ai-{模型/组件}-spring-boot-starter - 现在:
spring-ai-starter-{类型}-{具体名称}
ChatClient 的迁移
M3 时期,很多人是这么用的:
// M3 的写法
@Service
public class ChatService {
private final ChatClient chatClient;
// M3 时期 ChatClient 可以直接注入
public ChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String generateResponse(String userMessage, String systemPrompt) {
Prompt prompt = new Prompt(
List.of(
new SystemMessage(systemPrompt),
new UserMessage(userMessage)
)
);
ChatResponse response = chatClient.call(prompt);
return response.getResult().getOutput().getContent();
}
}迁移到 1.0 正式版:
// 1.0 正式版的写法
@Service
public class ChatService {
private final ChatClient chatClient;
// 注入 Builder,不直接注入 ChatClient
public ChatService(ChatClient.Builder chatClientBuilder) {
// 在这里可以预设系统提示词、默认参数等
this.chatClient = chatClientBuilder
.defaultOptions(
OpenAiChatOptions.builder()
.temperature(0.7)
.build()
)
.build();
}
public String generateResponse(String userMessage, String systemPrompt) {
return chatClient.prompt()
.system(systemPrompt)
.user(userMessage)
.call()
.content();
}
// 流式输出
public Flux<String> streamResponse(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.stream()
.content();
}
}这里有个坑:如果你在配置类里定义了 ChatClient Bean,要改成定义 ChatClient.Builder 的定制逻辑,不然会和自动配置冲突。
// 错误写法:定义了 ChatClient Bean,但 Spring AI 也会自动配置一个
// 导致注入时报 "expected single matching bean but found 2"
@Configuration
public class AiConfig {
@Bean // ❌ 不要这样做
public ChatClient chatClient(OpenAiChatModel model) {
return ChatClient.create(model);
}
}
// 正确写法:通过 Builder 定制
@Configuration
public class AiConfig {
@Bean
public ChatClient.Builder chatClientBuilder(
OpenAiChatModel model,
@Value("${app.ai.system-prompt}") String systemPrompt) {
return ChatClient.builder(model)
.defaultSystem(systemPrompt);
}
}RAG 相关的 API 变化
这块变化最多,也是我踩坑最集中的地方。
DocumentReader 接口变化:
// M3 时期
public class CustomDocumentReader implements DocumentReader {
@Override
public List<Document> read() {
// ...
}
}
// 1.0 正式版,接口改名了
public class CustomDocumentReader implements DocumentReader {
@Override
public List<Document> read() {
// 接口名没变,但 Document 类的 API 变了
}
}
// Document 的构建方式变化
// M3 时期
Document doc = new Document("content", Map.of("source", "manual.pdf"));
// 1.0 正式版,用 Builder
Document doc = Document.builder()
.text("content")
.metadata("source", "manual.pdf")
.build();VectorStore 的查询 API 变化:
// M3 时期
SearchRequest request = SearchRequest.query("搜索内容").withTopK(5);
List<Document> results = vectorStore.similaritySearch(request);
// 1.0 正式版
SearchRequest request = SearchRequest.builder()
.query("搜索内容")
.topK(5)
.similarityThreshold(0.7)
.build();
List<Document> results = vectorStore.similaritySearch(request);这个变化看起来小,但如果你代码里有十几个地方用了 SearchRequest.query(),会有一堆编译错误。
QuestionAnswerAdvisor 的使用方式变化
M3 时期的 RAG advisor 用法:
// M3 时期
@Bean
public QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) {
return new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults());
}
// 注入后使用
ChatResponse response = chatClient.call(
new Prompt(userMessage,
AiOptions.builder().advisor(questionAnswerAdvisor).build())
);1.0 正式版的 advisor 链式调用:
// 1.0 正式版——advisor 在 prompt 构建时指定
@Service
public class RagChatService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public RagChatService(ChatClient.Builder builder, VectorStore vectorStore) {
this.vectorStore = vectorStore;
this.chatClient = builder.build();
}
public String ragChat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.advisors(new QuestionAnswerAdvisor(
vectorStore,
SearchRequest.builder()
.topK(5)
.similarityThreshold(0.7)
.build()
))
.call()
.content();
}
// 带对话历史的 RAG
public String ragChatWithHistory(String userMessage, String conversationId) {
return chatClient.prompt()
.user(userMessage)
.advisors(
new QuestionAnswerAdvisor(vectorStore),
new MessageChatMemoryAdvisor(
new InMemoryChatMemory(),
conversationId,
10 // 保留最近10轮对话
)
)
.call()
.content();
}
}一个隐藏的大坑:Embedding 模型的 auto-configuration
M6 升到 1.0 时,有个坑让我找了两个小时。
我们的配置文件里只配了 chat model 的 API key,embedding model 用的是不同的配置:
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
embedding:
options:
model: text-embedding-3-small升级到 1.0 之后,应用启动报错:
No qualifying bean of type 'org.springframework.ai.embedding.EmbeddingModel' available原因是 1.0 正式版调整了 embedding 的 auto-configuration 触发条件。解决方法是显式引入 embedding 依赖:
<!-- 如果 embedding 和 chat 用同一个模型提供商,这个已经包含在 starter 里了 -->
<!-- 但如果你单独配置了 embedding,需要确认 starter 是否包含 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<!-- 这个 starter 包含 chat 和 embedding 的自动配置 -->
</dependency>如果你用的是不同供应商(比如 chat 用 OpenAI,embedding 用另一个),需要分别引入各自的 starter,然后用 @Primary 标注主要使用的 EmbeddingModel。
迁移建议
基于我们两个项目的经历:
- 不要一次性升级,先在分支上试,跑通所有测试再合并
- 从 M3 升来的,优先检查:
Document构建方式、SearchRequest构建方式、Starter 依赖名称 - 从 M6 升来的,主要检查:Advisor 的 API 细节变化、auto-configuration 行为变化
- 认真跑一遍集成测试,很多问题不是编译错误,是运行时行为不一样了(比如相似度阈值的默认值变了)
Spring AI 的正式版质量比 M 版本稳定多了,升级的成本是值得的。但别低估这个迁移的工作量,特别是项目里用了比较多 Spring AI 特性的情况下。
