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 实战系列(五):结构化输出,让大模型严格适配你的业务数据模型

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 冲突。

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

关键说明

  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 的流式响应接口,直观对比两者的开发体验。

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

关键说明

  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 原子 API ChatClient 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应用。

相关推荐
gaozhiyong08131 小时前
提示词的解剖学:Gemini 3.1 Pro 提示工程高级策略与国内实战
人工智能·算法·机器学习
Langchain1 小时前
2026 年 AI 最值得关注的方向:上下文工程!
人工智能·python·自然语言处理·llm·agent·大模型开发·rag
学习者0072 小时前
大模型之VLLMA
人工智能
iThinkAi智能体2 小时前
1个运营带4个实习生,周产350篇笔记:小红书图文矩阵真的没那么玄乎
人工智能·经验分享·笔记
chaofan9802 小时前
深度实战:2026年大模型应用如何解决“接口抖动”?五大主流平台横向测评
人工智能·自动化·api·claude opus
彭于晏Yan2 小时前
SpringBoot整合ECC实现文件签名与验签
java·spring boot·后端
pupudawang2 小时前
Spring EL 表达式的简单介绍和使用
java·后端·spring
2501_946018702 小时前
六渡婚恋推出“真经”公益课堂 ——以系统化成长支持助力家庭建设与社会发展
大数据·人工智能
jiankeljx2 小时前
Spring Initializr创建springboot项目,提示java 错误 无效的源发行版:16
java·spring boot·spring