大家好,我是 Mr.Sun,一名热爱技术和分享的程序员。
📖 个人博客:Mr.Sun的博客
✨ 微信公众号:「Java技术宇宙」
期待与你交流,让我们一起在技术道路上成长。
效果展示

一、接入阿里云百炼平台
之前接入了DeepSeek,为了有多个大模型切换功能,这里也接入一下阿里的Qwen大模型

由于兼容OpenAI接口规范,那么直接使用OpenAI的model就可以了
java
<!-- OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
java
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode # OpenAI 服务的访问地址,这里使用的是阿里云百炼
api-key: api-key # 填写阿里云百炼的 API Key, 该成你自己的
chat:
options:
model: qwen-plus # 模型名称
temperature: 0.7 # 温度值
这里的base-url填https://dashscope.aliyuncs.com/compatible-mode,不需要后面的v1
model: 从模型广场里找一个模型,然后点击详情进去看到code就是了
此时完整的POM文件和properties.yml如下:
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.mrsunn</groupId>
<artifactId>ai-robot</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.4.5</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai-vsersion>1.0.2</spring-ai-vsersion>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai-vsersion}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
yml
server:
port: 8080
spring:
ai:
deepseek:
api-key: api-key
base-url: https://api.deepseek.com # DeepSeek 的请求 URL, 可不填,默认值为 api.deepseek.com
chat:
options:
model: deepseek-reasoner # 使用深度思考模型
temperature: 0.8 # 温度值
openai:
api-key: api-key
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen-plus
temperature: 0.7
logging:
level:
org:
springframework:
ai:
chat:
client:
advisor: debug
二、配置多ChatClient
java
/**
* @author hwsun3
* @date 2025/9/18
*/
@Configuration
public class AIChatClientConfig {
@Autowired
ChatMemoryRepository chatMemoryRepository;
@Bean
public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.chatMemoryRepository(chatMemoryRepository)
.build();
return ChatClient.builder(chatModel)
.defaultSystem("你是阿里助手,请使用贴吧老哥的语气跟我对话")
.defaultAdvisors(
// 日志助手
new SimpleLoggerAdvisor(
request -> "Custom request: " + request.prompt().getUserMessage(),
response -> "Custom response: " + response.getResult(),
0),
// 记忆助手
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
@Bean
@Primary
public ChatClient deepSeekChatClient(DeepSeekChatModel chatModel) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.chatMemoryRepository(chatMemoryRepository)
.build();
return ChatClient.builder(chatModel)
.defaultSystem("你是DeepSeek助手,请使用贴吧老哥的语气跟我对话")
.defaultAdvisors(
// 日志助手
new SimpleLoggerAdvisor(
request -> "Custom request: " + request.prompt().getUserMessage(),
response -> "Custom response: " + response.getResult(),
0),
// 记忆助手
MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
}
这里把该配置的都配置了,两个模型都使用内存记录聊天记忆
需要把其中一个模型Bean加上@Primary,不然会启动报错
三、AI流式接口
配置跨域
java
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // 允许所有域名访问(生产环境应指定具体域名)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(false)
.maxAge(3600);
}
};
}
}
编写controller代码
java
/**
* @author hwsun3
* @date 2025/9/18
*/
@RestController
@RequestMapping("/ai")
public class AIChatController {
@Autowired
private ChatStrategyMap executeStrategy;
/**
* 流式对话接口
*
* @param message 消息内容
* @param conversationId 会话ID
* @param modelType 模型类型:bailian(百链),deepSeek
* @param openReasoner 是否开启推理模型(仅对deepSeek有效)
* @return 流式响应
*/
@GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message,
@RequestParam(value = "conversationId", defaultValue = "1") String conversationId,
@RequestParam(value = "modelType", defaultValue = "deepseek") String modelType,
@RequestParam(value = "openReasoner", defaultValue = "false") Boolean openReasoner) {
return executeStrategy.executeStrategy(modelType, message, conversationId, openReasoner);
}
}
produces = MediaType.TEXT_EVENT_STREAM_VALUE 使用SSE流式返回
编写策略类
java
@Component
public class ChatStrategyMap {
@Resource(name = "deepSeekChatClient")
private ChatClient deepSeekChatClient;
@Resource(name = "openAiChatClient")
private ChatClient openAiChatClient;
private final Map<String, Function<StrategyParams, Flux<String>>> strategyMap;
public ChatStrategyMap() {
this.strategyMap = new HashMap<>();
this.strategyMap.put("deepseek", this::executeDeepSeekStrategy);
this.strategyMap.put("bailian", this::executeBaiLianStrategy);
}
public Flux<String> executeStrategy(String modelType, String message,
String conversationId, Boolean openReasoner) {
Function<StrategyParams, Flux<String>> strategy =
strategyMap.getOrDefault(modelType.toLowerCase(), this::executeDeepSeekStrategy);
StrategyParams params = new StrategyParams(message, conversationId, openReasoner);
return strategy.apply(params);
}
private Flux<String> executeDeepSeekStrategy(StrategyParams params) {
DeepSeekChatOptions chatOptions = DeepSeekChatOptions.builder()
.model(params.openReasoner ?
DeepSeekApi.ChatModel.DEEPSEEK_REASONER.getValue() :
DeepSeekApi.ChatModel.DEEPSEEK_CHAT.getValue())
.temperature(0.8)
.build();
Prompt prompt = new Prompt(params.message, chatOptions);
AtomicBoolean hasSentSeparator = new AtomicBoolean(false);
return deepSeekChatClient.prompt(prompt)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, params.conversationId))
.stream()
.chatResponse()
.mapNotNull(chatResponse -> {
DeepSeekAssistantMessage assistantMessage =
(DeepSeekAssistantMessage) chatResponse.getResult().getOutput();
String content = getContentFromMessage(assistantMessage);
if (StringUtils.isBlank(content)) {
return null;
}
if (assistantMessage.getText() != null && !hasSentSeparator.get()) {
hasSentSeparator.set(true);
return "--- 思考过程结束 ---" + content;
}
return content;
});
}
private Flux<String> executeBaiLianStrategy(StrategyParams params) {
return openAiChatClient.prompt(params.message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, params.conversationId))
.stream()
.content();
}
private String getContentFromMessage(DeepSeekAssistantMessage message) {
if (message.getReasoningContent() != null) {
return message.getReasoningContent();
}
return message.getText();
}
private record StrategyParams(String message, String conversationId, Boolean openReasoner) {
}
}
record拓展点
上面的代码中,最后定义StrategyParams对象的时候,使用了record修饰,这个是Idea自动优化的,本着好奇的心态,做了一下研究
什么是 record?
record是 Java 提供的一种特殊的类声明方式,用于简洁地定义不可变的数据载体类(data carrier classes)。它主要用于存储数据,通常不包含复杂的业务逻辑。
这里使用到的 record是 Java 14 开始引入的预览特性(Preview Feature),并在 Java 16 中正式成为标准特性(Standard Feature)。
record的主要特点
- 自动生成以下内容:私有且 final 的字段(对应构造参数)公共的构造方法(规范构造器 canonical constructor)公共的访问器方法(getter,但方法命名是 field()而不是传统的 getField())equals()、hashCode()和 toString()方法
- 不可变性(Immutable):所有的字段默认都是 final的,创建后不能修改。
- 简洁:你只需要声明类的名称和它的组成部分(组件,即字段),编译器会帮你生成其余的样板代码。
java
private record StrategyParams(String message, String conversationId, Boolean openReasoner) {
}
等价于一个传统 Java 类,大致如下:
public final class StrategyParams {
private final String message;
private final String conversationId;
private final Boolean openReasoner;
public StrategyParams(String message, String conversationId, Boolean openReasoner) {
this.message = message;
this.conversationId = conversationId;
this.openReasoner = openReasoner;
}
public String message() { return message; }
public String conversationId() { return conversationId; }
public Boolean openReasoner() { return openReasoner; }
// 还有 equals(), hashCode(), toString() 等
}
但通过 record,你只需一行声明,编译器就帮你生成了所有这些。
什么时候使用 record?
适合使用 record的场景包括:
- 作为数据传输对象(DTO)
- 作为不可变的值对象(如坐标、配置项、参数封装等)
- 当一个类主要作用是保存数据,并且不需要自定义行为时
不适合的场景: - 如果你需要定义复杂的行为(方法逻辑)、状态变更、继承等,就不适合用 record(虽然 Java 16+ 后 record也可以实现接口,但不能继承类)
四、前端页面
在resource目录下创建:static/stream.html
这个前端页面也是我使用大模型实现的,接收接口的SSE流实现打字机效果
具体前端前端代码请查看:
Mr.Sun的个人博客
作者:Mr.Sun | 「Java技术宇宙」主理人
专注分享硬核技术干货与编程实践,让编程之路更简单。
📖 深度文章:个人博客「Mr.Sun的博客 」
🚀 最新推送:微信公众号「Java技术宇宙 」
扫码加我为好友,备注"加群 "免费加入技术交流群