山东大学软件学院项目实训 - 基于大模型的模拟面试系统(五)
本周主要围绕智能面试系统的 AI 对话功能进行开发,核心任务是实现面试官提示词生成器(InterviewerPromptGenerator)。本周主要完成了 AI 面试官聊天系统的核心功能开发,重点实现了分支式对话管理系统和 AI 消息轮询机制。组件实现自定义滚动条,自动检测内容高度和容器高度的差异,当内容高度大于容器高度时,显示滚动条。本周主要完成了在线评测系统(OJ)的 Docker 容器化评
·
一、本周工作内容
(一)前端滚动条与界面优化(作者:吴尤)
本周主要完成了前端界面的滚动条优化和模拟面试界面的开发工作。
1. 滚动条优化
- 问题分析:在前端开发中,使用
v-for对文件列表进行展示时,如果不加入分页管理或滚动条,会导致页面内容无限加载,影响用户体验。 - 技术实现:
-
外层容器设置:通过设置外层容器的高度和溢出隐藏属性,确保内容超出部分可以通过滚动条访问。
-
使用
el-scrollbar组件:利用 Element UI 的el-scrollbar组件实现自定义滚动条,自动检测内容高度和容器高度的差异,当内容高度大于容器高度时,显示滚动条。<el-scrollbar style="height:100%"> <el-table :data="files" style="width: 100%"> <!-- 表格内容 --> </el-table> </el-scrollbar>
-
2. 模拟面试界面开发
- 面试官界面:通过
fetchAiList方法从后端获取 AI 面试官列表数据,渲染为可滚动的面试官列表。每个面试官条目包含头像和名称,点击时触发handleInterviewerSelect方法,保存当前选中的面试官 ID 并加载对应的聊天记录。 - 面试记录界面:在用户切换到“记录”标签且已选择面试官时显示,通过
loadChatRecords方法加载当前面试官的所有聊天记录,并按创建时间倒序排列。每条记录显示话题名称和操作菜单,操作菜单通过el-popover实现悬停显示,包含重命名和删除功能。
(二)AI 对话功能开发(作者:孙旭)
本周主要围绕智能面试系统的 AI 对话功能进行开发,核心任务是实现面试官提示词生成器(InterviewerPromptGenerator)。
1. 消息接收层
- 技术实现:通过 Spring Boot 的
@RestController注解定义ChatController,接收用户发送的消息,并调用ChatService处理消息。@RestController @RequestMapping("/chat") public class ChatController { @Autowired private ChatService chatService; @PostMapping("/send") public Result<String> sendMessage(@RequestBody ChatMessage message) { return chatService.sendMessageToInterviewer(message); } }
2. 提示词生成层
- 问题分析:
generatePrompt方法需要访问数据库中的评估标准数据,但该方法必须是静态的,这与 Spring 的依赖注入模式冲突。 - 解决方案:最终采用
ValuationStandardHolder类实现,通过监听ContextRefreshedEvent事件,在 Spring 容器完全初始化后加载数据,避免了初始化时序问题。@Component public class ValuationStandardHolder implements ApplicationListener<ContextRefreshedEvent> { private static List<ValuationStandard> standardsList; @Override public void onApplicationEvent(ContextRefreshedEvent event) { standardsList = valuationStandardMapper.selectValuationList(); } }
(三)Docker 容器化评测环境开发(作者:吴浩明)
本周主要完成了在线评测系统(OJ)的 Docker 容器化评测环境开发。
1. 容器服务接口设计
- 技术实现:定义
DockerService接口,包含创建容器、执行编译命令和运行代码的方法。public interface DockerService { String createContainer(String language, Path tempDir); Map<String, Object> executeCompileCommand(String containerId, String[] command, String workDir); Map<String, Object> executeRunCommand(String containerId, String[] command, String workDir, String inputFile, int timeLimit); }
2. 容器创建与管理
- 技术实现:通过
docker run命令创建并启动容器,设置内存和 CPU 限制,挂载临时目录,并禁止网络访问。String[] command = { "docker", "run", "-d", "--name", containerId, "--memory", memoryLimit, "--cpus", cpuLimit, "-v", hostPath + ":" + containerPath, "--network", "none", "-w", containerPath, image, "tail", "-f", "/dev/null" };
(四)AI 面试官聊天系统开发(作者:王博凡)
本周主要完成了 AI 面试官聊天系统的核心功能开发,重点实现了分支式对话管理系统和 AI 消息轮询机制。
1. 分支式对话管理系统
- 架构设计:采用树形结构管理对话分支,每个分支节点包含分支 ID、索引、父分支索引、子分支数组和消息列表。
- 核心逻辑实现:通过
newChatBranch方法实现分支创建,支持用户编辑或重新生成消息时自动创建新分支。async newChatBranch(index) { try { // 情况1:是分支的第一个消息(index为0) if (index == 0) { const newBranch = { branchId: this.generateUuid(), chatId: this.chatRecordId, index: this.allBranches.length, parentBranchIndex: this.currentBranch.parentBranchIndex, children: [], messageLocals: [] }; // 添加到分支列表 this.allBranches.push(newBranch); // 在父分支的children中添加新分支 const parentBranch = this.allBranches.find( b => b.index == this.currentBranch.parentBranchIndex ); if (parentBranch) { parentBranch.children.push({ branchIndex: newBranch.index, tag: `分支${parentBranch.children.length + 1}` }); } // 切换当前分支 this.currentBranch = newBranch; this.modifiedBranch.push(parentBranch);//先不加入新增的branch,到后面消息接受完毕后再更新 } // 情况2:不是第一个消息 else { // 创建新父分支(包含index之前的消息) const newParentBranch = { branchId: this.generateUuid(), chatId: this.chatRecordId, index: this.allBranches.length, parentBranchIndex: this.currentBranch.parentBranchIndex, children: [ { branchIndex: this.currentBranch.index, // 新index分配给当前分支 tag: '原分支' } ], messageLocals: [] }; newParentBranch.messageLocals = this.currentBranch.messageLocals.slice(0, index).map(msg => ({ ...msg, branchId: newParentBranch.branchId // 更新branchId })) // 找到newParentBranch的父分支 const grandParentBranch = this.allBranches.find( b => b.index == newParentBranch.parentBranchIndex ); if (grandParentBranch) { // 遍历父分支的children数组 grandParentBranch.children.forEach(child => { if (child.branchIndex == this.currentBranch.index) { // 将当前分支的引用改为新父分支 child.branchIndex = newParentBranch.index; } }); } // 将当前分支从index开始的message分配给新分支 this.currentBranch.messageLocals = this.currentBranch.messageLocals.slice(index).map(msg => ({ ...msg, branchId: this.currentBranch.branchId // 保持当前branchId })); this.currentBranch.parentBranchIndex = newParentBranch.index; // 创建新子分支(包含index及之后的消息) const newChildBranch = { branchId: this.generateUuid(), chatId: this.chatRecordId, index: this.allBranches.length + 1, parentBranchIndex: newParentBranch.index, children: [], messageLocals: [] }; newParentBranch.children.push({ branchIndex: newChildBranch.index, tag: `分支${newParentBranch.children.length + 1}` }); // 添加到分支列表 this.allBranches.push(newParentBranch, newChildBranch); //新建的分支在正式接受到信息之后再保存 this.modifiedBranch.push(newParentBranch, this.currentBranch,grandParentBranch); // 切换当前分支到新创建的子分支 this.currentBranch = newChildBranch; } //console.log(this.currentBranch) // 重新构建经过currentBranch的路径 // console.log(this.branchPath) await this.buildPathForTargetBranch(this.currentBranch); } catch (error) { console.error('创建分支失败:', error); this.$message.error('创建分支失败'); } },
2. AI 消息轮询机制
- 技术实现:采用
requestAnimationFrame实现平滑轮询,通过后端异步将接受的内容加入轮询队列,实现前后端文字的流式输出。async sendMessageWithPolling(messageContent = null) { // 参数messageContent为null时表示来自聊天框的消息,否则表示修改后的消息 // 参数messageContent为null时表示来自聊天框的消息,否则表示修改后的消息 const messageText = messageContent !== null ? String(messageContent) : String(this.inputMessage); if (!messageText || !messageText.trim() || this.isLoading) return; if (!messageText.trim() || this.isLoading) return; if (!this.currentInterviewer) { this.$message.warning('请先选择面试官'); return; } // 如果当前分支是0号根节点,则创建新分支 if (!this.currentBranch || this.currentBranch.index == 0) { await this.createNewBranch(); this.rootBranch.children.push({ branchIndex: this.currentBranch.index, tag: '原分支' }); this.currentBranch.parentBranchIndex = 0; this.modifiedBranch.push(this.rootBranch); } const userMessage = this.createMessage('user', messageText); // 关键修改:处理文件上传逻辑 this.messageListForShow.push(userMessage); if (this.currentBranch) { if (!this.currentBranch.messageLocals) { this.currentBranch.messageLocals = []; } this.currentBranch.messageLocals.push(userMessage); } //先保存一下,给后面上传文件时用 this.modifiedBranch.push(this.currentBranch); await this.saveBranchList(this.modifiedBranch) this.modifiedBranch = []; this.buildContextMessages(); // 如果是来自聊天框的消息,清空输入框 if (!messageContent) { this.inputMessage = ''; } this.isLoading = true; let messageId = null; // 用于存储消息ID try { this.scrollToBottom(); // 发送消息到后端 // 关键修改:处理文件上传逻辑 const formData = new FormData(); // 1. 添加聊天请求元数据 formData.append('chatRequest', JSON.stringify({ messageList: this.chatMessages, interviewer: this.currentInterviewer })); // 2. 添加文件数据(如果有) this.processedFiles.forEach(file => { formData.append('files', file.raw); }); // 3. 添加消息ID formData.append('fileMessageId', userMessage.messageId); // 4. 发送请求 const response = await this.$axios.post('/api/chat/sendMessageWithPoll', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); // 5. 清空已处理文件 this.processedFiles = []; messageId = response.data; // 获取后端返回的消息ID await this.fetchData(this.chatRecordId); await this.buildPathForTargetBranch(this.currentBranch); // 创建AI消息占位对象 const aiMessage = { messageId: messageId, // 添加前缀便于识别 role: 'assistant', content: { text: '', // 初始为空 files: [] }, timestamp: new Date().toISOString() }; this.messageListForShow.push(aiMessage); this.currentAiMessageId = aiMessage.messageId; this.scrollToBottom(); // 开始轮询 if (!this.isPolling) { this.isPolling = true; this.startPolling(messageId); } } catch (error) { this.$message.error('发送消息失败'); this.isLoading = false; console.error(error); } }, async startPolling(messageId) { const POLLING_TIMEOUT = 20000; // 5秒超时 let pollingStartTime = Date.now(); const processBatch = async () => { if (!this.isPolling) return; try { // 检查是否超时 if (Date.now() - pollingStartTime > POLLING_TIMEOUT) { throw new Error('轮询超时,未收到有效响应'); } const params = new URLSearchParams(); params.append('messageId', messageId); // 确保参数名完全匹配 params.append('batchSize', '5'); // 字符串形式 const response = await this.$axios.get('/api/chat/pollMessages', { params: params, paramsSerializer: params => params.toString() // 使用默认序列化 }); if (response.data && response.data.length) { let shouldStop = false; // 重置超时计时器(每次收到有效数据就重置) pollingStartTime = Date.now(); let hasNewContent = false; // 批量处理消息 for (const msg of response.data) { // 检查是否收到停止信号 if (msg.finish === "stop") { shouldStop = true; } // 更新AI消息内容 const aiMsg = this.messageListForShow.find(m => m.messageId === messageId); if (aiMsg) { aiMsg.content = aiMsg.content || { text: '' }; const oldLength = aiMsg.content.text.length; await this.typeText(aiMsg, msg.text); if (aiMsg.content.text.length > oldLength) { hasNewContent = true; // 只有内容变化时才标记 } } } // 只有内容变化时才强制更新和滚动 if (hasNewContent) { this.$forceUpdate(); this.scrollToBottom(); } // 如果收到停止信号,则中止轮询 if (shouldStop) { this.stopPolling(); const aiMsg = this.messageListForShow.find(m => m.messageId === messageId); if (aiMsg) { // 将AI消息加入currentBranch this.currentBranch.messageLocals.push({ messageId:aiMsg.messageId, role: 'assistant', branchId: this.currentBranch.branchId, content: { text: aiMsg.content.text, files: [] }, timestamp: new Date() }); this.modifiedBranch.push(this.currentBranch); // 调用封装的方法保存currentBranch,并手动传入branchList console.log(this.modifiedBranch) await this.saveBranchList(this.modifiedBranch); this.modifiedBranch = []; await this.fetchData(this.chatRecordId) this.scrollToBottom(); } this.isLoading = false; return; } } // 继续轮询 this.pollingAnimationFrame = requestAnimationFrame(processBatch); } catch (error) { this.modifiedBranch = []; this.stopPolling(); this.isLoading = false; await this.loadChatMessages(this.chatRecordId) await this.buildPathForTargetBranch(this.rootBranch); this.$message.error('获取消息失败: ' + error.message); this.scrollToBottom(); } }; this.isPolling = true; processBatch(); }, stopPolling() { this.isPolling = false; if (this.pollingTimer) { clearTimeout(this.pollingTimer); this.pollingTimer = null; } },
(五)Token 过期重定向问题解决(作者:李一铭)
本周主要解决了前端 Token 过期或退出登录时的重定向问题。
1. 功能实现
- 技术实现:通过
auth.js检查 Token 是否过期,如果过期则清除 Token 并重定向到登录页面。export default { getToken() { return localStorage.getItem('auth._token.local') || ''; }, isTokenExpired() { const storedExpiration = localStorage.getItem('auth._token_expiration.local'); if (storedExpiration) { const isExpired = Date.now() >= parseInt(storedExpiration, 10); if (isExpired) { console.warn('[Auth] Token expired (based on localStorage expiration)'); return true; } } let token = this.getToken(); console.log(token); if (token == "false") return true; }, clearToken() { localStorage.removeItem('auth._token.local'); localStorage.removeItem('refreshToken'); }, saveToken(token, refreshToken) { localStorage.setItem('auth._token.local', token); if (refreshToken) { localStorage.setItem('refreshToken', refreshToken); } } };
二、本周工作成果与问题
(一)工作成果
- 前端滚动条与界面优化:完成了前端界面的滚动条优化和模拟面试界面的开发工作,提升了用户体验。
- AI 对话功能开发:实现了面试官提示词生成器,解决了静态方法与依赖注入的冲突问题。
- Docker 容器化评测环境开发:完成了在线评测系统的 Docker 容器化评测环境开发,实现了多语言容器环境配置和安全隔离机制。
- AI 面试官聊天系统开发:实现了分支式对话管理系统和 AI 消息轮询机制,优化了长文本流式接收
(二)问题
- 聊天界面太老,没有现代化的感觉。滚动条的全局设置。重命名以及删除方法
- 图片加载异常
- 考虑引入缓存机制,优化数据加载性能
- 增加大模型评测代码功能
- 增强安全防护机制,增加语音输入支持
- 完善异常处理机制,探寻agent的具体实现技术
更多推荐


所有评论(0)