《模拟面试器》项目案例笔记
一、项目一句话概括
一个 基于 Spring AI Alibaba + 通义千问 的 AI 模拟面试系统:上传简历 → AI 多维度评分 → AI 依据简历定制面试题 → 用户作答 → AI 给出逐题评分 + 分类能力雷达 + 源码级参考答案。
核心亮点:没有任何硬编码的题库和评分规则,全部能力由「结构化 Prompt + 约定 JSON Schema」驱动,后端负责把 LLM 的输出解析成强类型 Java 对象来回填页面。
二、技术栈
| 层次 | 选型 | 说明 |
|---|---|---|
| 基础框架 | Spring Boot 3.5.7 | Java 17,-parameters 编译参数 |
| AI 框架 | Spring AI 1.1.0 + Spring AI Alibaba 1.1.2.0 | DashScopeChatModel 直连通义千问 |
| PDF/文档解析 | spring-ai-tika-document-reader + PDFBox 3.0.4 |
Tika 统一抽取 PDF/DOC/DOCX/TXT 文本 |
| 模板引擎 | Thymeleaf | 服务端渲染 5 个页面(非前后端分离) |
| 前端 | 原生 HTML/CSS/JS + fetch |
无 Vue/React,零构建 |
| 工具 | Lombok、Jackson | DTO 用 @Builder,AI 响应用 JsonNode 手工解析 |
| 存储 | ConcurrentHashMap 内存 |
重启即丢,明确标注为简化实现 |
关键依赖(pom.xml):
xml
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
配置只需一行 API Key,通过环境变量注入(application.yml):
yaml
spring:
ai:
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY}
chat:
options:
enable-thinking: true # 开启深度思考,评估类任务质量更高
server:
port: 8080
三、目录结构与职责
src/main/java/com/cloud/alibaba/ai/example/claw/skillsagentexample/
├── ClawAgentExampleApplication.java # 启动类
├── controller/MockInterviewController.java # 页面路由 + 6 个 REST 接口
├── service/MockInterviewService.java # 3 次 LLM 调用 + 3 个 JSON 解析器 + 内存存储
└── service/dto/ # ResumeData / ResumeScoreResult / InterviewQuestions / InterviewEvaluation
src/main/resources/
├── templates/ index / upload / analysis / interview / result.html
├── prompt/ resume-analysis-system.st、resume-analysis-user.st
│ interview-question-system.st、interview-evaluation-system.st
└── application.yml
.st是 Spring AI 的PromptTemplate模板文件(StringTemplate 语法),通过@Value("classpath:/prompt/xxx.st") Resource注入。
四、核心业务流程(4 步闭环)
[1] 上传简历 upload.html → POST /api/resume/upload
↓ Tika 抽文本
[2] AI 简历评分 MockInterviewService.scoreResume() → 返回 ResumeScoreResult
↓ 生成 resumeId(UUID),数据落内存 Map
[3] AI 出题 + 作答 GET /interview/{id} → POST /api/interview/{id}/questions
前端拿到 JSON 渲染答题卡
[4] AI 评估 + 报告 POST /api/interview/{id}/submit → GET /result/{id}
resumeId 是整个流程的唯一线索,串联「简历原文 → 评分 → 题目 → 答案 → 评估报告」四个阶段。
接口清单
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /、/upload |
页面 |
| POST | /api/resume/upload |
上传+解析+AI 评分,返回 {resumeId, scoreResult} |
| GET | /analysis/{resumeId} |
评分结果页(Thymeleaf) |
| GET | /api/resume/{resumeId}/analysis |
评分结果 JSON |
| GET | /interview/{resumeId} |
答题页 |
| POST | /api/interview/{resumeId}/questions |
按简历生成面试题 |
| POST | /api/interview/{resumeId}/submit |
提交 Map<Integer,String> 答案 → 评估 |
| GET | /result/{resumeId} |
评估报告页 |
五、关键技术点拆解
5.1 文档解析:一次调用覆盖 4 种格式
java
TikaDocumentReader reader = new TikaDocumentReader(file.getResource());
String resumeText = reader.read().get(0).getText();
MockInterviewController.uploadResume() 中先用后缀做白名单校验(pdf/doc/docx/txt),再交给 Tika;pom.xml 里把 Spring AI 默认的 pdfbox 排除并显式引入 pdfbox 3.0.4,解决老版本 PDF 抽取乱码/依赖冲突问题。
5.2 三次 LLM 调用的统一范式
三个业务方法结构完全一致,这是本项目最值得复用的骨架:
java
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPromptResource)); // ① 角色+规则+输出格式
messages.add(new UserMessage(userPrompt)); // ② 简历/问答数据
Prompt prompt = new Prompt(messages,
DashScopeChatOptions.builder().temperature(0.7).build()); // ③ 参数
String response = chatModel.call(prompt).getResult().getOutput().getText();
return parseXxx(cleanJsonResponse(response)); // ④ 清洗 + 手工解析成 DTO
- 为什么不用 Spring AI 的
BeanOutputConverter? 现有实现用objectMapper.readTree()逐字段「有则取,无则给默认值」解析,容错更强:模型少返回一个字段不会整体失败(InterviewEvaluation有 4 层嵌套数组,稳定性优先)。 cleanJsonResponse():模型偶发返回json ...包裹,这里做去围栏trim。注意它只处理开头/结尾,属于轻量级兜底。temperature = 0.7:生成题要多样性,评分也要创造性,统一取 0.7。评分/评估类场景其实更适合 0.2~0.3,是一个可优化点。
5.3 提示词工程(本项目的真正"业务代码")
① 简历评分 resume-analysis-system.st --- 把"资深架构师"人设 + 评分细则写死:
- 五维评分:项目技术深度 40 / 技能匹配 20 / 内容完整 15 / 结构规范 5 / 表达专业 10
- 内置加分项 (有 AI 项目 +10、大厂 985/211 +20)与扣分项(自我介绍只写爱好 -5、全外包经历 -20、技能点不足 7-8 个 -20、全 CRUD -20)
- 强制约束:必须输出严谨 JSON、禁止虚构业务背景、建议必须给「原句 vs 优化句」对比
② resume-analysis-user.st --- 最有借鉴价值的一招:给模型喂「技术优化基准表」,用 few-shot 式的表达模板告诉模型什么叫"写得好":
| 多级缓存 | Redis + Caffeine 两级缓存架构,解决击穿/穿透/雪崩,支撑 30w+ QPS |
| 异步编排 | CompletableFuture 对多源 RPC 调用编排,RT 从秒级到百毫秒级 |
| 分布式事务 | 基于消息队列(延时消息)实现订单超时关闭或数据最终一致性 |
这样模型给出的建议就不是"建议多用 Redis"这种废话,而是可直接抄进简历的表达。这是把 LLM 从"打分机器"升级为"简历教练"的关键设计。
③ 出题 interview-question-system.st --- 核心约束力:
- 严禁出现简历未涉及的技术栈(保证"个性化"而非通用八股)
- 9 种
type枚举白名单:PROJECT / JAVA_BASIC / JAVA_COLLECTION / JAVA_CONCURRENT / MYSQL / REDIS / SPRING / SPRING_BOOT / AI - 难度梯度:基础 30% / 进阶 50% / 专家 20%;遵循「使用经验 → 核心原理 → 边界优化」递进
- 明确"不要出编码题"
④ 评估 interview-evaluation-system.st --- 四维加权:准确性 40% + 完整性 20% + 深度 25% + 表达 15%;并要求输出「逐题打分 + 分类汇总 + 优势/改进 + 带 keyPoints 的参考答案」。
所有 prompt 结尾统一写:"请直接输出一个 JSON 对象,不要包含 Markdown 代码块标签" ------ 最朴素但有效的结构化输出手段(不依赖 response_format / JSON Schema)。
5.4 数据模型(一次面试的完整画像)
ResumeData 是整个流程的聚合根:
java
private String resumeId;
private String resumeText; // 原始文本(后续出题/评估都要复用)
private ResumeScoreResult scoreResult; // 阶段二产物
private InterviewQuestions questions; // 阶段三产物
private InterviewEvaluation evaluation; // 阶段四产物
存储即 new ConcurrentHashMap<String, ResumeData>(),三个 saveXxx 方法都是「取出 → set → put 回去」,注意非原子但单机场景够用。
5.5 一个值得记录的实战坑(一致性问题)
README.md/interview-evaluation-system.st描述的结构分为 15 分 ,但resume-analysis-system.st的 Scoring Rubrics 写的是 structureScore (0-5 分)。- 后果:五维相加 = 40+20+15+5+10 = 90 分 ,而
overallScore要求 0-100。模型会"自由发挥"补这 10 分。 - 修复建议:统一成
structureScore 0-15(并把 ∑ 校验写进 prompt),或在服务端加一层归一化:overallScore = min(100, Σ维度)。 - 同类问题:README 说"支持 PDF/DOC/DOCX",实际 Tika 对带复杂排版/扫描件 PDF 抽取效果依赖文件本身,扫描件需要 OCR。
六、项目亮点(面试/答辩时的卖点)
- Prompt 即业务逻辑 :业务规则完全外置到 4 个
.st文件,改评分标准不用改 Java 代码、不用重新发版,这是典型 LLM 应用的设计哲学。 - 上下文复用 :同一份
resumeText贯穿三个环节,出题与评估都带上简历背景,保证"千人千面"。 - 结构化输出工程化:约束式 Prompt + JSON 去围栏 + 逐字段容错解析 + 强类型 DTO,形成完整闭环,前端直接消费。
- 评分粒度可解释 :不仅有总分,还有分维度、逐题反馈、
categoryScores能力分布、keyPoints要点清单,符合"可执行反馈"的产品设计原则。 - 零前端构建 :Thymeleaf + 原生 JS,一个
mvn spring-boot:run就能跑,毕设演示友好。
七、可改进清单(README 官方 TODO + 补充建议)
官方 TODO:
- 流式响应(SSE / WebFlux)替换同步等待,解决 AI 处理久导致页面卡住
- MySQL/Redis 持久化 + 用户系统 + 历史记录
- 多面试模式(前端 / 算法 / 大数据)
- 实时语音视频面试
补充建议(按性价比排序):
| 优先级 | 改进项 | 做法 |
|---|---|---|
| 高 | 结构化输出加固 | 换成 Spring AI BeanOutputConverter 或 DashScope JSON Schema / response_format 校验 + 失败重试(最多 2 次),比字符串裁剪稳得多 |
| 高 | 修复 90/100 分不一致 | prompt 与 README 统一维度分值,服务端加校验兜底 |
| 高 | 降本提速 | 评估环节可并行/异步;简历文本截断或摘要后再喂给模型;引入缓存(同一简历不出两次题) |
| 中 | 超时与降级 | LLM 调用加超时(RestClient timeout)+ 重试 + 友好错误页,避免用户干等 |
| 中 | 安全与合规 | 上传文件校验大小(README 说 10MB 但代码未限制)、病毒/类型白名单改为 MIME 检测而非后缀判断;简历含个人敏感信息,需考虑脱敏与数据留存策略 |
| 中 | Session 替代 resumeId 明文 | 当前 /analysis/{resumeId} 任何人拿到 UUID 即可查看他人简历,存在越权风险 |
| 低 | 评估报告导出 | 评估 JSON → PDF/Markdown 导出,提升毕设完成度 |
| 低 | 温度参数分场景 | 出题 0.8 / 评分评估 0.2,不要把稳定性交给同一个 0.7 |
八、本地运行步骤
bash
# 1. 准备 JDK 17 + Maven 3.6+
# 2. 设置环境变量(PowerShell)
$env:AI_DASHSCOPE_API_KEY = "sk-xxxxxxxxxxxx"
# 3. 启动
cd c:/Users/fei/Desktop/xs-interview-agent
mvn spring-boot:run
# 4. 打开 http://localhost:8080 → 上传 test-resume.txt 体验完整流程
仓库根目录提供了 test-resume.txt 作为测试简历,可直接用于冒烟验证。