VoiceInterview 语音面试模块设计与实现
这篇笔记记录我在 interview-guide 项目中对 VoiceInterview 模块的实现。核心目标是让语音面试具备“实时交互 + 可恢复会话 + 可追踪评估”的完整体验。
模块能力概览
- 实时语音对话:基于
WebSocket + 千问3语音模型(ASR/TTS/LLM 统一 API Key)。 - 流式体验优化:句子级并发 TTS,边生成边合成边播放,首包延迟约 200ms。
- 服务端 VAD:自动断句,提供实时字幕(含中间结果)。
- 回声防护:支持手动提交机制,避免 AI 播报被误录入。
- 会话连续性:支持暂停/恢复与多轮上下文记忆,超时可自动暂停。
- 监控埋点:通过 Micrometer 采集 TTS/ASR 延迟、会话时长等指标。
状态转换
关键接口设计
POST /api/voice-interview/sessions 创建语音面试会话
Controller 入口:
VoiceInterviewController.createSession(@Valid @RequestBody CreateSessionRequest request)
核心调用链:
voiceInterviewService.createSession(request);
实现要点:
- 兜底
skillId(未传则使用默认技能)。 - 兜底
llmProvider(空值走默认 provider)。 - 组装
VoiceInterviewSessionEntity(阶段开关、难度、简历 ID、JD 文本、计划时长等)。 - 默认
userId = "default"。 - 设置初始阶段(
intro/tech/project/hr中第一个启用阶段)。 - 持久化到数据库
voice_interview_sessions,并写入 Redis(带 TTL)。 - 返回
SessionResponseDTO(会话 ID、状态、阶段、配置等)。
GET /api/voice-interview/sessions/{sessionId} 根据会话 ID 获取详情
Controller 调用:
voiceInterviewService.getSessionDTO(sessionId);
实现要点:
- 先查 Redis 缓存,未命中再查数据库。
- 查到后组装
SessionResponseDTO。 - 查不到返回统一错误:
Session not found: {sessionId}。
POST /api/voice-interview/sessions/{sessionId}/end 结束会话并触发异步评估
Controller 调用:
voiceInterviewService.endSession(sessionId.toString());
结束与评估逻辑:
session.setEndTime(now);
session.setCurrentPhase(COMPLETED);
session.setStatus(COMPLETED);
session.setEvaluateStatus(PENDING);
sessionRepository.save(session);
voiceEvaluateStreamProducer.sendEvaluateTask(sessionId);
redisService.streamAdd(streamKey(), buildMessage(payload), AsyncTaskStreamConstants.STREAM_MAX_LEN);
说明:
- 接口立即返回
Result.success(),不阻塞等待评估结果。 - 前端通过
GET /api/voice-interview/sessions/{sessionId}/evaluation轮询评估状态。
PUT /api/voice-interview/sessions/{sessionId}/pause 暂停会话
核心调用:
voiceInterviewService.pauseSession(sessionId.toString(), reason);
实现要点:
- 仅
IN_PROGRESS状态允许暂停。 - 更新状态为
PAUSED,记录原因并刷新updatedAt。 - 同步持久化数据库与 Redis 缓存。
PUT /api/voice-interview/sessions/{sessionId}/resume 恢复会话
核心调用:
voiceInterviewService.resumeSession(sessionId.toString());
实现要点:
- 仅
PAUSED状态允许恢复。 - 恢复后状态改为
IN_PROGRESS,不重置题目进度与阶段。 - 保存数据库并同步 Redis,返回最新
SessionResponseDTO。
GET /api/voice-interview/sessions 获取会话列表(按 userId/status 可过滤)
调用链:
voiceInterviewService.getAllSessions(userId, status);
sessionRepository.findByUserIdAndStatusOrderByUpdatedAtDesc(userId, statusEnum);
返回:
Result<List<SessionMetaDTO>>
DELETE /api/voice-interview/sessions/{sessionId} 删除语音面试会话
调用链:
voiceInterviewService.deleteSession(sessionId);
实现要点:
- 校验会话存在性。
- 删除会话与关联数据(消息/评估等)。
- 清理 Redis 缓存。
GET /api/voice-interview/sessions/{sessionId}/messages 获取对话历史
调用链:
voiceInterviewService.getConversationHistoryDTO(sessionId);
返回:
Result<List<VoiceInterviewMessageDTO>>
GET /api/voice-interview/sessions/{sessionId}/evaluation 获取异步评估状态与结果
实现要点:
- 先校验会话存在(不存在抛
VOICE_SESSION_NOT_FOUND)。 - 读取
evaluateStatus、evaluateError。 - 若状态为
COMPLETED,再读取评估详情:
evaluationService.getEvaluation(sessionId);
- 返回
VoiceEvaluationStatusDTO(包含状态,完成时附评估结果)。
POST /api/voice-interview/sessions/{sessionId}/evaluation 手动触发异步评估
处理逻辑:
voiceInterviewService.getSession(sessionId);
evaluationService.getEvaluation(sessionId);
voiceInterviewService.triggerEvaluation(sessionId);
规则:
- 若已
COMPLETED:直接返回已有评估结果。 - 若为
PENDING/PROCESSING:返回当前状态,不重复触发。 - 其他可触发状态:入队评估任务,立即返回
PENDING,前端继续轮询。
小结
VoiceInterview 模块的关键不是“把语音跑通”,而是把实时链路和会话生命周期稳定地串起来。对我来说,只有在创建、暂停、恢复、结束、评估这一整条链都能可靠协同时,语音面试才是真正可持续迭代的产品能力。