本文基于 Spring AI 2.0.0 (截至 2026-08-20)编写,升级依据为官方 Upgrade Notes。该领域迭代极快,请以官方文档为准。
开篇:2.0 不是"下一个版本",是"换了一次底子"
摘要 :本文基于官方 Upgrade Notes,把 Spring AI 2.0 的三十多条变更归纳为 9 个破坏性变更------基线上跳 Boot 4 / Framework 7 / Jackson 3、Options 不可变 Builder 化、ChatClient 取代 ChatModel、模型提供商精简、工具调用循环上移、配置扁平化、MCP 生态换血、对话记忆重构,以及温度 / maxTokens / JSON Schema / 观测指标等细节变化;并附上一个 1.1.x 项目的完整升级记录与踩坑清单。

2026 年 6 月 12 日,Spring AI 2.0.0 GA 发布。如果你把这个版本当成普通的 1.x → 1.y 升级,编译报错会教你做人------这次的破坏面远超任何一个次版本:
- 基线直接从 Spring Boot 3.5 跳到 Boot 4.0/4.1 + Framework 7 ,连 JSON 库都从 Jackson 2 换成了 Jackson 3;
- 官方明确表态:
ChatClient是唯一推荐入口,ChatModel降级为底层构建块; - 工具调用循环从模型内部搬到了 Advisor 链上;
- MCP 生态整体换血:SDK 升 2.0、Streamable HTTP 成为默认传输、注解和传输模块全部搬进 Spring AI 名下。
社区对这次升级有一句很准确的概括:从"SDK 集合"走向"AI 原生运行时"。Spring AI 想做的不是"对接各家模型的胶水层",而是像 JDBC 之于数据库那样,成为 Java 生态里 AI 能力的统一运行时底座------为此它不惜把 API 掀翻重来。
这篇文章把官方 Upgrade Notes 里三十多条变更归纳成 9 个破坏性变更,逐条给出 1.x / 2.0 代码对比和迁移动作。最后是一个真实场景项目的完整升级记录。建议先收藏,升级时对照着做。
变更一:基线大跳版------Boot 4 / Framework 7 / Jackson 3 / JSpecify
这是所有变更里最"硬"的一个,因为它不由 Spring AI 决定,而是整个底座升级了。
新旧基线对比
| 项目 | 1.x | 2.0 |
|---|---|---|
| Spring Boot | 3.2 ~ 3.5 | 4.0 / 4.1 |
| Spring Framework | 6.x | 7.0 |
| Jackson | 2(com.fasterxml.jackson.*) |
3(tools.jackson.*) |
| 空安全 | 无统一标注 | JSpecify 全面标注 |
| JDK | 17+ | 17+(建议 21+) |
最容易踩的坑:Jackson 3 的包名
Jackson 3 不是小版本,而是把包名从 com.fasterxml.jackson.* 改成了 tools.jackson.* 。如果你在项目里自己写了几百行 ObjectMapper 定制代码,这一段跑不掉:
java
// 1.x
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
// 2.0
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.JsonNode;
Spring AI 2.0 为此新增了 JsonHelper 工具类,并给 JacksonUtils 增加了 getDefaultJsonMapper()。所有 JSON 编解码迁移到这套 API:
java
// 2.0 推荐
JsonHelper jsonHelper = new JsonHelper(); // 默认 JsonMapper
// 或注入自定义 JsonMapper(Jackson 3 已内置 java.time 支持,无需再注册 JavaTimeModule)
JsonMapper myMapper = JsonMapper.builder()
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.build();
JsonHelper customHelper = new JsonHelper(myMapper);
MyType obj = jsonHelper.fromJson(json, MyType.class);
String out = jsonHelper.toJson(obj);
旧的 JsonParser、ModelOptionsUtils 的 JSON 方法、McpJsonParser 全部删除或弃用,详见变更九。
升级前检查清单
- JDK:确认本地和 CI 环境是 Java 17+,否则直接启动失败;
- Maven / Gradle:Maven 3.6+、Gradle 8.x+;
- Jackson 依赖 :删掉自己显式声明的 Jackson 2 依赖,统一走 Boot 4 BOM;代码里
com.fasterxml.jacksonimport 批量改为tools.jackson; - 自定义配置类 :检查
WebMvcConfigurer、SecurityFilterChain等是否依赖 Framework 6 的已移除 API; - 第三方库兼容性:凡是没跟上 Boot 4 的库(尤其老版本 MyBatis、ShardingSphere 之类),升级前先查它们的 Boot 4 适配版本。
变更二:Options 体系重构------Builder 创建、不可变、无反射合并
1.x 的 *Options 是"可变 POJO + setter"的松散风格,2.0 把它们全部收紧为 Builder 创建、创建后不可变、通过 mutate() 派生新实例。
移除 copy() / fromOptions()
java
// 1.x
OllamaChatOptions options = originalOptions.copy();
options.setFoo("..."); // setter 修改
// 2.0
OllamaChatOptions options = originalOptions.mutate()
.foo("...")
.build();
copy() 和 *Options#fromOptions(*Options) 已被移除,编译器会直接帮你找出所有旧调用点。
集合字段不再可变
toolCallbacks、stopSequences、customHeaders 这类集合现在存储为不可修改集合 ,可空集合取代空集合。拿着 getter 返回的集合往里 add() 会抛 UnsupportedOperationException------这是运行时才暴露的坑,升级后记得回归测试所有改过 options 的代码路径。
默认值搬到 Options 构造函数
默认模型名、温度等不再由各 Model 实现和 *Properties 类拍板,而是收进 Options 构造函数。连带变化:
java
// 1.x(已弃用)
ChatOptions defaults = chatModel.getDefaultOptions();
// 2.0
ChatOptions defaults = chatModel.getOptions();
小写化的 n()
java
// 1.x
OpenAiChatOptions.builder().N(1).build();
// 2.0
OpenAiChatOptions.builder().n(1).build();
迁移动作
- 全局搜索
.copy()→ 重写为mutate()...build(); - 全局搜索
setXxx(→ 改成 builder 链式调用; getDefaultOptions()→getOptions();N(→n(。
变更三:ChatModel 降级为构建块,ChatClient 成为唯一推荐入口
官方公告原话:
"Spring AI 2.0 embraces using
ChatClientas the most common user-facing API whileChatModelis more of a lower-level building block."
翻译过来:ChatClient 是面向业务的 API,ChatModel 是给框架作者用的底层块 。如果你 1.x 时代还在直接 new OpenAiChatModel 并手写调用循环,这次必须改。
不再帮你合并 Options
1.x 里 ChatModel.call(prompt) 会做 options 合并(prompt 里的 options 覆盖模型默认值)。2.0 去掉了这套隐式合并逻辑:
ChatModel.call(Prompt)需要完整的ChatOptions实例,非 null 直接用,null 就用模型默认;- 所有
internalCall/internalStream方法改为private,第三方想绕过公开 API 驱动循环的路径被堵死; buildRequestPrompt从公共 API 移除。
编译期强制的 Builder
ChatClient 的 .options() / .defaultOptions() 现在接收的是 ChatOptions.Builder 而不是构建好的实例:
java
// 1.x
ChatOptions opts = AnthropicChatOptions.builder()
.maxTokens(100).temperature(0.7).build();
String response = chatClient.prompt("Tell me a joke")
.options(opts) // 传入实例
.call().content();
// 2.0
String response = chatClient.prompt("Tell me a joke")
.options(AnthropicChatOptions.builder() // 传入 Builder
.maxTokens(100).temperature(0.7))
.call().content();
这是故意为之:传入 Builder 可以避免"实例已构建、无法再合并"的语义混乱。旧代码编译直接报错,跟着签名改即可。
迁移动作
- 凡是
new XxxChatModel(...)+ 手写 while 循环的代码,一律改用ChatClient; .options(实例)→.options(Builder);- 直接调用
internalCall的自定义框架代码,改用ChatModel.call(prompt)。
变更四:模型提供商精简------OpenAI 3→1、Anthropic 2→1、Vertex 移除
2.0 对模型接入做了一次大扫除:每个提供商只保留一个官方 SDK 变体,其余变体全部合并或移交。
合并清单
| 提供商 | 1.x 变体 | 2.0 |
|---|---|---|
| OpenAI | spring-ai-openai(HTTP)、spring-ai-openai-sdk、spring-ai-azure-openai |
仅 spring-ai-openai,底层换官方 openai-java SDK |
| Anthropic | spring-ai-anthropic(HTTP)、spring-ai-anthropic-sdk |
仅 SDK 变体,底层换官方 anthropic-java |
| GenAI SDK 变体 + Vertex AI 变体 | 移除 Vertex,仅保留 GenAI SDK | |
| MiniMax | 专用支持 | 移除,改用 Anthropic 兼容端点 |
典型的 POM 迁移
xml
<!-- 1.x:Azure OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
</dependency>
<!-- 2.0:统一用 spring-ai-openai,类名去掉 Azure 前缀 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
代码侧:AzureOpenAiChatModel → OpenAiChatModel(去掉 Azure 前缀);AnthropicSdkChatModel 之类的 SDK 变体类名去掉 Sdk 后缀。另有两个模块移交外部维护:OCI Generative AI 移交 Oracle Spring Cloud,Azure Cosmos DB 存储由微软团队独立维护。
一个被低估的变化:Anthropic 的 maxTokens 默认值
AnthropicChatModel 的 maxTokens 默认值从 500 跳到 4096 。如果你的应用依赖"默认短回复"做了截断或超时假设,行为会变。老 API 类 org.springframework.ai.anthropic.api.AnthropicApi 及嵌套 record 全部删除,构造方式改为 Builder:
java
// 1.x
AnthropicApi anthropicApi = new AnthropicApi(apiKey);
AnthropicChatModel chatModel = new AnthropicChatModel(anthropicApi, options);
// 2.0
AnthropicChatModel chatModel = AnthropicChatModel.builder()
.apiKey(apiKey)
.defaultOptions(options)
.build();
访问兼容 API 的正确姿势
2.0 里 spring-ai-openai 是访问一切"OpenAI 兼容 API"(DeepSeek、Qwen、GLM 等)的唯一入口------模块底层已换成官方 openai-java SDK,不再有 HTTP/SDK 变体之分,base-url 仍是 spring.ai.openai.base-url,配置方式不变:
properties
# DeepSeek 兼容 OpenAI API
spring.ai.openai.base-url=https://api.deepseek.com
spring.ai.openai.api-key=${DEEPSEEK_API_KEY}
spring.ai.openai.chat.model=deepseek-chat
spring.ai.openai.chat.temperature=0.7
MiniMax 用户同理:Base URL 指向 https://api.minimax.io/anthropic,用 Anthropic starter 和 MiniMax 模型名(注意 MiniMax Embeddings 不再支持)。
变更五:工具调用循环上移------ToolCallingAdvisor 接管一切
这是 2.0 最深的架构变革,也最容易引发"静默行为变化":1.x 每个 ChatModel 内部都藏着一个私有的工具调用循环;2.0 把它从模型里抽出来 ,做成了 Advisor 链上的一等公民组件 ToolCallingAdvisor。
首先:删除你显式添加的 ToolCallingAdvisor
因为 ChatClient 现在总是自动注册 ToolCallingAdvisor(除非显式禁用),1.x 里手动加过的会重复执行:
java
// 1.x:手动添加
chatClient.prompt("What's the weather?")
.tools(weatherTool)
.advisors(ToolCallingAdvisor.builder().build()) // <-- 2.0 会重复!
.call().content();
// 2.0:什么都不用加,自动注册
chatClient.prompt("What's the weather?")
.tools(weatherTool)
.call().content();
新标记接口 ToolAdvisor 用于自定义 Advisor 声明"我也管工具生命周期",防止被自动注册逻辑重复添加。
移除 internalToolExecutionEnabled
1.x 里通过 internalToolExecutionEnabled(false) 关闭模型内置循环、改由自己驱动------这个开关连同 spring.ai.<provider>.chat.internal-tool-execution-enabled 属性一起删了:
java
// 1.x
ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new MyTools()))
.internalToolExecutionEnabled(false) // <-- 删除
.build();
// 2.0
ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new MyTools()))
.build();
需要手动驱动循环的场景,用 AdvisorParams.toolCallingAdvisorAutoRegister(false) 退出手动驱动,再自行检查 chatResponse.hasToolCalls()。
ToolExecutionEligibilityPredicate → ToolExecutionEligibilityChecker
判断"本次响应是否值得执行工具"的扩展点换了类型,配置目标改为 ToolCallingAdvisor:
java
ToolCallingAdvisor advisor = ToolCallingAdvisor.builder()
.toolExecutionEligibilityChecker(response ->
response != null && response.hasToolCalls()
&& !"stop".equals(response.getResult().getMetadata().getFinishReason()))
.build();
Boot 用户直接声明一个 ToolExecutionEligibilityChecker Bean 即可全局生效。
移除 streamToolCallResponses
该选项会把中间工具调用请求块流式转发,但配对的 ToolResponseMessage 不转发,导致对话历史损坏------2.0 直接删除。想观察每次工具调用迭代,用手动驱动循环的方式。
工具注册 API 收紧
ToolSpec Consumer API 移除:
java
// 1.x
chatClient.prompt()
.tools(t -> t.callbacks(myCallback).context("tenantId", "acme"))
.call().content();
// 2.0
chatClient.prompt()
.tools(myCallback)
.toolContext(Map.of("tenantId", "acme"))
.call().content();
toolCallbacks() / defaultToolCallbacks() 弃用,统一用 tools(Object...) / defaultTools(Object...)。
Spring Bean 工具解析移除 ------1.x 里裸 Function Bean + toolNames() 的声明式模式没了:
java
// 1.x
@Bean
@Description("Get the weather in location")
Function<WeatherRequest, WeatherResponse> currentWeather() { ... }
// 2.0
@Bean
ToolCallback currentWeather() {
return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
}
toolNames() 从所有 chat options 类和 ChatClient 中删除。另外 MethodToolCallbackProvider 的异常类型从 IllegalStateException 改为 IllegalArgumentException(无 @Tool 注解方法、重复工具名时触发),catch 旧异常类型的代码要同步。
迁移动作
- 删掉所有显式
ToolCallingAdvisor.builder().build(); - 删掉
internalToolExecutionEnabled(...)及其配置属性; - 删掉
streamToolCallResponses(...); tools(consumer)写法改为tools(对象) + toolContext(map);Function Bean + toolNames()全部重写为ToolCallbackBean。
变更六:配置属性扁平化------.options 段正式移除
1.x 的配置键长得像俄罗斯套娃:spring.ai.openai.embedding.options.model。2.0 把人为的 .options 段全部压平,并同步调整了一批属性名。
配置迁移表(高发项)
| 1.x | 2.0 |
|---|---|
spring.ai.openai.embedding.options.model=... |
spring.ai.openai.embedding.model=... |
spring.ai.openai.chat.options.model=... |
spring.ai.openai.chat.model=... |
spring.ai.anthropic.chat.options.temperature=... |
spring.ai.anthropic.chat.temperature=... |
spring.ai.ollama.chat.think-option=... |
spring.ai.ollama.chat.think=... |
spring.ai.<provider>.chat.internal-tool-execution-enabled=... |
已删除(见变更五) |
代码侧对应:*Properties#getOptions() 弃用,toOptions() 从嵌套 Options 类上移到根 *Properties 类:
java
// 1.x
String model = properties.getOptions().getModel();
OpenAiEmbeddingOptions options = properties.getOptions().toOptions();
// 2.0
String model = properties.getModel();
OpenAiEmbeddingOptions options = properties.toOptions();
新属性速览
properties
# 全局禁用自动工具执行(工具定义仍发给模型,但不自动执行)
spring.ai.chat.client.tool-calling.enabled=false
# 渐进式工具披露(T12 会展开讲)
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=regex # regex | lucene | vector
需要定制 ToolCallingAdvisor(注入 ToolExecutionEligibilityChecker、调整它在 advisor 链上的顺序)时,用 5 参的 ChatClient.builder() 重载传入自定义 ToolCallingAdvisor.Builder。
迁移动作
对 application.properties / application.yml 做一次全文搜索:options. 前缀全部删除;think-option 改名 think;顺便检查所有显式声明的模型名与温度------见变更九的"默认温度移除"。
变更七:MCP 生态大迁移------SDK 2.0 + Streamable HTTP + 包名搬家
2.0 的 MCP 部分变化最大,官方为其中三类提供了 OpenRewrite recipe(见下文),但理解"为什么搬"更重要。
1. MCP Java SDK 升到 2.0.0
对应 2025-11-25 版 MCP 规范。三个典型的 API 变化:
java
// CreateMessageRequest.maxTokens 变必填:builder 直接收 (messages, maxTokens)
// 1.x
CreateMessageRequest.builder().messages(messages).build();
// 2.0
CreateMessageRequest.builder(messages, 500).build();
// Tool.inputSchema() 类型变了
// 1.x
McpSchema.JsonSchema schema = tool.inputSchema();
// 2.0
Map<String, Object> schema = tool.inputSchema();
CreateMessageResult 的 model 字段同样变必填------无参 builder 弃用,构造响应时必须显式给出模型名(具体签名以 SDK javadoc 为准)。
另外 McpSchema 下多个接口(JSONRPCMessage、Request、Result 等)不再 sealed------穷举式 switch 必须补 default 分支,否则编译报错。
2. Streamable HTTP 成为默认传输,SSE 弃用
2025-11-25 规范里 SSE 传输已废弃,Streamable HTTP 成为默认 ,并新增无状态(stateless)变体用于水平扩展;STDIO 保留给本地进程。服务端工具输入校验默认开启(参数不符合 JSON Schema 返回 isError=true,可用 validateToolInputs(false) 关闭)。
3. 注解与传输模块搬进 Spring AI
mcp-annotations:外部库org.springaicommunity:mcp-annotations不再是依赖,@McpTool/@McpResource/@McpPrompt直接内置在 Spring AI。包名变化:
| 1.x | 2.0 |
|---|---|
org.springaicommunity.mcp.annotation.* |
org.springframework.ai.mcp.annotation.* |
org.springaicommunity.mcp.method.* |
org.springframework.ai.mcp.annotation.method.* |
org.springaicommunity.mcp.provider.* |
org.springframework.ai.mcp.annotation.provider.* |
- 传输模块 :groupId 从
io.modelcontextprotocol.sdk改为org.springframework.ai(artifact 名mcp-spring-webflux/mcp-spring-webmvc不变),纯自动配置用户只需改 POM;手写代码的用户注意包名:
| 类 | 1.x 包 | 2.0 包 |
|---|---|---|
WebFluxSseServerTransportProvider |
io.modelcontextprotocol.server.transport |
org.springframework.ai.mcp.server.webflux.transport |
WebMvcSseServerTransportProvider |
io.modelcontextprotocol.server.transport |
org.springframework.ai.mcp.server.webmvc.transport |
WebClientStreamableHttpTransport |
io.modelcontextprotocol.client.transport |
org.springframework.ai.mcp.client.webflux.transport |
4. Client Customizer 统一
McpAsyncClientCustomizer / McpSyncClientCustomizer 合并为泛型 McpClientCustomizer<B>:
java
// 1.x
@Bean
public McpSyncClientCustomizer mySyncCustomizer() {
return (name, spec) -> spec.requestTimeout(Duration.ofSeconds(30));
}
// 2.0
@Bean
public McpClientCustomizer<McpClient.SyncSpec> mySyncCustomizer() {
return (name, spec) -> spec.requestTimeout(Duration.ofSeconds(30));
}
5. 还有几个小坑
TypeReference→ParameterizedTypeReference(McpSyncRequestContext.elicit(...)等方法签名);Builder.customizeRequest()改名httpRequestCustomizer();- WebMvc 传输传给
securityValidator.validateHeaders()的 header 名全部小写化 ,headers.get("Authorization")要改成headers.get("authorization"); MCP Spring Transport、mcp-annotations、McpClientCustomizer三类迁移都有官方 OpenRewrite recipe,示例:
bash
mvn org.openrewrite.maven:rewrite-maven-plugin:6.32.0:run \
-Drewrite.configLocation=https://raw.githubusercontent.com/spring-projects/spring-ai/refs/heads/main/src/rewrite/migrate-to-2-0-0-M3.yaml \
-Drewrite.activeRecipes=org.springframework.ai.migration.M3MigrateMcpAnnotations \
-Dmaven.compiler.failOnError=false
迁移动作
- 升级依赖坐标(mcp-annotations 删除、传输模块 groupId 变更);
- 批量替换注解包名(或用 OpenRewrite);
- 检查
CreateMessageResult/CreateMessageRequest/inputSchema等强制变更; - 所有 SSE 配置迁移到 Streamable HTTP。
变更八:对话记忆重构------sequence_id 列与必填会话 ID
1. JDBC Chat Memory 新增 sequence_id 列(需要手动执行 SQL)
为什么加列?原来按 timestamp 排序,但 MySQL/MariaDB 的 TIMESTAMP 精度只有秒级,同秒内的消息顺序是不确定的 。2.0 引入 sequence_id BIGINT(Oracle 为 NUMBER(19),SQLite 为 INTEGER)作为稳定的顺序键,timestamp 保留为创建时间,并通过元数据暴露。
PostgreSQL 迁移 SQL(官方版):
sql
ALTER TABLE SPRING_AI_CHAT_MEMORY ADD COLUMN sequence_id BIGINT;
WITH ordered AS (
SELECT ctid, ROW_NUMBER() OVER (PARTITION BY conversation_id ORDER BY "timestamp") - 1 AS seq
FROM SPRING_AI_CHAT_MEMORY
)
UPDATE SPRING_AI_CHAT_MEMORY t
SET sequence_id = o.seq
FROM ordered o
WHERE t.ctid = o.ctid;
ALTER TABLE SPRING_AI_CHAT_MEMORY ALTER COLUMN sequence_id SET NOT NULL;
CREATE INDEX SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_SEQUENCE_ID_IDX
ON SPRING_AI_CHAT_MEMORY(conversation_id, sequence_id);
其他数据库调整行标识表达式,或直接删表后用新版 schema-<platform>.sql 重建。
另一个隐蔽影响:检索出的消息现在携带时间元数据,与代码里手工构造的"相同"消息不再 equals------凡是拿构造的消息对比库中消息的测试和缓存逻辑都要复查。
2. 会话 ID 必填,默认会话没了
1.x 里不传 CONVERSATION_ID 会自动落到 "default" 会话;2.0 直接抛 IllegalArgumentException:
java
// 1.x
ChatMemory.DEFAULT_CONVERSATION_ID // "default",已删除
// 2.0:每次调用显式传会话 ID
chatClient.prompt()
.user("Hello!")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "my-session"))
.call()
.content();
.conversationId("my-session") Builder 方法也已删除,只能在 advisor 参数里传。
3. PromptChatMemoryAdvisor 移除
历史消息不再以纯文本注入系统提示词,统一用 MessageChatMemoryAdvisor(Builder API 相同,注入的是结构化 chat messages):
java
// 1.x
PromptChatMemoryAdvisor.builder(chatMemory).build();
// 2.0
MessageChatMemoryAdvisor.builder(chatMemory).build();
4. Advisor 顺序与内部对话历史
Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER从HIGHEST_PRECEDENCE + 1000改为+ 200(在ToolCallingAdvisor的+ 300之外);ToolCallingAdvisor默认在工具调用迭代期间内部管理对话历史 ,记忆顾问只存最终的 user/assistant 交换,工具调用消息不再写入 ChatMemoryRepository;- 如果你的场景需要在循环内使用记忆(比如自定义轮询逻辑),需要显式调整:
java
var toolCallingAdvisor = ToolCallingAdvisor.builder()
.disableInternalConversationHistory()
.build();
var chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
.advisorOrder(Ordered.HIGHEST_PRECEDENCE + 400)
.build();
5. 相关小变更
ToolContext.TOOL_CALL_HISTORY常量与getToolCallHistory()移除,对话历史不再自动塞进ToolContext,工具只收到参数和自定义 context;MongoChatMemoryRepository修复了消息排序(现在天然按发送顺序返回),1.x 里Collections.reverse(...)的变通代码要删掉;BaseChatMemoryAdvisor.getConversationId(Map, String)变成getConversationId(Map)。
迁移动作
- 执行数据库迁移 SQL(这个没法自动);
- 所有记忆调用点补
CONVERSATION_ID参数; PromptChatMemoryAdvisor→MessageChatMemoryAdvisor;- 删除
Collections.reverse变通代码。
变更九:细节里的魔鬼------温度、maxTokens、JSON Schema 与观测指标
最后一批变更不"大",但每一个都能造成线上行为偏差。
1. 默认温度配置移除
Spring AI 不再自动给 Chat 模型配 0.7 温度,由各提供商原生默认决定(可能是 1.0、0.7 或模型特定值):
properties
# 想保持 1.x 行为就显式写
spring.ai.openai.chat.temperature=0.7
spring.ai.anthropic.chat.temperature=0.7
升级后"同样的提示词、不同的输出风格"十有八九是这个原因。
2. Anthropic maxTokens 500 → 4096
见变更四。长回复变长是行为变化,短回复场景注意成本。
3. BeanOutputConverter 的 JSON Schema 生成变更
底层换成了 JsonSchemaGenerator,影响:
- Kotlin 可空/默认值属性不再进入
required数组; @JsonProperty(required = false)不再视为必填;- Schema 增加 OpenAPI 风格
format提示(int32、int64、date-time); postProcessSchema(JsonNode)扩展点移除 ,改为重写generateSchema():
java
class CustomConverter extends BeanOutputConverter<MyType> {
CustomConverter() { super(MyType.class); }
@Override
protected String generateSchema() {
String schema = super.generateSchema();
// post-process schema
return schema;
}
}
4. 工具调用观测指标改名
| 指标 | 1.x | 2.0 |
|---|---|---|
| Span 名称 | tool_call <tool-name> |
execute_tool <tool-name> |
gen_ai.operation.name |
framework |
execute_tool |
新增 spring.ai.tool.type(如 function)、spring.ai.tool.call.id 两个 span 属性。依赖指标告警/看板的团队升级前先同步查询语句。
5. 模块清理(删依赖清单)
spring-ai-hanadb-store移除;spring-ai-spring-cloud-bindings整体移除;spring-ai-azure-cosmos-db-store、spring-ai-model-chat-memory-repository-cosmos-db移交微软外部维护;spring-ai-tool-search-tool(内置)拆分为独立模块spring-ai-tool-search-advisor,包名从org.springframework.ai.tool.toolsearch.advisor改为org.springframework.ai.chat.client.advisor.toolsearch;spring-ai-advisors-vector-store重命名为spring-ai-vector-store-advisor(纯模块名变更)。
6. JSON 工具类收敛
JsonParser 弃用、ModelOptionsUtils 的 JSON 方法移除、McpJsonParser 删除,统一迁移到 JsonHelper / JacksonUtils.getDefaultJsonMapper()(对照表见变更一)。
实战:一个 1.1.x 项目升级到 2.0 的完整记录
纸上谈兵结束。下面是一个典型项目的升级全过程------Spring Boot 3.5 + Spring AI 1.1.2 的问答 Agent 服务:DeepSeek 兼容 OpenAI 端点做对话(Embedding 走另一个 OpenAI 兼容端点------DeepSeek 不提供 embedding 接口)、两个 @Tool 工具、一个 MCP 客户端(Streamable HTTP)、MessageChatMemoryAdvisor 会话记忆、PGVector 向量库。
第 0 步:升级前基线
bash
# 升级前:项目能编译、单测通过、关键对话链路有回归用例
git checkout -b upgrade-spring-ai-2.0
第 1 步:POM 升级
xml
<!-- 1.x -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>
<properties>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>
<!-- 2.0 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
</parent>
<properties>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
同时执行:删除 spring-ai-spring-boot-testcontainers(Boot 4 的 spring-boot-testcontainers 已原生支持)、检查有没有被合并/移除的 starter、清理显式声明的 Jackson 2 依赖。
第 2 步:配置迁移(application.yml)
yaml
spring:
ai:
openai:
base-url: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY}
chat:
model: deepseek-chat
temperature: 0.7 # 显式声明,补偿默认温度移除
embedding: # DeepSeek 无 embedding 端点,单独覆盖 base-url/api-key
base-url: ${EMBEDDING_BASE_URL}
api-key: ${EMBEDDING_API_KEY}
model: text-embedding-3-small # 原来写 embedding.options.model
pgvector:
initialize-schema: true
chat / embedding 各自的 base-url 和 api-key 可以覆盖全局配置------对话走 DeepSeek、Embedding 走其他兼容端点,靠的就是这个能力。
第 3 步:代码迁移(按编译错误逐条来)
java
// ① Options 不可变:mutate() 替代 copy() + setter
// 1.x
var opts = chatModel.getDefaultOptions().copy();
opts.setTemperature(0.3);
// 2.0
var opts = chatModel.getOptions().mutate().temperature(0.3).build();
// ② ChatClient options 传 Builder
// 1.x
prompt().options(AnthropicChatOptions.builder().maxTokens(500).build())
// 2.0
prompt().options(AnthropicChatOptions.builder().maxTokens(500)) // 注意去掉 .build()
// ③ 删除显式 ToolCallingAdvisor(自动注册)
// 1.x
.defaultAdvisors(ToolCallingAdvisor.builder().build())
// 2.0
// 直接删掉这行
// ④ Function Bean → ToolCallback Bean(见变更五示例)
// ⑤ 记忆会话 ID 必填
// 1.x
MessageChatMemoryAdvisor.builder(chatMemory).conversationId("sess-1").build()
// 2.0
// builder 上不再有 conversationId();每次调用传 param
chatClient.prompt().user("...")
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "sess-1"))
.call().content();
// ⑥ MCP 注解包名批量替换
// org.springaicommunity.mcp.annotation.@McpTool
// → org.springframework.ai.mcp.annotation.@McpTool
第 4 步:数据库迁移
执行变更八的 sequence_id 迁移 SQL,然后跑一遍带记忆的对话回归用例。
第 5 步:验证清单与真实踩坑记录
我们这次升级遇到的实际问题,按杀伤力排序:
- 对话风格突变 ------原因是默认温度没了,DeepSeek 原生默认 1.0。显式配回 0.7 解决。这是最容易忽略的行为变化;
- 工具被调用两次 ------1.x 显式加的
ToolCallingAdvisor没删,2.0 自动注册后又加了一次。删掉显式注册后正常; - MCP 连接 401 ------
securityValidator里拿headers.get("Authorization")拿不到,2.0 全小写成authorization。按变更七改掉; - 记忆错乱 ------没传
CONVERSATION_ID直接IllegalArgumentException,排查后发现是conversationId()builder 删除后漏改的调用点; - 同秒消息顺序乱 ------因为还没执行
sequence_id迁移 SQL,先跑业务后迁移的后果。数据库迁移要排在回归之前; UnsupportedOperationException------老代码往stopSequencesgetter 返回的集合里 add,Options 不可变后爆雷。
全部处理完,回归通过。升级总耗时约 1.5 天,其中编译期错误约 3 小时 (Builder 化很省心),行为差异排查约 5 小时(温度、工具重复、MCP header 这类静默变化才是大头)。
小结
9 个变更可以压缩成一句话:Spring AI 2.0 用"收敛"换"确定性"------模型接入收敛到官方 SDK、工具循环收敛到 Advisor、MCP 生态收敛进 Spring AI 名下、Options 收敛为不可变 Builder、配置收敛为扁平键。代价是升级工作量不小,但换来的是一个 API 更一致、行为更可预测的运行时底座。
给升级者的三条建议:
- 先跑 OpenRewrite,MCP 三类迁移能省一半时间;数据库 SQL 必须手动执行;
- 把"行为差异"当一等公民对待:温度默认值、maxTokens、工具自动注册、MCP header 大小写------每个都值得一条回归用例;
- 锁定依赖基线 :2.0 起所有官方模块都在
spring-ai-bom里管理,别再手写版本号。
顺带预告:工具循环 Advisor 化的架构变革值得单独深挖(ToolCallingAdvisor 源码拆解、渐进式工具披露),系列后续文章会展开。