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

引言

如今,智能对话系统备受关注。本文将介绍如何整合 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 开放平台


        依赖:

<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>

结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值