SpringBoot3 + Vue2 整合 DeepSeek:实现流式输出与多轮对话
引言
如今,智能对话系统备受关注。本文将介绍如何整合 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 开放平台
依赖:
org.springframework.boot spring-boot-starter-webflux cn.hutool hutool-all 5.8.10
二、效果
三、数据表结构
四、后端
配置类-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 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 messages; private boolean stream; public String getModel() { return model; } public void setModel(String model) { this.model = model; } public List getMessages() { return messages; } public void setMessages(List 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{ /** * 返回成功数据 * * @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 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 selectCustomQAndAList() { return customQAMapper.selectCustomQAndAList(); } } @Mapper public interface CustomQAMapper { /** * 查询单调指定问题的答案 * @param problem * @return */ public CustomQA selectCustomQAndA(@Param("problem") String problem); /** * 查询全部问题 * @param * @return */ public List selectCustomQAndAList(); }
xml
select id, problem, answer, update_by, update_time, create_by, create_time from custom_q_a and problem = #{problem} and answer = #{answer} where problem = #{problem} insert into custom_q_a problem, file_path, #{problem}, #{answer}, delete from custom_q_a where id = #{id}
实现类-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 chatCompletions(String content, String history) { List 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 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(); } }
五、前端
{{ message.content }} {{ message.content }} {{item.problem}} 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 = ""; }, }, }; .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; }
结束
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。