本文基于「码上面试平台」当前代码整理。这里分析的是简历提题链路,不展示工作流配置中的凭证或敏感参数。
简历上传成功,不等于面试已经准备好
从页面看,这个功能像是“选一个 PDF,点上传,等题目出来”。从服务端看,它至少包含会话状态更新、Agent 配置解析、文件上传、模型调用、结果解析、缓存初始化和结构化持久化。
我会把它看成一个有明确成功条件的业务流程:只有题目、流程状态和后续需要的会话材料都准备到能继续答题的程度,接口才应该告诉前端提取成功。
先从 Controller 之后追入口
接口进入InterviewSessionFacade.extractInterviewQuestions后,先把会话标为RESUME_UPLOADING,然后创建InterviewQuestionReqDTO并委托给InterviewWorkflowService。实际实现InterviewAgentOrchestrationService再把调用交给InterviewQuestionExtractionService。
拆成这几层有一个实际好处:Controller 处理 HTTP 入口,Facade 维护会话级规则,Orchestration 选择业务能力,Extraction Service 承担外部 AI 与结果处理。读代码时顺着这条线追,比从某个 SDK 调用往回猜业务目的更快。
提取服务里有哪些关键步骤?
1. 解析业务 Agent 并准备幂等材料
服务根据INTERVIEW_QUESTION_EXTRACTION场景查找对应 Agent。它会在加锁前计算上传文件内容的 SHA-256 摘要;同一会话的简历提取属于重操作,会获取会话级锁,避免并发请求同时跑完整的提取链路。
这里的摘要不是文件 URL:同一份文件重复上传时,外部存储可能给出不同 URL,用内容摘要作为 AI Single-flight key 更接近“同一份输入”。但文件上传发生在 AI 调用之前,所以不要把 AI 结果复用误解成“整个上传流程完全没有重复动作”。
2. 上传文件并调用工作流
拿到锁后,服务先通过XingChenAIClient.uploadFile上传简历,再调用统一 AI Invoker,使用简历文件 URL 和提取场景请求模型。AI 调用受 Guard 和 Single-flight 保护;失败时会记录错误响应,最后在finally释放会话锁。
题目提取 Prompt 期望返回题目、建议、面试方向和简历评分等结构化字段。但工作流实际输出和 Java DTO 之间不是自动绑定的:字段名、嵌套层级和数组类型都要由解析器显式处理。
3. 先留原始响应,再尝试结构化解析
模型返回后,代码会先尝试将原始响应写入 Mongo 的面试题记录,再提取实际内容并解析结构化字段。这样做的思路是:结构化解析失败时,至少还有机会检查原始模型响应,定位是工作流返回错误、响应形状变化,还是字段本身缺失。
解析成功后,服务会:
- 校验并缓存题目列表。
- 初始化 Redis flow,当前题从第 1 题开始。
- 缓存建议、简历评分、面试方向和可复用的简历上下文。
- 将结构化材料回写到面试题文档,供缓存恢复和报告使用。
- 重置会话分数,避免旧运行态分数污染新一轮面试。
只有当结构化处理成功,提取响应才会设置成功标记。Facade 看到成功后才把会话改为READY并刷新运行态快照;失败则把会话退回DRAFT,让用户保留重新提取的入口。
这条链路最容易踩的坑
工作流字段是接口契约
当前提取代码读取questions、sugest、suggestions、resumeScore和type等字段;建议字段里保留了历史拼写sugest,面试方向还兼容多个名称。这里不能只改 Prompt 或工作流字段,还要核对 Java 解析、缓存、DTO 和持久化字段。
我倾向于把字段兼容集中在边界解析器,而不是让每个业务方法各自猜一遍别名。兼容字段也应该有淘汰条件,否则“兼容历史数据”会慢慢变成永久的隐式协议。
Redis 有题,不代表后续恢复材料完整
题目、flow、简历上下文和评分材料不一定在同一时刻写完。当前代码会同时维护 Redis 热态和 Mongo 中的原始/结构化记录,目的是让缓存丢失后还有恢复来源。复盘时需要分别确认:接口是否返回成功、Redis 是否可继续答题、Mongo 是否留有足够材料,而不能只验证页面上出现了题目。
还有一个值得做故障验证的边界:原始响应持久化和结构化字段回写的方法会捕获异常并记录日志;如果 Redis 侧已经准备成功而 Mongo 回写失败,当前提取仍可能返回成功。短期答题也许能继续,但后续缓存恢复的材料可能不完整。这里需要明确业务选择:是允许缓存短期可用,还是必须等持久化成功后才进入READY。
我的复盘:Agent 的结果只是中间产物
这条链路让我更愿意用“业务就绪”定义成功,而不是把 HTTP 200 或模型返回非空当成成功。简历提取不仅要问模型,还要把题目转成系统能推进的流程状态,并留下可恢复的数据。
代价也很直接:一次提题横跨外部文件服务、模型、Redis 和 Mongo,严格原子提交并不存在。设计重点就变成了哪些步骤必须成功、哪些失败可以降级、以及降级后能否清楚地恢复或重试。
源码入口
- 会话状态和提取委托:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/flow/session/InterviewSessionFacade.java - 工作流编排:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/flow/session/InterviewAgentOrchestrationService.java - 上传、AI 调用和字段组装:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/flow/extraction/InterviewQuestionExtractionService.java - 原始响应和结构化字段持久化:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/service/impl/InterviewQuestionServiceImpl.java - AI 响应提取和兼容解析:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/shared/InterviewResponseParser.java