1. 从一堆 Key 说起:SpringAI 多模型切换的真实痛点
如果你正在用 SpringAI 做 AI 应用,大概率遇到过这个场景:项目里先接了 DeepSeek,跑通了对话;过两天产品说想加个别的模型做对比,或者代码场景想换一个更擅长补全的模型。于是你打开application.yml,开始复制粘贴第二份api-key、第二个base-url,再写一个@Bean,再改一遍 Service 里的WebClient。等到第三个、第四个模型进来,配置文件已经变成一锅粥,每个厂商的 Key 散落在不同段落,改一个环境变量要翻半天。
这就是多厂商 Key 分散管理的典型问题。它不只是"看着乱",而是会实打实带来几个麻烦:环境切换时容易漏改某个 Key;不同模型的base_url格式不统一,有的带/v1有的不带;想临时切个模型验证效果,得改代码重新打包。对于 SpringAI 这种强调"一套 API 抽象多家模型"的框架来说,配置层反而成了最不优雅的地方。
这篇要解决的,就是让 SpringAI 项目接入 DeepSeek 并实现多模型切换时,只维护一份统一 Key 和一份 base_url,通过ChatClient的运行时参数完成模型切换。适合已经跑通过 SpringAI 基础对话、想进一步做多模型 demo 的开发者。核心思路是:把厂商差异收敛到配置层,把模型选择暴露到调用层。下面从环境准备开始,一步步给出可复制的配置骨架和验证步骤。
2. 前置准备:TaoToken 统一 Key 与依赖骨架
在动手改配置之前,先把"统一入口"这件事定下来。多模型切换之所以痛苦,根源是每个厂商一套鉴权。如果有一个兼容 OpenAI 协议的统一网关,把 DeepSeek 等模型的调用都收敛到同一个base_url和同一个 Key 上,SpringAI 侧就只需要认一个地址。
TaoToken 在这里扮演的就是这个统一入口的角色。它的 API 地址是https://taotoken.net/api,兼容 OpenAI 的/chat/completions协议,所以 SpringAI 的 OpenAI Starter 可以直接对接,不需要为 DeepSeek 单独写一套 WebClient。你需要先去控制台创建一个 API Key,这个 Key 会在后面的application.yml里作为唯一凭证使用。
创建 Key 的入口在控制台的 API Keys 页面:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite。拿到形如sk-xxxx的 Key 之后先放一边,我们来看依赖。
SpringAI 的版本迭代比较快,建议用 1.0.0 及以上的 milestone 或正式版。pom.xml里核心就两个依赖:SpringAI 的 OpenAI Starter 和 WebFlux(因为ChatClient的流式返回依赖 Reactor)。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>如果你用的是 Gradle,对应写法是implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0'。注意 SpringAI 的仓库需要额外配置 milestone 地址,如果你拉不到依赖,检查一下repositories里有没有加 Spring 的 milestone 仓库。这一步踩过坑的人不少,依赖拉不下来八成是仓库没配对。
依赖就绪后,项目结构建议保持简单:一个config包放配置类,一个service包放对话服务,一个controller包放接口。demo 阶段不需要过度分层,能跑通多模型切换才是重点。
3. 可复制配置:application.yml 统一 Key 与模型清单
这一节是全文的核心,配置写对了,后面代码就顺了。关键点有两个:一是把base-url指向 TaoToken 的统一地址,二是把"可选模型列表"做成配置项,而不是硬编码在 Java 里。
先看application.yml的完整骨架:
spring: ai: openai: # 统一入口:所有模型共用这一个 base-url base-url: https://taotoken.net/api # 统一 Key:所有模型共用这一个凭证 api-key: ${TAOTOKEN_API_KEY} chat: options: # 默认模型,不指定时用它 model: deepseek-chat temperature: 0.7 # 自定义:可切换的模型清单 springai: models: default-model: deepseek-chat available-models: - deepseek-chat - deepseek-coder - deepseek-reasoner这里有几个设计取舍值得说明。api-key用${TAOTOKEN_API_KEY}占位,是为了不把密钥写死在文件里,本地开发可以配环境变量,线上走配置中心。base-url只写https://taotoken.net/api,不要自己拼/v1,SpringAI 的 OpenAI 客户端会自动补全路径,手动加反而容易 404。
springai.models这一段是我自己加的命名空间,用来承载"模型清单"这个业务概念。default-model决定不传参时用哪个,available-models则是白名单——后面 Controller 收到模型名时会先校验是否在清单里,避免用户传个不存在的模型导致下游报错。这个白名单机制在多模型 demo 里很实用,相当于把"能切哪些模型"变成可配置项,加模型不用改代码。
如果你想把模型清单放到 Nacos 或 Apollo 做动态刷新,只需要把springai.models这段挪过去,Java 侧用@ConfigurationProperties或@RefreshScope绑定即可,结构不用动。这就是把清单外置的好处。
配置类负责把这段 YAML 绑成对象:
@Configuration @ConfigurationProperties(prefix = "springai.models") @Data public class ModelProperties { private String defaultModel; private List<String> availableModels = new ArrayList<>(); }@Data来自 Lombok,省掉 getter/setter。绑定完成后,ModelProperties就能在 Service 里注入了。到这里,统一 Key 和模型清单都就位了,接下来写对话服务。
4. 核心实现:ChatClient 动态切换 DeepSeek 与其他模型
SpringAI 的ChatClient是推荐的高层 API,比直接操作OpenAiChatModel更顺手。多模型切换的关键在于:ChatClient支持在每次请求时通过options覆盖模型名,而不需要为每个模型建一个 Bean。
先看 Service 的实现:
@Service public class MultiModelChatService { private final ChatClient chatClient; private final ModelProperties modelProperties; public MultiModelChatService(ChatClient.Builder builder, ModelProperties modelProperties) { this.chatClient = builder.build(); this.modelProperties = modelProperties; } /** * 指定模型对话;modelName 为空则用默认模型 */ public String chat(String message, String modelName) { String target = resolveModel(modelName); return chatClient.prompt() .user(message) .options(OpenAiChatOptions.builder() .model(target) .build()) .call() .content(); } /** * 校验并解析模型名,不在白名单则回退默认 */ private String resolveModel(String modelName) { if (modelName == null || modelName.isBlank()) { return modelProperties.getDefaultModel(); } if (!modelProperties.getAvailableModels().contains(modelName)) { // 不在清单里,回退默认,避免下游 400 return modelProperties.getDefaultModel(); } return modelName; } }这段代码里,OpenAiChatOptions.builder().model(target)就是切换模型的开关。因为base-url和api-key已经在全局配置里统一了,这里只需要改模型名,请求就会打到 TaoToken 的统一入口,由它路由到对应的 DeepSeek 模型。这就是"一次配置跑通多模型"的落地方式。
resolveModel做了两层保护:空值走默认,非法值也走默认。实测下来,这个回退逻辑能挡掉大部分因为前端传错参数导致的 400 错误。如果你想要更严格的策略,比如非法模型直接抛异常,把回退那行改成throw new IllegalArgumentException(...)即可。
Controller 层就很简单了,暴露两个入口:
@RestController @RequestMapping("/api/chat") public class ChatController { private final MultiModelChatService chatService; public ChatController(MultiModelChatService chatService) { this.chatService = chatService; } @GetMapping public Map<String, String> chat(@RequestParam String message, @RequestParam(required = false) String model) { String reply = chatService.chat(message, model); return Map.of("model", model == null ? "default" : model, "reply", reply); } }调用方式有两种:GET /api/chat?message=你好走默认模型;GET /api/chat?message=写个快排&model=deepseek-coder显式指定。返回体里带上实际使用的模型名,方便验证切换是否生效。
如果你需要流式输出,把.call().content()换成.stream().content()返回Flux<String>,Controller 返回类型改成Flux<String>即可,模型切换逻辑完全不变。这一点是ChatClient抽象带来的便利——切换模型和切换返回模式互不干扰。
5. 验证请求:确认多模型切换真的生效
配置和代码都写完了,接下来要验证。启动项目后,先用默认模型打一发:
curl "http://localhost:8080/api/chat?message=用一句话解释什么是递归"预期返回类似:
{"model":"default","reply":"递归是指一个函数在定义中调用自身的编程技巧……"}再显式指定deepseek-coder,问一个偏代码的问题:
curl "http://localhost:8080/api/chat?message=用Java写一个二分查找&model=deepseek-coder"如果返回的reply里包含完整的 Java 方法实现,说明模型切换生效了。你可以对比两次返回的风格差异——deepseek-chat偏通用解释,deepseek-coder在代码任务上通常更聚焦。这种对比本身就是多模型 demo 的价值所在。
再测一下白名单回退:故意传一个不存在的模型名。
curl "http://localhost:8080/api/chat?message=你好&model=not-exist-model"预期它不会报错,而是回退到默认模型正常返回。这说明resolveModel的保护逻辑起作用了。
如果你想更直观地看到模型切换,可以在 Service 里加一行日志,打印实际使用的模型名:
log.info("chat request routed to model={}", target);启动时把日志级别调到 INFO,每次请求都能在控制台看到路由到了哪个模型。这个技巧在排查"为什么切换没生效"时特别有用——如果日志里始终是默认模型,那问题多半出在参数没传进来或者白名单校验把它挡了。
验证通过后,一个统一 Key、一份配置、多模型切换的 SpringAI demo 就跑通了。整个过程没有为 DeepSeek 单独写 WebClient,也没有维护第二份鉴权信息。
6. 常见报错排查:从 401 到模型不存在的定位思路
多模型接入最容易卡在几个固定位置,这里按报错现象整理排查路径。
401 Unauthorized:九成是 Key 的问题。先确认TAOTOKEN_API_KEY环境变量真的注入了,可以在启动日志里打印一下 Key 的前几位(别打全)。如果 Key 是对的,检查base-url有没有多写或少写路径——正确值是https://taotoken.net/api,不要带/v1,也不要带/chat/completions。SpringAI 会自动拼接,手动加会变成双重路径导致鉴权失败。
404 Not Found:通常是base-url写错。有人习惯性写成https://taotoken.net/api/v1,结果请求打到/api/v1/chat/completions,而实际路径是/api/chat/completions。把/v1去掉即可。
模型不存在或 400 Bad Request:先确认你传的模型名在available-models清单里,且拼写和上游一致。deepseek-chat、deepseek-coder这些名字区分大小写,写成DeepSeek-Chat可能就找不到。如果清单里有但依然报错,去模型对话页面手动发一条消息验证该模型当前是否可用:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite。手动能通说明是代码侧参数没传对,手动也不通就是模型侧的问题。
切换不生效,始终返回默认模型:按这个顺序查。第一,Controller 的@RequestParam名字和 URL 参数是否一致,model对model,别一个叫modelName一个传model。第二,resolveModel的白名单校验是否把合法模型误判了,打印一下availableModels的内容确认配置绑定成功。第三,OpenAiChatOptions是否真的被应用,可以在 Service 里打印target确认。
依赖冲突导致启动失败:SpringAI 对 Spring Boot 版本有要求,如果启动时报NoSuchMethodError或ClassNotFoundException,多半是版本不匹配。建议 Spring Boot 3.2+ 配 SpringAI 1.0.0。用mvn dependency:tree看一下有没有旧版 OpenAI SDK 混进来。
排查的核心思路是:先确认统一入口(base-url + Key)通不通,再确认模型名对不对,最后确认参数有没有传到。这三层分开验证,比一股脑改代码高效得多。接入相关的完整参数说明可以对照文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite。
7. 从 demo 到长期编码:把统一 Key 用起来
跑通这个 demo 之后,你会发现统一 Key 的价值不只是"少写几行配置"。当你把 SpringAI 项目从验证阶段推进到日常编码辅助、Agent 工具链时,模型调用会变得高频且多样——写代码用 coder 类模型,写文档用通用模型,复杂推理用 reasoner 类模型。如果每个模型一套 Key,运维成本会随模型数量线性增长;统一入口之后,加模型只是往available-models里加一行。
如果你打算把这类多模型调用长期用在编码场景,可以了解一下 Coding Plan,它针对高频编码调用做了额度规划,配合这里的统一配置能进一步简化成本管理:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。对于需要频繁切换模型做对比验证的场景,模型对话页面可以快速手动试跑,省去每次改代码重启的麻烦。
回到代码本身,这个 demo 还有两个可以继续打磨的方向。一是把resolveModel的策略从"白名单回退"升级成"按请求内容自动选模型",比如检测到 prompt 里有代码块就路由到 coder 模型;二是把模型清单接到配置中心,实现不重启动态增删模型。这两步做完,多模型切换就从 demo 级别变成了可上生产的基础设施。而这一切的前提,都是先把统一 Key 和统一 base_url 这层地基打牢——地基稳了,上面怎么搭都快。