Resume Matcher 简历向导(Resume Wizard)重构设计解析:从手动表单到 AI 逐问式引导的完整实现
2026/9/12 9:04:20 网站建设 项目流程

Resume Matcher 简历向导(Resume Wizard)重构设计解析:从手动表单到 AI 逐问式引导的完整实现

【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters & more, locally with 100+ LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher

本文基于仓库设计文档 2026-06-04-resume-wizard-redesign-design.md 展开,结合后端服务、提示词模板、路由、前端组件与测试代码,逐层剖析这套"AI 每次只问一个问题、边答边生成简历草稿"的交互架构,涵盖状态机设计、单次complete_json轮次契约、节域合并守卫、服务端护栏、确定性回退与测试策略。

Resume Matcher 的/resume-wizard页面曾是一个手动多区域表单:顶部问题、中部六个分区按钮、底部自由文本答案区,右侧还有一个显示统计数字与常驻橙色警告的"Live Draft"面板。这套旧交互的问题在于:用户先选分区再打字,AI 只在所选分区内被动响应,且部分状态下的回答会被静默丢弃。本文要解析的重构方案,把该页面彻底改造成AI 主导、一次一问的流程:每次只呈现一个聚焦的问题,AI 将用户的随意回答重写为精炼、真实的简历内容,同时自主决定下一个该问什么,用户则在一旁的安静实时预览中看着简历逐渐成形,最终一键落定为主简历(Master Resume)并跳转到/builder继续编辑。读完本文,你将掌握该向导的完整状态机、轮次协议、防模型幻觉的护栏体系,以及前后端对应的实现落点。

背景:旧版向导为什么被推翻

设计文档开篇即点明了已发布版本(shipped version)的三个核心问题:

  1. 布局违背专注原则:问题在最上方、六个分区按钮组成的"墙"在中部、答案框在底部——信息密度过高,与"一次只做一件事"的向导直觉相悖。
  2. 右侧 Live Draft 面板噪音过大:展示Experience 1Skills 0之类的统计数字,以及常亮的橙色警告框,既吵闹又缺乏洞察。
  3. 流程本质是手动的:用户自行选分区、填写结构化答案,AI 只在已选分区内做出反应;更糟的是,在 intro 之后的分区选择状态下回答,会以review哨兵值触发一轮 AI 调用,结果被静默丢弃

此外,该设计文档明确说明它取代了旧的分区选择器(section-picker)方案,而旧方案中的一部分基础设施被复用:后端路由挂载、finalize→master-resume、schema/service/prompt 文件布局、localStorage 草稿、dashboard 入口与选择对话框均保持不变;被替换的是轮次协议、提示词与整个向导页面 UX

四项锁定决策

重构的核心决策被文档固化为四条不可动摇的原则:

决策内容
1. 逐问卡片格式每屏一个大问题、单一输入框、纤细的分段进度条,按Enter前进;彻底移除分区按钮网格
2. 右侧实时简历预览一面安静的真实简历页随回答逐步填充;无统计数字、无常驻警告
3. AI 行为 = 写作 + 自适应AI 把随意回答重写为精炼且真实的简历内容,并从"仍然薄弱"的部分中挑选下一个问题,在内容足够时给出完成信号
4. 轮次架构 = 每次回答一次自适应complete_json调用取代旧的两段式"先写再规划"拆分,换取回答→下一个问题的低延迟

第 4 点直接决定了后端轮次协议的形态,后面会看到它如何在 服务层 中落地为run_ai_turn单函数。

UX 与布局:Swiss 风格双栏结构

新界面沿用项目的 Swiss 设计语言(bg-background背景、2px 黑色描边、硬偏移阴影、衬线标题、等宽标签),整体是双栏布局:

  • 左侧 — 问题卡片(网格列定义为lg:grid-cols-[minmax(0,1fr)_360px],卡片占据1fr):
    • 顶部的分段进度条(分段数完全由服务端计算,见"护栏"一节);
    • 等宽 kicker,标出当前话题,例如ABOUT YOUR ROLE AT ACME(在实际 i18n 中会本地化为对应语言,如中文的"工作经历");
    • 大号衬线问题文本(取自 AI 返回的next_question.text);
    • 单一输入框Textarea:支持多行回答,保留仓库既有的 Enter 键stopPropagation模式,⌘/Ctrl+Enter或 Continue 按钮提交;
    • 底部动作区按步骤变化intro步骤显示Continue+Back to Dashboardquestion步骤显示Continue(主按钮)+Skip+← Back(当history为空时隐藏)+Review & finish+Back to Dashboardreview步骤显示Create master resume(主按钮,success 变体)+Keep adding+Back to Dashboard。每个区域最多一个主按钮(Swiss 规则)。
  • 右侧 — 实时预览(固定360px):渲染resume_data的真实缩略简历布局(姓名/头衔、经历、教育、项目、技能 chip)。空状态显示文案"Your resume appears here as you answer."(i18n 键resumeWizard.preview.empty,中文为"当你回答问题时,简历将在此处显示。")。新推断出的技能会获得短暂的绿色描边强调。没有计数、没有橙色警告框。
  • 移动端(< lg:预览折叠为Peek ▸切换开关,保证问题保持全焦点;展开时预览以滑动方式覆盖。

在 question-card.tsx 中可以看到这套布局的逐行实现:进度条用role="progressbar"+aria-valuenow实现无障碍语义,每个分段是一个flex-1的黑色/白色小条;Enter 键处理在handleKeyDown中先stopPropagationpreventDefault并触发提交,Shift+Enter则换行;底部动作区依据isReview/isQuestion分支渲染不同按钮组。isComplete时在问题步骤下还会显示一个绿色方块 +resumeWizard.readyHint(中文:"你已经有足够的内容来生成一份出色的简历——随时可以查看并完成。")作为"内容已足够"的弱信号。

实时预览实现在 live-preview.tsx:它按"有无任何内容"判断空状态,有内容时渲染姓名/头衔、经历(标题·公司、年份、要点列表)、项目、教育、技能 chip;技能去重用与后端casefold语义对齐的toLowerCase(注释明确说明不用toLocaleLowerCase以避免土耳其语等 locale 的大小写差异);inferredSkills中本轮新推断的技能以inferredKeys集合比对,命中则套用绿色边框并追加

/builder的入口瓷贴与MasterResumeChoiceDialog(主简历选择对话框)保持不变,重构不触碰这两处。

流程与状态机:intro → question → review → complete

整个向导是四步状态机,由ResumeWizardStep = Literal["intro", "question", "review", "complete"]定义(见 schemas/resume_wizard.py):

  • intro— 一张卡片同时询问姓名与目标岗位。提交后以section: "intro"运行自适应轮次:确定性提取姓名(以现有extract_intro_name兜底),让 AI 捕捉目标岗位/摘要方向,并产出第一个真正的next_question
  • question— 自适应主循环。每个回答 → 一次 AI 轮次 → 更新resume_data+ 下一个current_question+inferred_skills+is_complete。持续迭代,直到用户主动选择 review,或服务端在问题数达到上限时强制转入 review。
  • review确定性步骤(不调用 AI)。展示组装好的预览、温和的可选提示(即被迁移过来的旧警告,以更轻的方式呈现),以及Create master resume/Keep adding两个动作。
  • complete— finalize 成功;前端路由跳转/builder?id=…

值得注意的设计细节是:is_complete只是 AI 的建议,向导永远不会自动 finalize;用户始终保有"Keep adding"的控制权。这一点在服务端run_ai_turn的注释中被明确强调:is_complete仅用于浮现 "Review & finish",步骤仍停留在question

往返状态(ResumeWizardState

前端把状态保存在 React +localStorage(键resume_wizard_draft)中,并在每轮把它 POST 回后端(后端保持无状态,与现状一致)。新的状态形状:

字段类型说明
step'intro'\|'question'\|'review'\|'complete'状态机当前步
resume_dataResumeData复用的共享简历数据形状
current_question{ text: str, section: str }section∈ intro、workExperience、internships、education、personalProjects、skills、summary、contact、review
historylist[{ question, answer, section, resume_data_before }]resume_data_before快照使Back具备确定性
asked_countint驱动问题上限与进度条
inferred_skillslist[str]上一轮检测到的技能(用于绿色强调)
is_completeboolAI建议完成;绝不自动 finalize
progress{ current: int, total: int }服务端计算,不信任模型
warningslist[str]仅在review步骤填充

从旧状态中移除的字段options(分区选择器)、completed_sections、作为选择器的current_sectionpending_questions

Pydantic 模型在 schemas/resume_wizard.py 中完整定义了这套结构,并附带校验规则:ResumeWizardAnswer.text长度 1~6000、拒绝纯空白;ResumeWizardTurnRequest要求answer动作必须携带answerResumeWizardFinalizeRequest要求personalInfo.name非空(缺失即 422)。前端侧的 TypeScript 等价物在 lib/api/resume-wizard.ts 中,createInitialResumeWizardState()给出初始状态:step: 'intro'、空resume_data、intro 问题、progress: { current: 0, total: 8 }

/turn的五个动作

  • start— 返回build_initial_wizard_state()(intro 问题)。前端也可以本地构建初始状态;该端点仅为对等性保留。
  • answer— 核心自适应 AI 轮次(覆盖 intro 与每个分区)。
  • skip— 一次带 skip 标志的 AI 轮次:AI 返回下一个问题但不改写resume_data
  • back确定性操作,不调用 AI:弹出history,恢复上一个current_question+resume_data_before+inferred_skills
  • review确定性操作,不调用 AIstep='review',计算warnings

路由层的分派逻辑在 routers/resume_wizard.py 中一目了然:start/back/review直接返回确定性结果;asked_count >= RESUME_WIZARD_MAX_QUESTIONS时对 answer/skip 做成本守卫,直接转入 review 而不触发 LLM 调用;skip调用run_ai_turn(state, "", skip=True);其余走run_ai_turn(state, answer_text, skip=False)。异常处理统一收敛为 422(校验/值错误)或 500(未知错误),日志记录细节、返回给用户的只是通用消息。

后端:complete_json轮次契约

每次answer/skip只有一次LLM 调用,使用schema_type="resume",并以用户的内容语言生成(get_language_name(get_content_language())),因此问题与简历内容都会本地化。模型返回的 JSON 结构为:

{ "resume_data": { …完整 ResumeData 信封… }, "next_question": { "text": "…", "section": "workExperience" }, "inferred_skills": ["SQL"], "is_complete": false }

处理管线(见 services/resume_wizard.py 的run_ai_turn):

  1. 序列化当前草稿json.dumps(state.resume_data.model_dump(mode="json"), ensure_ascii=False),连同output_languagecurrent_sectionanswer_text一起格式化进提示词模板 prompts/resume_wizard.py。
  2. 输入清洗:回答先过_sanitize_user_input(来自 improver 服务)剥离提示注入模式,再过_scrub_secrets脱敏sk-…/AIza…/Bearer …这类凭据型 token——这是对 LLM 的纵深防御,测试test_ai_turn_sanitizes_user_answer_before_prompting专门验证注入语句会被[REDACTED]替换。
  3. 模型输出await complete_json(prompt, max_tokens=8192, schema_type="resume");非 dict 结果直接抛ValueError
  4. 校验与归一化resume_datanormalize_resume_data+ResumeData.model_validate(与全仓库其他路径一致)。
  5. 节域合并:见下文。
  6. 状态推进asked_count + 1is_complete = bool(result["is_complete"]) or asked_count >= 15;追加history条目;用_next_question决定下一个问题;最后compute_progress计算进度。

提示词模板 prompts/resume_wizard.py 中值得注意的约束:所有人类可读文本(下一个问题、标题、要点、摘要)都用{output_language}书写,但结构性值保持原样——next_question.section必须是精确的英文枚举值,日期保持用户给出的格式,不翻译分区键与日期。内容形状方面,工作/实习条目在事实充足时目标写 3 个要点,项目条目目标 2 个要点,技能只能来自用户给出的事实或已有草稿。

节域合并守卫:绝不覆盖无关分区

设计文档明确要求把旧的节域合并守卫保留并扩展:一轮只能写入current_question.section对应字段,绝不能动其他部分。映射关系为:

  • intro/contactpersonalInfo(name/title/contact)
  • summarysummary
  • workExperience/internshipsworkExperience
  • educationeducation
  • personalProjectspersonalProjects
  • skillsadditional.*
  • 未知分区对resume_datano-op(防御性)

该逻辑实现在_merge_section中:先existing.model_copy(deep=True),然后只针对当前分区做字段级写入。技能通过现有的大小写不敏感merge_unique_skills合并(保留首次出现的写法与顺序)。

列表类分区(经历、教育、项目)还额外依赖_merge_entries内容签名合并:模型若只回显用户刚描述的这条记录而漏掉其他条目,已有条目不会被抹除——按签名(例如 title+company+years 的 casefold 元组)做"缺失保留、回显替换、新增追加"。注释解释得很清楚:签名基于内容而非id,因为向导提示词并不要求模型输出id,条目默认id=0。随后_assign_entry_ids会按稳定顺序把三条列表(workExperience、education、personalProjects)的条目重编号为唯一的 1 起id——否则实时预览的 Reactkey={item.id}/builderMath.max(...ids)+1新增逻辑都会失效。

单元测试test_ai_turn_does_not_let_other_sections_be_clobbered精确验证了这一点:模型在skills轮次故意返回空workExperience,结果已有经历被保留、技能被合并为["SQL", "Python"]test_ai_turn_full_echo_keeps_all_experience_in_ordertest_ai_turn_partial_echo_does_not_drop_prior_experience则分别验证"全量回显保序"与"部分回显不丢旧条目"两种情形。

真实性规则:提示词中的硬约束

复用项目既有策略:激进地把用户自己的事实转化为有力的要点,但绝不捏造雇主、职位、日期、学位、指标、工具或技能。若某个事实缺失,模型必须把它放进next_question去追问,而不是自行编造。这与现有CRITICAL_TRUTHFULNESS_RULES及维护者策略保持一致。提示词中该规则被列为 non-negotiable 的五条:

  1. 绝不虚构雇主、职位、日期、学位、认证、奖项、指标、工具或技能;
  2. 只把用户自己给出的事实改写为有力内容,不添加其未提供的事实;
  3. 需要的事实缺失或模糊时,不要猜测——放入next_question追问;
  4. 保留已有草稿数据,除非用户明确修改;
  5. 构建通用主简历,而非针对某个岗位的定制简历(后者的定制发生在/tailor)。

服务端护栏:不盲信模型

  • 问题上限RESUME_WIZARD_MAX_QUESTIONS = 15:一旦asked_count >= cap,服务端把is_complete覆写为true;下一次answer/skip直接路由到 review。
  • 进度条服务端计算total = min(cap, max(8, asked_count + (0 if is_complete else 2)))current = asked_count。进度条反映的是这个确定性数字,而非模型输出。初始基线_PROGRESS_BASELINE = 8
  • is_complete只建议:仅浮现 "Review & finish",用户随时可以 "Keep adding"。Finalize 永远由用户发起。
  • 回退机制next_question缺失/空白时,用确定性的分区提示词_section_prompt(对应_SECTION_PROMPTS字典,内含 intro/contact/summary/workExperience/internships/education/personalProjects/skills/review 九个分区的默认问题文案)回退;resume_data非法时保留先前草稿、返回同一问题并提示重试(轮次抛错 → 按既有 handler 返回 422/500)。此外valid_section会把模型给出的未知分区钳制为review_next_gap_section按"workExperience → education → personalProjects → skills → review"的空白优先序兜底选下一个问题。

这些护栏都有测试背书:test_compute_progress_grows_with_questions_and_caps验证进度增长与封顶;test_ai_turn_question_cap_forces_completion验证模型返回is_complete=false时上限仍会强制完成;集成测试test_turn_answer_past_cap_routes_to_review_without_llm断言达到上限后complete_json根本没有被调用(成本守卫);test_ai_turn_missing_next_question_falls_back_to_gap验证缺失next_question时自动落到下一个空白分区。

Finalize:保持不变

POST /resume-wizard/finalize保持既有行为:校验姓名存在(ResumeWizardFinalizeRequest)、归一化、db.create_resume_atomic_master(...)、若已存在 ready 状态的主简历则返回 409、设置标题、返回{resume_id, processing_status:"ready", is_master}build_review_warnings保留,既用于review步骤也用于温和提示。

从 routers/resume_wizard.py 的实现看,finalize 还有两个工程细节:其一,主简历标题f"{name} Master Resume"原子创建时一并设置(而非创建后再更新),避免"已提交但无标题的主简历"导致重试时永久 409;其二,若创建出的记录意外不是 master(竞态),会删除该记录并同样返回 409。build_review_warnings的五条温和提示(见 services/resume_wizard.py)覆盖:缺姓名(finalize 的硬性 422 门槛,review 阶段提前暴露)、无任何联系方式、无经历/实习/项目、教育为空、技能为空。

文件清单:改动落在哪里

文件角色
schemas/resume_wizard.py新的 state/turn/finalize 模型
prompts/resume_wizard.py单一的自适应写手/规划提示词
services/resume_wizard.py自适应轮次、护栏、确定性 back/review/skip、进度、姓名提取、技能合并、警告
routers/resume_wizard.py/turn(answer/skip/back/review/start)+/finalize(不变),挂载不变
resume-wizard-page.tsx重建的编排器:持有状态、渲染 QuestionCard + LivePreview、处理五类动作、localStorage 持久化与安全的草稿归一化器(保留并适配新形状)
question-card.tsx进度条 + kicker + 问题 + textarea + 底部动作;轮次进行中的 "thinking" 状态
live-preview.tsx取代draft-preview.tsx:把resume_data渲染为真实简历布局(无计数/警告)、空状态、新推断技能的绿色强调
section-picker.tsx删除
lib/api/resume-wizard.ts更新为新形状的类型;postResumeWizardTurnfinalizeResumeWizardcreateInitialResumeWizardState以新形状保留
app/(default)/resume-wizard/page.tsx/resume-wizard/page.tsx)不变(薄封装)
messages/*.json(全部 5 种语言)刷新resumeWizard.*:kicker、title、intro 问题回退、actions {continue, skip, back, review, keepAdding, create, backToDashboard}preview {label, empty, unnamed}review {readyTitle, note*}errors {turnFailed, finalizeFailed};必须满足破坏构建的 locale 对齐规则

前端编排器:状态、草稿恢复与动作分派

resume-wizard-page.tsx 是前端的神经中枢。几个值得展开的实现要点:

  • 草稿持久化useEffect在每次状态变化时写localStorage['resume_wizard_draft'],但把写入包在 try/catch 里——持久化是"尽力而为"的锦上添花,配额/序列化错误绝不能崩溃向导;step === 'complete'时停止写入。
  • 防崩溃草稿归一化readSavedDraft会把可能损坏的持久化草稿逐字段洗成安全形状——normalizeDraftResumeData把三个列表字段强制转数组并给每条补 1 起idasEntriesWithIds),normalizeDraftPersonalInfo把所有 personalInfo 字段强制转字符串,避免数字型姓名让后续.trim()抛错把用户困在重载循环里。测试recovers from a corrupt saved draft without crashing the renderrecovers from a draft with non-string personalInfo fields专门覆盖这两类事故。
  • Keep Adding 的本地逻辑handleKeepAdding不调用 API,直接把步骤切回question并把current_question.section指向firstGapSection计算出的下一个空白分区——因为review分区在后端合并层是 no-op,若沿用 review 分区回答会被静默丢弃(这正是旧版 bug 的翻版,前端显式规避)。
  • Finalize 收尾:成功后将master_resume_id写入localStorage、清除草稿、incrementResumes()setHasMasterResume(true),最后router.push('/builder?id=…')

错误处理策略

  • 轮次失败→ 卡片内联错误提示,状态完整保留,可重试同一回答(与现有行为一致);后端记录细节、返回通用消息(resumeWizard.errors.turnFailed,中文"向导无法保存该回答。请重试。")。
  • Finalize 409(主简历已存在)→ 解释原因并提供 "Back to Dashboard"。
  • 网络/超时→ 沿用apiFetch的 240s 默认超时,配友好的 "timed out" 提示。

i18n:静态外壳本地化 + 动态内容随内容语言

静态外壳通过五个 locale 文件中的resumeWizard.*键翻译(中文版见 messages/zh.json 的resumeWizard段,包含actionspreviewerrorssections九分区标签、readyHintkeepAddingPrompt等);AI 生成的问题文本与简历内容则使用内容语言(轮次提示词接收output_language)——这比旧版英文-only 的动态文本是一个净改进。分区 kicker 由current_question.section经一张小型 i18n 标签表映射(t('resumeWizard.sections.' + section)),因此同样被本地化。设计文档特别提醒:动态问题/分区文本来自后端,静态外壳走键值翻译,两者必须满足会破坏构建的 locale 对齐规则(由i18n-locale-parity.test.ts守护,仓库根目录还有 scripts/check_locale_parity.py 可复跑校验)。

测试策略:反形式主义,目标破坏时必须失败

测试被设计为"anti-theater"——当实现回归时测试必须红:

  • 后端单元(tests/unit/test_resume_wizard_service.py):intro 姓名提取("I'm James"James"My name is Priya Sharma"Priya Sharma);自适应轮次只合并目标分区(mockcomplete_json);技能去重保序;问题上限强制is_complete进度服务端计算;确定性back恢复先前快照;review不调用 AI 直接构建警告;next_question缺失回退到分区提示词;skip 不改resume_data;答案清洗注入尝试;_assign_entry_ids重编号。
  • 后端集成(tests/integration/test_resume_wizard_api.py):/turnanswer带 mock LLM 返回下一个问题与更新数据;/turnback/review无需 LLM;/finalize创建 ready 主简历;/finalize已有主简历时 409;达到上限后 answer 直接转 review 且LLM 未被调用
  • 前端(tests/resume-wizard-api.test.ts、tests/resume-wizard-page.test.tsx、tests/dashboard-master-choice.test.tsx):初始状态形状;发起轮次;完整页面流(回答 → 下一个问题 + 预览更新 → Skip → Back → Review → Create → 路由到/builder、设置状态缓存、清除草稿);live-preview 渲染resume_data;选择对话框不变。另有i18n-locale-parity.test.ts守护五语对齐。resume-wizard-page.test.tsx的用例清单包括:渲染 intro 问题与答案框、提交 intro 答案显示下一个问题、Review 进入审查、finalize 后路由并清草稿、轮次失败保留问题、损坏草稿恢复、skip/back 分派、keep-adding 纯本地返回 question 步骤。

非目标与边界

  • 不改动/builder、上传解析流程、入口对话框、PDF/模板。
  • 不支持多主简历;单主简历不变量保持不变。
  • 实时预览是为向导专门构建的;复用真实简历渲染模板(components/resume/*)做小尺寸渲染被明确推迟——完整模板体验在 finalize 后的/builder中进行。

风险与缓解

风险缓解
单一提示词同时承担写作 + 规划 + 完整性判断严格 JSONschema_type="resume"、校验、next_question/resume_data的确定性回退
自适应循环永不结束/死循环问题上限(15)强制进入 review;手动 "Review & finish" 始终可用
每问题延迟单次调用;subtle "thinking" 状态;无第二次往返
模型覆盖无关分区保留节域合并守卫
localStorageresume_data_before快照膨胀JSON 很小;最多约 15 条;远低于配额
locale 漂移破坏next build对齐测试 + 五个文件镜像每一个resumeWizard.*

小结

这套重构的本质,是把"工具"变成"对话":一次回答只触发一次complete_json调用,模型同时完成内容写作、下一问规划与完整性判断;服务端用节域合并守卫、15 问上限、服务端进度计算与确定性回退兜住模型的不确定性;前端用history快照实现零 AI 成本的 Back、用 localStorage 草稿实现刷新恢复,并在 finalize 后无缝衔接/builder。如果你想深入某个环节,建议从 services/resume_wizard.py 的run_ai_turn读起,再对照 prompts/resume_wizard.py 的提示词约束和 tests/unit/test_resume_wizard_service.py 的边界用例,就能完整还原这套"AI 引导逐问式简历构建"从设计到落地的全过程。

【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters & more, locally with 100+ LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询