Spring AI 实战系列(三):多模型共存+双版本流式输出
一、系列回顾与本篇定位
1.1 系列回顾
- 第一篇:完成了 Spring AI 与阿里云百炼的基础集成,基于
ChatModel实现了同步对话、API Key安全注入,跑通了从0到1的Spring AI 开发。 - 第二篇:解锁了
ChatClient,实现了全局统一配置、一行代码完成大模型调用,告别了重复的样板代码。
系列栏目:Spring AI
Spring AI 实战教程(一)入门示例
Spring AI 实战系列(二):ChatClient封装,告别大模型开发样板代码
Spring AI 实战系列(三):多模型共存+双版本流式输出
Spring AI 实战系列(四):Prompt工程深度实战
Spring AI 实战系列(五):结构化输出,让大模型严格适配你的业务数据模型
Spring AI 实战系列(六):Tool Calling深度实战,让大模型自动调用你的业务接口
Spring AI实战系列(七):Chat Memory实战,基于Redis实现持久化多轮对话
Spring AI 实战系列(八):多模态能力—— 文生图、语音合成与向量嵌入实战
Spring AI 实战系列(九):RAG检索实战 —— 私有知识库
Spring AI 实战系列(十):MCP深度集成 —— 工具暴露与跨服务调用
1.2 本篇定位
本篇是系列进阶篇,解决开发中最常见的两个痛点:
- 多模型无缝切换与共存:一套 Spring Boot 项目同时对接DeepSeek、Qwen,根据业务场景动态选择模型,无需重复搭建环境。
- 双版本流式输出实现:分别用
ChatModel和ChatClient实现流式响应,对比两者的开发体验与适用场景,给生产环境选型提供明确建议。
二、核心痛点拆解
2.1 多模型共存的必要性
现在大模型市场百花齐放,没有任何一个模型能覆盖所有业务场景:
- DeepSeek-V3:推理速度快,适合高频、低复杂度的场景(如客服问答、代码补全提示)。
- Qwen-Max:专业能力强、多模态支持完善,适合复杂推理、文档分析、多模态交互的场景(如技术方案生成、企业知识库问答)。
如果每个模型单独建一个项目,不仅维护成本高,还无法共享业务逻辑、数据库连接等资源。
2.2 流式输出的必要性
大模型生成长文本(如技术方案、小说、代码)时,同步调用需要等待几十秒甚至几分钟,用户体验极差。流式输出可以像打字机一样逐字 / 逐 Token 返回结果,大幅提升用户的交互体验。
三、实战落地:多模型共存 + 双版本流式输出
3.1 环境前提
- 已完成 JDK 17+、Spring Boot 3.2.x 环境搭建
- 已配置阿里云百炼 API Key 环境变量
DASHSCOPE_API_KEY(注意:DeepSeek 现在也可以通过阿里云百炼的 API 调用,无需单独申请 DeepSeek 的 Key) - 已在
pom.xml中引入spring-ai-alibaba-starter-dashscope核心依赖(参考第一篇)
3.2 第一步:多模型全局配置类
我们创建LLMConfig.java,同时注册DeepSeek和Qwen的ChatModel与ChatClient Bean,通过@Qualifier注解区分注入,避免 Bean 冲突。
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Spring AI 多模型共存配置类 */ @Configuration public class LLMConfig { // 模型名称常量定义,统一管理,避免硬编码 private static final String DEEPSEEK_MODEL = "deepseek-v3"; private static final String QWEN_MODEL = "qwen-max"; // ==================== 1. ChatModel 原子API Bean 注册 ==================== /** * DeepSeek-V3 ChatModel 实例 * 通过阿里云百炼API调用,无需单独申请DeepSeek Key */ @Bean(name = "deepseek") public ChatModel deepSeekChatModel() { return DashScopeChatModel.builder() // 从系统环境变量读取API Key,避免硬编码泄露 .dashScopeApi(DashScopeApi.builder() .apiKey(System.getenv("DASHSCOPE_API_KEY")) .build()) // 全局默认模型参数,统一管理 .defaultOptions(DashScopeChatOptions.builder() .withModel(DEEPSEEK_MODEL) .withTemperature(0.7) .withMaxTokens(2000) .build()) .build(); } /** * Qwen-Max ChatModel 实例 */ @Bean(name = "qwen") public ChatModel qwenChatModel() { return DashScopeChatModel.builder() .dashScopeApi(DashScopeApi.builder() .apiKey(System.getenv("DASHSCOPE_API_KEY")) .build()) .defaultOptions(DashScopeChatOptions.builder() .withModel(QWEN_MODEL) .withTemperature(0.7) .withMaxTokens(2000) .build()) .build(); } // ==================== 2. ChatClient Fluent API Bean 注册 ==================== /** * DeepSeek-V3 ChatClient 实例 * 基于已注册的deepseek ChatModel构建 */ @Bean(name = "deepseekChatClient") public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepseek) { return ChatClient.builder(deepseek) // 可选:全局默认系统提示词,所有调用都会自动携带 .defaultSystem("你是一个专业的AI助手,回答问题简洁、高效、有逻辑") .build(); } /** * Qwen-Max ChatClient 实例 * 基于已注册的qwen ChatModel构建 */ @Bean(name = "qwenChatClient") public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) { return ChatClient.builder(qwen) .defaultSystem("你是一个专业的Java后端开发工程师,擅长Spring生态技术栈,回答问题专业、有可落地的代码示例") .build(); } }关键说明:
- 模型名称统一管理:用常量定义模型名称,避免硬编码分散在业务代码中,后续切换模型只需修改常量即可。
- API Key 复用:DeepSeek现在已接入阿里云百炼生态,无需单独申请 DeepSeek的API Key,直接复用
DASHSCOPE_API_KEY即可。 - Bean 命名规范:通过
@Bean(name = "xxx")和@Qualifier("xxx")明确区分不同模型的 Bean,避免Spring容器的注入歧义。 - 全局默认配置分离:ChatModel的全局默认参数(模型版本、温度、最大Token数)和 ChatClient 的全局默认系统提示词分离,职责清晰。
3.3 第二步:双版本流式输出接口开发
我们创建StreamOutputController.java,分别用ChatModel和ChatClient实现DeepSeek和Qwen 的流式响应接口,直观对比两者的开发体验。
import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; /** * Spring AI 多模型双版本流式输出接口 */ @RestController public class StreamOutputController { // ==================== 1. ChatModel 原子API 流式输出 ==================== @Resource(name = "deepseek") private ChatModel deepseekChatModel; @Resource(name = "qwen") private ChatModel qwenChatModel; /** * DeepSeek-V3 ChatModel 流式输出接口 */ @GetMapping(value = "/stream/chatflux1", produces = "text/html;charset=utf-8") public Flux<String> chatflux1(@RequestParam(name = "question", defaultValue = "你是谁") String question) { // ChatModel 原生流式调用,一行完成,但复杂场景需要手动组装提示词 return deepseekChatModel.stream(question); } /** * Qwen-Max ChatModel 流式输出接口 */ @GetMapping(value = "/stream/chatflux2", produces = "text/html;charset=utf-8") public Flux<String> chatflux2(@RequestParam(name = "question", defaultValue = "你是谁") String question) { return qwenChatModel.stream(question); } // ==================== 2. ChatClient Fluent API 流式输出 ==================== @Resource(name = "deepseekChatClient") private ChatClient deepseekChatClient; @Resource(name = "qwenChatClient") private ChatClient qwenChatClient; /** * DeepSeek-V3 ChatClient 流式输出接口 */ @GetMapping(value = "/stream/chatflux3", produces = "text/html;charset=utf-8") public Flux<String> chatflux3(@RequestParam(name = "question", defaultValue = "你是谁") String question) { // ChatClient 链式流式调用,一行完成,自动携带全局系统提示词 return deepseekChatClient.prompt(question).stream().content(); } /** * Qwen-Max ChatClient 流式输出接口 */ @GetMapping(value = "/stream/chatflux4", produces = "text/html;charset=utf-8") public Flux<String> chatflux4(@RequestParam(name = "question", defaultValue = "你是谁") String question) { return qwenChatClient.prompt(question).stream().content(); } }关键说明:
- produces 属性:必须设置
produces = "text/html;charset=utf-8"或produces = "text/plain;charset=utf-8",否则浏览器可能不会逐字显示,而是等待完整结果返回。 - ChatClient 简化调用:ChatClient 的
prompt(question)是prompt().user(question)的简写,代码更简洁;同时自动携带配置类中设置的全局系统提示词,无需每次调用手动拼接。 - 接口命名规范:接口名清晰区分模型(deepseek/qwen)和实现方式(chatflux1-2 是 ChatModel,chatflux3-4 是 ChatClient),便于测试和维护。
3.4 接口测试
启动项目后,在 Chrome 或 Edge 浏览器中访问以下接口,即可看到流式输出的效果:
- DeepSeek ChatModel:
http://localhost:8003/stream/chatflux1?question=写一篇1000字的Java后端成长路线 - Qwen-Max ChatClient:
http://localhost:8003/stream/chatflux4?question=用Spring Boot写一个完整的用户登录注册接口
四、ChatModel vs ChatClient 流式输出对比
| 对比维度 | ChatModel 原子 API | ChatClient Fluent API |
|---|---|---|
| 代码量 | 简单场景一行完成,复杂场景需要手动组装提示词、处理消息结构 | 所有场景一行完成,自动携带全局配置,无冗余代码 |
| 全局配置 | 仅支持模型参数的全局配置,系统提示词需每次调用手动拼接 | 支持模型参数、系统提示词、函数定义、Advisor 切面的全局统一配置 |
| 开发体验 | 底层灵活,但需要处理大量样板代码 | 高层封装,链式调用,开发效率高,代码可读性强 |
| 适用场景 | 需要极致底层灵活性的场景(如自定义消息结构、手动处理流式元数据) | 绝大多数企业级业务场景(如客服问答、技术方案生成、知识库问答) |
五、实践建议
- 多模型动态路由不要在 Controller 中硬编码注入不同的 ChatModel/ChatClient,而是通过配置文件或数据库动态选择模型,根据业务场景(如用户等级、问题复杂度)自动切换。
- 流式输出的异常处理流式输出过程中可能出现网络中断、模型限流等异常,需要通过
Flux.onErrorResume()等方法统一处理,给用户友好的提示。 - Token 消耗统计生产环境中需要统计每个模型的 Token 消耗,用于成本核算与监控。ChatClient 可以通过
stream().chatResponse()获取完整的响应元数据,包括 Token 使用量。 - 提示词模板外部化复杂的系统提示词不要硬编码在 Java 代码中,放到
application.yml配置文件或独立的资源文件中,通过@Value或Resource注入,便于产品与运营同学修改优化。
六、避坑指南
- 坑点 1:流式输出浏览器不逐字显示必须在Controller 的
@GetMapping注解中设置produces = "text/html;charset=utf-8"或produces = "text/plain;charset=utf-8",否则浏览器会等待完整结果返回。 - 坑点 2:多模型Bean注入歧义若项目中存在多个 ChatModel/ChatClient 实例,必须通过
@Bean(name = "xxx")和@Qualifier("xxx")明确区分注入,否则 Spring 会抛出NoUniqueBeanDefinitionException异常。 - 坑点 3:环境变量 API Key 读取失败
System.getenv()读取的是系统环境变量,IDE 本地运行时,需要在启动配置的Environment variables 中添加DASHSCOPE_API_KEY,否则会出现 API Key 为空的错误。 - 坑点 4:DeepSeek 模型名称错误通过阿里云百炼调用DeepSeek时,模型名称必须是
deepseek-v3或deepseek-chat,不能直接写deepseek,否则会出现模型不存在的错误。
七、本篇总结
本篇我们完成了Spring AI多模型共存与双版本流式输出的实战落地:
- 基于阿里云百炼生态,一套代码同时对接了 DeepSeek-V3和Qwen-Max 两个主流大模型,无需重复搭建环境。
- 分别用
ChatModel和ChatClient实现了流式响应,对比了两者的开发体验与适用场景。
八、下篇预告
本篇我们掌握了多模型共存与流式输出的核心能力,实现了从基础demo到工程化开发的进一步升级。在本系列的下一篇中,将深度拆解Spring AI Prompt工程全体系,从底层结构到模板化动态生成,带你彻底掌握驾驭大模型的核心能力。
传送门:Spring AI 实战系列(四):Prompt工程深度实战
如果本文对你有帮助,欢迎点赞、收藏、评论,跟着系列教程一步步完成Spring AI应用。