Spring AI 2.0 升级实战:9 个破坏性变更逐条迁移

本文基于 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);

旧的 JsonParserModelOptionsUtils 的 JSON 方法、McpJsonParser 全部删除或弃用,详见变更九。

升级前检查清单

  1. JDK:确认本地和 CI 环境是 Java 17+,否则直接启动失败;
  2. Maven / Gradle:Maven 3.6+、Gradle 8.x+;
  3. Jackson 依赖 :删掉自己显式声明的 Jackson 2 依赖,统一走 Boot 4 BOM;代码里 com.fasterxml.jackson import 批量改为 tools.jackson
  4. 自定义配置类 :检查 WebMvcConfigurerSecurityFilterChain 等是否依赖 Framework 6 的已移除 API;
  5. 第三方库兼容性:凡是没跟上 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) 已被移除,编译器会直接帮你找出所有旧调用点。

集合字段不再可变

toolCallbacksstopSequencescustomHeaders 这类集合现在存储为不可修改集合 ,可空集合取代空集合。拿着 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 ChatClient as the most common user-facing API while ChatModel is 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-sdkspring-ai-azure-openai spring-ai-openai,底层换官方 openai-java SDK
Anthropic spring-ai-anthropic(HTTP)、spring-ai-anthropic-sdk 仅 SDK 变体,底层换官方 anthropic-java
Google 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>

代码侧:AzureOpenAiChatModelOpenAiChatModel(去掉 Azure 前缀);AnthropicSdkChatModel 之类的 SDK 变体类名去掉 Sdk 后缀。另有两个模块移交外部维护:OCI Generative AI 移交 Oracle Spring Cloud,Azure Cosmos DB 存储由微软团队独立维护。

一个被低估的变化:Anthropic 的 maxTokens 默认值

AnthropicChatModelmaxTokens 默认值从 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()

ToolExecutionEligibilityPredicateToolExecutionEligibilityChecker

判断"本次响应是否值得执行工具"的扩展点换了类型,配置目标改为 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() 全部重写为 ToolCallback Bean。

变更六:配置属性扁平化------.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();

CreateMessageResultmodel 字段同样变必填------无参 builder 弃用,构造响应时必须显式给出模型名(具体签名以 SDK javadoc 为准)。

另外 McpSchema 下多个接口(JSONRPCMessageRequestResult 等)不再 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. 还有几个小坑

  • TypeReferenceParameterizedTypeReferenceMcpSyncRequestContext.elicit(...) 等方法签名);
  • Builder.customizeRequest() 改名 httpRequestCustomizer()
  • WebMvc 传输传给 securityValidator.validateHeaders() 的 header 名全部小写化headers.get("Authorization") 要改成 headers.get("authorization")
  • MCP Spring Transportmcp-annotationsMcpClientCustomizer 三类迁移都有官方 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_ORDERHIGHEST_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 参数;
  • PromptChatMemoryAdvisorMessageChatMemoryAdvisor
  • 删除 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 提示(int32int64date-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-storespring-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-urlapi-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 步:验证清单与真实踩坑记录

我们这次升级遇到的实际问题,按杀伤力排序:

  1. 对话风格突变 ------原因是默认温度没了,DeepSeek 原生默认 1.0。显式配回 0.7 解决。这是最容易忽略的行为变化
  2. 工具被调用两次 ------1.x 显式加的 ToolCallingAdvisor 没删,2.0 自动注册后又加了一次。删掉显式注册后正常;
  3. MCP 连接 401 ------securityValidator 里拿 headers.get("Authorization") 拿不到,2.0 全小写成 authorization。按变更七改掉;
  4. 记忆错乱 ------没传 CONVERSATION_ID 直接 IllegalArgumentException,排查后发现是 conversationId() builder 删除后漏改的调用点;
  5. 同秒消息顺序乱 ------因为还没执行 sequence_id 迁移 SQL,先跑业务后迁移的后果。数据库迁移要排在回归之前
  6. UnsupportedOperationException ------老代码往 stopSequences getter 返回的集合里 add,Options 不可变后爆雷。

全部处理完,回归通过。升级总耗时约 1.5 天,其中编译期错误约 3 小时 (Builder 化很省心),行为差异排查约 5 小时(温度、工具重复、MCP header 这类静默变化才是大头)。

小结

9 个变更可以压缩成一句话:Spring AI 2.0 用"收敛"换"确定性"------模型接入收敛到官方 SDK、工具循环收敛到 Advisor、MCP 生态收敛进 Spring AI 名下、Options 收敛为不可变 Builder、配置收敛为扁平键。代价是升级工作量不小,但换来的是一个 API 更一致、行为更可预测的运行时底座。

给升级者的三条建议:

  1. 先跑 OpenRewrite,MCP 三类迁移能省一半时间;数据库 SQL 必须手动执行;
  2. 把"行为差异"当一等公民对待:温度默认值、maxTokens、工具自动注册、MCP header 大小写------每个都值得一条回归用例;
  3. 锁定依赖基线 :2.0 起所有官方模块都在 spring-ai-bom 里管理,别再手写版本号。

顺带预告:工具循环 Advisor 化的架构变革值得单独深挖(ToolCallingAdvisor 源码拆解、渐进式工具披露),系列后续文章会展开。

参考资料

相关推荐
打呵欠的猫1 小时前
我用 AI 写了一个"需求翻译器",产品的 PRD 直接变成开发任务清单
前端·ai编程
七牛云行业应用1 小时前
DeepSeek Harness vs Codex vs Claude Code:三款 AI 编程 Harness 深度对比
人工智能·agent·ai编程
AprChell2 小时前
DeepSeek Harness 开源了一套 Vibe Coding 工程流水线
ai编程·deepseek·vibecoding
小星星_20262 小时前
DeepSeek Harness 深度解析:当 Agent Runtime 成为开源基础设施
ai编程
小星星_20262 小时前
AI Coding Agent 的真正战场:Harness 工程深度解析
ai编程
极客小俊3 小时前
Windows安装部署Claude Code+CC‑Switch+Agnes AI 保姆级教程
agent·ai编程·claude
console.log('npc')3 小时前
DeepSeek Harness 使用教程
大模型·ai编程·deepseek·harness
用户125758524363 小时前
对象存储 URL 为什么别到处拼:后台附件预览要验这一层
后端·go·ai编程