# 岗位简历技能差距分析 + 定制简历 — 完整方案 ## 一、需求概述 三步流程: 1. **差距分析**:根据岗位技能标签和用户简历,AI 判断缺失技能,纯计算匹配分 2. **定制简历**:用户选择要优化的模块和要新增的技能,AI 生成优化后的简历内容 3. **预览 + AI 对话编辑**:前端渲染定制简历,用户可通过 AI 对话继续编辑,用于投递时使用(不写回原简历) 定制简历存 Redis,过期时间 12 小时,不落库。一个用户同时只有一份定制简历。 --- ## 二、接口总览 | 序号 | 路径 | 方法 | 说明 | |------|------|------|------| | 1 | `/api/job/skill-gap` | POST | 差距分析 | | 2 | `/api/job/customize-resume` | POST | 生成定制简历 | | 3 | `/api/job/customize-resume` | GET | 查询定制简历 | | 4 | `/api/job/customize-resume` | PUT | 手动编辑定制简历 | | 5 | `/api/job/customize-resume/rollback` | POST | 回滚定制简历 | | 6 | `/api/job/customize-resume/ai-edit` | POST | AI 对话式编辑定制简历 | --- ## 三、接口一:差距分析 ### 接口信息 | 项目 | 值 | |------|-----| | 路径 | `POST /api/job/skill-gap` | | 入参 | `{ "jobId": Long }` | | 鉴权 | 需要登录态,从 token 取 userId | ### 处理流程 1. 从 token 取 userId 2. 查简历(自动选择,不传 resumeId): - 先查 `bg_user_resume` 中 `user_id=userId AND is_default=1`,按 `update_time DESC` 取第一条 - 没有默认简历 → 查 `user_id=userId`,按 `update_time DESC` 取第一条 - 没有任何简历 → 报错"请先创建简历" 3. 查岗位: - 查 `bg_job` 拿 id、title、skill_tags - 岗位不存在 → 报错 - skill_tags 为空 → 直接返回满分 10,gaps 为空数组 4. 查简历子表(拼 AI 输入): - `bg_user_resume_education` - `bg_user_resume_work` - `bg_user_resume_internship` - `bg_user_resume_project` - `bg_user_resume_competition` 5. 调 AI(一次): - 输入:岗位 skill_tags 列表 + 简历 skills 字段 + 各子表经历描述 - 输出:差距条目 JSON 数组,每条含 keyword(缺失技能,skill_tags 子集)、title(≤12字)、description(≤30字) 6. 计算匹配分:`score = (skill_tags总数 - gaps条目数) / skill_tags总数 × 10`,保留一位小数 ### 返回 ```json { "score": 2.5, "job": { "jobId": "1234567890", "title": "数据产品经理", "skillTags": ["Python", "SQL", "项目管理", "团队协作", "数据分析", "跨部门沟通"] }, "resume": { "resumeId": "1234567890", "resumeName": "李华_产品经理", "targetPosition": "电商产品经理" }, "gaps": [ { "keyword": "SQL", "title": "缺少 SQL 关键词", "description": "岗位要求数据查询分析能力,简历未体现。" }, { "keyword": "数据分析", "title": "数据分析证据不足", "description": "建议补充指标拆解与结论产出的经历。" } ] } ``` ### 边界处理 | 场景 | 处理 | |------|------| | 用户无简历 | 报错"请先创建简历" | | 岗位不存在 | 报错 | | skill_tags 为空 | 满分 10,gaps 为空数组 | | AI 调用失败 | 降级:全部标记为缺失,分数 0 | ### AI Prompt ``` 你是一个技能匹配助手。给定岗位要求的技能标签列表和用户简历信息,找出用户简历中未覆盖的技能并生成差距说明。 【岗位技能标签】 {skill_tags} 【用户简历】 {resume_json} 规则: 1. 逐个判断岗位技能标签,用户简历中是否体现了该技能(包括直接提及、经历中隐含的技能) 2. 只针对未覆盖的技能输出差距条目,已覆盖的技能不输出 3. 每个差距条目包含三个字段: - keyword:缺失的技能,必须是岗位技能标签中的原文,不要修改 - title:差距点的简短标题,不超过 12 个字(例如"缺少 SQL 关键词") - description:结合岗位要求的一句说明或建议,不超过 30 个字 4. 返回 JSON 数组格式,如: [{"keyword": "SQL", "title": "缺少 SQL 关键词", "description": "岗位要求数据查询分析能力,简历未体现。"}] 5. 如果全部覆盖,返回空数组 [] 6. 只返回 JSON 数组,不要其他内容 ``` --- ## 四、接口二:生成定制简历 ### 接口信息 | 项目 | 值 | |------|-----| | 路径 | `POST /api/job/customize-resume` | | 入参 | 见下方 | | 鉴权 | 需要登录态,从 token 取 userId | ### 入参 ```json { "jobId": "Long", "resumeId": "Long", "optimizeModules": ["summary", "skills", "experience"], "addSkills": ["Python", "SQL"] } ``` - `resumeId`:以哪份简历为模板(来自差距分析返回的 resumeId,用户可能切换过简历) - `optimizeModules`:用户勾选要优化的模块,可选值:summary(个人概述)、skills(技能)、experience(过往经历) - `addSkills`:用户勾选要新增的技能关键词(来自差距分析 gaps 的 keyword) ### 处理流程 1. 查简历主表 + 所有子表(完整简历数据) 2. 查岗位信息(title、description、requirement) 3. 按用户选择的模块分别处理(各模块并发执行,最后合并): **summary(个人概述)**: - 调 AI,根据岗位信息微调 summary,融入选中的技能关键词 - 避免过度优化,保持原文风格,只做轻微润色 **skills(技能)**: - 把 addSkills 追加到现有 skills 列表,不调 AI,纯内存操作 **experience(过往经历)**: - 按子表(education/work/internship/project/competition)为单位,每个子表一个 AI 调用,传入该子表的完整数据 - 让描述更贴合岗位方向,避免过度优化,基本保持原文不变 - 不融入 addSkills,经历描述不硬塞技能关键词 **addSkills 影响范围**:只影响 skills(直接追加)和 summary(自然融入),不影响 experience。 **并发策略**:summary 优化 和 各子表优化 全部并发执行(asyncio.gather),skills 纯内存操作不需要等待。最终合并所有结果。 4. 未勾选的模块保持原数据不动 5. 组装完整的定制简历数据,存 Redis(key:`customize:resume:{userId}`,过期 12 小时,重新生成会覆盖) 6. 返回成功标识,不返回简历数据(前端通过 GET 接口查询) ### 返回 ```json { "success": true } ``` 说明:简历数据前端通过 `GET /api/job/customize-resume` 查询。子表记录的 id 使用随机 8 位字符串作为标识(从数据库查出时生成),不使用数据库原始 id。 ### 边界处理 | 场景 | 处理 | |------|------| | 简历不存在 | 报错 | | 岗位不存在 | 报错 | | optimizeModules 为空 | 报错"请至少选择一个优化模块" | | AI 调用失败 | 该模块保持原数据不动,不影响其他模块 | --- ## 五、接口三:查询定制简历 ### 接口信息 | 项目 | 值 | |------|-----| | 路径 | `GET /api/job/customize-resume` | | 入参 | 无 | | 鉴权 | 需要登录态,从 token 取 userId | ### 处理流程 1. 从 Redis 取定制简历数据(key:`customize:resume:{userId}`) 2. 不存在 → 返回 null 3. 返回完整简历 JSON --- ## 六、接口四:修改定制简历(手动编辑) ### 接口信息 | 项目 | 值 | |------|-----| | 路径 | `PUT /api/job/customize-resume` | | 入参 | 完整简历 JSON(整体覆盖) | | 鉴权 | 需要登录态,从 token 取 userId | ### 处理流程 1. 校验入参 2. 整体覆盖 Redis 中的定制简历数据(key:`customize:resume:{userId}`) 3. 刷新过期时间为 12 小时 4. 不存在时也直接写入 --- ## 七、接口五:回滚定制简历 ### 接口信息 | 项目 | 值 | |------|-----| | 路径 | `POST /api/job/customize-resume/rollback` | | 入参 | 无 | | 鉴权 | 需要登录态,从 token 取 userId | ### 处理流程 1. 从 Redis 取回滚数据(key:`customize:resume:rollback:{userId}`) 2. 不存在 → 报错"没有可回滚的版本" 3. 用回滚数据覆盖当前定制简历(key:`customize:resume:{userId}`) 4. 删除回滚数据 5. 刷新定制简历过期时间为 12 小时 --- ## 七、接口五:AI 对话式编辑定制简历 ### 接口信息 | 项目 | 值 | |------|-----| | 路径 | `POST /api/job/customize-resume/ai-edit` | | 入参 | 见下方 | | 鉴权 | 需要登录态,从 token 取 userId | ### 入参 ```json { "jobId": "Long", "instruction": "精简一下第一段工作经历", "chatHistory": [ { "role": "user", "content": "优化描述" }, { "role": "assistant", "content": "你想优化哪一部分?" } ] } ``` - `instruction`:用户当前输入的指令 - `chatHistory`:之前的对话历史,前端维护,每次请求带上 ### 消息类型 返回两种消息类型: **message(普通对话)**:AI 追问、引导,不修改简历 ```json { "type": "message", "message": "你想优化哪一部分的描述?是最新的实习还是所有工作经历?" } ``` **updated(修改通知)**:AI 修改了简历,返回新版本 ```json { "type": "updated", "message": "完成!已更新:个人简介、技能、工作经验" } ``` ### 处理流程(两步走) 核心思路:将用户操作原子化,把模块级并发拆成记录级并发,减少单次 AI 输出量,提升响应速度。 #### 第一步:准备数据 1. 从 Redis 取当前定制简历(不存在则报错) 2. 查 `bg_job` 拿 title、description、requirement #### 第二步:规划 AI(意图识别 + 操作原子化) 输入:用户指令 + 对话历史 + 当前完整简历内容 + 岗位信息(title + description + requirement) 输出两种结果: **对话(指令不明确)**: ```json { "action": "chat", "message": "你想优化哪一部分?" } ``` → 直接返回 `{ "type": "message", "message": "..." }`,结束。 **操作计划(指令明确)**: ```json { "action": "modify", "operations": [ { "type": "delete", "module": "work", "id": "abc12345" }, { "type": "update", "module": "work", "id": "def67890", "instruction": "精简描述,突出量化成果" }, { "type": "update", "module": "resume", "instruction": "在summary中融入数据分析关键词" }, { "type": "add", "module": "internship", "instruction": "新增一段数据分析实习经历" } ] } ``` 操作类型: - **delete**:删除记录,需要 module + id - **update**:修改记录,需要 module + id + instruction(resume 主表不需要 id) - **add**:新增记录,需要 module + instruction 规则: - 每个操作对应一条原子操作,一个用户指令可拆出多条操作 - `instruction` 限 50 字以内 - `delete` 和 `update`(非 resume)必须带 `id`,从当前简历中匹配 - `update` module 为 `resume` 时不需要 `id`(主表是单对象) - `add` 不需要 `id` 模块划分(按表结构,共 6 个): | 模块名 | 对应表 | 可修改字段 | |--------|--------|-----------| | `resume` | `bg_user_resume` | avatarUrl、name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary | | `education` | `bg_user_resume_education` | 全部字段 | | `work` | `bg_user_resume_work` | 全部字段 | | `internship` | `bg_user_resume_internship` | 全部字段 | | `project` | `bg_user_resume_project` | 全部字段 | | `competition` | `bg_user_resume_competition` | 全部字段 | #### 第三步:按操作类型并发执行 先处理 delete(零 AI 开销),再并发执行所有 update 和 add(asyncio.gather): - **delete**:后端直接按 module + id 从数组移除,不调 AI - **update(含 resume 主表和子表记录)**:按 id 取出单条记录 + instruction + schema + 岗位信息(title + description + requirement)+ 最近 10 条对话历史,调 AI 修改,只输出单条记录。resume 主表和子表记录复用同一个 prompt - **add**:instruction + schema + 岗位信息(title + description + requirement)+ 最近 10 条对话历史,调 AI 生成一条新记录 #### 第四步:合并 - delete 已直接处理 - update 按 module + id 替换回原数据(resume 主表直接整体替换) - add 按 module 追加到对应数组 - AI 调用失败的操作跳过,不影响其他操作 #### 第五步:保存 + 返回 1. 当前简历存为回滚数据(key:`customize:resume:rollback:{userId}`,过期 30 分钟) 2. 新简历覆盖 Redis(key:`customize:resume:{userId}`),刷新过期时间 12 小时 3. `updatedModulesLabel` 由后端根据 operations 中的 module 去重映射中文名拼接 4. 返回 `type: updated` + 消息(前端通过 GET 接口查询新简历,通过回滚接口恢复) ### description 字段处理 子表的 description 字段格式为 `[{id, text}, {id, text}]`,AI 操作规则(通过 prompt 约束): - **修改**:保留原 id,只改 text - **新增**:AI 自行生成随机 8 位字符串作为 id - **删除**:直接从数组中移除 不做后端校验,完全依靠 prompt 约束 AI 行为。 ### 边界处理 | 场景 | 处理 | |------|------| | 定制简历不存在 | 报错"定制简历不存在,请先生成" | | 规划 AI 失败 | 返回 `type: message`,提示重试 | | 某个模块修改 AI 失败 | 该模块保持原数据,其他模块正常返回 | --- ## 八、AI Prompt 汇总 ### 1. 差距分析 Prompt ``` 你是一个技能匹配助手。给定岗位要求的技能标签列表和用户简历信息,找出用户简历中未覆盖的技能并生成差距说明。 【岗位技能标签】 {skill_tags} 【用户简历】 {resume_json} 规则: 1. 逐个判断岗位技能标签,用户简历中是否体现了该技能(包括直接提及、经历中隐含的技能) 2. 只针对未覆盖的技能输出差距条目,已覆盖的技能不输出 3. 每个差距条目包含三个字段: - keyword:缺失的技能,必须是岗位技能标签中的原文,不要修改 - title:差距点的简短标题,不超过 12 个字(例如"缺少 SQL 关键词") - description:结合岗位要求的一句说明或建议,不超过 30 个字 4. 返回 JSON 数组格式,如: [{"keyword": "SQL", "title": "缺少 SQL 关键词", "description": "岗位要求数据查询分析能力,简历未体现。"}] 5. 如果全部覆盖,返回空数组 [] 6. 只返回 JSON 数组,不要其他内容 ``` ### 2. 定制简历 - summary 优化 Prompt ``` 你是一个简历优化助手。根据目标岗位信息,微调用户的个人概述。 【目标岗位】 {job_title} 【需要融入的技能关键词】 {add_skills} 【原始个人概述】 {original_summary} 规则: 1. 保持原文风格和主体内容不变 2. 只做轻微润色,让概述更贴合目标岗位方向 3. 自然融入需要新增的技能关键词,不要生硬堆砌 4. 避免过度优化,改动越少越好 5. 直接输出优化后的文本,不要其他内容 ``` ### 3. 定制简历 - experience 优化 Prompt ``` 你是一个简历优化助手。根据目标岗位信息,微调用户的经历描述。 【目标岗位】 {job_title} {job_description} 【原始经历数据】 {original_module_data} 规则: 1. 基本保持原文不变,只在可以优化的地方做轻微调整 2. 让描述更贴合目标岗位方向,但不要编造内容 3. 避免过度优化,改动越少越好 4. description 字段是 [{id, text}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除 5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致) ``` ### 4. Agent - 规划 Prompt ``` 你是一个简历编辑助手。分析用户的指令,将其拆解为原子操作。 【目标岗位】 {job_title} {job_description} 【当前简历】 {resume_json} 【对话历史】 {chat_history} 【用户指令】 {instruction} 如果用户指令不明确或需要澄清,返回: {"action": "chat", "message": "你的追问内容"} 如果用户指令明确,将其拆解为原子操作列表,返回: {"action": "modify", "operations": [...]} 操作类型: 1. 删除记录:{"type": "delete", "module": "模块名", "id": "记录id"} 2. 修改记录:{"type": "update", "module": "模块名", "id": "记录id", "instruction": "修改说明(50字内)"} 3. 修改主表:{"type": "update", "module": "resume", "instruction": "修改说明(50字内)"} 4. 新增记录:{"type": "add", "module": "模块名", "instruction": "新增说明(50字内)"} 模块名可选:resume(主表,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl)、education(教育)、work(工作)、internship(实习)、project(项目)、competition(竞赛) 规则: 1. 每条操作对应一个最小粒度的修改,一个用户指令可拆出多条操作 2. delete 和 update(非resume)必须带 id,从当前简历中匹配 3. instruction 不超过50字,简明扼要 4. 只返回 JSON,不要其他内容 ``` ### 5. Agent - 单条记录修改 Prompt ``` 你是一个简历编辑助手。根据修改要求,修改简历中的一条记录。 【目标岗位】 {job_title} {job_description} 【修改要求】 {instruction} 【最近对话】 {chat_history} 【模块数据结构】 {module_schema} 【当前记录数据】 {record_data} 规则: 1. 严格按照修改要求操作 2. 未要求修改的字段保持不变 3. 不要编造用户简历中不存在的内容 4. 保持原文格式和结构 5. description 字段是 [{id, text}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除 6. 返回修改后的完整记录数据(JSON 格式,与输入格式一致) ``` ### 6. Agent - 新增记录 Prompt ``` 你是一个简历编辑助手。根据要求,生成一条新的简历记录。 【目标岗位】 {job_title} {job_description} 【新增要求】 {instruction} 【最近对话】 {chat_history} 【模块数据结构】 {module_schema} 规则: 1. 按照模块数据结构生成完整字段 2. id 使用随机8位字符串 3. description 中每个段落的 id 也使用随机8位字符串 4. 内容要合理真实,贴合目标岗位方向 5. 返回一条完整记录的 JSON,与模块数据结构一致 ``` ### 各模块数据结构定义(传入 prompt 的 module_schema) **resume(主表)**: ```json { "avatarUrl": "string", "name": "string", "email": "string", "mobileNumber": "string", "city": "string", "wechatNumber": "string", "portfolioUrl": "string", "skills": ["string"], "certificates": ["string"], "summary": "string" } ``` **education**: ```json [{ "id": "string(8位)", "school": "string", "major": "string", "degree": "大专/本科/硕士/博士", "studyType": "全日制/非全日制", "startDate": "2023.09", "endDate": "2024.06", "description": [{"id": "string(8位)", "text": "string"}] }] ``` **work**: ```json [{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }] ``` **internship**: ```json [{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }] ``` **project**: ```json [{ "id": "string(8位)", "companyName": "string", "projectName": "string", "role": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }] ``` **competition**: ```json [{ "id": "string(8位)", "competitionName": "string", "award": "string", "awardDate": "2023.07", "description": [{"id": "string(8位)", "text": "string"}] }] ``` --- ## 九、Redis 设计 ### Key 格式 - 定制简历:`customize:resume:{userId}` - 回滚数据:`customize:resume:rollback:{userId}` ### Value 结构 ```python class CustomizeResume: """定制简历缓存结构""" resume: ResumeProfile # 主表信息 education: list[Education] # 教育经历 work: list[Work] # 工作经历 internship: list[Internship] # 实习经历 project: list[Project] # 项目经历 competition: list[Competition] # 竞赛经历 class ResumeProfile: """主表可修改字段""" avatarUrl: str name: str email: str mobileNumber: str city: str wechatNumber: str portfolioUrl: str skills: list[str] certificates: list[str] summary: str class Education: id: str # 随机8位标识 school: str major: str degree: str # 大专/本科/硕士/博士 studyType: str # 全日制/非全日制 startDate: str # 格式:2023.09 endDate: str # 格式:2024.06 description: list[Paragraph] class Work: id: str companyName: str position: str startDate: str endDate: str description: list[Paragraph] class Internship: id: str companyName: str position: str startDate: str endDate: str description: list[Paragraph] class Project: id: str companyName: str projectName: str role: str startDate: str endDate: str description: list[Paragraph] class Competition: id: str competitionName: str award: str awardDate: str # 格式:2023.07 description: list[Paragraph] class Paragraph: id: str # 随机8位标识 text: str ``` 定制简历和回滚数据使用相同的 `CustomizeResume` 结构。代码实现时使用 Pydantic model,存取 Redis 通过 `model_dump_json()` / `model_validate_json()`。 ### 常量 ```python CUSTOMIZE_RESUME_KEY_PREFIX = "customize:resume:" CUSTOMIZE_RESUME_EXPIRE = 12 * 60 * 60 # 12小时 CUSTOMIZE_RESUME_ROLLBACK_KEY_PREFIX = "customize:resume:rollback:" CUSTOMIZE_RESUME_ROLLBACK_EXPIRE = 30 * 60 # 30分钟 ``` ### 过期时间 - 定制简历:12 小时,每次写入/修改时刷新 - 回滚数据:30 分钟,每次 AI 编辑时覆盖 --- ## 十、数据依赖 | 表 | 读写 | 用途 | |----|------|------| | `bg_job` | 读 | 取岗位信息(title、description、requirement、skill_tags) | | `bg_user_resume` | 读 | 取简历主表数据 | | `bg_user_resume_education` | 读 | 取教育经历 | | `bg_user_resume_work` | 读 | 取工作经历 | | `bg_user_resume_internship` | 读 | 取实习经历 | | `bg_user_resume_project` | 读 | 取项目经历 | | `bg_user_resume_competition` | 读 | 取竞赛经历 | | Redis | 写/读 | 存取定制简历,过期 12 小时 | 无新建表,无数据库写操作。 --- ## 十一、文件规划 ### 新建文件 | 文件 | 职责 | |------|------| | `app/models/job.py` | Job 表 ORM 模型(bg_job,只读) | | `app/core/schemas/skill_gap.py` | Pydantic Schema(请求参数 Param + 响应 Dto + Redis 缓存模型 CustomizeResume) | | `app/ai/skill_gap_analyzer/__init__.py` | 模块初始化 | | `app/ai/skill_gap_analyzer/prompts.py` | 所有 AI prompt 模板 | | `app/ai/skill_gap_analyzer/analyzer.py` | AI 调用逻辑(差距分析 + 定制简历优化 + Agent 规划/执行) | | `app/services/skill_gap_service.py` | 业务逻辑层(含 Redis 常量、简历查询、Redis 读写) | | `app/api/skill_gap.py` | 路由层(6 个接口) | ### 修改文件 | 文件 | 改动 | |------|------| | `app/main.py` | 注册 skill_gap 路由 |