InterviewGuide

简历分析与模拟面试练习平台
反馈(在 GitHub 新标签页打开)

主要技术:Spring Boot、Spring AI、PostgreSQL、Redis

项目目录

项目简介

InterviewGuide 是开源 AI 面试辅助平台,围绕简历分析、模拟面试和知识库问答组织求职练习。用户上传简历后,可以查看评分、优势与修改建议;进入模拟面试后记录主问题、追问和作答,再查看逐题评价与报告。平台还支持把自己的知识资料整理成题库,用已启用题目开展专项练习。

后端基于 Java、Spring Boot 与 Spring AI,前端使用 React、TypeScript。PostgreSQL 保存简历、题库和面试记录,pgvector 提供知识检索,Redis Streams 承接分析、出题和评估等耗时任务,把上传、等待与报告回看连接起来。

项目亮点

  • 队列与数据库共同跟踪分析任务。 简历分析使用消费组领取消息,先补领长期未确认消息,再读取新消息;数据库记录任务状态,定时巡检卡住的任务并有限补投,上传请求无需等待模型完成。
  • 旧消费者的结果有写入校验。 简历任务以状态和随机 attemptId 领取执行权,阶段心跳检查当前代次;保存分析结果时再在事务内锁行复核,阻止任务重新领取后旧消费者迟到覆盖。
  • 模型结果按统一结构交接。 简历评分和建议由 Java record、BeanOutputConverter 定义字段,统一调用器提供 Schema 校验与有限重试,供报告展示和保存复用。
  • 知识库出题与正式练习分开。 根据检索片段生成带答案、关键点、评分标准和追问的草稿;练习只从已启用题目中抽取,先核对主问题与追问容量,再保留父子问题关系组卷,交卷后复用统一评估服务。

技术架构

frontend/ 提供简历、知识库和面试界面,app/ 中的业务模块处理上传、任务提交、作答与报告读取。资料解析和知识检索为模型调用准备正文或参考片段;公共结构化调用器把模型输出转换为可交给业务模块保存的对象。

耗时处理通过 Redis Streams 交给对应消费者,结果落入 PostgreSQL 后由页面查询。简历分析另有执行代次和超时恢复;知识库出题则在保存前核对任务标识,再将生成草稿交给题库维护,启用后进入抽题与面试评估。

代码位置 职责
frontend/、app/src/main/java/interview/guide/modules/ 前端交互与简历、知识库、面试业务模块
app/src/main/java/interview/guide/common/async/、app/src/main/java/interview/guide/infrastructure/redis/ 公共消费模板、消息补领与确认
app/src/main/java/interview/guide/modules/resume/ 简历解析、分析代次、恢复与报告保存
app/src/main/java/interview/guide/modules/knowledgebase/ 知识检索、题目草稿、容量校验与抽题
app/src/main/java/interview/guide/common/ai/、app/src/main/java/interview/guide/modules/interview/ 结构化模型调用、作答合并与评估报告

如何写进简历

简历参谋 · 候选人简历分析助手 | 人力资源服务业场景示例

项目描述:面向招聘顾问的候选人简历分析工具,解析简历并结合岗位要求输出结构化评分、优势与修改建议,支持分析进度查询和原文依据回查。

技术栈:Java、Spring Boot、Spring AI、PostgreSQL、Redis

主要工作:

  • 异步分析与超时任务恢复:基于 Redis Streams 消费组承接耗时分析,补领未确认消息并通过数据库条件更新领取任务;结合阶段心跳、超时巡检与有限补投恢复卡住任务,将简历上传与模型评估解耦。
  • 执行代次与迟到结果隔离:以 attemptId 标识本轮执行权,进度与完成状态均按代次条件更新,保存分析结果时通过 PostgreSQL 事务行锁再次复核;防止任务重领后旧消费者覆盖新结果。
  • 结构化评价与岗位证据关联:基于 Spring AI、Java record 与 BeanOutputConverter 约束评分及建议结构,统一执行 Schema 校验与有限重试;将岗位要求、简历段落标识接入评价结果,使顾问能够回查匹配依据。

面试搭子 · 技术岗位模拟面试陪练 | 教育培训业场景示例

项目描述:面向技术培训机构的模拟面试陪练,从课程知识库生成题目与评分要点,按方向和难度组织主问、追问,并根据学员作答生成逐题评价与补练建议。

技术栈:Java、Spring Boot、Spring AI、PostgreSQL、pgvector、Redis

主要工作:

  • 课程知识检索与结构化出题:基于 pgvector 检索课程资料,通过 Spring AI 生成主问题、参考答案、评分要点和追问;结合章节范围组织上下文,按规范化题干去重并保存草稿,供教师审核后启用。
  • 生成任务隔离与短事务落库:将耗时检索和模型调用放在事务外执行,生成前核对任务标识,保存时以短事务复核当前任务并替换题目;拦截已被新任务替代的迟到结果,避免长事务占锁和旧题覆盖。
  • 主问追问关联与受限抽题:按方向、难度及知识库筛选启用题目,检查主问题数量与追问池容量,再按父子关系抽取题目;结合课程知识点覆盖组织训练,容量不足时返回缺口,避免追问错配或静默缺题。
  • 异步评估与分项训练反馈:通过 Redis Streams 异步处理作答评估,按题目索引合并回答,将参考答案和评分要点交给结构化评价服务;按知识点汇总逐题失分依据与补练建议,形成可回看的训练报告。

