优化 岗位编辑agent 性能

This commit is contained in:
zk
2026-04-14 12:12:46 +08:00
parent 9bc8bb492c
commit ebffbc8615
4 changed files with 326 additions and 120 deletions
+42 -13
View File
@@ -14,7 +14,7 @@ from langchain_core.prompts import ChatPromptTemplate
from app.ai.models import LLM
from app.ai.skill_gap_analyzer.prompts import (
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
@@ -100,11 +100,13 @@ _plan_chain = (
)
async def plan_edit(job_title: str, resume_json: str, chat_history: str, instruction: str) -> dict | None:
"""Agent 规划:分析用户指令,返回修改计划或对话回复"""
async def plan_edit(job_title: str, job_description: str, resume_json: str,
chat_history: str, instruction: str) -> dict | None:
"""Agent 规划:分析用户指令,返回原子操作列表或对话回复"""
try:
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,
})
result = _parse_json(raw)
@@ -114,24 +116,51 @@ async def plan_edit(job_title: str, resume_json: str, chat_history: str, instruc
return None
# ===== Agent - 模块修改 =====
# ===== Agent - 单条记录修改 =====
_module_edit_chain = (
_record_edit_chain = (
ChatPromptTemplate.from_messages([("system", AGENT_MODULE_EDIT_PROMPT), ("human", "请执行修改。")])
| LLM.GPT_4O.create(temperature=0.3)
| StrOutputParser()
)
async def execute_module_edit(job_title: str, module_instruction: str,
module_schema: str, module_data: str) -> dict | list | None:
"""Agent 模块修改:根据指令修改指定模块数据"""
async def execute_record_edit(job_title: str, job_description: str, instruction: str,
chat_history: str, module_schema: str,
record_data: str) -> dict | list | None:
"""修改单条记录:根据指令修改指定记录数据"""
try:
raw = await _module_edit_chain.ainvoke({
"job_title": job_title, "module_instruction": module_instruction,
"module_schema": module_schema, "module_data": module_data,
raw = await _record_edit_chain.ainvoke({
"job_title": job_title, "job_description": job_description,
"instruction": instruction, "chat_history": chat_history,
"module_schema": module_schema, "record_data": record_data,
})
return _parse_json(raw)
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
+53 -22
View File
@@ -52,10 +52,11 @@ EXPERIENCE_OPTIMIZE_PROMPT = """你是一个简历优化助手。根据目标岗
4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,决定需要修改简历的哪些模块
AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,将其拆解为原子操作
【目标岗位】
{job_title}
{job_description}
【当前简历】
{resume_json}
@@ -66,43 +67,73 @@ AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,
【用户指令】
{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,不要其他内容。"""
如果用户指令明确,将其拆解为原子操作列表,返回
{{"action": "modify", "operations": [...]}}
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_description}
【修改要求】
{module_instruction}
{instruction}
【最近对话】
{chat_history}
【模块数据结构】
{module_schema}
【当前模块数据】
{module_data}
【当前记录数据】
{record_data}
规则:
1. 严格按照修改要求操作,可以增删改
2. 未要求修改的记录保持不变
1. 严格按照修改要求操作
2. 未要求修改的字段保持不变
3. 不要编造用户简历中不存在的内容
4. 保持原文格式和结构
5. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接从数组中移除
6. 新增记录时按照模块数据结构生成完整字段,id 使用随机8位字符串
7. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
5. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
6. 返回修改后的完整记录数据(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
MODULE_SCHEMAS: dict[str, str] = {
+137 -43
View File
@@ -16,7 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.skill_gap_analyzer.analyzer import (
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.core.logger import log
@@ -40,6 +40,16 @@ CUSTOMIZE_RESUME_ROLLBACK_EXPIRE = 30 * 60 # 30分钟
_CHARS = string.ascii_letters + string.digits
# 模块名 → 中文标签映射
_MODULE_LABELS = {
"resume": "个人简介",
"education": "教育经历",
"work": "工作经历",
"internship": "实习经历",
"project": "项目经历",
"competition": "竞赛经历",
}
def _rand_id() -> str:
"""生成随机8位字符串标识"""
@@ -201,7 +211,7 @@ class SkillGapService:
async def ai_edit_customize_resume(self, user_id: int, job_id: int,
instruction: str, chat_history: list) -> dict:
"""AI 对话式编辑定制简历"""
"""AI 对话式编辑定制简历(原子化操作版)"""
# 1. 取当前定制简历
key = f"{CUSTOMIZE_RESUME_KEY_PREFIX}{user_id}"
raw = await RedisManager.client.get(key)
@@ -211,69 +221,153 @@ class SkillGapService:
resume_json = cr.model_dump_json(by_alias=True)
# 2. 查岗位
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 ""
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:
return {"type": "message", "message": "抱歉,我没有理解你的意思,请再描述一下。"}
if plan.get("action") == "chat":
return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")}
# 4. 按模块并发执行修改
modules = plan.get("modules", [])
if not modules:
# 4. 解析操作列表
operations = plan.get("operations", [])
if not operations:
return {"type": "message", "message": plan.get("message", "请再描述一下你的需求。")}
edit_tasks = []
for m in modules:
mod_name = m.get("module", "")
mod_instr = m.get("instruction", "")
# 截取最近10条对话历史
recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
recent_history_str = json.dumps(recent_history, ensure_ascii=False) if recent_history else ""
# 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, "")
mod_data = self._get_module_data(cr, mod_name)
edit_tasks.append((mod_name, execute_module_edit(job.title or "", mod_instr, schema, mod_data)))
keys = [t[0] for t in edit_tasks]
results = await asyncio.gather(*[t[1] for t in edit_tasks], return_exceptions=True)
# 5. 合并结果
for mod_key, result in zip(keys, results):
if isinstance(result, Exception):
log.warning(f"AI编辑模块[{mod_key}]失败: {result}")
continue
if result is None:
continue
self._apply_edit_result(cr, mod_key, result)
if op_type == "update":
record_data = self._get_record_data(cr, mod_name, op.get("id"))
if record_data is not None:
ai_tasks.append((
"update", mod_name, op.get("id"),
execute_record_edit(
job.title or "", job_desc, op_instruction,
recent_history_str, schema, record_data,
),
))
elif op_type == "add":
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. 保存回滚 + 新版本
rollback_key = f"{CUSTOMIZE_RESUME_ROLLBACK_KEY_PREFIX}{user_id}"
await RedisManager.client.set(rollback_key, raw, ex=CUSTOMIZE_RESUME_ROLLBACK_EXPIRE)
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
def _get_module_data(cr: CustomizeResume, mod_name: str) -> str:
"""获取指定模块的 JSON 数据"""
def _get_record_data(cr: CustomizeResume, mod_name: str, record_id: str | None) -> str | None:
"""获取单条记录的 JSON 数据resume 主表返回整个对象"""
if mod_name == "resume":
return cr.resume.model_dump_json(by_alias=True)
mapping = {"education": cr.education, "work": cr.work, "internship": cr.internship,
"project": cr.project, "competition": cr.competition}
mapping = {
"education": cr.education, "work": cr.work, "internship": cr.internship,
"project": cr.project, "competition": cr.competition,
}
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
def _apply_edit_result(cr: CustomizeResume, mod_name: str, result) -> None:
"""将 AI 编辑结果应用到定制简历"""
def _apply_delete(cr: CustomizeResume, mod_name: str, record_id: str) -> None:
"""删除指定模块中的一条记录"""
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:
if mod_name == "resume" and isinstance(result, dict):
cr.resume = ResumeProfile.model_validate(result)
elif mod_name == "education" and isinstance(result, list):
cr.education = [Education.model_validate(item) for item in result]
elif mod_name == "work" and isinstance(result, list):
cr.work = [Work.model_validate(item) for item in result]
elif mod_name == "internship" and isinstance(result, list):
cr.internship = [Internship.model_validate(item) for item in result]
elif mod_name == "project" and isinstance(result, list):
cr.project = [Project.model_validate(item) for item in result]
elif mod_name == "competition" and isinstance(result, list):
cr.competition = [Competition.model_validate(item) for item in result]
return
model_map = {
"education": Education, "work": Work, "internship": Internship,
"project": Project, "competition": Competition,
}
model_cls = model_map.get(mod_name)
if not model_cls or not isinstance(result, dict) or not record_id:
return
list_map = {
"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:
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}")
# ===== 内部工具方法 =====