引言

在当下 AI 应用开发中,经常会遇到需要同时接入多个大模型的场景,比如用通义千问做中文对话、用 DeepSeek 处理复杂推理。Spring AI Alibaba 为我们屏蔽了不同厂商 API 的差异,但多模型并存也带来了 ChatModel Bean 冲突、流式输出适配等问题。本文将带你从零搭建一个 Spring Boot 项目,优雅地解决“多模型共存 + SSE 流式输出”两大核心需求,并给出两种主流接入方案。


一、核心知识点前置

在动手写代码之前,先梳理几个关键概念:

  1. 多模型共存
    当容器中存在多个 ChatModel Bean 时,必须手动指定 Bean 名称,然后使用 @Resource(name="xxx") 按名称精准注入,放弃 Spring Boot 的自动装配。

  2. SSE (Server-Sent Events)
    一种基于 HTTP 的单向长连接协议,天然适合大模型“打字机”效果的流式返回。浏览器端直接监听接口,源源不断地接收文本片段,无需 WebSocket 的复杂握手。

  3. 两条实现路线

    • 底层 APIChatModel.stream(String prompt) 直接返回 Flux<String>
    • 上层封装ChatClient.prompt().stream().content() 更贴近业务语义,支持系统提示词、对话历史等
  4. 响应式环境要求
    返回类型 Flux<String> 属于响应式流,控制器必须运行在 WebFlux 环境下(引入 spring-boot-starter-webflux),普通 Spring MVC 无法推送流数据。


二、整体方案设计

我们面对两种典型的集成模式,区别在于引入的 Starter 数量:

  • 方案 A:只引入百炼一个 Starter
    Maven 中仅添加 spring-ai-alibaba-starter-dashscope。所有模型(包括通义千问、DeepSeek 等)都通过阿里云百炼平台调用,只需在代码中通过 model 参数切换。适合企业统一管控、希望在阿里云上集中管理模型调用。

  • 方案 B:同时引入百炼和 DeepSeek 两个 Starter
    Maven 中同时添加 spring-ai-alibaba-starter-dashscopespring-ai-deepseek-starter。通义千问走百炼,DeepSeek 直连 DeepSeek 官网,两套独立的配置和连接,适合需要发挥各厂商原生能力的场景。

本文会同时覆盖这两种方案,你可以根据实际情况选其一。


三、Maven 依赖配置

方案 A:仅引入百炼 Starter(一个 Starter)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
方案 B:引入百炼 + DeepSeek 两个 Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-deepseek-starter</artifactId>
</dependency>

四、application.yml 配置

方案 A(仅百炼)
spring:
ai:
dashscope:
api-key: sk-xxxx
chat:
options:
model: qwen-turbo # 默认模型,实际调用时可覆盖
方案 B(双 key)
spring:
ai:
dashscope:
api-key: sk-xxxx
chat:
options:
model: qwen-turbo
deepseek:
api-key: sk-xxxx
chat:
options:
model: deepseek-chat

五、配置类:手工打造多模型 Bean

这是本文最关键的一步。为避免自动装配的混乱,我们完全手动创建所有 Bean 并命名

5.1 方案 A:一个 Starter 多模型 Bean(均走百炼)
@Configuration
public class DashScopeMultiModelConfig {

@Bean
public DashScopeApi dashScopeApi(DashScopeProperties properties) {
return DashScopeApi.builder()
.apiKey(properties.getApiKey())
.build();
}

// Qwen 模型 Bean
@Bean("qwen")
public ChatModel qwenChatModel(DashScopeApi api) {
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel("qwen-turbo")
.build();
return new DashScopeChatModel(api, options);
}

// 通过百炼调用的 DeepSeek 模型(需在百炼平台开通)
@Bean("deepseek-via-bailian")
public ChatModel deepSeekViaBailianModel(DashScopeApi api) {
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel("deepseek-chat")
.build();
return new DashScopeChatModel(api, options);
}

@Bean("qwenChatClient")
public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是通义千问,简洁回答问题")
.build();
}

@Bean("deepseekBailianChatClient")
public ChatClient deepSeekBailianClient(@Qualifier("deepseek-via-bailian") ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是 DeepSeek 大模型")
.build();
}
}
5.2 方案 B:两个 Starter 独立 Bean(百炼 + DeepSeek 直连)
@Configuration
public class MultiLLMConfig {

// ================= 通义千问(百炼) =================
@Bean
public DashScopeApi dashScopeApi(DashScopeProperties props) {
return DashScopeApi.builder()
.apiKey(props.getApiKey())
.build();
}

@Bean("qwen")
public ChatModel qwenChatModel(DashScopeApi api, DashScopeProperties props) {
return new DashScopeChatModel(api, props.getChat().getOptions());
}

@Bean("qwenChatClient")
public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是通义千问,简洁回答问题")
.build();
}