面试题与回答

3 题
  1. 用 Redis 作消息队列时,如何处理宕机导致的消息丢失?

    展开收起

    核心回答

    InterviewGuide 用 Java 和 Spring 实现简历分析等异步任务,Redis Stream 负责投递,数据库保存正文、结果与任务状态。以简历分析为例,先保存 PENDING 任务,再发送包含 resumeId 和 retryCount 的消息;消费者先领取数据库任务,执行分析、保存结果并更新完成状态,最后确认消息。

    恢复有两层:Stream 层主动补领未确认消息,数据库层扫描长期卡住的任务并补投。 前者解决消息交给某个消费者后无人继续处理的问题,后者为消息丢失、裁剪或业务状态停滞提供恢复依据。它允许重投,依靠业务状态约束重复执行,并不承诺 Redis 永不丢消息。

    未确认消息怎样重新被消费

    消费者启动时准备消费组,每个实例使用独立消费者名称。循环先调用 autoClaim(),按游标补领空闲超过 5 分钟的待处理消息,每批最多 10 条;本轮没有补领结果时,再阻塞读取该组尚未投递的新消息。由此,原消费者宕机后,恢复并不依赖它以原名称重新上线。具体顺序见补领与新消息读取。

    补领成功只表示获得消息,还要通过数据库的 PENDING→PROCESSING 条件更新领取业务任务。已完成或无法领取的任务会确认消息并跳过。若旧任务仍停在 PROCESSING,补领消息不会直接使它重新执行,需要下一层数据库恢复将过期状态重置后补投。PEL 状态和业务任务状态是两套记录,不能混为一条状态机。

    业务失败、重试与 ACK 的顺序

    公共消费者的正常路径是:领取任务、执行业务、更新完成状态、ACK。异常路径会检查消息里的 retryCount:小于 3 时重新投递,达到上限后标记失败,再确认旧消息。这里最多是首次执行加 3 次消息重试,业务抛出的异常才会进入这条路径。

    简历分析重投前先条件重置为 PENDING,新消息写入增加后的重试次数;重投入队失败时,把仍待处理的任务标为 FAILED。格式错误消息会直接确认丢弃,ACK 失败则记录日志,后续仍可能再次被补领。见消费确认与异常处理及简历分析重投。

    当前超过重试上限的处理是数据库 FAILED 终态与手动重试,没有独立死信 Stream。若增加死信能力,可以保存原消息、业务标识、异常原因和重试历史,完成持久转存后再确认原消息。另一方面,分析服务会把部分模型调用异常转换为错误报告返回,队列可能仍按正常结果保存并完成;消息成功处理与模型分析结果有效也要分别判断。

    队列记录丢了,数据库怎样兜底

    上传流程先提交简历记录,再发送消息;发送失败会保留文件与记录,并把待处理任务标记失败,供用户重试。这是分步写入,未使用覆盖数据库和 Redis 的原子事务或事务发件箱。

    数据库恢复调度器默认每分钟分别扫描过期的 PENDING 和 PROCESSING:前者默认超过 10 分钟尝试补投;后者默认 15 分钟无进展时条件重置,再补投。恢复有次数上限,避免无限循环,具体见卡住任务恢复调度。阈值需要覆盖排队和实际业务耗时,否则会误判仍在执行的任务。

    消息发送使用近似 MAXLEN=1000 裁剪,未确认消息也可能先被裁掉;补领发现这类 ID 时只记录警告日志,无法恢复已裁剪的消息体。因此数据库任务和已保存的简历正文很关键,但仍需要核对恢复是否成功、失败任务是否有人处理。要进一步提高可靠性,应结合部署环境配置持久化与复制、按积压调整保留策略,并验证“入队前宕机、领取后宕机、结果保存后 ACK 前宕机”三个窗口,而不能只测试正常消费。

  2. 租约失效后为什么还需要 Fencing Token,如何防止旧 Worker 写回?

    展开收起

    核心回答

    InterviewGuide 的简历分析任务通过数据库中的状态和随机 attemptId 识别当前执行者。消费者原子地把任务从 PENDING 改成 PROCESSING,并记录本次尝试;超时恢复或重试会清除旧尝试,新消费者重新领取时生成新的标识。心跳、完成、失败和重试重置均检查当前尝试,分析结果保存也在数据库事务中验证所有权。

    这里采用的是当前尝试的等值校验,能阻止失去执行权的旧消费者写入分析结果或改动新尝试的状态。attemptId 是随机 UUID,并不是面向外部资源的单调递增 Fencing Token;它也无法撤销已经发给模型的分析请求。

    怎样领取与恢复任务

    AnalyzeStreamConsumer.tryMarkProcessing() 生成 UUID,调用 tryMarkAnalyzeProcessing()。只有数据库中仍为 PENDING 的任务才能更新成功,受影响行数为 1 时才继续分析;重复消息或其他消费者不会仅凭消息到达就取得业务执行权。消费者领取、条件领取。

    心跳通过 PROCESSING + analyzeAttemptId 条件更新进展时间,更新失败就抛出执行权失效异常。当前简历分析链在下载解析完成、模型分析返回后报告心跳,并非持续运行独立心跳线程。长调用可能超过无进展阈值,因此阈值需要考虑调用和重试的总耗时。业务阶段与心跳、恢复配置说明。

    恢复调度器扫描无进展任务,以状态和时间条件将过期的 PROCESSING 重置为 PENDING、清除旧 attemptId,再补投消息。旧消费者仍可能继续计算,但它持有的旧标识已经无法匹配当前数据库状态。恢复调度、恢复条件更新。

    为什么保存前再检查仍然有用

    仅靠模型返回后的心跳仍不够:消费者可能刚通过心跳检查,就再次暂停。saveAnalysisIfOwned() 会在事务内通过 findByIdForUpdate() 锁住简历行,重新检查状态和 attemptId,通过后才保存分析结果。这次校验与结果落库受同一个事务保护,恢复调度器不能在二者之间替换任务所有权。锁行查询、事务内验证与结果保存。

    完成、失败以及重试重置也带尝试条件。旧消费者失权后,它的重试重置更新不到当前任务,就不会重新投递旧尝试;旧的完成或失败回调也不会覆盖新尝试。条件状态更新、重试处理。

    分析结果保存与随后标记 COMPLETED 是两个独立事务,不能把这条链描述为“结果、终态和消息确认一次原子提交”。它保护的是相应数据库写回;模型可能被重复调用,端到端也不具备外部副作用只发生一次的保证。消费执行顺序。

    怎样验证旧结果确实被隔离

    可以让尝试 A 停在模型调用中,触发恢复并让 B 取得新 attemptId,再释放 A,检查 A 的心跳与结果写回被拒绝;还要覆盖“A 心跳成功后暂停、B 接管、A 再保存”的时序,以验证最终事务检查。分别核对分析记录、任务状态和外部模型调用次数,才能区分旧结果隔离与重复计算。

  3. 简历评分结果如何结构化输出?

    展开收起

    核心回答

    InterviewGuide 以 Java record 描述简历分析结果,用 BeanOutputConverter 生成格式要求,再通过统一的 StructuredOutputInvoker 调用与解析模型响应。结果包括总分、五项评分、摘要、优点及建议,最终保存为分析记录。

    这一实现解决了结构交接和重试复用,但仍要单独看分值约束与失败语义:当前 DTO 没有表达评分区间,服务外层还可能把失败转换为零分报告,因此结构能保存不等于分析有效。

    DTO 和 Prompt 怎样配合?

    内部 DTO 定义 overallScore、scoreDetail、summary、strengths、suggestions;建议包含类别、优先级、问题和推荐修改。服务把 outputConverter.getFormat() 追加到系统提示词,再将简历正文填入用户模板。当前分析方法接收的是简历正文,没有显式目标岗位参数。DTO 与格式构造。

    系统模板要求项目、技能、内容、结构、表达五项贡献分分别为 40/20/15/15/10,合计 100。对外响应类的部分注释仍是旧的分项上限,不能将注释直接当成当前执行规则;这些标准需要统一维护。实际评分模板、响应字段注释。

    转换失败怎样重试?

    统一调用器默认开启 Schema 校验,通过 call.entity(outputConverter, spec -> spec.validateSchema()) 获取对象。关闭校验的兼容分支才采用文本转换,并尝试修复字符串中的未转义引号;这不是任意 JSON 错误都能修好的通用修复器。两条解析分支。

    外层调用器按配置重试,后续尝试可增加严格 JSON 要求和上一轮错误,默认外层最多尝试两次。Schema 校验和 SDK 自身可能还有内部调用,所以两次不能直接当成全部 HTTP 请求的上限。重试封装、默认配置。

    为什么还需要业务校验和独立失败状态?

    评分 DTO 的分数字段只有 int,没有最小值、最大值或分项合计约束;转换后也没有重新计算总分。生成的对象即使通过现有结构校验,仍可能分值越界或总分不一致。完善时应让分项标准进入可执行约束,再检查总分与评分依据。

    此外,ResumeGradingService 外层捕获异常后返回一份零分错误报告。只要当前消费者仍拥有执行权,它就可以按普通分析结果保存,并随后完成任务。因此不能把任务 COMPLETED 或一条零分分析记录直接解释为评价成功。错误报告回退、结果保存入口。

    执行代次校验能阻止旧消费者写入,却不会判断报告内容有效。进一步完善时,可把分析调用失败、报告校验失败与正常评分分开,避免用户将系统错误理解为自己的简历得了零分;再用缺字段、越界分数、总分冲突和服务不可用样例分别验证。