Spring AI 0.8.1 升级到 1.0.0:资本市场估值问答服务的 Starter 迁...

发布时间:2026/10/7 22:40:44
Spring AI 0.8.1 升级到 1.0.0:资本市场估值问答服务的 Starter 迁... Spring AI 0.8.1 升级到 1.0.0资本市场估值问答服务的 Starter 迁移与兼容性处理上周三下午四点风控组的同事把一张截图甩进群内部估值问答页面又吐了半句就断流用户看到的最后一行是「根据您提供的 3 年期 IRS 现金流」。这不是第一次了。项目背景我们维护的是一套面向机构客户的利率与外汇风险对冲辅助系统。业务侧高频问题就三类某笔 IRS 在当前曲线下的 DV01 是多少、当前对冲比例偏离目标多少、某个利率情景下保证金缺口多大。技术栈是 Spring Boot 3.4.5 JDK 21.0.5估值引擎是团队自己用 Java 重写的 OIS 折现检索侧用 PostgreSQL 16.4 加 pgvector 0.7.4。整个后端 7 个人其中 2 人负责这条 AI 问答链路。2025 年初第一次接入大模型能力时选的是当时 GA 的 Spring AI 0.8.1坐标spring-ai-openai-spring-boot-starter。跑了一年半2026 年国庆前排期做 1.0.0 的迁移。写这篇文章是 2026-10-061.0.0 这条线在我们生产环境已经稳定运行了几个月数据是真实压测出来的。旧版本卡在哪三个痛点按严重程度倒着说。最要命的是依赖树冲突。0.8.1 时代spring-ai-openai-spring-boot-starter和spring-ai-spring-boot-starter同时存在传递依赖里拉进了两个spring-ai-core。mvn dependency:tree一跑0.8.1 和 0.8.0 并排躺在那里。本地开发机类加载顺序凑巧对灰度机器启动直接NoSuchMethodError。这种问题排查一次要半天。其次是工具注册。0.8.1 的函数回调走FunctionCallbackWrapper加Bean全局注册本质是单例。我们做的是多租户 SaaSA 客户不该看到 B 客户的持仓工具当时靠一段 200 行的 if-else 在调用前过滤工具名白名单。难维护且容易漏。第三个是流式响应丢尾包。工具调用完成后最后一个 chunk 的 finishReason 处理在 0.8.1 的 advisor 链路里存在竞态1000 次压测丢 37 次前端 SSE 收到的是半句话。这个只能靠业务侧兜底拼接很别扭。选型决策摆在面前的路有三条。第一条是留在 0.8.1只把依赖冲突手工 掉。成本最低但工具 API 的老化和流式缺陷没法根治等于把技术债再滚一年。第二条是绕过 Spring AI直接用 OpenAI 官方 Java SDK 自己封装一层。控制力最强但对话记忆、向量检索抽象、Advisor 链路全得自己写按我们两个人的投入至少三周起步而且以后换模型供应商要重写。第三条是升级到 Spring AI 1.0.0。这个版本 2025 年 5 月 GA是第一个 API 稳定版Starter 命名收敛成spring-ai-starter-model-和spring-ai-starter-vector-store-工具回调统一到ToolCallback基线要求 Spring Boot 3.4.x。我们主工程本来就在 3.4.5 上迁移面可控。选了第三条。核心判断是这一年半我们真正依赖的是 ChatClient、ChatMemory、VectorStore、Advisor 这四层抽象而 1.0.0 恰好把它们全定型了。1.0.0 改了什么对比表先放上来这是迁移时最常翻的一页。| 维度 | Spring AI 0.8.1 | Spring AI 1.0.0 | 迁移动作 ||---|---|---|---|| OpenAI Starter 坐标 | spring-ai-openai-spring-boot-starter | spring-ai-starter-model-openai | 改 artifactId删掉手写版本号 || 版本管理 | 各模块单独声明版本 | spring-ai-bom:1.0.0 | dependencyManagement 中 import || 工具回调 | FunctionCallback / FunctionCallbackWrapper | ToolCallback / MethodToolCallbackProvider | 重写注册 Bean || 消息取文本 | AssistantMessage#getContent() | AssistantMessage#getText() | 全局替换靠编译期兜底 || 对话记忆 | 直接 new ChatMemory 实现 | MessageWindowChatMemory ChatMemoryRepository | 补 Repository Bean || 向量检索请求 | new SearchRequest(...) | SearchRequest.builder() | 改构造方式 || Spring Boot 基线 | 3.2.x | 3.4.x | 同步对齐 |pgvector 的 Starter 同样改名从spring-ai-pgvector-store-spring-boot-starter变成spring-ai-starter-vector-store-pgvector。这个点我在迁移时差点漏掉因为它不在 OpenAI 那条依赖链上。迁移步骤第一步先锁 BOM再换坐标顺序很重要。先换坐标再锁 BOM中间会有一段时间依赖树是脏的编译报错信息会很难读。xmlorg.springframework.aispring-ai-bom1.0.0pomimportorg.springframework.aispring-ai-starter-model-openaiorg.springframework.aispring-ai-starter-vector-store-pgvector顺手把仓库配置里spring-milestones的地址删掉。GA 版本从中央仓库拉留着 milestone 源反而可能拉到同版本的快照我们就踩过一次。第二步配置属性对齐配置项主体结构没变但spring.ai.vectorstore.pgvector下的子键名有几个调整。yamlspring:ai:openai:api-key: ${OPENAI_API_KEY}chat:options:model: ${CHAT_MODEL_NAME}temperature: 0.1embedding:options:model: ${EMBEDDING_MODEL_NAME}vectorstore:pgvector:index-type: HNSWdistance-type: COSINE_DISTANCEdimensions: 1536initialize-schema: false模型名走环境变量注入不写死在仓库里。原因是估值问答对模型版本的敏感度很高换个快照模型回答风格就可能漂这类变更必须走配置中心而不是发版。第三步重写工具注册这是改动量最大的一块。javaConfigurationclass ToolConfig {BeanToolCallbackProvider riskToolProvider(RiskQueryService svc) {return MethodToolCallbackProvider.builder().toolObjects(new RiskTools(svc)).build();}}Componentclass RiskTools {private final RiskQueryService svc;RiskTools(RiskQueryService svc) {this.svc svc;}Tool(description 查询某笔利率互换在当前曲线下的 DV01入参为交易编号)Dv01Result queryDv01(ToolParam(description 交易编号如 IRS-2026-0001) String tradeId) {return svc.dv01(tradeId);}}调用侧的关键变化是工具可以按请求传入了不再依赖全局 BeanjavaChatClient client ChatClient.builder(chatModel).defaultSystem(你是利率风险助手只基于检索到的估值数据作答).build();String answer client.prompt().user(question).advisors(a - a.param(ChatMemory.CONVERSATION_ID, tenantId : sessionId).param(QuestionAnswerAdvisor.FILTER_EXPRESSION,tenant tenantId )).toolCallbacks(tenantToolCallbacks(tenantId)).call().content();流式路径把.call().content()换成.stream().content()即可返回Flux。我们那 200 行 if-else 白名单到这里压缩成 30 行的tenantToolCallbacks。第四步灰度验证新旧两条链路并行跑了 5 天。做法是把同一批问题双写用 0.8.1 的答案和新答案做语义相似度比对相似度低于 0.85 的捞出来人工看。总共 1200 条问题人工复核 46 条其中 3 条确认是新版本工具描述文案变更导致的调用差异改完描述就对齐了。兼容性坑Advisor 的全局注册这里要单独说。官方文档推荐用ChatClient.builder().defaultAdvisors(...)统一挂载理由是代码干净。但在我们多租户场景下反而更糟——租户过滤条件必须随请求变化挂在全局就意味着每个请求进来还得再覆盖一次白白多一层参数合并而且容易写出「忘了覆盖」的静默 bug。我们最后放弃了全局 Advisor改成每次prompt()时显式传。这个方案不算优雅但可读性明显更好。另一个坑是AssistantMessage#getContent()换成getText()。方法还在只是标记了废弃所以编译不报错运行时返回 null 的地方才暴露。建议迁移时直接全仓替换别留着。效果数据流式丢尾包压测 1000 次迁移前丢 37 次3.7%迁移后 0 次。首 token 延迟 P95含一次工具调用1.24s 降到 1.11s。提升不算大主要来自 advisor 链路少了一层包装。依赖树spring-ai-core从重复出现 2 次收敛到 1 次打进可执行 jar 的第三方 jar 数量从 214 降到 197。启动时间8.9s 降到 8.2s本地 MacBook Pro M3 上测的。工具白名单代码量200 行降到 30 行。感悟如果重来两件事会调整。一是 Starter 改名单独开一个 PR不要和 API 适配混在一起——那次 diff 有 1400 多行评审的人根本看不出哪里是真正的逻辑改动。二是先写兼容性测试再动代码我们那 46 条人工复核的问题如果提前有一组固定回归问题集能省掉大半时间。#后端 #Java #SpringBoot #SpringAI #版本迁移你在实际项目中有遇到类似问题吗欢迎在评论区分享你的经验和解决方案。