Resume-Matcher 简历增强(Enrichment)功能全解析:AI 定向提问与增量改写的工作流与源码实现
本篇技术指南围绕 Resume-Matcher 的 AI 简历增强功能展开,讲解其"分析弱点 → 定向提问 → 增量补充 bullet"的三段式工作流、三个核心设计约束(最多 6 个问题、只增不改、优先排序)、后端五个 API 端点与前端的向导式状态机实现,并结合源码与测试给出可直接落地的调用方式。读完本文,你将掌握该功能从 Prompt 设计、LLM 调用、数据落库到 React 向导交互的完整实现链路,并能在自己的简历数据处理管线中复用同样的设计模式。
功能概览:为什么需要"增强"而不是"重写"
主简历(Master Resume)是用户所有求职材料的源头,但它经常存在描述笼统、缺少量化指标、技术栈不明等问题。Resume-Matcher 的 enrichment 功能解决的不是"重写一份新简历",而是在保留原有内容的基础上,帮助用户把薄弱条目补充得更具体、更有说服力。它的核心思路来自 enrichment.md 中的三步闭环:
- 分析简历,找出 Experience(工作经历)与 Projects(项目)中描述薄弱或过于笼统的条目;
- 针对这些条目向用户提出定向澄清问题;
- 根据用户的回答生成额外的 bullet point,追加到原有描述之后。
整个过程由 LLM 驱动,但关键决策——问什么、改哪里、怎么合并——通过精心设计的 Prompt 与代码逻辑进行约束,避免 AI 自由发挥破坏简历事实。
工作流:从点击按钮到写入数据库的五个阶段
根据 enrichment.md 与前端 use-enrichment-wizard.ts 中的状态机定义,完整流程分为五个阶段:
- 用户在 简历详情页/resumes/[id]/page.tsx#L369) 点击 "Enhance Resume" 按钮,打开 EnrichmentModal;
- AI 分析简历(
analyzing),识别薄弱条目并生成澄清问题; - 用户逐题作答(
questions),最多 6 个问题; - AI 根据回答生成新的 bullet(
generating),进入预览页(preview)供用户确认; - 确认后写入主简历(
applying→complete),新 bullet 被追加到原有内容之后。
前端状态机完整覆盖了 idle / analyzing / questions / generating / preview / applying / complete / error / no-improvements 九种状态,其中 no-improvements 是重要的人性化分支:当 items_to_enrich 或 questions 为空时,向导直接提示"简历已足够好"并给出 analysis_summary,而不是强行让用户走完流程(见 use-enrichment-wizard.ts)。
三大核心设计决策
enrichment.md 明确列出了本功能的三个设计约束,这些约束在源码中都有硬性落实:
| 设计决策 | 说明 | 源码落实 |
|---|---|---|
| 最多 6 个问题 | 避免让用户感到疲惫,AI 在所有条目中合计最多生成 6 个问题(而非每个条目 6 个) | Prompt 中写入 "MAXIMUM 6 QUESTIONS TOTAL - this is a hard limit",见 enrichment.py;同时约束问题 ID 最多到 q_5 |
| 增量式增强 | 原有 bullet 一律保留,新生成的 bullet 追加在其后 | routers/enrichment.py 中 existing_desc + additional_bullets 直接拼接 |
| 问题优先排序 | AI 优先提出收益最高的问题 | Prompt 指令 "Prioritize the most impactful questions that will yield the best improvements",见 enrichment.py |
这里"只增不改"是刻意为之:它既保护了用户简历中已经写好的事实,也让 AI 生成的补充内容永远处于"可审查"状态——用户在预览步骤可以逐个确认新增内容,而不是被迫接受一次大范围改写。
弱点识别规则:AI 如何判断"这段写得不行"
分析阶段的 Prompt(ANALYZE_RESUME_PROMPT)内置了 6 条"描述薄弱指标",这是整个功能的判断基准:
- 泛化措辞:出现 "responsible for"、"worked on"、"helped with"、"assisted in"、"involved in" 等模板化短语;
- 缺少量化指标:没有数字、百分比、金额或可测量的结果;
- 范围不明:团队规模、项目体量、用户数、职责边界含糊;
- 没有技术栈:未提及具体技术、工具或方法论;
- 被动语态且无归属:看不出候选人个人的实际贡献;
- 过于简短:单条 bullet 无法说明工作内容。
Prompt 同时给出了正面范例供模型参照,例如:
- "Led migration of 15 microservices to Kubernetes, reducing deployment time by 60%"
- "Built real-time analytics dashboard using React and D3.js, serving 10K daily users"
- "Architected payment processing system handling $2M monthly transactions"
可以看出,这三条范例分别示范了量化影响、技术栈 + 用户规模、财务量级 + 架构职责,正好与提问阶段想要抽取的信息维度(metrics、technologies、scope、ownership)一一对应。Prompt 还要求输出 JSON 中携带 weakness_reason,让用户在看问题时能理解"为什么问我这些"。
API 端点:三个核心接口与两个衍生接口
enrichment.md 列出了三个核心端点。需要补充的是:实际路由在 main.py 中以 /api/v1 前缀注册(router 自身前缀为 /enrichment),因此完整路径为 /api/v1/enrichment/...。此外,同一模块还扩展实现了 AI Regenerate 功能的两个端点,本文一并说明。
POST /api/v1/enrichment/analyze/{resume_id}
分析简历并生成问题。实现位于 routers/enrichment.py:
- 从数据库取出
processed_data,若为空返回 400(提示重新上传简历); - 将整个
processed_data序列化为 JSON 填入 Prompt,并按 get_language_name 注入output_language,保证生成的问题、占位符、摘要全部使用用户当前内容语言(项目支持中、英、日、西、法等多语言); - 调用
complete_json(prompt, max_tokens=8192, schema_type="enrichment"),并设置 180 秒硬超时; - 超时返回 504,JSON 解析失败返回 422,其余异常返回 500。
响应结构由 schemas/enrichment.py 定义:
{
"items_to_enrich": [
{
"item_id": "exp_0",
"item_type": "experience",
"title": "Software Engineer",
"subtitle": "Company Name",
"current_description": ["bullet 1", "bullet 2"],
"weakness_reason": "Missing quantifiable impact and specific technologies used"
}
],
"questions": [
{
"question_id": "q_0",
"item_id": "exp_0",
"question": "What specific metrics improved as a result of your work?",
"placeholder": "e.g., Reduced API response time by 40%, saved $50K annually"
}
],
"analysis_summary": "Brief summary of overall resume strength and areas for improvement"
}
注意 item_id 的命名规范:工作经历用 exp_0、exp_1(按数组下标),项目用 proj_0、proj_1,问题 ID 为 q_0 ~ q_5。这一约定是整个模块后续定位条目、按索引写回数据库的关键。
POST /api/v1/enrichment/enhance
根据用户回答生成增强描述。这是模块中实现最讲究性能的端点(routers/enrichment.py),包含双路径逻辑:
- 快速路径(Fast path):如果所有
AnswerInput都携带了item_id,则通过 _extract_item_from_resume 直接从processed_data按exp_0 / proj_0模式解析出条目详情,完全跳过第二次 LLM 分析调用——这是避免"问完问题还要再花 180 秒重新分析"的关键优化; - 兼容路径(Legacy path):若答案缺少
item_id(如旧版本前端或直接调 API 的客户端),则重新调用ANALYZE_RESUME_PROMPT,把question_id映射回item_id,再按条目聚合答案。
无论哪条路径,最终都会对每个条目调用 ENHANCE_DESCRIPTION_PROMPT(enrichment.py),要求模型生成 2-4 条新的 bullet。该 Prompt 的核心约束是:
- 只 ADD,不 REPLACE,不重复已有 bullet;
- 只使用用户回答中提供的信息,禁止虚构指标;
- 动作导向(Led、Built、Architected、Optimized 等强动词开头);
- 量化优先,包含技术栈,明确个人贡献;
- 过去经历用过去时,当前经历用现在时。
响应的 enhanced_description 字段就是将要追加的新 bullet 列表(代码还保留了旧 key additional_bullets 的向后兼容读取)。
POST /api/v1/enrichment/apply/{resume_id}
把增强结果写入主简历(routers/enrichment.py):
- 深拷贝
processed_data,避免直接修改数据库中的原始对象; - 按
item_type分发到workExperience或personalProjects数组,用item_id.split("_")[1]解析下标; - 将新增 bullet 与原有 description 列表拼接(
existing_desc + additional_bullets),并处理了 description 可能是字符串的边缘情况; - 同步更新
content与processed_data两个字段后写回数据库; - 返回
{"message": "Enhancements applied successfully", "updated_items": N}。
衍生接口:AI Regenerate(按反馈重写)
同一模块(同一 router、同样的 enrichment 前缀)还实现了两个衍生端点:
- POST /api/v1/enrichment/regenerate:接收用户反馈指令(
instruction上限 2000 字符,由 RegenerateRequest 校验),对选中的 experience / project / skills 条目整体重写。与 enhance 的"追加"不同,这里是"替换",且同样禁止编造事实("Do NOT add any new facts, metrics...")。所有条目通过asyncio.gather并行处理,单个失败不会拖垮整体,失败项以errors列表返回; - POST /api/v1/enrichment/apply-regenerated/{resume_id}:落库前执行严格的一致性校验——通过标题、副标题、原文内容三层匹配定位条目,若简历在重写期间被改动导致无法唯一匹配,则整体拒绝(409 Conflict),防止覆盖用户数据。这一逻辑可以从 apply_regenerated_items 中的
_find_unique_index_by_metadata与_lines_equal辅助函数推断,属于"安全落库"的防御性设计。
前端对应的 API 客户端封装在 lib/api/enrichment.ts,五个函数(analyzeResume、generateEnhancements、applyEnhancements、regenerateItems、applyRegeneratedItems)与后端端点一一对应,错误信息统一取 data.detail 展示。
LLM 调用层:JSON 模式、截断检测与重试
所有端点都经由 complete_json 调用 LLM,这是保证"结构化输出可解析"的公共底座:
- 强制 JSON-only 系统提示("You must respond with valid JSON only");
- 模型支持时启用
response_format={"type": "json_object"}(JSON mode),失败后自动降级为纯提示约束; - 截断感知重试:按
schema_type="enrichment"检查返回结果是否缺少items_to_enrich / questions / analysis_summary等关键键,若截断则附带提示语重试(最多 2 次重试),提示语见 llm.py; - 通过
_extract_json从响应中稳健抽取 JSON 对象(支持前后缀文本包裹)。
分析阶段使用 max_tokens=8192(为多语言输出留足空间),生成阶段使用默认 4096,技能重写阶段为 2048,超时由 calculate_timeout 依据模型与 provider 动态计算。这意味着该功能天然兼容项目支持的 100+ 种 LLM(本地模型、OpenAI 兼容聚合器等),只要模型能输出 JSON 即可接入。
前端实现:向导式交互的工程细节
前端通过 useReducer 实现严格的单向状态流(use-enrichment-wizard.ts),三个核心工程细节值得借鉴:
- 问题导航:
NEXT_QUESTION / PREV_QUESTION / GO_TO_QUESTION对currentQuestionIndex做边界钳制;question-step.tsx 提供进度条、所属条目徽标(工作经历用 Briefcase 图标、项目用 FolderKanban 图标)、以及 Ctrl/⌘ + Enter 快捷提交; - 空答案过滤:生成阶段将
answers中空白作答过滤掉(answer.trim() !== ''),避免把空字符串塞给 LLM; - 错误重试分级:
retry根据当前状态智能回退——已有预览则重试 apply,已有答案则回到问题页,否则从头分析,见 use-enrichment-wizard.ts。
弹窗组件 enrichment-modal.tsx 使用原生 <dialog> 实现,并在 analyzing / generating / applying 三个加载态禁止关闭(阻止 ESC 与背景点击),避免用户在 LLM 调用中途误操作丢失进度。
质量保障:测试覆盖
后端对该模块的约束在测试中有直接体现:
- test_regenerate_endpoints.py 验证
RegenerateRequest.instruction的 2000 字符上限(2001 字符触发ValidationError),并验证多条目并行处理路径; schema_type="enrichment"的截断检测逻辑在 test_llm.py 中覆盖。
从测试结构可以推断(见 conftest.py 对 enrichment 相关 mock 的引用),团队将"LLM 不可靠"视为默认前提,测试重点是校验模式、超时、错误码与落库安全性,而非 LLM 的输出内容。
关键文件索引
| 文件 | 作用 |
|---|---|
| prompts/enrichment.py | 4 组 Prompt:分析、增强、条目重写、技能重写 |
| routers/enrichment.py | 5 个端点:analyze / enhance / apply / regenerate / apply-regenerated |
| schemas/enrichment.py | 全部请求/响应 Pydantic 模型 |
| llm.py | complete_json:JSON 模式、截断检测、重试与超时 |
| use-enrichment-wizard.ts | 向导状态机(useReducer) |
| lib/api/enrichment.ts | 前端 API 客户端 |
| components/enrichment/ | 弹窗、提问页、预览页、加载步骤组件 |
| test_regenerate_endpoints.py | 端点与校验约束测试 |
设计模式总结:可复用的"人机协作改写"范式
纵观整个功能,它实际上示范了一个通用的人机协作内容改写范式:AI 负责发现薄弱点并提出好问题(分析阶段),人负责提供事实增量(提问阶段),AI 再负责把事实组织成高质量表述(生成阶段),最后由代码以"只追加、可预览、先校验再落库"的方式保证数据安全。三个核心约束——问题数量上限、增量不改写、事实不虚构——分别从体验、数据完整性和可信度三个维度保护了用户,这套设计同样适用于求职信、自我介绍等任何由用户资料驱动的 AI 生成场景。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python230
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java291
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java200
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript190
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python300