山东大学软件学院项目实训-基于大模型的模拟面试系统-面试对话标题自动总结

面试对话标题自动总结

主要实现思路 :每当AI回复用户之后,调用方法查看当前对话是否大于三条,如果大于则将用户的两条和AI回复的一条对话传给DeepSeek让其进行总结(后端),总结后调用updateChatTopic进行更新标题,此外本次标题的更改还实现了仿打字机效果。

后端实现

首先,要在.env文件中配置DEEPSEEK_API,然后在application.yml中添加:

yml 复制代码
deepseek:
  api:
    key: ${DEEPSEEK_API} # DeepSeek API Key,从环境变量中获取

之后创建DeesSeekService.java

java 复制代码
package com.sdumagicode.backend.openai.service;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;

import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * DeepSeek API服务类
 * 用于与DeepSeek API进行交互
 */
@Service
public class DeepSeekService {

    private static final Logger logger = LoggerFactory.getLogger(DeepSeekService.class);
    private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";

    @Value("${deepseek.api.key}")
    private String apiKey;

    private final OkHttpClient client;
    private final ObjectMapper objectMapper;

    public DeepSeekService() {
        this.client = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .build();
        
        this.objectMapper = new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
                .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
    }

    /**
     * 发送聊天请求到DeepSeek API
     * 
     * @param messages 消息列表
     * @param model 模型名称,默认为 "deepseek-chat"
     * @param temperature 温度参数,控制随机性
     * @return API响应的JSON字符串
     * @throws IOException 如果API请求失败
     */
    public String chatCompletion(List<Map<String, String>> messages, String model, double temperature) throws IOException {
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("messages", messages);
        requestBody.put("model", model);
        requestBody.put("temperature", temperature);

        String jsonBody = objectMapper.writeValueAsString(requestBody);
        
        RequestBody body = RequestBody.create(
            MediaType.parse("application/json"), jsonBody);
            
        Request request = new Request.Builder()
            .url(API_URL)
            .addHeader("Authorization", "Bearer " + apiKey)
            .addHeader("Content-Type", "application/json")
            .post(body)
            .build();
            
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("API请求失败: " + response.code() + " " + response.message());
            }
            return response.body().string();
        }
    }
    
    /**
     * 发送聊天请求到DeepSeek API(使用默认参数)
     * 
     * @param messages 消息列表
     * @return API响应的JSON字符串
     * @throws IOException 如果API请求失败
     */
    public String chatCompletion(List<Map<String, String>> messages) throws IOException {
        return chatCompletion(messages, "deepseek-chat", 0.7);
    }
    
    /**
     * 创建聊天消息
     * 
     * @param role 角色 (system, user, assistant)
     * @param content 消息内容
     * @return 包含角色和内容的Map
     */
    public Map<String, String> createMessage(String role, String content) {
        Map<String, String> message = new HashMap<>();
        message.put("role", role);
        message.put("content", content);
        return message;
    }
    
    /**
     * 流式聊天请求(SSE)
     * 
     * @param messages 消息列表
     * @param model 模型名称
     * @param temperature 温度参数
     * @param callback 回调函数,用于处理每个SSE事件
     * @throws IOException 如果API请求失败
     */
    public void streamingChatCompletion(List<Map<String, String>> messages, String model, double temperature, Callback callback) throws IOException {
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("messages", messages);
        requestBody.put("model", model);
        requestBody.put("temperature", temperature);
        requestBody.put("stream", true);

        String jsonBody = objectMapper.writeValueAsString(requestBody);
        
        RequestBody body = RequestBody.create(
            MediaType.parse("application/json"), jsonBody);
            
        Request request = new Request.Builder()
            .url(API_URL)
            .addHeader("Authorization", "Bearer " + apiKey)
            .addHeader("Content-Type", "application/json")
            .post(body)
            .build();
            
        client.newCall(request).enqueue(callback);
    }
} 

该文件用于与DeepSeek AI API进行交互。主要功能包括:

  1. 基本配置

    • 使用OkHttpClient进行HTTP请求
    • 配置ObjectMapper处理JSON序列化/反序列化
    • 从配置文件读取API密钥
  2. 主要方法

    • chatCompletion:向DeepSeek API发送聊天请求,有两个版本:
      • 完整参数版:可指定消息、模型和温度
      • 简化版:使用默认参数
    • createMessage:创建聊天消息对象
    • streamingChatCompletion:流式聊天请求,支持SSE(服务器发送事件)
    • listModels:获取可用模型列表
  3. 技术特点

    • 使用Spring的@Service注解标记为服务
    • 通过@Value注入API密钥
    • 支持异步回调处理流式响应
    • 包含完整的异常处理和日志记录

这个服务类是后端与DeepSeek AI API通信的核心组件,负责处理所有AI聊天相关的请求。

DeepSeekController.java

java 复制代码
package com.sdumagicode.backend.openai;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sdumagicode.backend.core.result.GlobalResult;
import com.sdumagicode.backend.core.result.GlobalResultGenerator;
import com.sdumagicode.backend.openai.service.DeepSeekService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * DeepSeek API控制器
 * 提供DeepSeek模型API的接口
 */