// ================= DeepSeek 直连 =================
@Bean
public DeepSeekApi deepSeekApi(DeepSeekProperties props) {
return DeepSeekApi.builder()
.apiKey(props.getApiKey())
.build();
}

@Bean("deepseek")
public ChatModel deepseekChatModel(DeepSeekApi api, DeepSeekProperties props) {
return new DeepSeekChatModel(api, props.getChat().getOptions());
}

@Bean("deepseekChatClient")
public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("你是 DeepSeek 大模型")
.build();
}
}

六、Controller:SSE 流式输出实战

下面展示基于方案 B 的四种典型接口,方案 A 只需要把注入的 Bean 名替换为对应的百炼 Bean 即可。

@RestController
public class StreamController {

@Resource(name = "deepseek")
private ChatModel deepseekChatModel;

@Resource(name = "qwen")
private ChatModel qwenChatModel;

@Resource(name = "deepseekChatClient")
private ChatClient deepseekChatClient;

@Resource(name = "qwenChatClient")
private ChatClient qwenChatClient;

// 1. DeepSeek ChatModel 流式 SSE
@GetMapping(value = "/stream/deepseek-model", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamDeepSeekModel(@RequestParam(defaultValue = "你是谁") String question) {
return deepseekChatModel.stream(question);
}

// 2. Qwen ChatModel 流式 SSE
@GetMapping(value = "/stream/qwen-model", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamQwenModel(@RequestParam(defaultValue = "你是谁") String question) {
return qwenChatModel.stream(question);
}

// 3. DeepSeek ChatClient 流式 SSE
@GetMapping(value = "/stream/deepseek-client", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamDeepSeekClient(@RequestParam(defaultValue = "你是谁") String question) {
return deepseekChatClient.prompt(question)
.stream()
.content();
}

// 4. Qwen ChatClient 流式 SSE
@GetMapping(value = "/stream/qwen-client", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamQwenClient(@RequestParam(defaultValue = "你是谁") String question) {
return qwenChatClient.prompt(question)
.stream()
.content();
}
}

关键点解析

  • produces = MediaType.TEXT_EVENT_STREAM_VALUE 告知浏览器这是 SSE 流。
  • Flux<String> 使得每个文本片段都能被及时推送。
  • ChatClient.prompt().stream().content() 是最推荐的方式,可轻松添加系统消息、历史消息等。

七、前端对接示例(原生 JS)

const eventSource = new EventSource('/stream/deepseek-client?question=你好');
eventSource.onmessage = (event) => {
document.getElementById('output').innerText += event.data;
};
eventSource.onerror = (err) => {
eventSource.close();
};

八、方案对比与选型建议

对比维度 方案 A(仅百炼 Starter) 方案 B(百炼 + DeepSeek 两个 Starter)
引入的依赖 spring-ai-alibaba-starter-dashscope spring-ai-alibaba-starter-dashscope + spring-ai-deepseek-starter
配置复杂度 需手动指定每个模型的 model 名称 各 Starter 独立配置 api-key 和参数
厂商特性支持 依赖百炼封装,可能丢失细节 可调用各厂商原生参数和特性
适用场景 统一管控、企业级项目 需要利用厂商差异化能力

如果项目只需要简单对话,方案 A 更简洁;若需深度定制 prompt 参数、访问 DeepSeek 特有功能,方案 B 更灵活。


九、总结

本文完整演示了 Spring AI Alibaba 在多模型共存环境下的整合思路,重点解决了 Bean 冲突和 SSE 流式输出的落地细节。核心要点回顾:

  • 手动创建并命名 ChatModel Bean,杜绝自动装配;
  • 在 WebFlux 环境下使用 Flux<String> + text/event-stream 实现 SSE;
  • 根据需求选择“一个百炼 Starter”统一接入,或“百炼 + DeepSeek 两个 Starter”混合接入。

掌握了这套方法,无论是同时接入 2 个还是 10 个大模型,都能轻松驾驭。希望这篇文章能帮助你在 AI 应用开发中少走弯路,快速搭建出健壮的流式对话服务。

更多推荐