SpringBoot3 + Vue2 整合 DeepSeek:实现流式输出与多轮对话

06-01 1098阅读

引言

如今,智能对话系统备受关注。本文将介绍如何整合 Spring Boot 3、Vue 2 和 DeepSeek 技术,打造一个具备流式输出与多轮对话功能的智能聊天系统。Spring Boot 3 用于搭建稳定后端,Vue 2 构建友好前端界面,DeepSeek 提供强大的对话能力。通过这个项目,我们希望实现流畅、自然的对话体验,让用户与系统交流更加高效、便捷。

参考:springboot对接deepseek & sse流式输出 & 多轮对话推理demo & 接入豆包/千帆/讯飞_deepseek sse-CSDN博客

一、环境准备

  1. 开发工具: IntelliJ IDEA

  2. 技术栈: SpringBoot 3、Vue 2、EventSource

     DeepSeek API: 注册 DeepSeek 账号并获取 API Key,👉 DeepSeek 开放平台

SpringBoot3 + Vue2 整合 DeepSeek:实现流式输出与多轮对话

        依赖:

    org.springframework.boot
    spring-boot-starter-webflux


    cn.hutool
    hutool-all
    5.8.10

二、效果

SpringBoot3 + Vue2 整合 DeepSeek:实现流式输出与多轮对话SpringBoot3 + Vue2 整合 DeepSeek:实现流式输出与多轮对话

三、数据表结构

SpringBoot3 + Vue2 整合 DeepSeek:实现流式输出与多轮对话

四、后端

配置类-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,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码