@RestController
@RequestMapping("/api/v1/deepseek")
public class DeepSeekController {

    @Autowired
    private DeepSeekService deepSeekService;
    private final ObjectMapper objectMapper = new ObjectMapper();
    /**
     * 发送聊天请求
     *
     * @param requestBody 请求体,包含messages, model, temperature
     * @return 聊天响应
     */
    @PostMapping("/chat")
    public GlobalResult<String> chatCompletion(@RequestBody Map<String, Object> requestBody) {
        try {
            @SuppressWarnings("unchecked")
            List<Map<String, String>> messages = (List<Map<String, String>>) requestBody.get("messages");
            String model = requestBody.containsKey("model") ? (String) requestBody.get("model") : "deepseek-chat";
            double temperature = requestBody.containsKey("temperature") ? 
                Double.parseDouble(requestBody.get("temperature").toString()) : 0.7;
                
            String response = deepSeekService.chatCompletion(messages, model, temperature);
            return GlobalResultGenerator.genSuccessResult(response);
        } catch (Exception e) {
            return GlobalResultGenerator.genErrorResult("聊天请求失败:" + e.getMessage());
        }
    }
    
    /**
     * 生成对话摘要
     * 
     * @param requestBody 包含对话消息和chatId的请求体
     * @return 生成的摘要
     */
    @PostMapping("/summarize")
    public GlobalResult<String> summarizeConversation(@RequestBody Map<String, Object> requestBody) {
        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> messages = (List<Map<String, Object>>) requestBody.get("messages");
            // 准备系统提示词和用户消息
            List<Map<String, String>> promptMessages = new ArrayList<>();
            // 添加系统提示
            Map<String, String> systemPrompt = new HashMap<>();
            systemPrompt.put("role", "system");
            systemPrompt.put("content", "你是一个专业的面试对话摘要生成器。请根据以下面试对话生成一个简短的标题,标题应概括对话的主要内容。"
                    + "标题必须精炼,不超过20个字,不要加任何前缀和标点符号,直接输出标题文本。标题应突出面试的主要话题或技能领域。");
            promptMessages.add(systemPrompt);
            
            // 构建对话历史
            StringBuilder conversationBuilder = new StringBuilder();
            for (Map<String, Object> message : messages) {
                String role = (String) message.get("role");
                String content = (String) message.get("content");
                
                if (content == null || content.trim().isEmpty()) {
                    continue;
                }
                
                conversationBuilder.append(role.equals("assistant") ? "面试官: " : "候选人: ");
                conversationBuilder.append(content).append("\n\n");
            }
            
            // 添加用户消息,包含对话内容
            Map<String, String> userMessage = new HashMap<>();
            userMessage.put("role", "user");
            userMessage.put("content", "以下是一段面试对话,请为其生成一个简短的标题:\n\n" + conversationBuilder.toString());
            promptMessages.add(userMessage);
            
            // 调用DeepSeek API生成摘要,使用较低的温度以获得更确定性的结果
            String response = deepSeekService.chatCompletion(promptMessages, "deepseek-chat", 0.3);
            
            // 解析响应,提取摘要文本
            JsonNode responseJson = objectMapper.readTree(response);
            String summary = responseJson.path("choices").get(0).path("message").path("content").asText();
            
            // 清理摘要文本,去除多余的引号、空格和换行符
            summary = summary.replaceAll("\"", "").trim();
            summary = summary.replaceAll("\\r?\\n", " ").trim();
            
            return GlobalResultGenerator.genSuccessResult(summary);
        } catch (Exception e) {
            return GlobalResultGenerator.genErrorResult("生成摘要失败:" + e.getMessage());
        }
    }

前端实现

自动总结标题功能在handlePollingCompleted方法中实现,主要流程如下:

javascript 复制代码
async handlePollingCompleted() {
  try {
    if (!this.activeChatRecord) return;
    
    // 检查是否有足够内容生成摘要且未生成过
    if (
      this.$refs.chatArea &&
      this.$refs.chatArea.messageListForShow &&
      !this.summarizedChatIds.has(this.activeChatRecord)
    ) {
      const messages = this.$refs.chatArea.messageListForShow;
      
      // 至少需要3条消息才生成摘要
      if (messages.length >= 3) {
        // 准备对话数据
        const dialogData = messages.map(msg => ({
          role: msg.role,
          content: msg.content.text
        }));
        
        try {
          // 调用DeepSeek API生成摘要
          const summaryResponse = await axios.post('/api/deepseek/summarize', {
            messages: dialogData,
            chatId: this.activeChatRecord
          });
          
          if (summaryResponse.message) {
            const summary = summaryResponse.message;
            
            // 更新对话标题
            await axios.post('/api/chat/updateChatTopic', null, {
              params: {
                chatId: this.activeChatRecord,
                newTopic: summary
              }
            });
            
            // 记录已生成摘要的对话ID
            this.summarizedChatIds.add(this.activeChatRecord);
            
            // 重新加载聊天记录显示新标题
            await this.loadChatRecords();
            
            // 启动打字机效果展示新标题
            this.startTypingAnimation(this.activeChatRecord, summary);
          }
        } catch (error) {
          console.warn('生成对话摘要失败:', error);
          // 即使失败也标记为已处理,避免重复尝试
          this.summarizedChatIds.add(this.activeChatRecord);
        }
      }
    }
    
    // 后续处理actions的代码...
  } catch (error) {
    console.error('获取actions失败:', error);
  }
}

