Spring AI 实战系列(三):多模型共存+双版本流式输出

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 本篇定位

本篇是系列进阶篇,解决开发中最常见的两个痛点:

  1. 多模型无缝切换与共存:一套 Spring Boot 项目同时对接DeepSeek、Qwen,根据业务场景动态选择模型,无需重复搭建环境。
  2. 双版本流式输出实现:分别用ChatModelChatClient实现流式响应,对比两者的开发体验与适用场景,给生产环境选型提供明确建议。

二、核心痛点拆解

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的ChatModelChatClient 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(); } }

关键说明

  1. 模型名称统一管理:用常量定义模型名称,避免硬编码分散在业务代码中,后续切换模型只需修改常量即可。
  2. API Key 复用:DeepSeek现在已接入阿里云百炼生态,无需单独申请 DeepSeek的API Key,直接复用DASHSCOPE_API_KEY即可。
  3. Bean 命名规范:通过@Bean(name = "xxx")@Qualifier("xxx")明确区分不同模型的 Bean,避免Spring容器的注入歧义。
  4. 全局默认配置分离:ChatModel的全局默认参数(模型版本、温度、最大Token数)和 ChatClient 的全局默认系统提示词分离,职责清晰。

3.3 第二步:双版本流式输出接口开发

我们创建StreamOutputController.java,分别用ChatModelChatClient实现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(); } }

关键说明

  1. produces 属性:必须设置produces = "text/html;charset=utf-8"produces = "text/plain;charset=utf-8",否则浏览器可能不会逐字显示,而是等待完整结果返回。
  2. ChatClient 简化调用:ChatClient 的prompt(question)prompt().user(question)的简写,代码更简洁;同时自动携带配置类中设置的全局系统提示词,无需每次调用手动拼接。
  3. 接口命名规范:接口名清晰区分模型(deepseek/qwen)和实现方式(chatflux1-2 是 ChatModel,chatflux3-4 是 ChatClient),便于测试和维护。

3.4 接口测试

启动项目后,在 Chrome 或 Edge 浏览器中访问以下接口,即可看到流式输出的效果:

  • DeepSeek ChatModelhttp://localhost:8003/stream/chatflux1?question=写一篇1000字的Java后端成长路线
  • Qwen-Max ChatClienthttp://localhost:8003/stream/chatflux4?question=用Spring Boot写一个完整的用户登录注册接口

四、ChatModel vs ChatClient 流式输出对比

对比维度ChatModel 原子 APIChatClient Fluent API
代码量简单场景一行完成,复杂场景需要手动组装提示词、处理消息结构所有场景一行完成,自动携带全局配置,无冗余代码
全局配置仅支持模型参数的全局配置,系统提示词需每次调用手动拼接支持模型参数、系统提示词、函数定义、Advisor 切面的全局统一配置
开发体验底层灵活,但需要处理大量样板代码高层封装,链式调用,开发效率高,代码可读性强
适用场景需要极致底层灵活性的场景(如自定义消息结构、手动处理流式元数据)绝大多数企业级业务场景(如客服问答、技术方案生成、知识库问答)

五、实践建议

  1. 多模型动态路由不要在 Controller 中硬编码注入不同的 ChatModel/ChatClient,而是通过配置文件或数据库动态选择模型,根据业务场景(如用户等级、问题复杂度)自动切换。
  2. 流式输出的异常处理流式输出过程中可能出现网络中断、模型限流等异常,需要通过Flux.onErrorResume()等方法统一处理,给用户友好的提示。
  3. Token 消耗统计生产环境中需要统计每个模型的 Token 消耗,用于成本核算与监控。ChatClient 可以通过stream().chatResponse()获取完整的响应元数据,包括 Token 使用量。
  4. 提示词模板外部化复杂的系统提示词不要硬编码在 Java 代码中,放到application.yml配置文件或独立的资源文件中,通过@ValueResource注入,便于产品与运营同学修改优化。

六、避坑指南

  1. 坑点 1:流式输出浏览器不逐字显示必须在Controller 的@GetMapping注解中设置produces = "text/html;charset=utf-8"produces = "text/plain;charset=utf-8",否则浏览器会等待完整结果返回。
  2. 坑点 2:多模型Bean注入歧义若项目中存在多个 ChatModel/ChatClient 实例,必须通过@Bean(name = "xxx")@Qualifier("xxx")明确区分注入,否则 Spring 会抛出NoUniqueBeanDefinitionException异常。
  3. 坑点 3:环境变量 API Key 读取失败System.getenv()读取的是系统环境变量,IDE 本地运行时,需要在启动配置的Environment variables 中添加DASHSCOPE_API_KEY,否则会出现 API Key 为空的错误。
  4. 坑点 4:DeepSeek 模型名称错误通过阿里云百炼调用DeepSeek时,模型名称必须是deepseek-v3deepseek-chat,不能直接写deepseek,否则会出现模型不存在的错误。

