从零生成主简历:Resume-Matcher 的 AI Resume Wizard 全流程设计解析
2026/9/11 15:47:17 网站建设 项目流程

从零生成主简历:Resume-Matcher 的 AI Resume Wizard 全流程设计解析

【免费下载链接】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

导读

Resume-Matcher 是一款在本地运行、支持 100+ LLM 的开源简历构建工具。对于尚未持有 PDF/DOCX 简历的用户,项目提供了一条全新的入口:AI Resume Wizard(AI 简历向导)——通过"一次一个问题"的自适应 AI 对话,从零构建一份结构化的通用主简历(master resume),并最终沉淀为与"上传+解析"路径完全一致的下游数据形态。本文基于 Resume Wizard 设计文档,结合仓库中已落地的后端路由、服务、Schema、Prompt 模板与前端页面源码,完整讲解该功能的架构设计、数据契约、交互流程、容错机制与测试策略,读者读完可以掌握如何在项目内实现并验证一个"AI 引导 + 应用管控"的问答式简历生成管线。


一、设计目标:为"没有简历的人"补上主简历创建路径

Resume-Matcher 的核心工作流围绕"主简历(master resume)+ 职位描述(JD)"的匹配与定制展开。此前,dashboard 将"缺少主简历"作为初始化入口:用户通过POST /api/v1/resumes/upload上传 PDF/DOCX,系统创建一条普通简历记录,并在不存在健康主简历时将其标记为 master,同时把master_resume_id写入本地存储,从而放行用户进入后续功能。

Wizard 设计文档的核心目标正是为"手上没有现成 PDF/DOCX"的用户提供第二条路径:通过一次一步的 AI 问答,从零构建一份真实(truthful)的结构化主简历,其下游产物与"上传并解析出的主简历"完全等价:

字段上传解析路径Wizard 最终产物
is_mastertrue(当无健康主简历时)true(当无健康主简历时)
processing_status"ready""ready"
content_type"json""json"
processed_data兼容ResumeData兼容ResumeData(经校验)

也就是说,Wizard 输出的不是一份游离的"草稿文件",而是一条已持久化、可用于后续 tailor(职位定制)流程的标准主简历记录

二、产品流程:上传 or AI 向导,二选一

当不存在主简历时,dashboard 的设置磁贴(setup tile)会弹出一个Swiss 风格二选一对话框,对应前端组件 master-resume-choice-dialog.tsx:

  • 上传已有简历:继续复用ResumeUploadDialog,走原有上传解析快路径;
  • 用 AI Wizard 从零创建:跳转到/resume-wizard路由。
// apps/frontend/components/dashboard/master-resume-choice-dialog.tsx <DialogContent className="max-w-2xl bg-background border-2 border-black shadow-[4px_4px_0px_0px_#000000] ..."> <DialogHeader>...</DialogHeader> <div className="grid gap-4 bg-background p-6 md:grid-cols-2"> <section>{/* Upload 选项:Upload 图标 + ResumeUploadDialog */}</section> <section>{/* Wizard 选项:Bot 图标 + 路由到 /resume-wizard */}</section> </div> </DialogContent>

Wizard 页面位于 apps/frontend/app/(default)/resume-wizard/page.tsx/resume-wizard/page.tsx),是一个客户端组件('use client'),实际渲染逻辑在 resume-wizard-page.tsx 中。

Wizard 的引导式开场(Intro)

设计文档规划了一个简短的 AI 引导开场,源码中通过_INTRO_QUESTION常量(resume_wizard.py 服务)与前端INTRO_QUESTION(lib/api/resume-wizard.ts)保持一致:

"Hi — I'll help you build your master resume. What's your name, and what kind of role are you going for?"

  1. 询问用户身份与目标角色方向;
  2. 即使回答是口语化的(如 "Hi, I'm James."),也能提取出姓名;
  3. 用姓名个性化下一条问题("So James, where would you like to begin?");
  4. 展示章节选择:Work Experience、Internships、Education、Projects、Skills、Review。

姓名提取在服务端由extract_intro_name()实现(services/resume_wizard.py),它使用三条正则依次匹配:

_INTRO_NAME_PATTERNS = ( re.compile(r"\bI(?:'| a)m\s+([A-Z][A-Za-z]+(?:\s+[A-Z][A-Za-z]+)?)"), re.compile(r"\b[Mm]y name is\s+([A-Z][A-Za-z]+(?:\s+[A-Z][A-Za-z]+)?)"), re.compile(r"\b[Nn]ame(?:'s| is)?\s+([A-Z][A-Za-z]+(?:\s+[A-Z][A-Za-z]+)?)"), )

注意一个细节:关键字("my name"/"name")允许大小写,但捕获的姓名必须以大写字母开头,因此用[Mm]/[Nn]显式限定而非re.IGNORECASE——否则会误把 "domain name facebook is" 这类句子中的小写词捕获成姓名。该提取作为"intro 回答后姓名仍为空"时的确定性兜底,防止 LLM 未能正确解析姓名(见run_ai_turn中的 fallback 逻辑,services/resume_wizard.py)。

章节行为与基线输出

Wizard 的章节与简历 Schema 的映射关系在设计中明确给出,服务端_VALID_SECTIONS_SECTION_PROMPTS(services/resume_wizard.py)落地了这一映射:

用户可见章节ResumeData 字段章节提示词(节选)
Work ExperienceworkExperiencetitle、company、dates、what you did、measurable impact
InternshipsworkExperience(合并层映射)title、company、dates、what you worked on、what changed
Educationeducationschool、degree、dates、honors、standout coursework
ProjectspersonalProjectswhat you built、why it mattered、tech、results
Skillsadditional.technicalSkills(+ languages / certificationsTraining / awards)tools、technologies、skills
Contact / Summary / ReviewpersonalInfo/summary/ 审查联系方式、职业描述、缺口检查

基线输出(Prompt 模板中同样强制,见 prompts/resume_wizard.py):

  • 每段工作/实习经历 3 条 bullet;
  • 每个项目 2 条 bullet;
  • 技能从用户回答中持续推断,并在 finalize 前实时显示在技能章节。

用户可以在 finalize 之前跳过任意章节并随时返回;Review 步骤会识别缺失但有用的信息——源码中build_review_warnings()(services/resume_wizard.py)产生确定性提示:

def build_review_warnings(data: ResumeData) -> list[str]: if not info.name.strip(): warnings.append("Add your name — it's required to create your resume.") if not any(value.strip() for value in contact): warnings.append("Add at least one contact method (email, phone, or a link).") if not data.workExperience and not data.personalProjects: warnings.append("Add at least one experience, internship, or project.") if not data.education: warnings.append("Education is empty — skip only if that's intentional.") if not data.additional.technicalSkills: warnings.append("Skills are empty — add tools or technologies you've used.")

其中"姓名"是 finalize 的唯一硬性要求(请求缺少姓名会直接 422),因此在 review 阶段就提前提示,避免用户在最终提交时遭遇笼统报错。

三、AI Harness:结构化回合制状态机

设计文档明确了 AI Harness 的核心原则:后端暴露resume-wizardAPI 命名空间,接收"当前 wizard 状态 + 最新用户动作/回答",永远返回结构化响应,而不是自由文本。应用自身掌控 Schema 与允许的章节动作——AI 可以写 bullet、提取事实、推断技能、改写措辞、追问澄清问题,但每一轮返回的简历草稿在交给客户端前都必须通过ResumeData校验

回合状态模型

状态 Schema 定义在 schemas/resume_wizard.py,由以下核心模型组成:

ResumeWizardSection = Literal["intro", "contact", "summary", "workExperience", "internships", "education", "personalProjects", "skills", "review"] ResumeWizardStep = Literal["intro", "question", "review", "complete"] ResumeWizardAction = Literal["start", "answer", "skip", "back", "review"] class ResumeWizardState(BaseModel): step: ResumeWizardStep = "intro" resume_data: ResumeData = Field(default_factory=ResumeData) current_question: ResumeWizardQuestion = Field(default_factory=ResumeWizardQuestion) history: list[ResumeWizardHistoryEntry] = Field(default_factory=list) asked_count: int = 0 inferred_skills: list[str] = Field(default_factory=list) is_complete: bool = False progress: ResumeWizardProgress = Field(default_factory=ResumeWizardProgress) warnings: list[str] = Field(default_factory=list)

其中history记录每个已答问题的回答前草稿快照resume_data_before),这是back动作能够确定性还原上一问与草稿的基础。asked_count服务端自增,配合进度计算compute_progress()(services/resume_wizard.py)让进度条永远由服务端计算,绝不信任模型输出

回合动作路由

POST /api/v1/resume-wizard/turn的动作分发在 routers/resume_wizard.py:

action = request.action if action == "start": return ResumeWizardTurnResponse(state=build_initial_wizard_state()) if action == "back": return ResumeWizardTurnResponse(state=apply_back(request.state)) if action == "review": return ResumeWizardTurnResponse(state=apply_review(request.state)) # 成本护栏:达到问题数上限后不再调用 LLM,直接引导进入 review if request.state.asked_count >= RESUME_WIZARD_MAX_QUESTIONS: return ResumeWizardTurnResponse(state=apply_review(request.state)) if action == "skip": state = await run_ai_turn(request.state, "", skip=True) ... state = await run_ai_turn(request.state, answer_text, skip=False)

值得注意的工程细节:

  • RESUME_WIZARD_MAX_QUESTIONS = 15(services/resume_wizard.py)是一道成本护栏:一旦asked_count达到 15,后续 answer/skip 回合不再产生 LLM 调用,直接路由到 review,避免无上限消耗 token;
  • backreview纯确定性操作(无 LLM 调用),back通过弹栈 history 快照还原,review仅计算温柔提示;
  • skip也调用 LLM,但 Prompt 中注入的是固定指令——"(The user skipped this question. Do NOT modify resume_data. Ask the next most useful question for a different section.)",让模型转向另一个章节提问。

AI 回合核心逻辑

run_ai_turn()(services/resume_wizard.py)是整条管线的枢纽,按序完成:

  1. 序列化当前草稿json.dumps(state.resume_data.model_dump(mode="json"), ensure_ascii=False)
  2. 净化用户回答:非 skip 时对回答先做_sanitize_user_input(Prompt 注入模式剥离)再_scrub_secrets(脱敏sk-…/AIza…/Bearer …等凭证样式 token),避免敏感信息进入 LLM;
  3. 组装 Prompt并调用complete_json(prompt, max_tokens=8192, schema_type="resume")
  4. 解析与校验:结果必须是 dict;resume_data经过normalize_wizard_resume_dataResumeData.model_validate严格校验;
  5. 分区合并_merge_section()只把 LLM 输出合并进当前活动章节,绝不覆盖其他章节。

关键设计:分区合并与条目去重

_merge_section()(services/resume_wizard.py)按章节分发合并策略:

  • intro/contact:仅当新值非空时覆盖personalInfo各字段;
  • summary:非空时替换摘要;
  • workExperience/internships/education/personalProjects:调用_merge_entries()签名去重合并
  • skillsmerge_unique_skills()保留首次出现的拼写与顺序,大小写不敏感去重(casefold),同时合并languagescertificationsTrainingawards
  • 未知/review章节:绝不改动resume_data

_merge_entries()的注释解释了动机:模型有时只回显用户刚描述的一条记录而非完整列表,若直接整体替换就会抹掉早前录入的条目。因此采用"内容签名"(而非id,因为 wizard 条目的 id 默认为 0)作为联合键:模型省略的旧条目保留、同签名条目原位替换、真正的新条目追加。

def _merge_entriesT -> list[T]: # 签名键:如 title+company+years 的 casefold 元组 # 省略的保留 / 同签名替换 / 新条目追加

合并后还会执行_assign_entry_ids()(services/resume_wizard.py):LLM 省略id字段导致条目 id 全为 0,而下游的 live preview React key 与 builder 的Math.max(...ids)+1逻辑都依赖唯一 id,因此按位置确定性重编 1-based id。

下一问的选择

_next_question()(services/resume_wizard.py)优先采用模型返回的next_question(其section必须钳制到合法枚举,valid_section()兜底为review);若模型未提供,则回退到_next_gap_section()——按"工作经历 → 教育 → 项目 → 技能 → review"的顺序自动定位第一个明显为空的章节。

def _next_gap_section(data: ResumeData) -> str: if not data.workExperience: return "workExperience" if not data.education: return "education" if not data.personalProjects: return "personalProjects" if not data.additional.technicalSkills: return "skills" return "review"

前端 resume-wizard-page.tsx 的firstGapSection()复刻了同一启发式,用于 review 后"继续补充"(Keep Adding)时定位下一个内容缺口——注释特别指出review章节在后端合并中是 no-op,若目标设为其会静默丢弃回答。

四、Prompting:结构化简历写作助手的约束体系

设计文档要求新增 resume-wizard Prompt 模板,让模型扮演"结构化简历写作助手",核心规则包括:

  • 构建通用主简历,而非针对特定职位的定制简历;
  • 不得虚构公司、日期、指标、工具、学位、奖项或技能;
  • 回答含糊或缺关键事实时必须追问澄清
  • 优先产出基于用户事实的精炼行动导向 bullet
  • 使用配置的内容语言({output_language});
  • 只输出请求的 JSON 对象
  • 保留既有草稿数据,除非用户明确修改。

这些规则完整落在 prompts/resume_wizard.py 的RESUME_WIZARD_TURN_PROMPT中。模板额外强调了语言边界:人类可读文本(下一问、标题、bullet、摘要)用{output_language}输出,但结构化值保持原样——next_question.section必须是精确的英文枚举值,日期保持给定格式,不翻译章节键与日期。

模板中的输出 JSON 骨架(每轮必须返回):

{ "resume_data": { "personalInfo": {"name": "", "title": "", "email": "", "phone": "", "location": "", "website": "", "linkedin": "", "github": ""}, "summary": "", "workExperience": [], "education": [], "personalProjects": [], "additional": {"technicalSkills": [], "languages": [], "certificationsTraining": [], "awards": []}, "sectionMeta": [], "customSections": {} }, "next_question": {"text": "Your next concise question", "section": "workExperience"}, "inferred_skills": ["Skill"], "is_complete": false }

其中is_complete仅是建议信号(提示前端亮起 "Review & finish" 提示),step始终停留在question绝不自动 finalize——是否进入 review 由客户端决定(见 services/resume_wizard.py 的注释)。

章节提示词同样落地:work experience 要求 title、company、dates、responsibilities、tools、scale、impact;projects 要求 what was built、why it mattered、technologies used、user/usage context、links(见_SECTION_PROMPTS)。

五、Backend API:turn 与 finalize 两个端点

设计文档规划的端点与源码一一对应(挂载于 apps/backend/app/main.py 的app.include_router(resume_wizard_router, prefix="/api/v1")):

POST /api/v1/resume-wizard/turn

  • 请求体:ResumeWizardTurnRequest——state(完整往返的状态)+action+ 可选的answer
  • answertext约束为min_length=1, max_length=6000,且必须非空白(字段校验器拒绝纯空白);
  • action == "answer"时必须携带answer,否则模型校验器抛错(422);
  • 响应体:ResumeWizardTurnResponse——新的state(含更新后的resume_data、进度、当前章节、推断技能、下一问、警告、完成状态)。

POST /api/v1/resume-wizard/finalize

finalize 端点 执行"校验终稿 → 创建主简历":

  1. 幂等保护:先查当前主简历,若已存在processing_status == "ready"的主简历,直接 409 拒绝("A master resume already exists. Delete it before creating a new one.");
  2. Schema 校验normalize_resume_data(request.state.resume_data.model_dump(mode="json"))后再ResumeData.model_validate,保证落库的是严格合法的ResumeData
  3. 原子创建db.create_resume_atomic_master(...)——content为规范 JSON(ensure_ascii=False, sort_keys=True),content_type="json"filename=f"AI Resume Wizard - {name}.json"processing_status="ready",并同步设置title。注释指出:title 放在原子创建内,避免"已提交但无标题的主简历"在重试时触发 409 死锁;
  4. 最终校验:若创建结果is_master=False(竞态下主简历已被占用),删除刚创建的非 master 记录并返回 409,做到非破坏性拒绝。

最终响应:{message, request_id, resume_id, processing_status: "ready", is_master}

若已存在主简历,finalize 拒绝创建——显式的替换(replacement)流程不在本次实现范围内(Out of Scope)。

错误处理:完整上下文留服务端,浏览器只见通用消息

后端所有异常统一处理(routers/resume_wizard.py):ValueError→ 422 "Could not update the resume draft.";其余异常 → 500 "Resume wizard failed. Please try again.",同时logger.error记录详细上下文。模型异常细节、provider 密钥、原始堆栈永不进入浏览器

JSON 修复与重试

设计文档要求:模型返回非法 JSON 时,后端使用既有complete_json行为 + 仅基于 prompt 的 JSON 修复指令重试;仍失败则服务端记录详细错误并返回通用客户端错误。这正对应run_ai_turncomplete_json(prompt, max_tokens=8192, schema_type="resume")的调用——complete_json是项目 LLM 层的既有能力(app/llm.py)。

六、Frontend UI:聚焦工具的 Swiss 设计

设计文档明确了/resume-wizard页面的视觉规范,前端 resume-wizard-page.tsx 与 question-card.tsx 逐条落地:

设计规范源码实现
Canvas 背景#F0F0E8主题 tokenbg-background
方形圆角、黑色边框border-2 border-black rounded-none
硬偏移阴影shadow-[4px_4px_0px_0px_#000000](shadow-sw-lg)
衬线标题、无衬线正文、等宽元数据font-serif/font-sans/font-mono
无装饰渐变、无圆角卡片、无营销 hero网格布局lg:grid-cols-[minmax(0,1fr)_360px]

首屏即向导工具本身,页面结构为两栏:

  • 左栏QuestionCard——顶部服务端计算的进度条(role="progressbar",每格为黑/白方块)、章节标签(等宽蓝色小字)、衬线大字号问题、答案Textarea、操作按钮组;
  • 右栏LivePreview——实时结构化预览,展示已收集的姓名/头衔、经历(title · company、年份、bullet)、项目、教育、技能标签。

交互操作矩阵

QuestionCard根据step渲染不同的操作:

  • question 步骤:Continue(Enter 提交、Shift+Enter 换行,见handleKeyDown)、Skip、Review、Back(有 history 时显示);
  • review 步骤:Create(canFinalize为 false 时禁用,即无姓名时不可创建)、Keep Adding(回到 question 并定位到下一个缺口章节);
  • isComplete为 true 时在 question 步骤显示绿色 "ready" 提示(resumeWizard.readyHint)。

键盘交互遵循仓库惯例:Enter 永不冒泡到父级表单/对话框(event.stopPropagation()),Shift+Enter 插入换行。

Live Preview 的技能推断展示

live-preview.tsx 将technicalSkillsinferred_skills合并展示,dedupeSkills()使用toLowerCase()(与后端casefold对齐,注释专门指出toLocaleLowerCase会在土耳其语等 locale 上因点/无点 I 产生分歧),本轮新推断的技能用绿色边框 + ✓ 标记,用户可在 finalize 前实时核对 AI 推断是否越界。

本地草稿持久化与刷新恢复

resume-wizard-page.tsx将草稿持久化到localStorageresume_wizard_draft键。读取时做深度容错归一化readSavedDraft/normalizeDraftResumeData):step/section 钳制到合法枚举、personalInfo每字段强制字符串(防止数字型 name 让.trim()抛错陷入刷新死循环)、列表字段强制数组并重编 1-based id。写入是 best-effort——配额/序列化失败静默忽略,绝不让草稿保存问题崩溃向导。

Finalize 后的落点

handleFinalize()(resume-wizard-page.tsx):

localStorage.setItem(MASTER_RESUME_KEY, response.resume_id); // master_resume_id localStorage.removeItem(DRAFT_STORAGE_KEY); incrementResumes(); setHasMasterResume(true); setState((current) => ({ ...current, step: 'complete' })); router.push(`/builder?id=${response.resume_id}`);

即:存入master_resume_id→ 更新状态缓存 → 路由到/builder?id=<resume_id>供用户最终人工审查与编辑。这是设计文档强调的刻意安排——"AI 向导产出强初稿,但用户在使用简历进行 tailor 之前仍应能检查与编辑"。

七、Frontend 数据契约与 API 助手

设计文档要求前端在 apps/frontend/lib/api/resume-wizard.ts 增加 API 助手,源码完全对应:

  • postResumeWizardTurn(payload)POST /resume-wizard/turn
  • finalizeResumeWizard(state)POST /resume-wizard/finalize
  • createInitialResumeWizardState()生成本地初始状态(与后端build_initial_wizard_state字段一致:step: 'intro'、空resume_data、intro 问题、progress: {current: 0, total: 8})。

前端类型ResumeWizardState/ResumeWizardSection/ResumeWizardAction与后端 Pydantic Schema 字段一一对应,构成可往返的契约。所有 i18n 文案走useTranslations()resumeWizard.*键,并由 check_locale_parity.py 检查各 locale 键一致。

八、错误处理与恢复动作(前端侧)

设计文档要求客户端错误以 Swiss 风格警示呈现并附带恢复动作,前端落地为红框警示区(border-2 border-red-600 bg-red-100role="alert")+ 错误翻译键(resumeWizard.errors.turnFailed/finalizeFailed),用户可执行的恢复路径包括:

  • 重试 AI 回合(再次点击 Continue/Skip/Review);
  • 继续编辑当前回答(setErrorKey(null)后重新输入);
  • 返回 dashboard(页面右上角 Back to Dashboard 按钮);
  • 转而走上传路径(dashboard 的 Upload 选项)。

九、测试策略与质量门禁

设计文档列出的测试点全部有落地文件:

后端(apps/backend/tests/integration/test_resume_wizard_api.py):

  • Wizard Schema 校验与经ResumeData的强制类型转换;
  • intro 回答提取姓名并产出章节选择;
  • 章节更新产生基线 bullet 数量;
  • 技能推断仅使用用户提供的事实;
  • finalize 在无主简历时创建 ready 主简历;
  • finalize 在主简历已存在时拒绝。

前端(apps/frontend/tests/):

  • resume-wizard-api.test.ts:API 助手请求/响应形状;
  • resume-wizard-page.test.tsx:初始渲染、章节切换、finalize 存储master_resume_id并路由到 builder;
  • resume-wizard-question-card.test.tsx:question/review 步骤操作矩阵;
  • resume-wizard-live-preview.test.tsx:技能去重与推断标记。

质量门禁:前端改动须在apps/frontend下运行npm run lintnpm run format;后端改动运行针对新 router/service 的定向 pytest。

十、Out of Scope:明确的边界

设计文档与实现保持一致,以下内容不在 Wizard 范围内:

  • 不做职位定制:Wizard 构建的是通用主简历,JD 定制仍走既有 tailor 管线;
  • 不替换既有上传解析器、tailor 流程、enrichment 流程或 builder;
  • 不改动CI、Docker 或 GitHub workflow 文件;
  • 不实现主简历的显式替换流程(finalize 遇到已存在主简历时以 409 非破坏性拒绝)。

结语:一份"AI 引导、应用管控"的可信简历管线

Resume-Matcher 的 AI Resume Wizard 展示了混合式问答生成的成熟工程范式:AI 负责对话与起草,应用负责 Schema、状态机与校验。从 intro 的姓名提取、分区合并防覆盖、内容签名去重、服务端进度计算与 15 问成本护栏,到 review 的确定性缺口提示与 finalize 的原子建库,再到前端 Swiss 风格工具化界面与 localStorage 刷新恢复——整条管线强调"诚实生成、可追溯、可回退、可最终人工编辑"。它没有把简历生成交给黑盒,而是把 LLM 的能力约束在结构化契约之内,这也正是设计文档中"truthful structured master resume"从原则走向实现的完整路径。

【免费下载链接】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),仅供参考

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

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

立即咨询