关键设计要点:

  1. 触发时机 :当轮询完成后自动触发,通过handlePollingCompleted方法
  2. 条件判断
    • 必须有活跃的聊天记录
    • 对话消息数量至少3条
    • 该对话ID未被记录在summarizedChatIds集合中
  3. 数据收集 :从messageListForShow中提取对话内容并格式化
  4. API调用 :通过/api/deepseek/summarize请求生成摘要
  5. 标题更新 :通过/api/chat/updateChatTopic更新对话标题
  6. 状态管理
    • 使用summarizedChatIds集合跟踪已处理的对话
    • 无论成功失败都标记为已处理,避免重复请求
  7. 用户体验:使用打字机效果动态展示新标题

打字机效果

打字机效果在文件中的实现主要包含三个核心方法和相应的CSS样式,用于在聊天记录标题中实现逐字显示的动画效果。

数据结构

javascript 复制代码
typingAnimation: {
  chatId: null,      // 当前执行动画的对话ID
  originalText: '',  // 完整文本
  displayText: '',   // 当前显示的文本(逐渐增加)
  isActive: false,   // 动画激活状态
  charIndex: 0,      // 当前字符索引
  timerId: null      // 定时器ID
}

核心方法

  1. 启动动画:
javascript 复制代码
startTypingAnimation(chatId, text) {
  this.stopTypingAnimation();  // 先停止已有动画
  if (!text || !chatId || text.length < 3) return;
  
  this.typingAnimation = {
    chatId, originalText: text, displayText: '',
    isActive: true, charIndex: 0, timerId: null
  };
  
  setTimeout(() => this.animateNextChar(), 200); // 延迟启动
}
  1. 字符添加动画:
javascript 复制代码
animateNextChar() {
  const { charIndex, originalText } = this.typingAnimation;
  
  if (charIndex <= originalText.length) {
    this.typingAnimation.displayText = originalText.substring(0, charIndex);
    this.typingAnimation.charIndex = charIndex + 1;
    
    // 随机速度模拟自然打字
    const baseSpeed = 70;
    const randomVariation = Math.random() * 100;
    const speed = baseSpeed + randomVariation;
    
    this.typingAnimation.timerId = setTimeout(() => {
      this.animateNextChar();
    }, speed);
  } else {
    this.stopTypingAnimation(true);
  }
}
  1. 停止动画:
javascript 复制代码
stopTypingAnimation(completed = false) {
  if (this.typingAnimation.timerId) {
    clearTimeout(this.typingAnimation.timerId);
  }
  
  if (completed) {
    // 完成后短暂延迟
    setTimeout(() => {
      this.typingAnimation.isActive = false;
    }, 500);
  } else {
    // 立即重置
    this.typingAnimation = {
      chatId: null, originalText: '', displayText: '',
      isActive: false, charIndex: 0, timerId: null
    };
  }
}

视觉效果

  1. 闪烁光标: 使用CSS动画模拟打字光标闪烁
css 复制代码
.cursor {
  width: 2px;
  height: 16px;
  background-color: #409eff;
  animation: blink 0.7s infinite;
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}
  1. 调用时机:
  • 生成聊天摘要后: this.startTypingAnimation(this.activeChatRecord, summary);
  • 重命名对话后: this.startTypingAnimation(chatId, this.newTopicName);
相关推荐
拉不动的猪29 分钟前
# 关于初学者对于JS异步编程十大误区
前端·javascript·面试
熊猫钓鱼>_>3 小时前
Java面向对象核心面试技术考点深度解析
java·开发语言·面试·面向对象··class·oop
进击的野人5 小时前
CSS选择器与层叠机制
css·面试
T___T7 小时前
全方位解释 JavaScript 执行机制(从底层到实战)
前端·面试
9号达人7 小时前
普通公司对账系统的现实困境与解决方案
java·后端·面试
勤劳打代码8 小时前
条分缕析 —— 通过 Demo 深入浅出 Provider 原理
flutter·面试·dart
努力学算法的蒟蒻8 小时前
day10(11.7)——leetcode面试经典150
面试
进击的野人9 小时前
JavaScript 中的数组映射方法与面向对象特性深度解析
javascript·面试
南山安9 小时前
以腾讯面试题深度剖析JavaScript:从数组map方法到面向对象本质
javascript·面试
橘颂TA10 小时前
【剑斩OFFER】算法的暴力美学——二分查找
算法·leetcode·面试·职场和发展·c/c++