七、本篇总结

本篇我们完成了Spring AI多模型共存与双版本流式输出的实战落地:

  • 基于阿里云百炼生态,一套代码同时对接了 DeepSeek-V3和Qwen-Max 两个主流大模型,无需重复搭建环境。
  • 分别用ChatModelChatClient实现了流式响应,对比了两者的开发体验与适用场景。

八、下篇预告

本篇我们掌握了多模型共存与流式输出的核心能力,实现了从基础demo到工程化开发的进一步升级。在本系列的下一篇中,将深度拆解Spring AI Prompt工程全体系,从底层结构到模板化动态生成,带你彻底掌握驾驭大模型的核心能力。

传送门:Spring AI 实战系列(四):Prompt工程深度实战

如果本文对你有帮助,欢迎点赞、收藏、评论,跟着系列教程一步步完成Spring AI应用。

Read more

Claude Code免费使用教程,前端必看!

Claude Code免费使用教程,前端必看!

目前claude有两种使用方式,一种是官方购买渠道(太贵了,用不起,扎心。。。),还一种就是通过api方式,就是下面我讲的通过any-router提供的api调通就行~相当于中转站,主要是免费啊,谁能说不香! 1.注册LinuxDo账户 目前AnyRouter取消了github登录方式,只能通过LinuxDo账户登录,或者edu的邮箱登录,这里选择使用LinuxDo登录。 linux do官方网址:https://linux.do/   linux do邀请码:2E917F23-D9BF-44FE-BCBD-AE6AB3B1FC17 提示:如果Linuxdo邀请码失效,注册页面填写邀请码的那个输入框下面有邀请码链接,如图: 申请理由稍微写写,别全打逗号啥的,认真写下很快就过了。   2.any Router登录使用 上面linux do账号注册完毕就可以,登录any router了 any router网址:https://anyrouter.top/register?aff=iVs0    (貌似目前需要挂绿色软件才能登录上去) 一定要复制上面的网址(别删

得物前端部门全部解散!!!

👉 这是一个或许对你有用的社群 🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:  * 《项目实战(视频)》:从书中学,往事中“练” * 《互联网高频面试题》:面朝简历学习,春暖花开 * 《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题 * 《精进 Java 学习指南》:系统学习,互联网主流技术栈 * 《必读 Java 源码专栏》:知其然,知其所以然 👉这是一个或许对你有用的开源项目 国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构 RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRM、AI大模型、IoT物联网等功能:多模块:

【前端实战】从 try-catch 回调到链式调用:一种更优雅的 async/await 错误处理方案

【前端实战】从 try-catch 回调到链式调用:一种更优雅的 async/await 错误处理方案

目录 【前端实战】从 try-catch 回调到链式调用:一种更优雅的 async/await 错误处理方案 一、问题背景:async/await 真的解决了一切麻烦吗? 二、真实业务场景下的痛点 1、错误需要“分阶段处理” 2、try-catch 的引入打破了 async/await 的链式范式 三、借鉴 Go、Rust 语言特性,错误也是一种结果 1、错误优先风格替代 try-catch 2、封装一个 safeAsync 工具函数 四、进阶版 safeAsync 函数设计 五、结语         作者:watermelo37         ZEEKLOG优质创作者、华为云云享专家、阿里云专家博主、腾讯云“

WebGIS 开发工程师成长指南

WebGIS 开发工程师成长指南

WebGIS 开发工程师成长指南 成为企业真正需要的 WebGIS 开发工程师 📅 更新时间:2026 年 3 月 📌 一、什么是 WebGIS 开发工程师? WebGIS 是Web 开发技术与**地理信息系统(GIS)**的结合产物,通过浏览器实现地理信息的交互操作和服务。 核心工作内容 * 开发基于 Web 的地图应用系统 * 实现地图展示、缩放、平移、查询等基础功能 * 进行空间数据分析和可视化 * 集成遥感数据、矢量数据、三维模型等 * 开发 GIS 业务功能模块(如路径规划、空间分析、热力图等) * 编写技术文档和维护开发资料 🎯 二、企业核心技能要求 1️⃣ 前端开发基础(必会) 技能要求重要程度HTML/CSS/JavaScript扎实基础,ES6+ 语法⭐