上个月做智能客服工作台,前期图快直接用了某家模型厂商自带的SDK,结果到二期产品说"普通用户走便宜模型、付费用户走强模型、运营活动临时切换",我一翻代码就头疼——业务层已经被厂商SDK绑死了,请求对象、返回结构、错误处理全是那家特有的写法。后来我把方案改成Spring AI + 通义千问,多模型切换这件事变得异常简单,核心逻辑就几行配置的事。这篇文章就把整套方案完整展开,包括可运行的Demo、三种切换方式、以及我实际调试中踩过的版本、模型名、超时之类的坑。适合正在做Java AI应用、又不想被单一模型厂商绑死的开发者,尤其对国内直连通义千问有需求的朋友。
1. 为什么我最终选Spring AI + 通义千问的组合
1.1 AI厂商SDK各搞各的,抽象层成了刚需
接多个大模型最难受的地方,不是模型效果,而是写代码的方式完全不一样。OpenAI的SDK是一套client.chat.completions.create的写法,通义千问的原生SDK走的是DashScope那套接口,某些国产模型又是另一套认证和消息格式。你换一个厂商,不只是改一个配置的事,请求参数、返回结构、错误码、重试策略全都要重写。业务代码一旦被某个厂商SDK渗透,后面每一次切换都要脱一层皮。
Spring AI解决的就是这个问题。它定义了一套统一的接口,ChatModel是模型层抽象,ChatClient是对外使用的门面。业务代码只面向ChatClient写,底层到底是通义千问、OpenAI还是本地Ollama,对业务层完全透明。这个思路很像当年的JDBC——不同数据库各有各的协议,但Java开发者只要写一套JDBC代码,换数据库改驱动和连接串就行。大模型接入现在也等来了这么一层标准。
Spring AI 2.0.1这版把ChatClient的API做得很顺手,链式调用非常直观。我在切到Spring AI之后,第一感受是以前几十行SDK胶水代码,现在一个prompt()方法就搞定了;第二感受是提示词、温度参数、流式处理全都统一了,换模型基本就是改配置。如果你现在还在业务代码里到处new厂商的Client,真心建议尽早抽一层,不然后面切换模型的时候一定后悔。
1.2 通义千问值得接的几个理由
选模型厂商,我主要看四点:访问体验、模型能力、价格、接入成本。
访问体验是说,通义千问走的是阿里云百炼(DashScope)平台,国内直连,响应很稳定。之前项目里用海外模型,要考虑网络延迟、额外的网关组件,光是运维成本就够劝退的。
模型能力上,qwen系列现在的梯队拉得很清楚——qwen-turbo速度快、价格低,适合高并发客服、简单分类;qwen-plus是综合性价比之王,大部分业务场景都能顶住;qwen-max是旗舰,复杂推理、长文本、代码生成这类硬任务最稳。实测同一个Prompt,日常闲聊几个模型的差异不明显,但涉及多步推理、代码审查、长对话记忆的时候,max确实更稳。中文场景里,qwen这代模型的表现属于第一梯队。而且qwen在Spring AI里有官方Starter,连适配层都省了。
价格上,通义千问相比同档位海外模型便宜不少,对创业团队和中小项目友好。接入成本上,只要在百炼平台领一个API Key,依赖里加一个spring-ai-starter-model-qwen,剩下的就是Spring配置文件和业务代码的事。
所以最终组合很自然:Spring AI负责统一抽象,通义千问负责模型能力,中间不需要额外网关,一套代码跑多个模型。
2. 版本适配与依赖引入:这里藏着第一个坑
2.1 Spring AI 2.0.1适配哪个Spring Boot
Spring AI 2.0.1不是一个独立跑的框架,它是Spring生态里的AI模块,所以版本匹配非常重要。我自己用的组合是JDK 17 + Spring Boot 3.4.5 + Spring AI 2.0.1。如果你的项目还在Spring Boot 3.2.x甚至更早,直接把spring-ai-bom引入,启动时大概率会报Bean创建失败或者MethodNotFoundException,这类错误非常误导人,因为日志和你写的代码没有直接关系。
建议直接用Spring Boot的BOM统一管理Spring AI版本。在pom.xml里通过dependencyManagement引入spring-ai-bom,依赖项本身就不写版本号了,由框架自动对齐。这样能最大程度避免我踩过的"版本漂移"问题——所谓版本漂移,就是你在网上看到一段可用的配置,复制过来发现自己的依赖版本跟人家差了一大截,跑起来全是怪问题。
2.2 依赖坐标和BOM的写法
pom.xml核心部分如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>2.0.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-qwen</artifactId> </dependency> </dependencies>注意spring-ai-starter-model-qwen正是Spring AI官方对接通义千问/DashScope的Starter。你可能在社区里见过spring-ai-alibaba,那是另一套东西,由阿里社区维护,定位偏向Spring Cloud Alibaba生态,两者很容易被搞混,这点我在避坑指南里专门讲。
2.3 最小可运行配置
依赖引入之后,application.yml只需要一个API Key和一个模型名就能跑:
spring: application: name: spring-ai-qwen-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7这里第一个坑就来了:配置前缀是spring.ai.dashscope,不是spring.ai.qwen。因为通义千问走的平台是阿里云百炼DashScope,Spring AI官方对接的是DashScope的OpenAI兼容接口。写错前缀不会启动报错,但配置完全不生效,调用时要么用默认模型,要么直接403。
API Key建议从环境变量读,别硬编码在配置文件里。这个真的不是废话,我见过太多人把真实Key写到配置文件然后误推到代码仓库,被爬虫扒下来狂刷额度。Key的格式是sk-开头的一长串字符,拿到的第一时间就放进环境变量。
3. 三步跑通通义千问:拿Key、写配置、发消息
3.1 第一步:在百炼平台拿API Key
先打开阿里云百炼控制台,开通DashScope模型服务之后,在API-KEY管理页面创建一个Key。OpenAI兼容模式不用单独开,默认就支持。
拿到Key之后放到环境变量:
export DASHSCOPE_API_KEY=sk-xxxx如果用的是IDEA,在Run Configuration里配上环境变量即可。这个Key只在本机有效,不要在代码里留任何真实Key的痕迹。
3.2 第二步:写好application.yml
配置在上一章已经给过,这里补充几个关键参数的含义:
model:决定走哪个模型。qwen-plus是通用默认,qwen-turbo轻量快速,qwen-max旗舰复杂任务。temperature:控制随机性,取值0到2之间。对话场景0.7是常见起点,代码生成建议0.2,需要稳定确定性输出的场景调更低。- 通义千问的嵌入模型(embedding)也会被自动配置,如果只做聊天,理论上可以排除,但实际影响不大,不用管。
3.3 第三步:用ChatClient发第一条消息
Controller层代码非常简洁:
@RestController @RequestMapping("/api/ai") public class AiController { private final ChatClient chatClient; public AiController(ChatClient.Builder chatClientBuilder) { this.chatClient = chatClientBuilder.build(); } @GetMapping("/chat") public Map<String, String> chat(@RequestParam("msg") String msg) { String reply = chatClient.prompt(msg).call().content(); return Map.of("reply", reply); } }chatClient.prompt(msg).call().content()这行就完成了整个调用。prompt()传入用户消息,call()发起同步请求,content()把响应文本取出来。
如果要带系统提示词,写法也不复杂:
String reply = chatClient.prompt() .system("你是一个严谨的Java技术顾问,回答尽量简洁,必要时给出代码示例") .user(msg) .call() .content();启动项目后,浏览器直接访问:
http://localhost:8080/api/ai/chat?msg=用一句话介绍Spring AI能正常返回中文回答,说明接入成功了。这一步走通,Spring AI + 通义千问的最小闭环就建立了。
如果要做打字机效果的流式输出,用stream()方法:
@GetMapping(value = "/chat/stream", produces = "text/event-stream;charset=UTF-8") public Flux<String> chatStream(@RequestParam("msg") String msg) { return chatClient.prompt(msg).stream().content(); }前提是项目里引入了WebFlux依赖,返回值类型必须是Flux<String>,别在纯MVC项目里硬写这个方法,否则返回类型不受支持。
4. 多模型切换的三种落地姿势
4.1 姿势一:配置文件直接切换
最简单的方式,改配置文件里的model字段,重启生效:
spring: ai: dashscope: chat: options: model: qwen-max如果你接了Nacos这类配置中心,把这段配置放到配置中心里,改配置可以不重启服务就热生效。这种方式最适合快速验证模型差异:同一个问题分别用qwen-turbo、qwen-plus、qwen-max跑一遍,看效果再定方案。缺点是路由粒度很粗,所有请求都走同一个模型,没法按业务场景区分。
4.2 姿势二:多Bean + 业务路由
生产环境最常见的需求是:普通问答走qwen-turbo省钱,复杂任务走qwen-plus,深度分析走qwen-max。这时需要在Spring容器里注册多个ChatClient Bean,每个对应不同模型:
@Configuration public class MultiModelConfig { @Bean("qwenTurboClient") public ChatClient qwenTurboClient(DashScopeApi dashScopeApi) { DashScopeChatOptions options = DashScopeChatOptions.builder() .withModel("qwen-turbo") .build(); ChatModel model = new DashScopeChatModel(dashScopeApi, options); return ChatClient.builder(model).build(); } @Bean("qwenPlusClient") @Primary public ChatClient qwenPlusClient(DashScopeApi dashScopeApi) { DashScopeChatOptions options = DashScopeChatOptions.builder() .withModel("qwen-plus") .build(); ChatModel model = new DashScopeChatModel(dashScopeApi, options); return ChatClient.builder(model).build(); } @Bean("qwenMaxClient") public ChatClient qwenMaxClient(DashScopeApi dashScopeApi) { DashScopeChatOptions options = DashScopeChatOptions.builder() .withModel("qwen-max") .build(); ChatModel model = new DashScopeChatModel(dashScopeApi, options); return ChatClient.builder(model).build(); } }我把实现类DashScopeChatModel显式写出来,是为了让"每个Client对应哪个模型"一眼清楚。你的Spring AI版本如果API有细微差异,以本地引入jar包的源码为准(有的版本方法名差个前缀,整体思路不变)。
然后写一个路由Service,按业务维度选择模型:
@Service public class ModelRouter { private final ChatClient turboClient; private final ChatClient plusClient; private final ChatClient maxClient; public ModelRouter( @Qualifier("qwenTurboClient") ChatClient turboClient, @Qualifier("qwenPlusClient") ChatClient plusClient, @Qualifier("qwenMaxClient") ChatClient maxClient) { this.turboClient = turboClient; this.plusClient = plusClient; this.maxClient = maxClient; } public String chatByLevel(String userLevel, String msg) { ChatClient target = switch (userLevel) { case "vip" -> maxClient; default -> plusClient; }; return target.prompt(msg).call().content(); } }这种姿势的核心价值是逻辑清晰,谁走哪个模型一眼可读,代码也好维护。切换模型只需要改路由规则的判断条件,不改配置不动模型Bean。
4.3 姿势三:运行时动态切换
更进阶的需求是:模型选择本身是运行时参数。比如API网关让调用方自己传model参数,或者根据当前请求的复杂程度自动判断是否升级模型。这时候可以维护一个模型名到ChatClient的映射:
@Component public class ChatClientRegistry { private final Map<String, ChatClient> clientMap = new ConcurrentHashMap<>(); public ChatClientRegistry(List<ChatClient> chatClients, @Qualifier("qwenTurboClient") ChatClient turbo, @Qualifier("qwenPlusClient") ChatClient plus, @Qualifier("qwenMaxClient") ChatClient max) { clientMap.put("turbo", turbo); clientMap.put("plus", plus); clientMap.put("max", max); } public ChatClient get(String model) { ChatClient client = clientMap.get(model); if (client == null) { throw new IllegalArgumentException("未注册的模型: " + model); } return client; } }结合@Qualifier和Bean命名,把turbo、plus、max分别注册进去之后,路由逻辑就可以完全动态化了。我在实际项目里加了一个简单策略:请求里带model参数就走对应模型,不带就走默认plus。这个方案灵活性最高,适合SaaS平台、AI网关、面向C端用户的AI应用。
三种方案怎么选,参考这个对比:
| 方案 | 切换粒度 | 改动成本 | 适用场景 | | 配置文件切换 | 全局 | 最低 | 快速验证、配置中心热更新 | | 多Bean路由 | 业务维度 | 中 | 按用户等级、任务复杂度分流 | | 运行时动态 | 请求维度 | 中高 | 用户自选模型、按成本动态调度 |
5. 完整可运行代码:一个可切换的接入Demo
5.1 工程结构一览
给出一份可以直接跑的Demo工程,目录结构如下:
spring-ai-qwen-demo/ ├── pom.xml └── src/main/ ├── java/com/example/qwen/ │ ├── QwenDemoApplication.java │ ├── config/ │ │ └── MultiModelConfig.java │ ├── router/ │ │ ├── ChatClientRegistry.java │ │ └── ModelRouter.java │ └── web/ │ └── AiController.java └── resources/ └── application.yml5.2 核心代码实现
启动类就是一个普通Spring Boot应用:
@SpringBootApplication public class QwenDemoApplication { public static void main(String[] args) { SpringApplication.run(QwenDemoApplication.class, args); } }MultiModelConfig注册三个ChatClient,对应三个模型。为了简洁,我这里用ChatClientRegistry做动态路由,同时保留ModelRouter演示业务路由。Controller层接收model参数和消息内容,交给动态路由:
@RestController @RequestMapping("/api/ai") public class AiController { private final ChatClientRegistry registry; public AiController(ChatClientRegistry registry) { this.registry = registry; } @GetMapping("/chat") public Map<String, String> chat( @RequestParam(value = "model", defaultValue = "plus") String model, @RequestParam("msg") String msg) { ChatClient target = registry.get(model); return Map.of( "model", model, "reply", target.prompt(msg).call().content() ); } }路由注册那里,ChatClientRegistry构造方法里我用了参数注入三个Client,你也可以改成List<ChatClient>配合自定义标记遍历注册。前者直白,后者灵活,根据项目规模选择。
5.3 测试与验证方法
启动后,用curl分别验证三种模型:
curl "http://localhost:8080/api/ai/chat?model=turbo&msg=你好" curl "http://localhost:8080/api/ai/chat?model=plus&msg=你好" curl "http://localhost:8080/api/ai/chat?model=max&msg=你好"每个请求返回的model字段会告诉你当前走的是哪个模型。要想明显看到模型差异,可以问一个需要推理的问题,比如问"一根绳子绕地球一圈,如果加长10米,绳子离地面的平均高度大概是多少",max模型给的推理过程会完整很多。也可以直接观察请求耗时,qwen-turbo明显比qwen-max快。
6. 避坑指南:从配置到上线的实战教训
6.1 版本搭配错误的典型症状
我踩过最狠的坑,是把spring-ai-bom引入到一个Spring Boot 3.2.x的旧项目里。启动直接报:
APPLICATION FAILED TO START Parameter 0 of method qwenChatModel in ... required a bean of type '...' that could not be found.这种"Bean not found"的错误,第一反应基本都会去查Bean定义,结果搞半天才发现是版本不匹配。Spring AI 2.x对Spring Boot版本是有要求的,建议直接用最新的Spring Boot 3.4.x,别为了兼容老项目硬上,不然你会遇到一堆莫名其妙的类加载问题。
另外,网上大量教程停留在Spring AI 1.0的API,那时是chatModel.call(new Prompt(...)),1.0.0 GA之后逐步引入ChatClient,2.0的API更顺手但代码长得和旧教程不一样。搜资料时认准"Spring AI 2.0"字样,遇到旧代码注意区分。
6.2 模型名称和选型别想当然
百炼控制台显示的模型名(qwen-plus、qwen-max等)要和配置里完全一致,一个字符都不能差。我见过把qwen-max的横杠写成下划线的,调用直接报模型不存在。模型选型上,给个参考:
| 模型 | 适合场景 | 速度 | 价格档位 | | qwen-turbo | 简单问答、高并发分类、文本改写 | 最快 | 低 | | qwen-plus | 通用客服、内容生成、中等复杂度任务 | 快 | 中 | | qwen-max | 复杂推理、长文档、代码审查 | 较慢 | 高 |
还有一个容易被忽略的点:超长输入要留意上下文窗口限制。一次对话如果塞进很长的文档,会报ContextLengthExceed,需要提前对输入做截断或摘要。qwen-plus和qwen-max的上下文比turbo长,但都不是无限。
6.3 超时配置影响真实体感
Spring AI调用DashScope默认用的是RestClient。在公司网络环境或者首次请求冷启动时,特别容易因为握手慢而超时。我习惯把连接超时和读取超时调大,连接5秒、读取120秒——大模型生成本来就慢,读取超时设太短会误伤正常请求。
配置方式可以通过自定义RestClient.Builder实现,或者准备一个独立的OkHttpClient替换默认客户端。这里不展开网络细节,但请记住:超时时间直接决定用户体感,设置不到位,模型调用稍慢一点就报超时,接口层面看起来就是"系统不稳定"。
6.4 流式输出的坑
流式输出我在前面给了代码,但必须再提醒一次:在Spring MVC项目中,@GetMapping返回Flux<String>需要引入spring-boot-starter-webflux,否则返回类型不受支持。如果你的项目是Spring MVC + WebFlux共存,还要注意包扫描和依赖冲突的问题。AI应用的实时对话体验确实值得上流式,但请先在WebFlux环境下把SSE测通,再往业务里接。
6.5 spring-ai-alibaba和spring-ai-starter-model-qwen别装混
很多人搜"spring ai alibaba停更了吗",这里统一回答:阿里社区维护的spring-ai-alibaba,和Spring官方出的spring-ai-starter-model-qwen,是两个东西。前者是结合Spring Cloud Alibaba的AI组件,演进节奏看社区;后者是Spring官方支持的DashScope/通义千问接入Starter,跟着Spring AI主线版本持续迭代,Spring AI 2.0.1还是有活跃发版的。
要是为了在Spring Boot项目里接通义千问,直接用spring-ai-starter-model-qwen。要是用了Spring Cloud Alibaba全家桶,并且想要更整合的AI能力,再去看spring-ai-alibaba。二者不是替代关系,但不要装错依赖,否则可能出现配置属性冲突、Bean重复注册等怪问题。
实际做下来,Spring AI + 通义千问这套组合,最大的价值不是省掉那几行SDK调用代码,而是让"模型"变成了一个可配置、可路由的服务。以后无论你想接入一个开源模型,还是换掉某个供应商,业务层代码基本不用动,只是新增一个Bean和一行配置的事。
最后再分享一个小技巧:在开发环境,你完全可以用同一套接入结构同时对接通义千问和本地Ollama——比如qwen系列的开源版本,一套代码,线上走百炼、本地调试走免费模型,开发成本直接降下来。我在把demo工程落地到自己的项目后,最深的感受是:技术选型时花点时间把抽象层做好,后面每一次模型切换都会轻松很多。希望这份带完整代码的踩坑总结,能让你直接把通义千问跑起来,少走我走过的弯路。