Files
offerpai_python_ai/.kiro/specs/resume-diagnose.md
T
2026-04-07 20:15:43 +08:00

428 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 简历诊断功能 - 技术方案
## 一、功能概述
对用户已有简历的**描述文本**进行 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 | 模块记录IDsummary 时为 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 '模块记录IDsummary时为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只改写textsummary模块为纯文本字符串',
`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. 更新项目结构文档