第2325篇:Spring AI 1.0特性深度解析——从快照版本到正式版的工程迁移指南
第2325篇:Spring AI 1.0特性深度解析——从快照版本到正式版的工程迁移指南
适读人群:正在使用Spring AI快照版本的Java工程师,或准备在项目中引入Spring AI 1.0的开发者 | 阅读时长:约18分钟 | 核心价值:掌握Spring AI 1.0核心变化,避免迁移踩坑
去年年初,我们团队在一个内部知识管理项目里引入了Spring AI 0.8.1-SNAPSHOT。当时想法很简单:先趟路,等正式版出来再迁移。
结果没想到,Spring AI在快照阶段的API变化比我预料的频繁得多。从0.8到0.9,ChatClient的构建方式改了;从0.9到1.0-M1,Advisor体系重构了;等到1.0 GA真正发布,我们才发现之前积累的代码有将近30%需要动。
这不是Spring AI团队的问题——快速迭代的框架在GA之前做API调整是正常的。但我们低估了迁移成本,也没有做好隔离层设计,导致迁移时花了将近两周。
这篇文章把1.0 GA的核心变化和迁移要点整理出来,希望后来的人少踩一些坑。
Spring AI 1.0:架构层面的关键变化
1. ChatClient成为一等公民
在0.8.x版本里,很多示例代码是直接用ChatModel的:
// 0.8.x的写法(依然可用,但不推荐)
@Service
public class OldChatService {
private final ChatModel chatModel;
public String chat(String message) {
return chatModel.call(new Prompt(message))
.getResult().getOutput().getContent();
}
}Spring AI 1.0把ChatClient正式确立为应用层的核心抽象,ChatModel退居底层实现层:
// 1.0推荐的写法
@Service
public class NewChatService {
private final ChatClient chatClient;
// 推荐在构造函数注入Builder,而不是直接注入ChatClient
public NewChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是专业的技术助手")
.build();
}
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
// 流式输出
public Flux<String> chatStream(String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
}为什么要注入Builder而不是ChatClient?因为ChatClient是有状态的——它携带了默认系统提示、默认工具等配置。如果你直接注入单例的ChatClient,所有服务共享一个实例,做多角色场景时会互相干扰。通过Builder,每个服务可以构建自己专属的ChatClient实例。
2. Advisor机制的重大重构
这是迁移里变化最大的部分,也是最容易踩坑的地方。
0.9.x里,Advisor的接口是这样的:
// 0.9.x的Advisor接口(已废弃)
public interface RequestResponseAdvisor {
AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context);
ChatClientResponse adviseResponse(ChatClientResponse response, Map<String, Object> context);
}1.0 GA里,Advisor体系拆分为两类:
// 1.0 GA:CallAroundAdvisor(用于非流式调用)
public interface CallAroundAdvisor extends Advisor {
AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);
}
// 1.0 GA:StreamAroundAdvisor(用于流式调用)
public interface StreamAroundAdvisor extends Advisor {
Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain);
}内置的几个常用Advisor也做了相应更新:
// 记忆Advisor - 写法变化了
// 旧写法
new MessageChatMemoryAdvisor(chatMemory, DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10)
// 新写法(1.0 GA)
MessageChatMemoryAdvisor.builder(chatMemory)
.conversationId("session-001")
.chatMemoryRetrieveSize(10)
.build()
// RAG Advisor
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.defaults().withTopK(5))
.build()迁移一个自定义Advisor的例子:
// 迁移前:0.9.x版本的日志Advisor
public class LoggingAdvisor implements RequestResponseAdvisor {
@Override
public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
log.info("请求:{}", request.userText());
return request;
}
@Override
public ChatClientResponse adviseResponse(ChatClientResponse response, Map<String, Object> context) {
log.info("响应:{}", response.chatResponse().getResult().getOutput().getContent());
return response;
}
}
// 迁移后:1.0 GA版本
@Component
public class LoggingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
private static final Logger log = LoggerFactory.getLogger(LoggingAdvisor.class);
@Override
public String getName() {
return "LoggingAdvisor";
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
long start = System.currentTimeMillis();
log.info("[AI请求] 用户输入:{}", advisedRequest.userText());
AdvisedResponse response = chain.nextAroundCall(advisedRequest);
long duration = System.currentTimeMillis() - start;
String content = response.response().getResult().getOutput().getContent();
log.info("[AI响应] 耗时:{}ms,输出长度:{}", duration, content.length());
return response;
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
log.info("[AI流式请求] 用户输入:{}", advisedRequest.userText());
long start = System.currentTimeMillis();
return chain.nextAroundStream(advisedRequest)
.doOnComplete(() -> {
long duration = System.currentTimeMillis() - start;
log.info("[AI流式响应完成] 耗时:{}ms", duration);
});
}
}3. Tool/Function Calling的注解方式统一
在1.0之前,Function Calling有多种写法:有用@Bean注册Function的,有实现java.util.function.Function接口的,也有用@Tool注解的。
1.0 GA里,推荐统一使用@Tool注解方式,这是最简洁、最符合Spring AI设计理念的:
@Component
public class OrderTools {
private final OrderRepository orderRepository;
public OrderTools(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Tool(description = "根据订单号查询订单状态,返回订单详情包括状态、金额、创建时间")
public OrderDetail queryOrder(
@ToolParam(description = "订单号,格式为ORD-YYYYMMDD-XXXXX") String orderId) {
return orderRepository.findById(orderId)
.map(order -> new OrderDetail(
order.getId(),
order.getStatus().name(),
order.getAmount(),
order.getCreatedAt().toString()
))
.orElseThrow(() -> new IllegalArgumentException("订单不存在: " + orderId));
}
@Tool(description = "取消订单,只有待支付状态的订单可以取消")
public String cancelOrder(
@ToolParam(description = "订单号") String orderId,
@ToolParam(description = "取消原因") String reason) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("订单不存在"));
if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
return "取消失败:订单状态为" + order.getStatus() + ",无法取消";
}
order.setStatus(OrderStatus.CANCELLED);
order.setCancelReason(reason);
orderRepository.save(order);
return "订单" + orderId + "已成功取消";
}
public record OrderDetail(String id, String status, BigDecimal amount, String createdAt) {}
}使用时:
// 在ChatClient中直接传入工具类实例
String result = chatClient.prompt()
.user("帮我查一下订单ORD-20260401-00123的状态")
.tools(orderTools) // 传入工具实例,框架自动发现@Tool方法
.call()
.content();依赖管理:BOM版本变化
这是迁移里最容易忘的一步,也是最容易导致ClassNotFoundException的地方。
<!-- 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>
<!-- 注意:快照版本需要额外配置snapshot仓库,1.0 GA已在Maven Central -->
<!-- 以下仓库配置在1.0 GA后可以移除 -->
<!--
<repositories>
<repository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
-->部分Starter的artifactId命名在1.0做了规范化:
<!-- OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- 如果继续用旧的artifactId,1.0仍然兼容,但建议迁移到新命名 -->
<!-- 旧:spring-ai-openai-spring-boot-starter -->
<!-- 新:spring-ai-starter-model-openai -->
<!-- PGVector向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<!-- Qdrant -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
</dependency>配置项变化:application.yml的更新
# Spring AI 1.0 GA配置示例(对比旧版变化)
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.openai.com
chat:
options:
model: gpt-4o
temperature: 0.7
max-tokens: 2048
embedding:
options:
model: text-embedding-3-small
# 向量存储配置
vectorstore:
pgvector:
index-type: HNSW # 1.0新增:支持HNSW索引(比IVFFLAT更快)
distance-type: COSINE_DISTANCE
dimensions: 1536
initialize-schema: true # 自动建表,生产环境建议关闭,手动管理schema一个容易漏的配置项——initialize-schema在快照版本里默认是true,1.0 GA里建议显式指定,避免在生产环境意外重建表结构:
// 生产环境建议用代码控制schema初始化
@Configuration
public class VectorStoreConfig {
@Bean
@Profile("!prod") // 非生产环境自动初始化
public PgVectorStore devVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
return PgVectorStore.builder(jdbcTemplate, embeddingModel)
.initializeSchema(true)
.build();
}
@Bean
@Profile("prod") // 生产环境不自动初始化
public PgVectorStore prodVectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
return PgVectorStore.builder(jdbcTemplate, embeddingModel)
.initializeSchema(false) // 通过Flyway/Liquibase管理schema
.build();
}
}迁移路径:从0.8.x/0.9.x到1.0的实操步骤
Step 1:先升级,看编译错误
直接把BOM版本号改到1.0.0,然后编译。编译错误就是你的迁移清单。常见错误:
RequestResponseAdvisor找不到 → 迁移到CallAroundAdvisorAdvisedRequest.userText()方法签名变化 → 查官方JavaDocChatClientResponse被移除 → 改用AdvisedResponse
Step 2:重点检查Advisor链顺序
1.0的Advisor执行顺序由getOrder()控制,数值越小越先执行(越靠近"外层")。内置Advisor的顺序:
LoggingAdvisor (LOWEST_PRECEDENCE = 外层)
└─ SafeGuardAdvisor
└─ MessageChatMemoryAdvisor
└─ QuestionAnswerAdvisor (最内层,离LLM最近)
└─ 实际LLM调用如果你有自定义Advisor,要明确设置getOrder(),不然可能跟内置Advisor顺序冲突,导致记忆或RAG功能不符合预期。
Step 3:验证流式输出
流式输出的API在1.0里有细节调整:
// 1.0的流式输出,注意返回类型
Flux<String> stream = chatClient.prompt()
.user("讲个故事")
.stream()
.content(); // 直接得到Flux<String>
// 如果需要元数据(token用量等)
Flux<ChatResponse> fullStream = chatClient.prompt()
.user("讲个故事")
.stream()
.chatResponse(); // 得到完整的ChatResponse流
// Controller层的SSE处理
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream()
.content()
.map(content -> ServerSentEvent.builder(content).build())
.doOnError(e -> log.error("流式输出错误", e));
}一个真实迁移案例:知识库系统
我们内部的知识管理系统迁移过程中,遇到了一个特别麻烦的问题:旧的QuestionAnswerAdvisor接受VectorStore作为构造参数,新版本改成了Builder模式。而且新版本的检索结果排序逻辑也变了。
// 旧版(0.9.x)
QuestionAnswerAdvisor ragAdvisor = new QuestionAnswerAdvisor(vectorStore,
SearchRequest.defaults().withTopK(5));
// 新版(1.0 GA)
QuestionAnswerAdvisor ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.defaults()
.withTopK(5)
.withSimilarityThreshold(0.7)) // 新增:过滤低相关性结果
.build();迁移后发现召回率下降了——因为新版默认启用了相似度阈值过滤,之前有些"勉强相关"的文档被过滤掉了。这不是bug,而是更合理的默认行为,但如果你的知识库文档质量参差不齐,需要手动调低阈值。
调试方法:
// 打开DEBUG日志,查看每次检索的结果和分数
logging:
level:
org.springframework.ai: DEBUG
// 或者临时打印检索结果
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(10)
.withSimilarityThreshold(0.0) // 临时设为0,看所有结果
);
docs.forEach(doc ->
log.info("相似度:{},内容片段:{}",
doc.getMetadata().get("distance"),
doc.getContent().substring(0, Math.min(100, doc.getContent().length()))));写在最后
Spring AI 1.0 GA标志着这个框架从实验性阶段进入了生产可用阶段。和所有框架的1.0版本一样,它不是终点,而是起点——之后的API稳定性会有更强的保证。
从快照版本迁移到1.0,最难的不是代码变化,而是心态:要接受"之前写的代码可能需要重写"这个现实。建议在迁移时不要边改边修,而是先把所有编译错误列出来,制定迁移计划,再集中处理。
另外,1.0 GA之后建议在项目里加一层AiGateway或AiService的抽象,把所有Spring AI的调用收归到一处。下次升级框架时,只需要改这一层,不会影响业务代码。
