尧图精选

Spring AI跨模型调用实战:一套代码灵活切换OpenAI与Anthropic

🕒 发布时间:2026/9/16 2:34:50 📁 来源:尧图网络
做 AI 应用开发最头疼的不是 Prompt 调不好而是今天用 OpenAI 写好的一套调用代码明天想换成 Anthropic几乎等于重写一遍。Spring AI 的出现就是要把这一层“模型差异”屏蔽掉让我们能用一套可移植的 API 代码同时支持 OpenAI、Anthropic 等多个大模型。这篇博文我会从实际项目出发讲清楚 Spring AI 的核心抽象、可移植代码的落地方式以及我在切换模型时踩过的各种坑。适合正在做 AI 应用集成、想把模型供应商从单点绑死变成可替换方案的开发者参考。1. 为什么需要“跨模型”的 API 封装1.1 多模型接入的痛点我最早做 AI 功能时图省事直接引用了 OpenAI 的官方 SDK业务代码里到处都是OpenAiClient、ChatMessage这些类型。后来客户要求增加 Anthropic 的支持我本来以为加个依赖、改个配置就行结果发现完全不是这么回事。OpenAI 的消息结构是messages数组里带role: system/user/assistantAnthropic 的请求则把system单独拎出来工具调用的参数格式、流式返回的事件类型、错误码语义也都不一样。如果继续用各家原生 SDK每一个新模型接入都是一次代码重构。这种问题在真实项目里会被放大。一个稍微上点规模的应用至少会涉及文本生成、对话历史、函数调用、图片理解、流式输出这几个能力点每个能力点都要为不同供应商各写一套适配代码。更要命的是业务方随时可能因为成本、效果或合规要求换供应商而换一次供应商就要动一大堆业务代码这个成本没人愿意承担。所以问题的本质是我们需要的不是“某一个模型的 Client”而是一个能屏蔽底层差异的“模型抽象层”。这个抽象层要能让我们用同一套代码调用不同大模型供应商切换只是配置变化而不是代码重写。1.2 Spring AI 解决的是哪一层问题Spring AI 正是奔着这个目标去的。它的定位很像 Spring 生态一贯的风格帮你把通用流程固化好把差异点留给扩展配置。在 Spring AI 的世界里你不再直接操作某个厂商的 Client而是操作统一的ChatModel、Prompt、ChatResponse这些概念。至于底层是访问 OpenAI、Anthropic还是走 OpenAI 兼容协议的 DeepSeek、Ollama都由对应的 Starter 去适配。我用一个生活化类比来解释这件事。原生 SDK 就像你给每个家电品牌单独配一个遥控器电视遥控器不能开空调空调遥控器不能开电视Spring AI 则是把所有遥控器统一成一个万能遥控器按键语义是相通的你只需要在设置里指定你控制的是哪个品牌的设备。虽然万能遥控器不可能覆盖每一台机器的每一个奇怪功能但日常 90% 的操作它都能覆盖剩下 10% 的差异化功能也提供了扩展口子。在实际项目中Spring AI 还帮我们解决了一个很容易被忽视的问题流式输出和函数调用的差异。OpenAI 的流式返回是ChatCompletionChunkAnthropic 的流式返回是MessageStreamEvent两者事件类型完全不同。Spring AI 把它统一成了FluxChatResponse业务代码只需要stream()然后订阅即可不用关心底层事件协议。2. 可移植 API 的核心Spring AI 的抽象设计2.1 ChatModel 和 ChatClient 的关系Spring AI 的抽象层次可以分为两层底层是ChatModel上层是ChatClient。ChatModel是模型无关的统一接口最核心的方法就是接收一个Prompt返回ChatResponse。每一个具体供应商都实现这个接口比如OpenAiChatModel、AnthropicChatModel。但ChatModel直接使用起来还不够顺手所以 Spring AI 提供了ChatClient。ChatClient采用了 Builder 模式支持prompt()、user()、system()、call()、stream()这种链式写法。我推荐业务代码尽量依赖ChatClient因为它更贴近自然语言的描述习惯而且能让我们用统一的代码风格处理不同模型的输入输出。下面的代码展示了ChatModel和ChatClient的基本用法区别// 直接用 ChatModel ChatResponse response chatModel.call( new Prompt(写一首关于秋天的诗)); String text response.getResult().getOutput().getText(); // 用 ChatClient String text chatClient.prompt() .user(写一首关于秋天的诗) .call() .content();很明显ChatClient更简洁而且不需要手动处理Prompt、ChatResponse这些包装类。如果你的项目里既要支持 OpenAI又要支持 Anthropic你只需要注入一个ChatClient具体底层是哪个模型对业务代码完全透明。2.2 自动配置与 Bean 装配Spring AI 每个模型供应商都有一个对应的 Starter比如spring-ai-starter-model-openai、spring-ai-starter-model-anthropic。当你把某个 Starter 引入 classpath 后Spring AI 会自动根据配置文件里的spring.ai.openai.api-key、spring.ai.anthropic.api-key等属性创建对应的ChatModelBean。这意味着什么呢意味着你的业务代码不需要自己new任何供应商对象也不用关心依赖注入的具体实现类。只要配置文件里有对应的 keySpring 容器里自然就有对应的模型对象。当项目里同时存在多个ChatModelBean 时Spring 会按照 Bean 名称区分它们比如 OpenAI 的 Bean 名通常是openAiChatModelAnthropic 的 Bean 名通常是anthropicChatModel。这时候我们可以在自己的配置类里用Qualifier精确指定要用哪个模型或者干脆再做一层自己的路由封装。后面我会详细演示。2.3 参数传递与 ChatOptions跨模型调用时模型参数temperature、maxTokens、topP 等也需要做统一处理。Spring AI 提供了ChatOptions接口每个供应商的实现类会负责把统一参数映射成自家 API 需要的字段。参数设置有两种方式一种是写在application.yml里作为全局默认另一种是在每次请求时临时覆盖。我建议把全局默认写在配置里把需要频繁调整的参数放在请求级别。spring: ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o temperature: 0.7 anthropic: api-key: ${ANTHROPIC_API_KEY} chat: options: model: claude-3-5-sonnet-20241022 temperature: 0.7请求级覆盖可以这么写String result chatClient.prompt() .user(用一句话介绍你自己) .options(ChatOptions.builder() .temperature(0.2) .build()) .call() .content();这种设计保证了业务代码不需要关心不同模型的参数名差异。你只需要知道“我想让温度低一点”这个业务意图Spring AI 会把它翻译成 OpenAI 的temperature、Anthropic 的temperature甚至是其他模型对应的参数。3. 手写一个可移植的跨模型调用代码3.1 准备工程与依赖我建议从 Spring Boot 3.3 以上版本开始Java 17 以上然后引入 Spring AI 相关依赖。下面用 Maven 演示Gradle 思路一样。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-anthropic/artifactId version1.0.0/version /dependency /dependencies需要注意 Spring AI 的 BOM 管理。实际项目里我一般会引入spring-ai-bom避免不同组件版本混乱dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement引入两个 Starter 后Spring 容器里会同时存在 OpenAI 和 Anthropic 的ChatModelBean。这样我们可以在一套代码里随时切换非常方便。3.2 同时配置 OpenAI 与 Anthropic在application.yml中同时配置两个供应商的 key 和默认模型。我习惯用环境变量注入密钥避免把密钥写死在代码仓库里。spring: application: name: spring-ai-multi-model-demo ai: openai: api-key: ${OPENAI_API_KEY} base-url: https://api.openai.com chat: options: model: gpt-4o temperature: 0.7 max-tokens: 1024 anthropic: api-key: ${ANTHROPIC_API_KEY} base-url: https://api.anthropic.com chat: options: model: claude-3-5-sonnet-20241022 temperature: 0.7 max-tokens: 1024 # 自定义属性指定当前激活的供应商 app: ai: active-provider: openai这里的自定义属性app.ai.active-provider是我自己加的用于在运行时决定默认使用哪个模型。你也可以把它放到环境变量或者配置中心里这样切换模型不用重新编译。3.3 通过一个属性切换当前模型为了把“模型选择”从业务代码中彻底抽离我通常会写一个简单的配置类根据active-provider的值挑选对应的ChatModel再构造一个统一的ChatClient。Configuration public class AiModelConfig { Bean public ChatClient chatClient( Value(${app.ai.active-provider:openai}) String activeProvider, Qualifier(openAiChatModel) ChatModel openAiChatModel, Qualifier(anthropicChatModel) ChatModel anthropicChatModel) { ChatModel chatModel anthropic.equalsIgnoreCase(activeProvider) ? anthropicChatModel : openAiChatModel; return ChatClient.builder(chatModel) .defaultSystem(你是一个乐于助人的 AI 助手) .build(); } }然后在业务类里注入这个ChatClient完全不关心底层是谁。Service public class AiChatService { private final ChatClient chatClient; public AiChatService(ChatClient chatClient) { this.chatClient chatClient; } public String chat(String message) { return chatClient.prompt() .user(message) .call() .content(); } }这时候切换模型只需要改application.yml里的app.ai.active-provider。我实测下来从 OpenAI 切到 Anthropic从改代码到验证完十分钟就够。业务代码一行没动。3.4 流式调用与工具调用的可移植写法如果要做流式输出同样可以用统一 API。下面这段代码同时兼容 OpenAI 和 Anthropicpublic FluxString chatStream(String message) { return chatClient.prompt() .user(message) .stream() .content(); }调用方直接订阅FluxString即可。比如在 WebFlux 接口里返回给前端GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestParam String message) { return aiChatService.chatStream(message); }工具调用Function Calling的差异更大但 Spring AI 也做了封装。我们可以通过Tool注解直接暴露一个 Java 方法供模型调用Component public class WeatherTools { Tool(根据城市名查询当前天气) public String getWeather(String city) { return city 晴25摄氏度; } }业务代码里只需要在ChatClient上启用工具String result chatClient.prompt() .user(北京天气怎么样) .tools(new WeatherTools()) .call() .content();Spring AI 会自动把这个 Java 方法转换成符合 OpenAI 或 Anthropic 协议的工具描述。这里要注意工具方法所在的类必须能被 Spring 管理或者你显式new一个实例传进去否则工具无法被识别。3.5 用 OpenAI 兼容端点再接一个模型除了 OpenAI 和 Anthropic 原生协议现在很多模型供应商提供了 OpenAI 兼容接口比如 DeepSeek、Ollama、智谱等。遇到这种供应商我们不需要再引入额外 Starter而是复用 OpenAI 的 Starter修改base-url和api-key对应的模型名即可。spring: ai: openai: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com chat: options: model: deepseek-chat这样一来你的应用不仅能切 OpenAI 和 Anthropic还能把 DeepSeek 这类模型也纳入同一套可移植代码。我实际遇到过团队内部用 Ollama 跑本地模型做测试接入方式也是一样的只需要把base-url指向http://localhost:11434。这种兼容性设计极大降低了模型选型的试错成本。4. 跨模型接入中的常见坑与排查方法4.1 换个模型就报 400 的典型原因跨模型接入最常见的错误就是 HTTP 400。很多人的第一反应是代码写错了但在 Spring AI 场景下400 往往来自三类原因第一类模型名不对。同一个供应商的模型名也在不断变化比如 OpenAI 的gpt-4o后面可能带日期后缀Anthropic 的 Claude 模型名也要精确到版本号。如果你从网上找的示例代码里拷了一个不存在的模型名服务端就会返回 400。第二类参数超出模型支持范围。不同模型的max_tokens上限不一样同一个参数在 A 模型上合法在 B 模型上可能超限。Spring AI 虽然统一了参数名但它不会替你把超限值拉回到合法范围。第三类消息结构或者工具描述不符合模型要求。这个我们下一节单独说。我的排查顺序是先看日志里实际发送给供应商的请求体再对照该模型官网的 API 文档检查字段名和取值。Spring Boot 开启 debug 日志能看到部分请求信息如果还不够可以临时在ChatClient调用链路里加一个过滤器。4.2 函数调用的 schema 校验错误如果你在启用工具调用时看到类似下面这样的报错说明工具方法生成的 JSON Schema 不合法api error: 400 invalid schema for function artifact: ^(?!.*$)[^\p{cc}\p{cno}]*$这个错误我第一次遇到时非常懵。后来查了 Spring AI 的 GitHub issue 才发现问题出在工具方法的参数类型太复杂尤其是包含了泛型、接口类型或者奇怪的注解。模型服务端在解析函数描述时会严格校验正则表达式和字段类型一旦 Spring AI 生成的 schema 格式不匹配就会直接返回 400。解决方式有几种把工具方法的参数改成简单的 DTO字段类型用String、Integer、Double、ListString这类基础类型。避免在参数对象里使用Object类型或接口类型。升级 Spring AI 版本因为早期版本的工具 schema 生成逻辑确实有一些 bug。如果自定义了JsonSchema注解检查里面的正则和描述是否合法。我建议团队内部约定所有Tool方法的参数对象必须是明确的、可序列化的 DTO不要用 Map 接所有参数。这样跨模型时最稳。4.3 模型名不存在的判断方式有些模型服务商会提示支持的模型名列表比如api error: 400 the supported api model names are deepseek-flash, deepseek-v4这类错误其实帮我们省了排查时间它明确告诉你当前配置的模型名不在支持列表里。处理方式是去对应模型平台的文档确认最新模型名或者在配置中心里把模型名参数化方便快速修改。这里有一个经验不要把模型名硬编码在 Java 代码里尽量放在application.yml或者环境变量中。因为模型名更新频繁硬编码意味着每次改名都要重新编译发布。我见过太多同学把模型名写在常量类里结果换模型时改了配置不生效排查半天才发现是常量类里那个值没动。4.4 版本升级带来的配置差异Spring AI 的版本迭代比较快不同版本之间的配置项和 API 存在差异。比如早期版本用spring.ai.openai.api-key后来在某些版本里改成了spring.ai.openai.api-key但子模块的配置路径可能有变化。Anthropic 的配置也出现过类似调整。如果你照着网络上旧版本的教程配置启动时可能会看到属性无法绑定的报错。我的做法是安装完依赖后先看一眼对应版本的官方文档或spring-configuration-metadata.json确认配置项前缀再动手。另外Spring AI 1.0 和后续 2.0 之间的ChatClientAPI 也有变化。1.0 更推荐用ChatClient.builder(chatModel).build()而早期 M 版喜欢用ChatClient.create(chatModel)虽然两者都能用但建议统一到一个版本。我的原则是除非有明确 bug 或新功能需求否则不要频繁升级 Spring AI 版本。4.5 排查思路总结我整理了跨模型调用中最常见的几个错误及其排查方向错误现象可能原因排查/解决401 UnauthorizedAPI Key 未配置、格式错误、权限不足检查环境变量、密钥是否过期400 model not found模型名不匹配或不存在对照官方文档确认模型名400 invalid schema工具方法参数类型复杂、schema 生成错误简化参数类型升级 Spring AI 版本429 Too Many Requests限流或额度不足检查配额增加退避重试500 / upstream error供应商服务异常熔断降级到备用模型连接超时网络不通或服务端地址错误检查 base-url、网络连通性这里我不建议在业务代码里裸处理这些错误而是统一封装一个AiCallException把底层错误转换成语义明确的自定义异常。这样上层逻辑不用关心你对接的是哪家供应商。5. 再进一步模型路由与故障降级5.1 运行时按条件选择模型前面我们通过配置切换模型但真实项目里往往需要运行时动态选择。比如同一个应用里摘要任务用便宜的模型复杂推理用强模型或者在不同渠道之间做模型隔离。这时候我们可以做一个ModelRouter组件把模型选择从配置提升到规则维度。Component public class ModelRouter { private final MapString, ChatClient chatClientMap; public ModelRouter(ChatClient openAiChatClient, ChatClient anthropicChatClient) { chatClientMap new HashMap(); chatClientMap.put(openai, openAiChatClient); chatClientMap.put(anthropic, anthropicChatClient); } public ChatClient getClient(String provider) { return chatClientMap.getOrDefault(provider, chatClientMap.get(openai)); } }然后业务方就可以根据业务场景自由选择ChatClient client modelRouter.getClient(anthropic); String result client.prompt() .user(解释一下微服务架构) .call() .content();这种写法保留了可移植性同时又把模型选择权交还给了业务。5.2 失败切换从 Anthropic 降级到 OpenAI跨模型方案的价值还体现在故障降级上。去年我负责的一个 API 服务某段时间 Anthropic 的接口连续出现 5xx导致线上质量告警。我们当时的处理方案很简单如果 Anthropic 调用失败自动降级到 OpenAI 模型重试。实现方式并不复杂核心是在调用模型时捕获异常并触发备用模型。可以用 Spring Retry 的Retryable配合Recover也可以用 Resilience4j 的 Fallback 功能。我比较常用的是自定义 fallback 方法public String chatWithFallback(String message) { try { return anthropicChatClient.prompt() .user(message) .call() .content(); } catch (Exception e) { // 记录日志切到备用模型 log.warn(anthropic 调用失败切到 openai, e); return openAiChatClient.prompt() .user(message) .call() .content(); } }当然这只是最简单的降级逻辑。生产环境还要考虑超时时间、重试次数、熔断状态等等。但有了跨模型抽象降级不再需要写两套业务逻辑只需要替换调用目标。5.3 模型能力差异化处理最后提醒一点跨模型抽象能统一大部分 API 调用但模型能力并不是完全一致的。比如某个模型支持图片输入另一个模型可能只支持文本某个模型支持并行工具调用另一个模型可能只支持单工具。在设计可移植代码时要为这种差异留出后门。Spring AI 的做法是允许你直接访问底层原生 API如果你确实需要某个模型的独有能力可以注入具体的ChatModel实现类而不是统一的ChatClient。比如Service public class AdvancedImageService { private final OpenAiChatModel openAiChatModel; public AdvancedImageService(OpenAiChatModel openAiChatModel) { this.openAiChatModel openAiChatModel; } public String describeImage(String imageUrl) { // 使用 OpenAI 特有的多模态参数 ... } }我的建议是90% 的通用业务尽量走ChatClient统一封装10% 的差异化能力通过 provider-specific Bean 单独处理。这样既保证了可移植性又不会因为抽象层太薄而限制能力发挥。最后再分享一点个人体会用 Spring AI 做跨模型集成最大的收获不是省了那几行代码而是改变了团队对模型供应商的认知。以前大家默认模型是绑死的选型时非常谨慎现在模型变成了一个可配置项哪个效果好、哪个成本低随时可以换。我实际测下来从纯 SDK 直连改成 Spring AI 抽象第一次切换模型可能要多花半天来调整配置但之后每次新增模型基本都在半小时以内。这让我觉得这个抽象层是值得的。另外工具调用这块一定要提前设计好参数结构尽量用简单 DTO不要用自由 Map。否则跨模型时 schema 校验很容易出问题。如果你现在的项目还在用原生 SDK 直连模型建议先抽一层自己的接口至少把供应商 SDK 隔离在某个包里再逐步迁移到 Spring AI这样风险会小很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →