Files
offerpai_python_ai/.kiro/specs/skill-gap-analysis.md
T
2026-04-09 18:22:10 +08:00

20 KiB
Raw Blame History

岗位简历技能差距分析 + 定制简历 — 完整方案

一、需求概述

三步流程:

  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_resumeuser_id=userId AND is_default=1,按 update_time DESC 取第一条
    • 没有默认简历 → 查 user_id=userId,按 update_time DESC 取第一条
    • 没有任何简历 → 报错"请先创建简历"
  3. 查岗位:
    • bg_job 拿 id、title、skill_tags
    • 岗位不存在 → 报错
    • skill_tags 为空 → 直接返回满分 10missingSkills 为空数组
  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 数组,必须是 skill_tags 的子集
  6. 计算匹配分:score = (skill_tags总数 - missingSkills数) / skill_tags总数 × 10,保留一位小数

返回

{
  "score": 2.5,
  "job": {
    "jobId": "1234567890",
    "title": "数据产品经理",
    "skillTags": ["Python", "SQL", "项目管理", "团队协作", "数据分析", "跨部门沟通"]
  },
  "resume": {
    "resumeId": "1234567890",
    "resumeName": "李华_产品经理",
    "targetPosition": "电商产品经理"
  },
  "missingSkills": ["Python", "SQL", "数据分析", "跨部门沟通"]
}

边界处理

场景 处理
用户无简历 报错"请先创建简历"
岗位不存在 报错
skill_tags 为空 满分 10missingSkills 为空数组
AI 调用失败 降级:全部标记为缺失,分数 0

AI Prompt

你是一个技能匹配助手。给定岗位要求的技能标签列表和用户简历信息,判断用户简历中未覆盖的技能。

【岗位技能标签】
{skill_tags}

【用户简历】
{resume_json}

规则:
1. 逐个判断岗位技能标签,用户简历中是否体现了该技能(包括直接提及、经历中隐含的技能)
2. 只输出用户简历未覆盖的技能,必须是岗位技能标签的子集,原文输出不要修改
3. 返回 JSON 数组格式,如:["Python", "SQL"]
4. 如果全部覆盖,返回空数组 []
5. 只返回 JSON 数组,不要其他内容

四、接口二:生成定制简历

接口信息

项目
路径 POST /api/job/customize-resume
入参 见下方
鉴权 需要登录态,从 token 取 userId

入参

{
  "jobId": "Long",
  "resumeId": "Long",
  "optimizeModules": ["summary", "skills", "experience"],
  "addSkills": ["Python", "SQL"]
}
  • resumeId:以哪份简历为模板(来自差距分析返回的 resumeId,用户可能切换过简历)
  • optimizeModules:用户勾选要优化的模块,可选值:summary(个人概述)、skills(技能)、experience(过往经历)
  • addSkills:用户勾选要新增的技能关键词(来自差距分析的 missingSkills

处理流程

  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 纯内存操作不需要等待。最终合并所有结果。

  1. 未勾选的模块保持原数据不动
  2. 组装完整的定制简历数据,存 Redis(key:customize:resume:{userId},过期 12 小时,重新生成会覆盖)
  3. 返回成功标识,不返回简历数据(前端通过 GET 接口查询)

返回

{
  "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 取回滚数据(keycustomize:resume:rollback:{userId}
  2. 不存在 → 报错"没有可回滚的版本"
  3. 用回滚数据覆盖当前定制简历(keycustomize:resume:{userId}
  4. 删除回滚数据
  5. 刷新定制简历过期时间为 12 小时

七、接口五:AI 对话式编辑定制简历

接口信息

项目
路径 POST /api/job/customize-resume/ai-edit
入参 见下方
鉴权 需要登录态,从 token 取 userId

入参

{
  "jobId": "Long",
  "instruction": "精简一下第一段工作经历",
  "chatHistory": [
    { "role": "user", "content": "优化描述" },
    { "role": "assistant", "content": "你想优化哪一部分?" }
  ]
}
  • instruction:用户当前输入的指令
  • chatHistory:之前的对话历史,前端维护,每次请求带上

消息类型

返回两种消息类型:

message(普通对话)AI 追问、引导,不修改简历

{
  "type": "message",
  "message": "你想优化哪一部分的描述?是最新的实习还是所有工作经历?"
}

updated(修改通知)AI 修改了简历,返回新版本

{
  "type": "updated",
  "message": "完成!已更新:个人简介、技能、工作经验"
}

处理流程(两步走)

第一步:准备数据

  1. 从 Redis 取当前定制简历(不存在则报错)
  2. bg_job 拿 title、description

第二步:规划 AI(意图识别)

输入:用户指令 + 对话历史 + 当前完整简历内容 + 岗位 title

输出两种结果:

对话(指令不明确)

{ "action": "chat", "message": "你想优化哪一部分?" }

→ 直接返回 { "type": "message", "message": "..." },结束。

修改计划(指令明确)

{
  "action": "modify",
  "modules": [
    { "module": "resume", "instruction": "在 summary 中融入数据分析技能,skills 添加 Python" },
    { "module": "work", "instruction": "精简第一段工作经历,突出量化成果" }
  ],
  "updatedModulesLabel": "个人简介、工作经验"
}

模块划分(按表结构,共 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 全部字段

第三步:按模块并发执行修改

根据修改计划,对每个模块并发调 AI(asyncio.gather):

  • 每个模块传入该模块的完整数据(如 work 传 [{工作1}, {工作2}] 整体)
  • AI 返回修改后的该模块完整数据
  • 所有模块统一走 AI,包括 skills 等简单操作(因为需要 AI 理解用户自然语言指令)

第四步:合并

  • AI 调用失败的模块保持原数据不动
  • 成功的模块直接用 AI 返回结果替换

把所有模块结果合并回完整简历。

第五步:保存 + 返回

  1. 当前简历存为回滚数据(keycustomize:resume:rollback:{userId},过期 30 分钟)
  2. 新简历覆盖 Rediskeycustomize:resume:{userId}),刷新过期时间 12 小时
  3. 返回 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. 返回 JSON 数组格式,如:["Python", "SQL"]
4. 如果全部覆盖,返回空数组 []
5. 只返回 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}

【当前简历】
{resume_json}

【对话历史】
{chat_history}

【用户指令】
{instruction}

如果用户指令明确,返回修改计划 JSON
{"action": "modify", "modules": [{"module": "模块名", "instruction": "具体修改要求"}], "updatedModulesLabel": "中文模块名列表"}

如果用户指令不明确或需要澄清,返回对话 JSON
{"action": "chat", "message": "你的追问内容"}

模块名可选:
- resume:主表(个人信息,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl
- education:教育经历
- work:工作经历
- internship:实习经历
- project:项目经历
- competition:竞赛经历
只返回 JSON,不要其他内容。

5. Agent - 模块修改 Prompt

你是一个简历编辑助手。根据修改要求,修改简历的指定模块。

【目标岗位】
{job_title}

【修改要求】
{module_instruction}

【模块数据结构】
{module_schema}

【当前模块数据】
{module_data}

规则:
1. 严格按照修改要求操作,可以增删改
2. 未要求修改的记录保持不变
3. 不要编造用户简历中不存在的内容
4. 保持原文格式和结构
5. description 字段是 [{id, text}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接从数组中移除
6. 新增记录时按照模块数据结构生成完整字段,id 使用随机8位字符串
7. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)

各模块数据结构定义(传入 prompt 的 module_schema

resume(主表)

{ "avatarUrl": "string", "name": "string", "email": "string", "mobileNumber": "string", "city": "string", "wechatNumber": "string", "portfolioUrl": "string", "skills": ["string"], "certificates": ["string"], "summary": "string" }

education

[{ "id": "string(8位)", "school": "string", "major": "string", "degree": "大专/本科/硕士/博士", "studyType": "全日制/非全日制", "startDate": "2023.09", "endDate": "2024.06", "description": [{"id": "string(8位)", "text": "string"}] }]

work

[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]

internship

[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]

project

[{ "id": "string(8位)", "companyName": "string", "projectName": "string", "role": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]

competition

[{ "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 结构

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()

常量

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 路由