
一、系列回顾与本篇定位
1.1 系列回顾
- 第一篇 :完成了 Spring AI 与阿里云百炼的基础集成,基于
ChatModel实现了同步对话、API Key安全注入,跑通了从0到1的Spring AI 开发。 - 第二篇 :解锁了
ChatClient,实现了全局统一配置、一行代码完成大模型调用,告别了重复的样板代码。
**系列栏目:**Spring AI
Spring AI 实战系列(二):ChatClient封装,告别大模型开发样板代码
Spring AI 实战系列(三):多模型共存+双版本流式输出
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 冲突。
java
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 的流式响应接口,直观对比两者的开发体验。
java
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应用。