
做后端开发这两年被各家 AI 模型的 API 折腾过不少次。项目初期通常只接一个 OpenAI 的接口等后面要上 Anthropic 的 Claude、国内厂商的 DeepSeek 或智谱 GLM 时才发现代码里到处是散落的 request、response 类调用换模型基本等于把业务层重写一遍。Spring AI 的出现正好解决了这个尴尬它把不同模型厂商的差异收敛到一套统一的接口里让业务代码只需要面对 ChatClient 或 ChatModel背后是 OpenAI、Anthropic 还是别家改配置就能切换。这篇文章会从实际项目出发讲清楚 Spring AI 做跨模型 API 调用的核心设计、代码怎么写、有哪些绕不开的坑以及我实际切换模型时踩过的真实问题。适合正在做 AI 应用集成、想在多个模型之间保留切换余地的后端开发者如果你只是刚接触 Spring AI也能按文中的步骤直接把项目跑起来。1. 为什么跨模型调用会成为刚需从 API 碎片化说起1.1 各家模型 API 的协议差异到底有多大先说一个现实OpenAI 的 Chat Completions 协议和 Anthropic 的 Messages API 在请求结构、消息角色、参数命名上都有明显差异。比如 OpenAI 用messages里的role区分 system、user、assistantAnthropic 则把 system 单独抽出来作为顶层参数再比如 OpenAI 有temperature、top_pAnthropic 也有类似参数但取值语义和范围不完全一致工具调用的格式更是各写各的OpenAI 的tools、tool_calls和 Anthropic 的tools、tool_use不兼容。如果业务代码直接调各家 SDK每接一个模型就要在各处补一层适配。更麻烦的是模型返回的 JSON 结构五花八门上层解析逻辑也得跟着写多个分支。这种代码其实不算业务逻辑纯属被协议差异绑架的体力活。1.2 Spring AI 的切入点稳定抽象层替代散弹式调用Spring AI 做的事情通俗讲就是造了一个中间翻译层。它定义了自己的 ChatClient、ChatModel、Message、Tool 等概念底层再针对不同模型做协议转换。业务代码写的是 Spring AI 的统一 API具体翻译成 OpenAI 格式还是 Anthropic 格式由框架替你完成。这套设计天然适合两种场景。一是应用早期不确定最终用哪个模型先用一套代码把功能跑通后面按价格、效果、合规要求随时切。二是生产环境同时接多个模型做容灾或分流比如主用 OpenAI出问题时切到 Anthropic或者在两个模型之间做 A/B 对比。Spring AI 通过spring.ai.model.chat这类配置项切换默认模型通过Qualifier或自定义 Bean 保留多个模型实例同时存在。2. 项目初始化和依赖接入Spring AI 的模板化配置2.1 Maven 依赖怎么加才对我这个项目用的是 Spring Boot 3.3 Spring AI 1.0.0依赖结构如下dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-anthropic-spring-boot-starter/artifactId version1.0.0/version /dependency !-- 其他模型可选按需引入例如 DeepSeek 可用 OpenAI 兼容模式接入 --这里有个容易踩的坑Spring AI 的 BOM 管理Spring Boot 3.3 要和 Spring AI 1.0.0 配套Spring Boot 3.4 则建议直接用 1.0.0 GA 以上版本。版本不匹配会出现NoSuchBeanDefinitionException或者类加载冲突。建议优先用 Spring Initializr 生成项目或者查官方文档里列出的版本兼容矩阵别盲目升版本。2.2 配置文件里的 Key 和模型名别写死接入 OpenAI 和 Anthropic 的最小配置长这样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关键点是 API Key 一定走环境变量或配置中心不要提交到 Git。生产环境里我还习惯把 Key 放在 Vault 或云厂商的密钥管理服务里应用启动时注入。这样做不只是避免泄露也方便 Key 轮换时不用重新发版。如果你要接 DeepSeek、智谱这类国内模型它们大多提供 OpenAI 兼容的接口可以用 Spring AI 的 OpenAI starter把base-url重定向到对应厂商地址。配置示例spring: ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat这种方法很实用但要注意兼容度问题厂商声称兼容 OpenAI实际对 tools、response_format 等参数的支持可能打折后面的函数调用部分我会细讲。3. 可移植调用代码的核心设计ChatClient 与 ChatModel 抽象3.1 用 ChatClient 写业务代码别直接碰 ChatModelSpring AI 的ChatClient是面向业务代码的门面类似 Spring 生态里 RestClient 的定位。它提供了流式调用、参数绑定、默认系统提示词等能力关键是 API 风格不随底层模型变化。一个最简单的聊天调用Service public class ChatCompletionsService { private final ChatClient chatClient; public ChatCompletionsService(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个专业的运维助手回答要简洁、准确。) .build(); } public String ask(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }这套代码里完全看不到 OpenAI 或 Anthropic 的痕迹。切换模型时只改配置文件里的spring.ai.model.chat或者改注入的 Bean 类型业务代码一行不动。这是“可移植”最直接的体现。如果应用里同时需要多个模型实例可以用Qualifier(anthropicChatModel)这种形式注入具体的ChatModel再用它构造独立的 ChatClient。我比较建议在配置类里显式声明几个 ChatClient Bean名称加后缀区分比如openAiChatClient、claudeChatClient避免到处用Qualifier导致代码变乱。3.2 流式输出和结构化输出怎么处理流式调用在 Spring AI 里做成FluxString代码对底层模型透明GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(String message) { return chatClient.prompt() .user(message) .stream() .content(); }如果你要的不是纯文本而是 JSON 对象建议用entity方法绑定一个 POJOpublic record ArtifactInfo(String name, String description, String language) {} ArtifactInfo info chatClient.prompt() .user(把这段代码识别的结果整理成结构化数据: codeSnippet) .call() .entity(ArtifactInfo.class);entity底层依赖模型的结构化输出能力对不同模型的处理方式也不同。OpenAI 的 response_format 支持严格 JSON SchemaAnthropic 也有类似机制Spring AI 会在中间做转换。实际项目中我建议entity和.call().content()结合使用前者管解析、后者管兜底一旦返回内容不合法还能把原始文本记下来排查。4. 函数调用与结构化输出跨模型一致性的难点4.1 Function Calling 的正确打开方式函数调用是让模型能够触发外部工具的关键能力。Spring AI 里注册函数有两种方式注解声明和手动注册。我比较常用的是在配置类里注册一个回调 BeanBean Description(查询指定服务器当前的 CPU 使用率) public FunctionServerQueryRequest, ServerMetric serverMetricFunction() { return request - new ServerMetric(...); }然后在 ChatClient 里启用String answer chatClient.prompt() .user(查一下 web-01 服务器的 CPU 使用率) .functions(serverMetricFunction) .call() .content();Spring AI 会把 Java 方法的参数类型、描述自动转成模型能识别的 JSON Schema。这里必须注意不同模型对Description的描述质量非常敏感描述写得含糊模型就不清楚该不该调、该传什么参数。4.2 JSON Schema 校验报错的排查思路热搜词里出现了一个典型报错API error: 400 invalid schema for function artifact: ^(?!__.*__$)[^\p{cc}\p{c}。这类问题我见过不少根因通常是模型端使用的 JSON Schema 版本较新包含复杂的正则表达式、Unicode 属性类如\p{Cc}、\p{C}而目标模型服务端的 Schema 解析器不支持这类写法。解决方向有几种简化属性校验规则把正则约束去掉让模型自行判断业务侧再校验。函数参数实体类的JsonProperty(required true)只标注真正必要的字段减少模型生成错误内容的概率。如果只在某个模型上出错优先怀疑模型端的 Schema 兼容性而不是 Java 侧代码。函数调用参数定义得越小、越扁平各模型成功调用的概率越高。嵌套对象越深模型生成合法 JSON 的难度越大报错率和漏参率都会上升。4.3 模型名不存在supported api model names 报错还有一个热门报错是API error: 400 The supported API model names are deepseek-flash, deepseek-v4。这种问题常见于两个场景一是期待用 OpenAI 协议接 DeepSeek 时配置里仍然写着gpt-4o二是厂商模型名被后端网关做了白名单限制不支持任意传入。我遇到这种问题的排查顺序先看厂商最近的模型列表文档确认模型名准确无误再看配置文件里的model是否被环境变量覆盖最后看网关或中转层有没有对模型名做映射。多模型接入时最好把模型名配置做成环境相关测试环境用便宜模型生产环境用旗舰模型不要硬编码在代码里。5. 生产环境常见错误排查与避坑手册5.1 连接不上服务的通病Unable to connect to Anthropic services或者Failed to connect to api.anthropic.com这类错误多数不是代码问题而是网络和超时配置。模型 API 的访问稳定性直接影响线上体验建议在配置里显式设置连接超时和读取超时spring: ai: anthropic: base-url: https://api.anthropic.com api-key: ${ANTHROPIC_API_KEY}Spring AI 底层使用 RestClient超时配置继承 Spring 的 HTTP 客户端设置。如果默认超时时间太短长文本生成时很容易在读取阶段断掉但也要防止超时无限长拖垮线程池。生产环境我一般把连接超时设 5 秒、读取超时设 60 秒左右具体根据模型响应耗时调整。如果是调用国内模型反而出现连接失败优先检查 API 地址有没有被错误配置为国外地址以及本地 DNS 解析是否正常。这类网络问题的根因很杂排查时先抓异常堆栈确认是连接建立阶段失败还是要响应超时方向完全不同。5.2 API Key 的安全管理和轮换密钥泄漏是 AI 应用事故重灾区。热搜里出现“openai api key分享”“免费的api密钥”这类词我必须多提醒一句任何来源不明的共享 Key 都不要用轻则额度被跑光重则引发的费用或合规问题足够让你买单。正确的做法是Key 走环境变量/配置中心不写进代码仓库。给 Key 设置预算上限避免异常调用导致账单爆炸。定期轮换 Key至少每三个月换一次。后台开启用量监控发现异常调用及时吊销。我在本地开发时还会准备一个专用的低配额 Key和测试环境分离。这样即使本地 Key 泄露影响面也可控。5.3 多模型容灾与降级策略同时接多个模型不代表代码写两遍利用 Spring AI 的抽象层可以做得很清爽。我在生产项目里用一个 Provider 枚举加一个工厂类管理多个 ChatClientpublic enum ModelProvider { OPENAI, ANTHROPIC, DEEPSEEK } Component public class ChatClientRouter { private final MapModelProvider, ChatClient clients; private final MapModelProvider, HealthChecker healthCheckers; public ChatClient resolve(ModelProvider preferred) { if (healthCheckers.get(preferred).isHealthy()) { return clients.get(preferred); } return clients.get(ModelProvider.OPENAI); // 默认兜底 } }这里的 HealthChecker 可以做成一个轻量探活接口比如调用一个极短的提示词判断返回耗时和是否抛错。不要每次都做完整健康检查会放大模型 API 的调用成本设定一个定时任务每隔 30 秒更新一次健康状态就够了。业务代码里只依赖ChatClientRouter.resolve()返回的客户端完全不感知底层模型切换这就是“可移植 API 代码”在生产环境里的实际价值。6. 从单模型到多模型的迁移经验总结6.1 迁移过程中最容易忽略的差异点切换到不同模型时即使 Spring AI 帮你屏蔽了大部分协议差异以下这些差异依然会穿透抽象层影响业务Token 计费不同同一个提示词在不同模型的成本可能差数倍需要按模型做成本统计。上下文长度不同OpenAI 某型号支持 128KAnthropic 某型号支持 200K超长文档处理逻辑要随模型调整。系统提示词风格敏感度不同Claude 对指令遵循的风格和 GPT 不一样同一套系统提示词在两个模型上的效果可能差异很大。结构化输出能力不同如果业务强制依赖严格 JSON Schema需要单独验证目标模型是否支持。流式协议细节不同虽然 Spring AI 统一了返回类型但不同模型的 token 粒度、停顿频率略有差异前端体验会有不同。这些差异不是 Spring AI 能帮你隐藏的需要业务层根据模型类型做策略化配置。我通常会把模型能力信息做成配置表比如ModelCapability.maxTokens、ModelCapability.supportJsonSchema运行时读取后用做分支判断。6.2 从 Spring AI 1.0 到 2.0 的变化值得关注Spring AI 2.0 已经在路上了代码结构和配置项会有调整比如自动配置装配逻辑变化、ChatClient API 更强调流式和函数调用。如果你是从 1.0 老项目升级建议先看迁移文档重点检查自定义的 ChatModel Bean、spring.ai.model.chat配置项是否被新机制替代。另外Spring AI Alibaba、Spring AI Graph、Multi Agent 这些方向也在快速演进。如果你要做 Agent 编排或图结构任务流可以提前关注这些子项目避免自己造一套流程引擎。实际开发里框架的演进速度赶不上模型迭代速度核心策略还是“业务代码尽量薄框架能力尽量包”。6.3 最后分享一个实践心得我手里接过的 AI 项目里凡是把模型厂商写死在业务代码里的后面都付出了至少一到两周的成本去重构。而一开始就用 Spring AI 抽象层的项目后面加新模型大多只花了半天到一天。踩过几次坑之后我现在的习惯是新项目不论最开始用哪个模型都先把 ChatClient 封装好、配置外置、Provider 分层把“换模型”当成一等公民来设计而不是上线以后才补这个能力。这套做法真正的价值不在于省掉几行代码而在于保留了技术选型上的自由度。AI 模型这个领域迭代太快今天的最优选择并不代表三个月后仍是最优。只要多留一个切换的出口项目就多一分主动权。