428 lines
19 KiB
Markdown
428 lines
19 KiB
Markdown
# 简历诊断功能 - 技术方案
|
||
|
||
## 一、功能概述
|
||
|
||
对用户已有简历的**描述文本**进行 AI 诊断,找出问题、给出改进建议和 AI 改写版本,生成诊断报告。
|
||
|
||
### 诊断范围(只诊断描述文本)
|
||
|
||
| 来源表 | 字段 | module_type |
|
||
|--------|------|-------------|
|
||
| bg_user_resume | summary(个人概述) | summary |
|
||
| bg_user_resume_education | description | education |
|
||
| bg_user_resume_work | description | work |
|
||
| bg_user_resume_internship | description | internship |
|
||
| bg_user_resume_project | description | project |
|
||
| bg_user_resume_competition | description | competition |
|
||
|
||
### 执行策略:先分后合
|
||
|
||
```
|
||
第一阶段:所有模块记录并行 AI 诊断(asyncio.gather)
|
||
第二阶段:汇总 issues → 代码算评级 → AI 生成整体评价 → 写入数据库
|
||
```
|
||
|
||
AI 模型:`LLM.DOUBAO_SEED_PRO`,暂不接入功能权限校验。
|
||
|
||
---
|
||
|
||
## 二、问题分类与诊断判断思路
|
||
|
||
### 2.1 紧急修复(urgent)
|
||
|
||
#### typo — 错别字 / 语病
|
||
|
||
**判断思路:**
|
||
- 检查文本中是否存在明显的**错别字**(如"功则"应为"功能"、"负责"写成"付责")
|
||
- 检查是否存在**语病**:主谓搭配不当、语序混乱、成分残缺
|
||
- 检查**标点符号**使用错误:中英文标点混用、缺少句号、逗号过多造成长句
|
||
- 检查**用词不当**:近义词误用、口语化表达出现在正式简历中
|
||
|
||
**AI 判断信号:**
|
||
- 同音字替换(的/得/地 混用)
|
||
- 句子读不通顺,需要反复阅读才能理解
|
||
- 专业术语拼写错误
|
||
- 一句话中出现多个逗号,缺少句号断句
|
||
|
||
---
|
||
|
||
### 2.2 重点优化(important)
|
||
|
||
#### no_result — 缺少成果
|
||
|
||
**判断思路:**
|
||
- 描述只写了"做了什么"(任务/职责),没有写"做出了什么结果"
|
||
- 典型的**流水账式描述**:只有动作没有产出
|
||
- 缺少对业务/团队/项目的**实际贡献**和**影响**
|
||
|
||
**AI 判断信号:**
|
||
- 只有动词+宾语("负责XX系统开发"、"参与XX项目"),没有后续的结果说明
|
||
- 全是过程描述,找不到"提升了"、"优化了"、"实现了"、"完成了"等结果性表述
|
||
- 对比 STAR 法则:有 Situation + Task + Action,但缺 Result
|
||
|
||
**正面示例(有成果):**
|
||
> 负责用户中心系统重构,将接口响应时间从 800ms 降至 200ms,用户投诉率下降 60%
|
||
|
||
**反面示例(缺成果):**
|
||
> 负责用户中心系统重构,使用 Spring Boot + Redis 实现了新的架构
|
||
|
||
#### no_quantify — 缺少量化
|
||
|
||
**判断思路:**
|
||
- 有成果描述但**没有具体数字**支撑
|
||
- 使用了模糊表达:"大幅提升"、"显著改善"、"大量用户",但没有具体数值
|
||
- 缺少以下任何维度的量化:人数、金额、百分比、时间周期、覆盖范围、处理规模
|
||
|
||
**AI 判断信号:**
|
||
- 出现"大幅"、"显著"、"有效"、"极大"等模糊程度副词,但没有跟随数字
|
||
- 提到了结果但只是定性描述,没有定量数据
|
||
- 可以合理推断应该有数据但未提供(如"提升了性能"没说提升多少)
|
||
|
||
**正面示例(有量化):**
|
||
> 优化数据库查询,将平均响应时间从 2s 降至 200ms,日处理订单量从 5 万提升至 20 万
|
||
|
||
**反面示例(缺量化):**
|
||
> 优化数据库查询,显著提升了系统性能,改善了用户体验
|
||
|
||
#### weak_relevance — 岗位相关性弱
|
||
|
||
> **前置条件:仅在 `target_position` 有值时才判断,未填目标岗位则跳过此维度,计数为 0。**
|
||
|
||
**判断思路:**
|
||
- 需要结合 `target_position`(目标岗位)进行判断
|
||
- 描述内容与目标岗位的**核心职责**关联度低
|
||
- 花大量篇幅描述与目标岗位**无关的技能或经历**
|
||
- 对于目标岗位来说,这段描述**无法体现匹配度**
|
||
|
||
**AI 判断信号:**
|
||
- 目标岗位是"Java后端工程师",但描述中全是前端或运营内容
|
||
- 描述的技能/工具与目标岗位的 JD 常见要求差距大
|
||
- 可转移技能存在但未被强调,反而突出了无关内容
|
||
|
||
---
|
||
|
||
### 2.3 表达提升(expression)
|
||
|
||
#### not_concise — 表述不精炼
|
||
|
||
**判断思路:**
|
||
- 句子**偏长**(单句超过 50 字),信息密度低
|
||
- 存在**赘词和重复表达**:"进行了开发"可简化为"开发了"
|
||
- **信息堆叠**:一句话塞了太多内容,应拆分为多个要点
|
||
- 使用了**空泛的修饰词**:"充分"、"积极"、"认真"等无实质信息
|
||
|
||
**AI 判断信号:**
|
||
- 单个描述段落超过 80 字但核心信息只有一个
|
||
- 出现"进行了"、"完成了对...的"、"负责了...的工作"等冗余句式
|
||
- 同一段落中重复表达相似的意思
|
||
- 可以删去一半文字而不损失关键信息
|
||
|
||
**正面示例(精炼):**
|
||
> 设计并实现分布式缓存方案,QPS 从 1000 提升至 8000,缓存命中率 95%
|
||
|
||
**反面示例(不精炼):**
|
||
> 在项目中,我积极主动地参与了分布式缓存方案的设计与实现工作,通过对缓存策略的深入研究和反复优化,最终成功地将系统的 QPS 从原来的 1000 提升到了 8000
|
||
|
||
#### format_inconsistent — 格式不统一
|
||
|
||
**判断思路:**
|
||
- 同一份简历中**时间格式不统一**(有的写"2023.06",有的写"2023年6月",有的写"2023/06")
|
||
- **标点风格不统一**:有的段落用分号结尾,有的用句号,有的不加标点
|
||
- **数字写法不统一**:有的用阿拉伯数字,有的用中文数字
|
||
- **项目符号不统一**:有的用"•",有的用"-",有的用"1."
|
||
- **人称不统一**:有的用"我",有的用第三人称,有的省略主语
|
||
|
||
**AI 判断信号:**
|
||
- 同一段描述中出现两种以上的格式风格
|
||
- 与模块上下文中的时间格式不一致
|
||
- 段落之间的排版结构差异明显
|
||
|
||
---
|
||
|
||
## 三、综合评级规则
|
||
|
||
由代码硬算,不依赖 AI 判断:
|
||
|
||
| 评级 | 条件 | 评语 |
|
||
|------|------|------|
|
||
| A(优秀) | urgent=0, important<=1, expression<=1 | 您的简历相当出彩,在求职市场中格外抢眼,能清晰展现您的优势与经历,已经超越绝大多数候选人了。 |
|
||
| B(良好) | urgent=0, important 2-3, expression<=2 | 简历已经很棒了,但还有提升的潜力。再调整一下细节,会有更具有竞争力! |
|
||
| C(一般) | urgent=1, 或 important 3-4 | 你的简历还有打磨空间,多推敲细节、补充些具体内容,整体会更出彩。 |
|
||
| D(待提升) | urgent>=2, 或 (important>=4 且 has_weak_relevance) | 您的简历目前还有较大提升空间,建议尽快补充关键经历、完善内容表达,并优化整体结构。 |
|
||
|
||
判断优先级:从 D → C → B → A 依次判断,命中即返回。
|
||
|
||
---
|
||
|
||
## 四、数据库表设计
|
||
|
||
### bg_resume_diagnosis_report
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | BigInteger | 主键,雪花ID |
|
||
| resume_id | BigInteger | 关联 bg_user_resume.id |
|
||
| user_id | BigInteger | 用户ID |
|
||
| grade | VARCHAR(1) | 评级 A/B/C/D |
|
||
| summary | TEXT | AI 生成的整体评价 |
|
||
| urgent_total | Integer | 紧急修复总数 |
|
||
| important_total | Integer | 重点优化总数 |
|
||
| expression_total | Integer | 表达提升总数 |
|
||
| create_time | DateTime | 创建时间 |
|
||
| update_time | DateTime | 更新时间 |
|
||
|
||
### bg_resume_diagnosis_issue
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | BigInteger | 主键,雪花ID |
|
||
| report_id | BigInteger | 关联 report.id |
|
||
| resume_id | BigInteger | 关联 bg_user_resume.id |
|
||
| user_id | BigInteger | 用户ID |
|
||
| module_type | VARCHAR(32) | summary/education/work/internship/project/competition |
|
||
| module_record_id | BigInteger | 模块记录ID(summary 时为 resume_id) |
|
||
| finding | TEXT | 诊断发现 |
|
||
| importance | TEXT | 为什么重要 |
|
||
| suggestion | TEXT | 改进建议 |
|
||
| urgent_issues | JSON | {"typo": 0} |
|
||
| important_issues | JSON | {"no_result": 0, "no_quantify": 0, "weak_relevance": 0} |
|
||
| expression_issues | JSON | {"not_concise": 0, "format_inconsistent": 0} |
|
||
| optimized_content | JSON | AI改写后的内容。子表模块(education/work/internship/project/competition)与原 description 格式一致 `[{id, text}]`,保持原 id 不变只改写 text;summary 模块为纯文本字符串 |
|
||
| status | Integer | 0=待处理 1=已处理 |
|
||
| user_feedback | Integer | 0=未评价 1=符合 2=不符合 |
|
||
| create_time | DateTime | 创建时间 |
|
||
| update_time | DateTime | 更新时间 |
|
||
|
||
### 建表 SQL
|
||
|
||
```sql
|
||
CREATE TABLE `bg_resume_diagnosis_report` (
|
||
`id` bigint NOT NULL COMMENT '主键,雪花ID',
|
||
`resume_id` bigint NOT NULL COMMENT '关联bg_user_resume.id',
|
||
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||
`grade` varchar(1) DEFAULT NULL COMMENT '评级 A/B/C/D',
|
||
`summary` text COMMENT 'AI生成的整体评价',
|
||
`urgent_total` int NOT NULL DEFAULT '0' COMMENT '紧急修复总数',
|
||
`important_total` int NOT NULL DEFAULT '0' COMMENT '重点优化总数',
|
||
`expression_total` int NOT NULL DEFAULT '0' COMMENT '表达提升总数',
|
||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_resume_id` (`resume_id`),
|
||
KEY `idx_user_id` (`user_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='简历诊断报告表';
|
||
|
||
CREATE TABLE `bg_resume_diagnosis_issue` (
|
||
`id` bigint NOT NULL COMMENT '主键,雪花ID',
|
||
`report_id` bigint NOT NULL COMMENT '关联report.id',
|
||
`resume_id` bigint NOT NULL COMMENT '关联bg_user_resume.id',
|
||
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||
`module_type` varchar(32) NOT NULL COMMENT '模块类型: summary/education/work/internship/project/competition',
|
||
`module_record_id` bigint NOT NULL COMMENT '模块记录ID,summary时为resume_id',
|
||
`finding` text COMMENT '诊断发现',
|
||
`importance` text COMMENT '为什么重要',
|
||
`suggestion` text COMMENT '改进建议',
|
||
`urgent_issues` json DEFAULT NULL COMMENT '紧急修复子类型计数 {"typo": 0}',
|
||
`important_issues` json DEFAULT NULL COMMENT '重点优化子类型计数 {"no_result": 0, "no_quantify": 0, "weak_relevance": 0}',
|
||
`expression_issues` json DEFAULT NULL COMMENT '表达提升子类型计数 {"not_concise": 0, "format_inconsistent": 0}',
|
||
`optimized_content` json DEFAULT NULL COMMENT 'AI改写后的内容,子表模块与原description格式一致[{id,text}]保持原id只改写text,summary模块为纯文本字符串',
|
||
`status` int NOT NULL DEFAULT '0' COMMENT '0=待处理 1=已处理',
|
||
`user_feedback` int NOT NULL DEFAULT '0' COMMENT '0=未评价 1=符合 2=不符合',
|
||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
PRIMARY KEY (`id`),
|
||
KEY `idx_report_id` (`report_id`),
|
||
KEY `idx_resume_id` (`resume_id`),
|
||
KEY `idx_user_id` (`user_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='简历诊断问题表';
|
||
```
|
||
|
||
---
|
||
|
||
## 五、API 设计
|
||
|
||
### 1. POST /resume/diagnose — 触发诊断
|
||
|
||
**请求体:** `{"resume_id": 123}`
|
||
|
||
**执行流程:**
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────┐
|
||
│ 1. RequestContext.user_id.get() 获取当前用户 │
|
||
├──────────────────────────────────────────────────┤
|
||
│ 2. 短事务1(只读) │
|
||
│ async for session in get_db(): │
|
||
│ service = ResumeDiagnoseService(session) │
|
||
│ resume, tasks = await service │
|
||
│ .load_resume_data(resume_id, user_id) │
|
||
│ → 加载主表 + 5 张子表数据 │
|
||
│ → 组装 AI 任务列表 │
|
||
│ → 事务结束,释放数据库连接 │
|
||
├──────────────────────────────────────────────────┤
|
||
│ 3. 校验 tasks 非空 │
|
||
│ → 空则 raise ValueError("无可诊断内容") │
|
||
├──────────────────────────────────────────────────┤
|
||
│ 4. 并行 AI 诊断(不持有数据库连接) │
|
||
│ ai_results = await diagnose_all(tasks) │
|
||
│ → asyncio.gather 并行调用 N 条诊断链 │
|
||
│ → 每条链独立容错,失败返回空结果 │
|
||
├──────────────────────────────────────────────────┤
|
||
│ 5. 汇总统计 + 评级(纯计算,无 IO) │
|
||
│ → 遍历 ai_results 统计问题数量 │
|
||
│ → 过滤无问题的记录(所有计数都为 0 则跳过) │
|
||
│ → 代码硬算评级(_calc_grade) │
|
||
├──────────────────────────────────────────────────┤
|
||
│ 6. AI 生成整体评价(不持有数据库连接) │
|
||
│ summary = await generate_summary(...) │
|
||
│ → 传入评级、统计、all_findings │
|
||
│ → 返回纯文本评价 │
|
||
├──────────────────────────────────────────────────┤
|
||
│ 7. 短事务2(纯写入,无 AI 调用) │
|
||
│ async for session in get_db(): │
|
||
│ service = ResumeDiagnoseService(session) │
|
||
│ report_id = await service.save_report(...) │
|
||
│ → 写入 report + issues 表 │
|
||
│ → 事务提交 │
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**响应:** `{"reportId": 456}`
|
||
|
||
### 2. GET /resume/diagnose/{resume_id} — 查询最近一次报告
|
||
|
||
**执行流程:**
|
||
1. 获取 user_id
|
||
2. 查 report 表(ORDER BY create_time DESC LIMIT 1)
|
||
3. 查该 report 下所有 issues
|
||
4. 返回 `{"report": {...}, "issues": [...]}`
|
||
|
||
**响应示例:**
|
||
```json
|
||
{
|
||
"report": {
|
||
"id": "123",
|
||
"resumeId": "456",
|
||
"grade": "B",
|
||
"summary": "您的简历整体质量良好...",
|
||
"urgentTotal": 0,
|
||
"importantTotal": 3,
|
||
"expressionTotal": 1,
|
||
"createTime": "2026-04-07 10:30:00"
|
||
},
|
||
"issues": [
|
||
{
|
||
"id": "789",
|
||
"moduleType": "work",
|
||
"moduleRecordId": "1001",
|
||
"finding": "工作描述缺少量化数据...",
|
||
"importance": "量化数据能让招聘者...",
|
||
"suggestion": "建议在描述中添加...",
|
||
"urgentIssues": {"typo": 0},
|
||
"importantIssues": {"no_result": 0, "no_quantify": 1, "weak_relevance": 0},
|
||
"expressionIssues": {"not_concise": 0, "format_inconsistent": 0},
|
||
"optimizedContent": [{"id": "abc123", "text": "负责XX系统后端开发,日均处理100万+请求..."}],
|
||
"status": 0,
|
||
"userFeedback": 0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 3. PUT /resume/diagnose/issue/{issue_id}/resolve — 标记已处理
|
||
|
||
**请求体:** `{"user_feedback": 1}` (1=符合 2=不符合)
|
||
|
||
**执行流程:**
|
||
1. 获取 user_id
|
||
2. 查 issue 记录(校验 user_id)
|
||
3. 设置 status=1, user_feedback
|
||
4. 返回 null
|
||
|
||
---
|
||
|
||
## 六、文件变更清单
|
||
|
||
### 新增 7 个文件
|
||
|
||
```
|
||
app/models/resume_diagnosis_report.py — 诊断报告 ORM
|
||
app/models/resume_diagnosis_issue.py — 诊断问题 ORM
|
||
app/ai/resume_diagnoser/__init__.py — 包初始化(空文件)
|
||
app/ai/resume_diagnoser/prompts.py — Prompt 模板
|
||
app/ai/resume_diagnoser/diagnoser.py — AI 诊断引擎
|
||
app/services/resume_diagnose_service.py — 业务逻辑
|
||
app/api/resume_diagnose.py — API 路由
|
||
```
|
||
|
||
### 修改 1 个文件
|
||
|
||
```
|
||
app/main.py — 注册新路由(加 2 行)
|
||
```
|
||
|
||
---
|
||
|
||
## 七、模块设计
|
||
|
||
### AI 诊断模块 (`app/ai/resume_diagnoser/`)
|
||
|
||
**prompts.py** — 两个 Prompt 模板:
|
||
- `DIAGNOSE_MODULE_PROMPT`:第一阶段,单条记录诊断,输入 module_type/target_position/context/description_text(子表传原始 JSON `[{id,text}]`,summary 传纯文本),输出 JSON
|
||
- `SUMMARY_PROMPT`:第二阶段,汇总评价,输入统计数据 + 所有 findings,输出纯文本
|
||
|
||
**diagnoser.py** — 参照 `app/ai/resume_extractor/extractor.py` 的模式:
|
||
- 诊断链:`ChatPromptTemplate → DOUBAO_SEED_PRO(temperature=0) → JsonOutputParser`
|
||
- 汇总链:`ChatPromptTemplate → DOUBAO_SEED_PRO(temperature=0.3) → StrOutputParser`
|
||
- `diagnose_all(tasks)` — asyncio.gather 并行
|
||
- `generate_summary(...)` — AI 生成整体评价
|
||
- `_safe_invoke()` — 容错
|
||
|
||
### Service 层 (`app/services/resume_diagnose_service.py`)
|
||
|
||
```
|
||
ResumeDiagnoseService(session: AsyncSession)
|
||
├─ load_resume_data(resume_id, user_id) → (resume, tasks)
|
||
│ 加载主表 + 5 张子表,组装 AI 任务列表
|
||
│
|
||
├─ save_report(resume_id, user_id, grade, summary, stats, tasks, ai_results) → report_id
|
||
│ 纯写入:接收已算好的 grade、summary、统计数据,写入 report + issues
|
||
│ 跳过无问题的记录(所有计数为 0 则不创建 issue 行)
|
||
│
|
||
├─ get_latest_report(resume_id, user_id) → dict | None
|
||
└─ resolve_issue(issue_id, user_id, user_feedback) → None
|
||
|
||
工具函数(无状态,路由层或 Service 外部可调用):
|
||
├─ _build_description_text(description) — 子表传原始 JSON 字符串 [{id,text}],summary 传纯文本
|
||
├─ _calc_grade(urgent, important, expression, has_weak_relevance) — 评级硬算
|
||
├─ _aggregate_results(tasks, ai_results) — 统计汇总 + 过滤无问题记录
|
||
└─ _issue_to_dict(issue) — ORM → camelCase dict
|
||
```
|
||
|
||
**description_text 传入格式:**
|
||
- 子表模块:传原始 JSON 字符串 `[{"id": "abc123", "text": "负责XX系统..."}, ...]`,AI 能看到每个段落的 id
|
||
- summary 模块:传纯文本(summary 字段本身就是 VARCHAR)
|
||
|
||
**optimized_content 返回格式:**
|
||
- 子表模块:与原始 description 格式一致 `[{id, text}]`,保持原 id 不变,只改写 text
|
||
- summary 模块:纯文本字符串(与原始 summary 字段格式一致)
|
||
|
||
### API 路由 (`app/api/resume_diagnose.py`)
|
||
|
||
```
|
||
router = APIRouter(prefix="/resume/diagnose", tags=["简历诊断"])
|
||
├─ POST "" — 触发诊断
|
||
├─ GET "/{resume_id}" — 查询报告
|
||
└─ PUT "/issue/{issue_id}/resolve" — 标记已处理
|
||
```
|
||
|
||
---
|
||
|
||
## 八、实施顺序
|
||
|
||
1. ORM 模型(`resume_diagnosis_report.py` + `resume_diagnosis_issue.py`)
|
||
2. AI 模块(`__init__.py` + `prompts.py` + `diagnoser.py`)
|
||
3. Service 层(`resume_diagnose_service.py`)
|
||
4. API 路由(`resume_diagnose.py`)+ 修改 `main.py`
|
||
5. 更新项目结构文档
|