优化 岗位编辑agent 性能
This commit is contained in:
@@ -282,14 +282,16 @@
|
|||||||
|
|
||||||
### 处理流程(两步走)
|
### 处理流程(两步走)
|
||||||
|
|
||||||
|
核心思路:将用户操作原子化,把模块级并发拆成记录级并发,减少单次 AI 输出量,提升响应速度。
|
||||||
|
|
||||||
#### 第一步:准备数据
|
#### 第一步:准备数据
|
||||||
|
|
||||||
1. 从 Redis 取当前定制简历(不存在则报错)
|
1. 从 Redis 取当前定制简历(不存在则报错)
|
||||||
2. 查 `bg_job` 拿 title、description
|
2. 查 `bg_job` 拿 title、description、requirement
|
||||||
|
|
||||||
#### 第二步:规划 AI(意图识别)
|
#### 第二步:规划 AI(意图识别 + 操作原子化)
|
||||||
|
|
||||||
输入:用户指令 + 对话历史 + 当前完整简历内容 + 岗位 title
|
输入:用户指令 + 对话历史 + 当前完整简历内容 + 岗位信息(title + description + requirement)
|
||||||
|
|
||||||
输出两种结果:
|
输出两种结果:
|
||||||
|
|
||||||
@@ -299,18 +301,31 @@
|
|||||||
```
|
```
|
||||||
→ 直接返回 `{ "type": "message", "message": "..." }`,结束。
|
→ 直接返回 `{ "type": "message", "message": "..." }`,结束。
|
||||||
|
|
||||||
**修改计划(指令明确)**:
|
**操作计划(指令明确)**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "modify",
|
"action": "modify",
|
||||||
"modules": [
|
"operations": [
|
||||||
{ "module": "resume", "instruction": "在 summary 中融入数据分析技能,skills 添加 Python" },
|
{ "type": "delete", "module": "work", "id": "abc12345" },
|
||||||
{ "module": "work", "instruction": "精简第一段工作经历,突出量化成果" }
|
{ "type": "update", "module": "work", "id": "def67890", "instruction": "精简描述,突出量化成果" },
|
||||||
],
|
{ "type": "update", "module": "resume", "instruction": "在summary中融入数据分析关键词" },
|
||||||
"updatedModulesLabel": "个人简介、工作经验"
|
{ "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 个):
|
模块划分(按表结构,共 6 个):
|
||||||
|
|
||||||
| 模块名 | 对应表 | 可修改字段 |
|
| 模块名 | 对应表 | 可修改字段 |
|
||||||
@@ -322,25 +337,27 @@
|
|||||||
| `project` | `bg_user_resume_project` | 全部字段 |
|
| `project` | `bg_user_resume_project` | 全部字段 |
|
||||||
| `competition` | `bg_user_resume_competition` | 全部字段 |
|
| `competition` | `bg_user_resume_competition` | 全部字段 |
|
||||||
|
|
||||||
#### 第三步:按模块并发执行修改
|
#### 第三步:按操作类型并发执行
|
||||||
|
|
||||||
根据修改计划,对每个模块并发调 AI(asyncio.gather):
|
先处理 delete(零 AI 开销),再并发执行所有 update 和 add(asyncio.gather):
|
||||||
- 每个模块传入该模块的完整数据(如 work 传 `[{工作1}, {工作2}]` 整体)
|
|
||||||
- AI 返回修改后的该模块完整数据
|
- **delete**:后端直接按 module + id 从数组移除,不调 AI
|
||||||
- 所有模块统一走 AI,包括 skills 等简单操作(因为需要 AI 理解用户自然语言指令)
|
- **update(含 resume 主表和子表记录)**:按 id 取出单条记录 + instruction + schema + 岗位信息(title + description + requirement)+ 最近 10 条对话历史,调 AI 修改,只输出单条记录。resume 主表和子表记录复用同一个 prompt
|
||||||
|
- **add**:instruction + schema + 岗位信息(title + description + requirement)+ 最近 10 条对话历史,调 AI 生成一条新记录
|
||||||
|
|
||||||
#### 第四步:合并
|
#### 第四步:合并
|
||||||
|
|
||||||
- AI 调用失败的模块保持原数据不动
|
- delete 已直接处理
|
||||||
- 成功的模块直接用 AI 返回结果替换
|
- update 按 module + id 替换回原数据(resume 主表直接整体替换)
|
||||||
|
- add 按 module 追加到对应数组
|
||||||
把所有模块结果合并回完整简历。
|
- AI 调用失败的操作跳过,不影响其他操作
|
||||||
|
|
||||||
#### 第五步:保存 + 返回
|
#### 第五步:保存 + 返回
|
||||||
|
|
||||||
1. 当前简历存为回滚数据(key:`customize:resume:rollback:{userId}`,过期 30 分钟)
|
1. 当前简历存为回滚数据(key:`customize:resume:rollback:{userId}`,过期 30 分钟)
|
||||||
2. 新简历覆盖 Redis(key:`customize:resume:{userId}`),刷新过期时间 12 小时
|
2. 新简历覆盖 Redis(key:`customize:resume:{userId}`),刷新过期时间 12 小时
|
||||||
3. 返回 `type: updated` + 消息(前端通过 GET 接口查询新简历,通过回滚接口恢复)
|
3. `updatedModulesLabel` 由后端根据 operations 中的 module 去重映射中文名拼接
|
||||||
|
4. 返回 `type: updated` + 消息(前端通过 GET 接口查询新简历,通过回滚接口恢复)
|
||||||
|
|
||||||
### description 字段处理
|
### description 字段处理
|
||||||
|
|
||||||
@@ -427,10 +444,11 @@
|
|||||||
### 4. Agent - 规划 Prompt
|
### 4. Agent - 规划 Prompt
|
||||||
|
|
||||||
```
|
```
|
||||||
你是一个简历编辑助手。分析用户的指令,决定需要修改简历的哪些模块。
|
你是一个简历编辑助手。分析用户的指令,将其拆解为原子操作。
|
||||||
|
|
||||||
【目标岗位】
|
【目标岗位】
|
||||||
{job_title}
|
{job_title}
|
||||||
|
{job_description}
|
||||||
|
|
||||||
【当前简历】
|
【当前简历】
|
||||||
{resume_json}
|
{resume_json}
|
||||||
@@ -441,47 +459,81 @@
|
|||||||
【用户指令】
|
【用户指令】
|
||||||
{instruction}
|
{instruction}
|
||||||
|
|
||||||
如果用户指令明确,返回修改计划 JSON:
|
如果用户指令不明确或需要澄清,返回:
|
||||||
{"action": "modify", "modules": [{"module": "模块名", "instruction": "具体修改要求"}], "updatedModulesLabel": "中文模块名列表"}
|
|
||||||
|
|
||||||
如果用户指令不明确或需要澄清,返回对话 JSON:
|
|
||||||
{"action": "chat", "message": "你的追问内容"}
|
{"action": "chat", "message": "你的追问内容"}
|
||||||
|
|
||||||
模块名可选:
|
如果用户指令明确,将其拆解为原子操作列表,返回:
|
||||||
- resume:主表(个人信息,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl)
|
{"action": "modify", "operations": [...]}
|
||||||
- education:教育经历
|
|
||||||
- work:工作经历
|
操作类型:
|
||||||
- internship:实习经历
|
1. 删除记录:{"type": "delete", "module": "模块名", "id": "记录id"}
|
||||||
- project:项目经历
|
2. 修改记录:{"type": "update", "module": "模块名", "id": "记录id", "instruction": "修改说明(50字内)"}
|
||||||
- competition:竞赛经历
|
3. 修改主表:{"type": "update", "module": "resume", "instruction": "修改说明(50字内)"}
|
||||||
只返回 JSON,不要其他内容。
|
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
|
### 5. Agent - 单条记录修改 Prompt
|
||||||
|
|
||||||
```
|
```
|
||||||
你是一个简历编辑助手。根据修改要求,修改简历的指定模块。
|
你是一个简历编辑助手。根据修改要求,修改简历中的一条记录。
|
||||||
|
|
||||||
【目标岗位】
|
【目标岗位】
|
||||||
{job_title}
|
{job_title}
|
||||||
|
{job_description}
|
||||||
|
|
||||||
【修改要求】
|
【修改要求】
|
||||||
{module_instruction}
|
{instruction}
|
||||||
|
|
||||||
|
【最近对话】
|
||||||
|
{chat_history}
|
||||||
|
|
||||||
【模块数据结构】
|
【模块数据结构】
|
||||||
{module_schema}
|
{module_schema}
|
||||||
|
|
||||||
【当前模块数据】
|
【当前记录数据】
|
||||||
{module_data}
|
{record_data}
|
||||||
|
|
||||||
规则:
|
规则:
|
||||||
1. 严格按照修改要求操作,可以增删改
|
1. 严格按照修改要求操作
|
||||||
2. 未要求修改的记录保持不变
|
2. 未要求修改的字段保持不变
|
||||||
3. 不要编造用户简历中不存在的内容
|
3. 不要编造用户简历中不存在的内容
|
||||||
4. 保持原文格式和结构
|
4. 保持原文格式和结构
|
||||||
5. description 字段是 [{id, text}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接从数组中移除
|
5. description 字段是 [{id, text}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
|
||||||
6. 新增记录时按照模块数据结构生成完整字段,id 使用随机8位字符串
|
6. 返回修改后的完整记录数据(JSON 格式,与输入格式一致)
|
||||||
7. 返回修改后的完整模块数据(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)
|
### 各模块数据结构定义(传入 prompt 的 module_schema)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from langchain_core.prompts import ChatPromptTemplate
|
|||||||
from app.ai.models import LLM
|
from app.ai.models import LLM
|
||||||
from app.ai.skill_gap_analyzer.prompts import (
|
from app.ai.skill_gap_analyzer.prompts import (
|
||||||
SKILL_GAP_PROMPT, SUMMARY_OPTIMIZE_PROMPT, EXPERIENCE_OPTIMIZE_PROMPT,
|
SKILL_GAP_PROMPT, SUMMARY_OPTIMIZE_PROMPT, EXPERIENCE_OPTIMIZE_PROMPT,
|
||||||
AGENT_PLAN_PROMPT, AGENT_MODULE_EDIT_PROMPT, MODULE_SCHEMAS,
|
AGENT_PLAN_PROMPT, AGENT_MODULE_EDIT_PROMPT, AGENT_MODULE_ADD_PROMPT, MODULE_SCHEMAS,
|
||||||
)
|
)
|
||||||
from app.core.logger import log
|
from app.core.logger import log
|
||||||
|
|
||||||
@@ -100,11 +100,13 @@ _plan_chain = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def plan_edit(job_title: str, resume_json: str, chat_history: str, instruction: str) -> dict | None:
|
async def plan_edit(job_title: str, job_description: str, resume_json: str,
|
||||||
"""Agent 规划:分析用户指令,返回修改计划或对话回复"""
|
chat_history: str, instruction: str) -> dict | None:
|
||||||
|
"""Agent 规划:分析用户指令,返回原子操作列表或对话回复"""
|
||||||
try:
|
try:
|
||||||
raw = await _plan_chain.ainvoke({
|
raw = await _plan_chain.ainvoke({
|
||||||
"job_title": job_title, "resume_json": resume_json,
|
"job_title": job_title, "job_description": job_description,
|
||||||
|
"resume_json": resume_json,
|
||||||
"chat_history": chat_history, "instruction": instruction,
|
"chat_history": chat_history, "instruction": instruction,
|
||||||
})
|
})
|
||||||
result = _parse_json(raw)
|
result = _parse_json(raw)
|
||||||
@@ -114,24 +116,51 @@ async def plan_edit(job_title: str, resume_json: str, chat_history: str, instruc
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ===== Agent - 模块修改 =====
|
# ===== Agent - 单条记录修改 =====
|
||||||
|
|
||||||
_module_edit_chain = (
|
_record_edit_chain = (
|
||||||
ChatPromptTemplate.from_messages([("system", AGENT_MODULE_EDIT_PROMPT), ("human", "请执行修改。")])
|
ChatPromptTemplate.from_messages([("system", AGENT_MODULE_EDIT_PROMPT), ("human", "请执行修改。")])
|
||||||
| LLM.GPT_4O.create(temperature=0.3)
|
| LLM.GPT_4O.create(temperature=0.3)
|
||||||
| StrOutputParser()
|
| StrOutputParser()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def execute_module_edit(job_title: str, module_instruction: str,
|
async def execute_record_edit(job_title: str, job_description: str, instruction: str,
|
||||||
module_schema: str, module_data: str) -> dict | list | None:
|
chat_history: str, module_schema: str,
|
||||||
"""Agent 模块修改:根据指令修改指定模块数据"""
|
record_data: str) -> dict | list | None:
|
||||||
|
"""修改单条记录:根据指令修改指定记录数据"""
|
||||||
try:
|
try:
|
||||||
raw = await _module_edit_chain.ainvoke({
|
raw = await _record_edit_chain.ainvoke({
|
||||||
"job_title": job_title, "module_instruction": module_instruction,
|
"job_title": job_title, "job_description": job_description,
|
||||||
"module_schema": module_schema, "module_data": module_data,
|
"instruction": instruction, "chat_history": chat_history,
|
||||||
|
"module_schema": module_schema, "record_data": record_data,
|
||||||
})
|
})
|
||||||
return _parse_json(raw)
|
return _parse_json(raw)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"AI模块修改失败: {e}")
|
log.warning(f"AI单条记录修改失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Agent - 新增记录 =====
|
||||||
|
|
||||||
|
_record_add_chain = (
|
||||||
|
ChatPromptTemplate.from_messages([("system", AGENT_MODULE_ADD_PROMPT), ("human", "请生成新记录。")])
|
||||||
|
| LLM.GPT_4O.create(temperature=0.3)
|
||||||
|
| StrOutputParser()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def execute_record_add(job_title: str, job_description: str, instruction: str,
|
||||||
|
chat_history: str, module_schema: str) -> dict | None:
|
||||||
|
"""新增一条记录:根据指令生成新记录"""
|
||||||
|
try:
|
||||||
|
raw = await _record_add_chain.ainvoke({
|
||||||
|
"job_title": job_title, "job_description": job_description,
|
||||||
|
"instruction": instruction, "chat_history": chat_history,
|
||||||
|
"module_schema": module_schema,
|
||||||
|
})
|
||||||
|
result = _parse_json(raw)
|
||||||
|
return result if isinstance(result, dict) else None
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"AI新增记录失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -52,10 +52,11 @@ EXPERIENCE_OPTIMIZE_PROMPT = """你是一个简历优化助手。根据目标岗
|
|||||||
4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
|
4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
|
||||||
5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
|
5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
|
||||||
|
|
||||||
AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,决定需要修改简历的哪些模块。
|
AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,将其拆解为原子操作。
|
||||||
|
|
||||||
【目标岗位】
|
【目标岗位】
|
||||||
{job_title}
|
{job_title}
|
||||||
|
{job_description}
|
||||||
|
|
||||||
【当前简历】
|
【当前简历】
|
||||||
{resume_json}
|
{resume_json}
|
||||||
@@ -66,43 +67,73 @@ AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,
|
|||||||
【用户指令】
|
【用户指令】
|
||||||
{instruction}
|
{instruction}
|
||||||
|
|
||||||
如果用户指令明确,返回修改计划 JSON:
|
如果用户指令不明确或需要澄清,返回:
|
||||||
{{"action": "modify", "modules": [{{"module": "模块名", "instruction": "具体修改要求"}}], "updatedModulesLabel": "中文模块名列表"}}
|
|
||||||
|
|
||||||
如果用户指令不明确或需要澄清,返回对话 JSON:
|
|
||||||
{{"action": "chat", "message": "你的追问内容"}}
|
{{"action": "chat", "message": "你的追问内容"}}
|
||||||
|
|
||||||
模块名可选:
|
如果用户指令明确,将其拆解为原子操作列表,返回:
|
||||||
- resume:主表(个人信息,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl)
|
{{"action": "modify", "operations": [...]}}
|
||||||
- education:教育经历
|
|
||||||
- work:工作经历
|
|
||||||
- internship:实习经历
|
|
||||||
- project:项目经历
|
|
||||||
- competition:竞赛经历
|
|
||||||
只返回 JSON,不要其他内容。"""
|
|
||||||
|
|
||||||
AGENT_MODULE_EDIT_PROMPT = """你是一个简历编辑助手。根据修改要求,修改简历的指定模块。
|
操作类型:
|
||||||
|
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,不要其他内容"""
|
||||||
|
|
||||||
|
AGENT_MODULE_EDIT_PROMPT = """你是一个简历编辑助手。根据修改要求,修改简历中的一条记录。
|
||||||
|
|
||||||
【目标岗位】
|
【目标岗位】
|
||||||
{job_title}
|
{job_title}
|
||||||
|
{job_description}
|
||||||
|
|
||||||
【修改要求】
|
【修改要求】
|
||||||
{module_instruction}
|
{instruction}
|
||||||
|
|
||||||
|
【最近对话】
|
||||||
|
{chat_history}
|
||||||
|
|
||||||
【模块数据结构】
|
【模块数据结构】
|
||||||
{module_schema}
|
{module_schema}
|
||||||
|
|
||||||
【当前模块数据】
|
【当前记录数据】
|
||||||
{module_data}
|
{record_data}
|
||||||
|
|
||||||
规则:
|
规则:
|
||||||
1. 严格按照修改要求操作,可以增删改
|
1. 严格按照修改要求操作
|
||||||
2. 未要求修改的记录保持不变
|
2. 未要求修改的字段保持不变
|
||||||
3. 不要编造用户简历中不存在的内容
|
3. 不要编造用户简历中不存在的内容
|
||||||
4. 保持原文格式和结构
|
4. 保持原文格式和结构
|
||||||
5. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接从数组中移除
|
5. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
|
||||||
6. 新增记录时按照模块数据结构生成完整字段,id 使用随机8位字符串
|
6. 返回修改后的完整记录数据(JSON 格式,与输入格式一致)"""
|
||||||
7. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
|
|
||||||
|
AGENT_MODULE_ADD_PROMPT = """你是一个简历编辑助手。根据要求,生成一条新的简历记录。
|
||||||
|
|
||||||
|
【目标岗位】
|
||||||
|
{job_title}
|
||||||
|
{job_description}
|
||||||
|
|
||||||
|
【新增要求】
|
||||||
|
{instruction}
|
||||||
|
|
||||||
|
【最近对话】
|
||||||
|
{chat_history}
|
||||||
|
|
||||||
|
【模块数据结构】
|
||||||
|
{module_schema}
|
||||||
|
|
||||||
|
规则:
|
||||||
|
1. 按照模块数据结构生成完整字段
|
||||||
|
2. id 使用随机8位字符串
|
||||||
|
3. description 中每个段落的 id 也使用随机8位字符串
|
||||||
|
4. 内容要合理真实,贴合目标岗位方向
|
||||||
|
5. 返回一条完整记录的 JSON,与模块数据结构一致"""
|
||||||
|
|
||||||
# 各模块数据结构定义(传入 prompt 的 module_schema)
|
# 各模块数据结构定义(传入 prompt 的 module_schema)
|
||||||
MODULE_SCHEMAS: dict[str, str] = {
|
MODULE_SCHEMAS: dict[str, str] = {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from app.ai.skill_gap_analyzer.analyzer import (
|
from app.ai.skill_gap_analyzer.analyzer import (
|
||||||
analyze_skill_gap, optimize_summary, optimize_module,
|
analyze_skill_gap, optimize_summary, optimize_module,
|
||||||
plan_edit, execute_module_edit,
|
plan_edit, execute_record_edit, execute_record_add,
|
||||||
)
|
)
|
||||||
from app.ai.skill_gap_analyzer.prompts import MODULE_SCHEMAS
|
from app.ai.skill_gap_analyzer.prompts import MODULE_SCHEMAS
|
||||||
from app.core.logger import log
|
from app.core.logger import log
|
||||||
@@ -40,6 +40,16 @@ CUSTOMIZE_RESUME_ROLLBACK_EXPIRE = 30 * 60 # 30分钟
|
|||||||
|
|
||||||
_CHARS = string.ascii_letters + string.digits
|
_CHARS = string.ascii_letters + string.digits
|
||||||
|
|
||||||
|
# 模块名 → 中文标签映射
|
||||||
|
_MODULE_LABELS = {
|
||||||
|
"resume": "个人简介",
|
||||||
|
"education": "教育经历",
|
||||||
|
"work": "工作经历",
|
||||||
|
"internship": "实习经历",
|
||||||
|
"project": "项目经历",
|
||||||
|
"competition": "竞赛经历",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _rand_id() -> str:
|
def _rand_id() -> str:
|
||||||
"""生成随机8位字符串标识"""
|
"""生成随机8位字符串标识"""
|
||||||
@@ -201,7 +211,7 @@ class SkillGapService:
|
|||||||
|
|
||||||
async def ai_edit_customize_resume(self, user_id: int, job_id: int,
|
async def ai_edit_customize_resume(self, user_id: int, job_id: int,
|
||||||
instruction: str, chat_history: list) -> dict:
|
instruction: str, chat_history: list) -> dict:
|
||||||
"""AI 对话式编辑定制简历"""
|
"""AI 对话式编辑定制简历(原子化操作版)"""
|
||||||
# 1. 取当前定制简历
|
# 1. 取当前定制简历
|
||||||
key = f"{CUSTOMIZE_RESUME_KEY_PREFIX}{user_id}"
|
key = f"{CUSTOMIZE_RESUME_KEY_PREFIX}{user_id}"
|
||||||
raw = await RedisManager.client.get(key)
|
raw = await RedisManager.client.get(key)
|
||||||
@@ -211,69 +221,153 @@ class SkillGapService:
|
|||||||
resume_json = cr.model_dump_json(by_alias=True)
|
resume_json = cr.model_dump_json(by_alias=True)
|
||||||
# 2. 查岗位
|
# 2. 查岗位
|
||||||
job = await self._get_job(job_id)
|
job = await self._get_job(job_id)
|
||||||
# 3. 规划 AI
|
job_desc = f"{job.description or ''}\n{job.requirement or ''}"
|
||||||
|
# 3. 规划 AI(意图识别 + 操作原子化)
|
||||||
history_str = json.dumps(chat_history, ensure_ascii=False) if chat_history else "无"
|
history_str = json.dumps(chat_history, ensure_ascii=False) if chat_history else "无"
|
||||||
plan = await plan_edit(job.title or "", resume_json, history_str, instruction)
|
plan = await plan_edit(job.title or "", job_desc, resume_json, history_str, instruction)
|
||||||
if not plan:
|
if not plan:
|
||||||
return {"type": "message", "message": "抱歉,我没有理解你的意思,请再描述一下。"}
|
return {"type": "message", "message": "抱歉,我没有理解你的意思,请再描述一下。"}
|
||||||
if plan.get("action") == "chat":
|
if plan.get("action") == "chat":
|
||||||
return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")}
|
return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")}
|
||||||
# 4. 按模块并发执行修改
|
# 4. 解析操作列表
|
||||||
modules = plan.get("modules", [])
|
operations = plan.get("operations", [])
|
||||||
if not modules:
|
if not operations:
|
||||||
return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")}
|
return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")}
|
||||||
edit_tasks = []
|
# 截取最近10条对话历史
|
||||||
for m in modules:
|
recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
|
||||||
mod_name = m.get("module", "")
|
recent_history_str = json.dumps(recent_history, ensure_ascii=False) if recent_history else "无"
|
||||||
mod_instr = m.get("instruction", "")
|
# 5. 按操作类型分发执行
|
||||||
|
# 先处理 delete(零 AI 开销)
|
||||||
|
for op in operations:
|
||||||
|
if op.get("type") == "delete":
|
||||||
|
self._apply_delete(cr, op.get("module", ""), op.get("id", ""))
|
||||||
|
# 并发执行 update 和 add
|
||||||
|
ai_tasks = []
|
||||||
|
for op in operations:
|
||||||
|
op_type = op.get("type", "")
|
||||||
|
mod_name = op.get("module", "")
|
||||||
|
op_instruction = op.get("instruction", "")
|
||||||
schema = MODULE_SCHEMAS.get(mod_name, "")
|
schema = MODULE_SCHEMAS.get(mod_name, "")
|
||||||
mod_data = self._get_module_data(cr, mod_name)
|
if op_type == "update":
|
||||||
edit_tasks.append((mod_name, execute_module_edit(job.title or "", mod_instr, schema, mod_data)))
|
record_data = self._get_record_data(cr, mod_name, op.get("id"))
|
||||||
keys = [t[0] for t in edit_tasks]
|
if record_data is not None:
|
||||||
results = await asyncio.gather(*[t[1] for t in edit_tasks], return_exceptions=True)
|
ai_tasks.append((
|
||||||
# 5. 合并结果
|
"update", mod_name, op.get("id"),
|
||||||
for mod_key, result in zip(keys, results):
|
execute_record_edit(
|
||||||
if isinstance(result, Exception):
|
job.title or "", job_desc, op_instruction,
|
||||||
log.warning(f"AI编辑模块[{mod_key}]失败: {result}")
|
recent_history_str, schema, record_data,
|
||||||
continue
|
),
|
||||||
if result is None:
|
))
|
||||||
continue
|
elif op_type == "add":
|
||||||
self._apply_edit_result(cr, mod_key, result)
|
ai_tasks.append((
|
||||||
|
"add", mod_name, None,
|
||||||
|
execute_record_add(
|
||||||
|
job.title or "", job_desc, op_instruction,
|
||||||
|
recent_history_str, schema,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
# 并发执行
|
||||||
|
if ai_tasks:
|
||||||
|
coros = [t[3] for t in ai_tasks]
|
||||||
|
results = await asyncio.gather(*coros, return_exceptions=True)
|
||||||
|
for (op_type, mod_name, record_id, _), result in zip(ai_tasks, results):
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
log.warning(f"AI编辑[{op_type}/{mod_name}/{record_id}]失败: {result}")
|
||||||
|
continue
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
if op_type == "update":
|
||||||
|
self._apply_record_update(cr, mod_name, record_id, result)
|
||||||
|
elif op_type == "add":
|
||||||
|
self._apply_record_add(cr, mod_name, result)
|
||||||
# 6. 保存回滚 + 新版本
|
# 6. 保存回滚 + 新版本
|
||||||
rollback_key = f"{CUSTOMIZE_RESUME_ROLLBACK_KEY_PREFIX}{user_id}"
|
rollback_key = f"{CUSTOMIZE_RESUME_ROLLBACK_KEY_PREFIX}{user_id}"
|
||||||
await RedisManager.client.set(rollback_key, raw, ex=CUSTOMIZE_RESUME_ROLLBACK_EXPIRE)
|
await RedisManager.client.set(rollback_key, raw, ex=CUSTOMIZE_RESUME_ROLLBACK_EXPIRE)
|
||||||
await self._save_customize_resume(user_id, cr)
|
await self._save_customize_resume(user_id, cr)
|
||||||
label = plan.get("updatedModulesLabel", "简历内容")
|
# 拼接更新模块标签
|
||||||
return {"type": "updated", "message": f"完成!已更新:{label}"}
|
updated_modules = list(dict.fromkeys(op.get("module", "") for op in operations))
|
||||||
|
label = "、".join(_MODULE_LABELS.get(m, m) for m in updated_modules if m)
|
||||||
|
return {"type": "updated", "message": f"完成!已更新:{label or '简历内容'}"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_module_data(cr: CustomizeResume, mod_name: str) -> str:
|
def _get_record_data(cr: CustomizeResume, mod_name: str, record_id: str | None) -> str | None:
|
||||||
"""获取指定模块的 JSON 数据"""
|
"""获取单条记录的 JSON 数据,resume 主表返回整个对象"""
|
||||||
if mod_name == "resume":
|
if mod_name == "resume":
|
||||||
return cr.resume.model_dump_json(by_alias=True)
|
return cr.resume.model_dump_json(by_alias=True)
|
||||||
mapping = {"education": cr.education, "work": cr.work, "internship": cr.internship,
|
mapping = {
|
||||||
"project": cr.project, "competition": cr.competition}
|
"education": cr.education, "work": cr.work, "internship": cr.internship,
|
||||||
|
"project": cr.project, "competition": cr.competition,
|
||||||
|
}
|
||||||
items = mapping.get(mod_name, [])
|
items = mapping.get(mod_name, [])
|
||||||
return json.dumps([item.model_dump(by_alias=True) for item in items], ensure_ascii=False)
|
if not record_id:
|
||||||
|
return None
|
||||||
|
for item in items:
|
||||||
|
if item.id == record_id:
|
||||||
|
return item.model_dump_json(by_alias=True)
|
||||||
|
log.warning(f"未找到记录[{mod_name}/{record_id}]")
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _apply_edit_result(cr: CustomizeResume, mod_name: str, result) -> None:
|
def _apply_delete(cr: CustomizeResume, mod_name: str, record_id: str) -> None:
|
||||||
"""将 AI 编辑结果应用到定制简历"""
|
"""删除指定模块中的一条记录"""
|
||||||
|
if not record_id or mod_name == "resume":
|
||||||
|
return
|
||||||
|
mapping = {
|
||||||
|
"education": cr.education, "work": cr.work, "internship": cr.internship,
|
||||||
|
"project": cr.project, "competition": cr.competition,
|
||||||
|
}
|
||||||
|
items = mapping.get(mod_name)
|
||||||
|
if items is not None:
|
||||||
|
for i, item in enumerate(items):
|
||||||
|
if item.id == record_id:
|
||||||
|
items.pop(i)
|
||||||
|
break
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _apply_record_update(cr: CustomizeResume, mod_name: str, record_id: str | None, result) -> None:
|
||||||
|
"""将 AI 修改结果替换回对应记录"""
|
||||||
try:
|
try:
|
||||||
if mod_name == "resume" and isinstance(result, dict):
|
if mod_name == "resume" and isinstance(result, dict):
|
||||||
cr.resume = ResumeProfile.model_validate(result)
|
cr.resume = ResumeProfile.model_validate(result)
|
||||||
elif mod_name == "education" and isinstance(result, list):
|
return
|
||||||
cr.education = [Education.model_validate(item) for item in result]
|
model_map = {
|
||||||
elif mod_name == "work" and isinstance(result, list):
|
"education": Education, "work": Work, "internship": Internship,
|
||||||
cr.work = [Work.model_validate(item) for item in result]
|
"project": Project, "competition": Competition,
|
||||||
elif mod_name == "internship" and isinstance(result, list):
|
}
|
||||||
cr.internship = [Internship.model_validate(item) for item in result]
|
model_cls = model_map.get(mod_name)
|
||||||
elif mod_name == "project" and isinstance(result, list):
|
if not model_cls or not isinstance(result, dict) or not record_id:
|
||||||
cr.project = [Project.model_validate(item) for item in result]
|
return
|
||||||
elif mod_name == "competition" and isinstance(result, list):
|
list_map = {
|
||||||
cr.competition = [Competition.model_validate(item) for item in result]
|
"education": cr.education, "work": cr.work, "internship": cr.internship,
|
||||||
|
"project": cr.project, "competition": cr.competition,
|
||||||
|
}
|
||||||
|
items = list_map.get(mod_name, [])
|
||||||
|
new_item = model_cls.model_validate(result)
|
||||||
|
for i, item in enumerate(items):
|
||||||
|
if item.id == record_id:
|
||||||
|
items[i] = new_item
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"应用AI编辑结果[{mod_name}]失败: {e}")
|
log.warning(f"应用AI编辑结果[{mod_name}/{record_id}]失败: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _apply_record_add(cr: CustomizeResume, mod_name: str, result) -> None:
|
||||||
|
"""将 AI 新增的记录追加到对应模块"""
|
||||||
|
try:
|
||||||
|
model_map = {
|
||||||
|
"education": (Education, cr.education),
|
||||||
|
"work": (Work, cr.work),
|
||||||
|
"internship": (Internship, cr.internship),
|
||||||
|
"project": (Project, cr.project),
|
||||||
|
"competition": (Competition, cr.competition),
|
||||||
|
}
|
||||||
|
entry = model_map.get(mod_name)
|
||||||
|
if not entry or not isinstance(result, dict):
|
||||||
|
return
|
||||||
|
model_cls, items = entry
|
||||||
|
items.append(model_cls.model_validate(result))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"应用AI新增记录[{mod_name}]失败: {e}")
|
||||||
|
|
||||||
# ===== 内部工具方法 =====
|
# ===== 内部工具方法 =====
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user