Interview Mock Interview Module Design and Implementation
This note records how I implemented the Interview module in the interview-guide project, including the core APIs and evaluation pipeline. The main goal is to build a complete closed loop for question generation, answering, evaluation, and report export, while keeping text interviews and voice interviews aligned under the same evaluation logic.
Module Capability Overview
- Skill-driven question generation: supports 10+ interview tracks (Java backend, major-company tracks, frontend, Python, algorithms, system design, test development, AI Agent, etc.). Each track is defined by
SKILL.mdfor scope and difficulty distribution. - Historical question deduplication: previously asked questions in historical sessions are excluded during session creation to reduce repeated assessment.
- Interview stage duration linkage: after total duration changes, each stage (self-introduction, technical assessment, project deep-dive, reverse Q&A) is auto-allocated by ratio.
- Intelligent follow-up flow: supports multi-round follow-up configuration (default: 1 round) to simulate realistic interview interactions.
- Unified evaluation engine: text and voice interviews share the same evaluation architecture (batch evaluation + structured output + summarization + fallback).
- Report export: supports asynchronous generation and export of PDF interview reports.
- Interview center: unified entry for continue/restart/history operations.
Core State Flow
Key API Design
GET /api/interview/sessions List Interview Sessions
Purpose:
- Used by the interview history page, returns session list in reverse creation order.
Call chain:
persistenceService.findAll().stream();
POST /api/interview/sessions Create Interview Session
Rate limiting:
- Global limit + IP limit (5)
Core logic:
sessionService.createSession(request);
persistenceService.getHistoricalQuestions(skillId, request.resumeId());
sessionRepository.findTop10ByResumeIdAndSkillIdOrderByCreatedAtDesc(...);
sessionRepository.findTop10BySkillIdOrderByCreatedAtDesc(...);
questionService.generateQuestionsBySkill(...);
sessionCache.saveSession(...);
persistenceService.saveSession(...);
GET /api/interview/sessions/{sessionId} Get Session Info
Core logic:
sessionService.getSession(sessionId);
sessionCache.getSession(sessionId);
restoreSessionFromDatabase(sessionId);
GET /api/interview/sessions/{sessionId}/question Get Current Question
Core logic:
sessionService.getCurrentQuestionResponse(sessionId);
getCurrentQuestion(sessionId);
getOrRestoreSession(sessionId);
- If session is in
CREATEDstate, return question bycurrentIndex.
POST /api/interview/sessions/{sessionId}/answers Submit Answer and Move Forward
Rate limiting:
- Global limit (10)
Core logic:
sessionService.submitAnswer(request);
- Updates answer, session state, cache, and DB.
- If this is the last question:
persistenceService.updateEvaluateStatus(sessionId, AsyncTaskStatus.PENDING, null);
evaluateStreamProducer.sendEvaluateTask(sessionId);
POST /api/interview/sessions/{sessionId}/answers Save Draft Answer (No Progress)
Core logic:
sessionService.saveAnswer(request);
- Syncs both Redis and DB.
POST /api/interview/sessions/{sessionId}/complete Early Submit
Core logic:
sessionService.completeInterview(sessionId);
sessionCache.updateSessionStatus(sessionId, SessionStatus.COMPLETED);
- Persists DB status.
evaluateStreamProducer.sendEvaluateTask(sessionId);
GET /api/interview/sessions/unfinished/{resumeId} Find Unfinished Session
Core logic:
sessionService.findUnfinishedSessionOrThrow(resumeId);
findUnfinishedSession(resumeId);
sessionCache.findUnfinishedSessionId(resumeId);
persistenceService.findUnfinishedSession(resumeId);
GET /api/interview/sessions/{sessionId}/report Generate Interview Evaluation Report
Core logic:
sessionService.generateReport(sessionId);
evaluationService.evaluateInterview(...);
unifiedEvaluationService.evaluate(...);
evaluateInBatches(...);
summarizeBatchResults(...);
structuredOutputInvoker.invoke(...);
securedSystemPrompt = systemPromptWithFormat + ANTI_INJECTION_INSTRUCTION;
Uses anti-injection instruction to reduce prompt contamination risk from user input.
GET /api/interview/sessions/{sessionId}/details Get Interview Detail
Call chain:
historyService.getInterviewDetail(sessionId);
interviewPersistenceService.findBySessionId(sessionId);
GET /api/interview/sessions/{sessionId}/export Export Interview Report as PDF
Call chain:
historyService.exportInterviewPdf(sessionId);
interviewPersistenceService.findBySessionId(sessionId);
pdfExportService.exportInterviewReport(session);
DELETE /api/interview/sessions/{sessionId} Delete Interview Session
Call chain:
persistenceService.deleteSessionBySessionId(sessionId);
sessionRepository.findBySessionId(sessionId);
sessionRepository.delete(session);
Evaluation Engine Implementation Highlights
- A single evaluation pipeline supports both text and voice interviews, reducing branch complexity.
- Batch-first then summarize strategy balances long-context stability and structured output quality.
- Anti-injection prompt composition is applied to reduce malicious-input interference.
- In failure scenarios, unified invoker + fallback fields avoid hard report failures.
Summary
The Interview module now covers the full workflow from session creation, dynamic question generation, answer progression, asynchronous evaluation, to report export. For me, the key value is separating interview process management from evaluation result production into two evolvable layers, so future changes to question strategy or model upgrades can stay controlled.