引言
如今,智能对话系统备受关注。本文将介绍如何整合 Spring Boot 3、Vue 2 和 DeepSeek 技术,打造一个具备流式输出与多轮对话功能的智能聊天系统。Spring Boot 3 用于搭建稳定后端,Vue 2 构建友好前端界面,DeepSeek 提供强大的对话能力。通过这个项目,我们希望实现流畅、自然的对话体验,让用户与系统交流更加高效、便捷。
参考:springboot对接deepseek & sse流式输出 & 多轮对话推理demo & 接入豆包/千帆/讯飞_deepseek sse-CSDN博客
一、环境准备
-
开发工具: IntelliJ IDEA
-
技术栈: SpringBoot 3、Vue 2、EventSource
DeepSeek API: 注册 DeepSeek 账号并获取 API Key,👉 DeepSeek 开放平台
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
二、效果
三、数据表结构
四、后端
配置类-WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
.addMapping("/**")
.maxAge(3600)
.allowCredentials(true)
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("token", "Authorization");
}
}
控制层-DeepSeekController
@RestController
@RequestMapping("/deepseek")
public class DeepSeekController {
@Autowired
private DeepSeekClient deepSeekClient;
@Autowired
private CustomQAService customQAService;
@RequestMapping(value = "chatCompletions", produces = "text/event-stream;charset=utf-8")
public Flux<String> chatCompletions(@RequestParam(required = true, value = "content") String content,
@RequestParam(required = false, value = "history") String history) {
return deepSeekClient.chatCompletions(content, history);
}
/**
* 查询单调指定问题的答案
* @param problem
* @return
*/
@GetMapping("/selectCustomQAndA/{problem}")
public AjaxResult selectCustomQAndA(@PathVariable String problem){
return AjaxResult.success(customQAService.selectCustomQAndA(problem));
}
/**
* 查询全部问题
* @param
* @return
*/
@GetMapping("/selectCustomQAndAList")
public AjaxResult selectCustomQAndAList(){
return AjaxResult.success(customQAService.selectCustomQAndAList());
}
}
属性类-AiChatRequest、AiChatMessage、CustomQA 、AjaxResult
public class AiChatMessage {
private String role;
private String content;
public AiChatMessage(String role, String content) {
this.role = role;
this.content = content;
}
public AiChatMessage() {
this.role = role;
this.content = content;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
public class AiChatRequest {
private String model;
private List<AiChatMessage> messages;
private boolean stream;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<AiChatMessage> getMessages() {
return messages;
}
public void setMessages(List<AiChatMessage> messages) {
this.messages = messages;
}
public boolean isStream() {
return stream;
}
public void setStream(boolean stream) {
this.stream = stream;
}
}
public class CustomQA extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long id;
private String problem;
private String answer;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getProblem() {
return problem;
}
public void setProblem(String problem) {
this.problem = problem;
}
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("problem", getProblem())
.append("answer", getAnswer())
.toString();
}
}
public class AjaxResult extends HashMap<String, Object>{
/**
* 返回成功数据
*
* @return 成功消息
*/
public static AjaxResult success(Object data)
{
return AjaxResult.success("操作成功", data);
}
}
service以及mapper层
public interface CustomQAService {
/**
* 查询单调指定问题的答案
* @param problem
* @return
*/
CustomQA selectCustomQAndA(String problem);
/**
* 查询全部问题
* @param
* @return
*/
List<CustomQA> selectCustomQAndAList();
}
@Service
public class CustomQAServiceImpl implements CustomQAService {
@Autowired
private CustomQAMapper customQAMapper;
/**
* 查询单调指定问题的答案
* @param problem
* @return
*/
@Override
public CustomQA selectCustomQAndA(String problem) {
return customQAMapper.selectCustomQAndA(problem);
}
/**
* 查询全部问题
* @param
* @return
*/
@Override
public List<CustomQA> selectCustomQAndAList() {
return customQAMapper.selectCustomQAndAList();
}
}
@Mapper
public interface CustomQAMapper {
/**
* 查询单调指定问题的答案
* @param problem
* @return
*/
public CustomQA selectCustomQAndA(@Param("problem") String problem);
/**
* 查询全部问题
* @param
* @return
*/
public List<CustomQA> selectCustomQAndAList();
}
xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.自己的路径.mapper.CustomQAMapper">
<resultMap type="CustomQA" id="CustomQAResult">
<result property="id" column="id" />
<result property="problem" column="problem" />
<result property="answer" column="answer" />
</resultMap>
<sql id="selectCustomQAVo">
select id, problem, answer, update_by, update_time, create_by, create_time from custom_q_a
</sql>
<select id="selectCustomQAndAList" parameterType="CustomQA" resultMap="CustomQAResult">
<include refid="selectCustomQAVo"/>
<where>
<if test="problem != null and problem != ''"> and problem = #{problem}</if>
<if test="answer != null and answer != ''"> and answer = #{answer}</if>
</where>
</select>
<select id="selectCustomQAndA" parameterType="CustomQA" resultMap="CustomQAResult">
<include refid="selectCustomQAVo"/>
where problem = #{problem}
</select>
<insert id="insertCustomQA" parameterType="CustomQA" useGeneratedKeys="true" keyProperty="id">
insert into custom_q_a
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="problem != null">problem,</if>
<if test="filePath != null">file_path,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="problem != null">#{problem},</if>
<if test="answer != null">#{answer},</if>
</trim>
</insert>
<delete id="deleteCustomQAById" parameterType="Long">
delete from custom_q_a where id = #{id}
</delete>
</mapper>
实现类-DeepSeekClient
PlatformConfig.DEEPSEEK_API:https://api.deepseek.com
PlatformConfig.DEEPSEEK_KEY:Bearer sk-xxxxx(最开始自己创建的api key)
@Component
public class DeepSeekClient {
private static final ObjectMapper mapper = new ObjectMapper();
private static final Logger log = LoggerFactory.getLogger(DeepSeekClient.class);
public Flux<String> chatCompletions(String content, String history) {
List<AiChatMessage> messages = new ArrayList<>();
// 如果有历史对话,解析并添加到 messages 中
if (history != null && !history.isEmpty()) {
try {
JsonNode historyNode = mapper.readTree(history);
for (JsonNode node : historyNode) {
String role = node.get("role").asText();
String messageContent = node.get("content").asText();
messages.add(new AiChatMessage(role, messageContent));
}
// 如果历史记录超过 8 条,移除最早的一条
if (messages.size() > 8) {
messages.remove(0);
}
} catch (Exception e) {
log.error("解析历史对话失败: {}", history);
}
}
// 添加系统消息和当前用户消息
messages.add(new AiChatMessage("system", "Hello."));
messages.add(new AiChatMessage("user", content));
AiChatRequest request = new AiChatRequest();
request.setModel("deepseek-chat");
request.setMessages(messages);
request.setStream(true);
return WebClient.builder()
.baseUrl(PlatformConfig.DEEPSEEK_API)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("Authorization", PlatformConfig.DEEPSEEK_KEY)
.build()
.post()
.uri("/chat/completions")
.body(BodyInserters.fromObject(request))
.retrieve()
.bodyToFlux(String.class)
.flatMap(this::handleResult);
}
private Flux<String> handleResult(String result) {
if ("[DONE]".equals(result)) {
return Flux.empty();
} else {
try {
JsonNode jsonNode = mapper.readTree(result);
String content = jsonNode.get("choices").get(0).get("delta").get("content").asText();
return Flux.just(content);
} catch (Exception e) {
log.error("解析失败: {}", result);
}
}
return Flux.empty();
}
}
五、前端
<template>
<div class="chat-container">
<div class="chat-box" style="height: 1200px; overflow: auto;">
<!-- 遍历消息列表 -->
<div v-for="(message, index) in messages" :key="index" class="message-item">
<!-- 用户消息 -->
<div v-if="message.role === 'user'" class="user-message">
<div class="bubble user">{{ message.content }}</div>
</div>
<!-- AI 消息 -->
<div v-else class="assistant-message">
<div class="bubble assistant">{{ message.content }}</div>
</div>
</div>
</div>
<div style="display: flex; flex-wrap: wrap;">
<div v-for="item in QAndAList" :key="item.problem" style="margin: 5px;">
<el-button class="button-color" @click="CheckProblem(item.problem)">{{item.problem}}</el-button>
</div>
</div>
<div class="input-container">
<el-input type="textarea" :rows="4" placeholder="请输入您的问题..." class="message-input" v-model="content" @keyup.enter.native="submit"></el-input>
<el-button @click="submit" class="send-button" :disabled="isButtonDisabled">
<i style="font-size: 20px" class="el-icon-position"></i>
</el-button>
</div>
</div>
</template>
<script>
import {selectCustomQAndAList, selectCutomQAndA} from "@/api/work/CutomQA";
export default {
name: "ChatWindow",
data() {
return {
content: "",
messages: [], // 存储对话内容
eventSource: null,
awaitingResponse: false, // 标志是否正在等待回答
fullResponse: "", // 存储完整的回答
isButtonDisabled: false, // 控制按钮禁用状态
QAndAList:[] //全部问题集合
};
},
created() {
this.getCutomQAndA();
},
methods: {
//点击问题按钮
CheckProblem(Q){
this.messages.push({
role: "user",
content: Q
});
// 使用 setTimeout 延迟 1 秒执行 为实现对话效果
setTimeout(() => {
selectCutomQAndA(Q).then(rep => {
this.messages.push({
role: "assistant",
content: rep.data.answer
});
})
}, 1000); // 1000 毫秒 = 1 秒
},
//获取所有问题
getCutomQAndA(){
selectCustomQAndAList().then(rep => {
this.QAndAList = rep.data
})
},
// 为实现页面流式输出效果,实时更新对话框
updateLastAssistantMessage(content) {
const lastMessageIndex = this.messages.length - 1;
if (lastMessageIndex >= 0 && this.messages[lastMessageIndex].role === 'assistant') {
this.messages[lastMessageIndex].content = content;
}
},
// 添加ai对话
addAssistantMessage(content) {
this.messages.push({
role: 'assistant',
content: content
});
// 如果消息记录超过 8 条,移除最早的一条
if (this.messages.length > 8) {
this.messages.shift();
}
},
// 发送
submit() {
if (!this.content) {
alert("没有输入内容");
return;
}
// 添加用户消息
this.messages.push({ role: "user", content: this.content });
// 如果消息记录超过 8 条,移除最早的一条
if (this.messages.length > 8) {
this.messages.shift();
}
// 禁用按钮
this.isButtonDisabled = true;
// 添加一个空的助手消息,用于后续更新
this.addAssistantMessage("");
// 重置标志和存储
this.awaitingResponse = true;
this.fullResponse = "";
// 将对话历史转换为 JSON 字符串
const history = JSON.stringify(this.messages);
// 创建新的 EventSource 连接
let eventSource = new EventSource(
`http://localhost:8890/deepseek/chatCompletions?content=${encodeURIComponent(this.content)}&history=${encodeURIComponent(history)}`
);
this.eventSource = eventSource;
eventSource.onopen = () => {
console.log("onopen 连接成功");
};
eventSource.onerror = (e) => {
console.log("onerror 连接断开", e);
this.awaitingResponse = false;
this.isButtonDisabled = false; // 解除按钮禁用
this.eventSource.close(); // 关闭连接,不再自动重连
};
eventSource.onmessage = (e) => {
console.log("收到消息: ", e.data);
const message = e.data.trim();
if (message !== "") {
this.fullResponse += message; // 拼接回答
this.updateLastAssistantMessage(this.fullResponse); // 更新最后一个对话框的内容
} else {
this.awaitingResponse = false; // 收到空消息,表示回答结束
this.isButtonDisabled = false; // 解除按钮禁用
}
};
// 初始化输入框
this.content = "";
},
},
};
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: calc(100vh - 40px);
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.chat-box {
flex: 1;
padding: 10px;
overflow-y: auto;
}
.message-item {
margin-bottom: 10px;
}
/* 用户消息靠右 */
.user-message {
display: flex;
justify-content: flex-end; /* 靠右对齐 */
}
/* AI 消息靠左 */
.assistant-message {
display: flex;
justify-content: flex-start; /* 靠左对齐 */
}
/* 消息气泡样式 */
.bubble {
max-width: 70%;
padding: 10px;
border-radius: 10px;
}
/* 用户消息气泡样式 */
.user {
background-color: #e0f7fa; /* 用户消息背景色 */
margin-left: auto; /* 靠右对齐 */
}
/* AI 消息气泡样式 */
.assistant {
background-color: #f5f5f5; /* AI 消息背景色 */
margin-right: auto; /* 靠左对齐 */
}
/* 输入框样式 */
.message-input {
flex: 1;
font-size: 14px;
}
/* 输入框样式 */
.message-input:disabled,
.send-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 发送按钮样式 */
.send-button {
padding: 0 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
/* 输入框样式 */
.input-container {
display: flex;
gap: 10px;
}
/* 问题按钮样式 */
.button-color{
background: #8eeeb0;
color: #FFFFFF;
}
</style>
结束