案例:Spring Ai/Alibaba《模拟面试器》项目案例

《模拟面试器》项目案例笔记


一、项目一句话概括

一个 基于 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。

六、项目亮点(面试/答辩时的卖点)

  1. Prompt 即业务逻辑 :业务规则完全外置到 4 个 .st 文件,改评分标准不用改 Java 代码、不用重新发版,这是典型 LLM 应用的设计哲学。
  2. 上下文复用 :同一份 resumeText 贯穿三个环节,出题与评估都带上简历背景,保证"千人千面"。
  3. 结构化输出工程化:约束式 Prompt + JSON 去围栏 + 逐字段容错解析 + 强类型 DTO,形成完整闭环,前端直接消费。
  4. 评分粒度可解释 :不仅有总分,还有分维度、逐题反馈、categoryScores 能力分布、keyPoints 要点清单,符合"可执行反馈"的产品设计原则。
  5. 零前端构建 :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 作为测试简历,可直接用于冒烟验证。


相关推荐
付威20231 小时前
我用 100 行核心代码,做了一个能接入飞书的 Hermes 式智能体
人工智能·后端
IamZJT_1 小时前
拆开 DeepSeek Harness 10|创造模式在“创造”什么?让 Agent 组装一种新工作方式
人工智能
snakeshe10101 小时前
Python 入门核心:分支、循环、列表容器
人工智能
启效云1 小时前
AI赋能制造丨启效云亮相2026工业母机产业链高质量发展大会
大数据·人工智能·制造
此时不提桶,更待何时1 小时前
06-16-B-Kafka面试与生产事故实战
分布式·面试·kafka
Rain的Java大神之路1 小时前
🔥13年Java老兵转型AI Agent:90%的人挂在同一个坑,根本不用学Python!(万字实战复盘,建议收藏)
java·后端·面试
TK泰妞1 小时前
如何利用跨境女装提示词库提升销售效率?从商品图片到TikTok内容的完整方法
大数据·人工智能
鲜于言悠9051 小时前
opencode
人工智能
猛犸象限1 小时前
【CJMP Grok Bot实践】用 CJMP 搬游戏时碰到的三个缺口,和我们的绕法
人工智能·grok