添加简历诊断功能
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""简历诊断 AI 引擎:并行诊断 + 汇总评价"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
from app.ai.models import LLM
|
||||
from app.ai.resume_diagnoser.prompts import DIAGNOSE_MODULE_PROMPT, SUMMARY_PROMPT
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
def _parse_json(text: str) -> dict:
|
||||
"""解析 AI 输出的 JSON,自动去除 markdown 代码块包裹,容错处理"""
|
||||
cleaned = re.sub(r"^```(?:json)?\s*\n?", "", text.strip())
|
||||
cleaned = re.sub(r"\n?```\s*$", "", cleaned)
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
# AI 可能在 JSON 字符串值中嵌入了未转义的引号,尝试提取最外层 { }
|
||||
match = re.search(r"\{[\s\S]*\}", cleaned)
|
||||
if match:
|
||||
return json.loads(match.group())
|
||||
raise
|
||||
|
||||
|
||||
# 诊断链(StrOutputParser 拿原始文本,再手动解析 JSON,避免 markdown 代码块导致解析失败)
|
||||
_diagnose_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", DIAGNOSE_MODULE_PROMPT), ("human", "请开始诊断。")])
|
||||
| LLM.CLAUDE_SONNET_4.create(temperature=0)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
# 汇总评价链(纯文本输出)
|
||||
_summary_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", SUMMARY_PROMPT), ("human", "请生成整体评价。")])
|
||||
| LLM.CLAUDE_SONNET_4.create(temperature=0.3)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def diagnose_all(tasks: list[dict]) -> list[dict]:
|
||||
"""并行诊断所有模块记录
|
||||
|
||||
tasks: [{"module_type": ..., "target_position": ..., "context": ..., "description_text": ...}, ...]
|
||||
返回: 与 tasks 一一对应的诊断结果列表
|
||||
"""
|
||||
log.info(f"开始{len(tasks)}路并行AI诊断")
|
||||
results = await asyncio.gather(*[_safe_invoke(task) for task in tasks])
|
||||
log.info("并行AI诊断完成")
|
||||
return results
|
||||
|
||||
|
||||
async def generate_summary(grade: str, urgent_total: int, important_total: int,
|
||||
expression_total: int, target_position: str, all_findings: str) -> str:
|
||||
"""AI 生成整体评价文本"""
|
||||
inp = {
|
||||
"grade": grade, "urgent_total": str(urgent_total),
|
||||
"important_total": str(important_total), "expression_total": str(expression_total),
|
||||
"target_position": target_position or "未指定", "all_findings": all_findings,
|
||||
}
|
||||
try:
|
||||
return await _summary_chain.ainvoke(inp)
|
||||
except Exception as e:
|
||||
log.warning(f"AI生成整体评价失败: {e}")
|
||||
return "简历诊断已完成,请查看各模块的详细诊断结果。"
|
||||
|
||||
|
||||
async def _safe_invoke(task: dict) -> dict:
|
||||
"""单条记录诊断,失败返回空结果"""
|
||||
raw = ""
|
||||
try:
|
||||
raw = await _diagnose_chain.ainvoke(task)
|
||||
return _parse_json(raw)
|
||||
except Exception as e:
|
||||
log.warning(f"AI诊断[{task.get('module_type', '')}]失败: {e}\n原始输出: {raw[:500]}")
|
||||
return _empty_result()
|
||||
|
||||
|
||||
def _empty_result() -> dict:
|
||||
return {
|
||||
"finding": "", "importance": "", "suggestion": "",
|
||||
"urgent_issues": {"typo": 0},
|
||||
"important_issues": {"no_result": 0, "no_quantify": 0, "weak_relevance": 0},
|
||||
"expression_issues": {"not_concise": 0, "format_inconsistent": 0},
|
||||
"optimized_content": None,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"""简历诊断 Prompt 模板
|
||||
|
||||
注意:prompt 中的 JSON 示例花括号必须用 {{ }} 转义,避免被 ChatPromptTemplate 当作变量。
|
||||
"""
|
||||
|
||||
DIAGNOSE_MODULE_PROMPT = """你是一位资深简历顾问和求职专家。请对以下简历模块的描述文本进行专业诊断。
|
||||
|
||||
## 模块信息
|
||||
- 模块类型:{module_type}
|
||||
- 目标岗位:{target_position}
|
||||
- 模块上下文:{context}
|
||||
|
||||
## 待诊断文本
|
||||
{description_text}
|
||||
|
||||
## 诊断维度
|
||||
|
||||
### 紧急修复
|
||||
- typo:错别字、语法错误、语病、标点符号使用错误、中英文标点混用、用词不当
|
||||
|
||||
### 重点优化
|
||||
- no_result:只描述了做了什么(任务/职责),但没有体现最终结果、产出或影响,像流水账
|
||||
- no_quantify:有成果描述但缺少具体数字支撑,使用了"大幅""显著""有效"等模糊表达,缺少人数、金额、百分比、时间等量化数据
|
||||
- weak_relevance:描述内容与目标岗位的核心职责关联度低,花大量篇幅描述与目标岗位无关的内容(注意:如果目标岗位为"未指定",此项必须为0)
|
||||
|
||||
### 表达提升
|
||||
- not_concise:句子偏长信息密度低,存在赘词重复表达(如"进行了开发"应简化为"开发了"),使用空泛修饰词("充分""积极""认真")
|
||||
- format_inconsistent:时间格式、标点风格、数字写法、项目符号、人称使用不统一
|
||||
|
||||
## 输出要求
|
||||
严格输出以下JSON格式,每个问题类别的值为该类问题出现的次数(0表示无此问题):
|
||||
```json
|
||||
{{
|
||||
"finding": "用2-3句话概述发现的主要问题",
|
||||
"importance": "用1-2句话说明为什么这些问题对简历质量很重要",
|
||||
"suggestion": "给出具体可执行的改进建议",
|
||||
"urgent_issues": {{"typo": 0}},
|
||||
"important_issues": {{"no_result": 0, "no_quantify": 0, "weak_relevance": 0}},
|
||||
"expression_issues": {{"not_concise": 0, "format_inconsistent": 0}},
|
||||
"optimized_content": ["改写后的段落1", "改写后的段落2"]
|
||||
}}
|
||||
```
|
||||
|
||||
## 关于 optimized_content 的格式要求
|
||||
- optimized_content 必须是一个纯文本字符串数组
|
||||
- 如果原文是 JSON 数组格式(如 [{{"id": "xxx", "text": "段落内容"}}]),则只提取每个元素的 text 内容进行改写,返回改写后的纯文本数组,段落数量必须与原文一一对应
|
||||
- 如果原文是纯文本(非JSON),则返回包含一个元素的数组:["改写后的完整文本"]
|
||||
- 如果原文没有明显问题,返回原文内容不做修改
|
||||
|
||||
只输出JSON,不要输出其他内容。"""
|
||||
|
||||
SUMMARY_PROMPT = """你是一位资深简历顾问。请根据以下简历诊断结果,生成一段整体评价。
|
||||
|
||||
## 诊断统计
|
||||
- 评级:{grade}
|
||||
- 紧急修复问题:{urgent_total} 个
|
||||
- 重点优化问题:{important_total} 个
|
||||
- 表达提升问题:{expression_total} 个
|
||||
|
||||
## 目标岗位
|
||||
{target_position}
|
||||
|
||||
## 各模块诊断发现
|
||||
{all_findings}
|
||||
|
||||
## 评级含义
|
||||
- A(优秀):简历相当出彩,在求职市场中格外抢眼
|
||||
- B(良好):简历已经很棒,但还有提升潜力
|
||||
- C(一般):简历还有打磨空间,需要推敲细节
|
||||
- D(待提升):简历有较大提升空间,需要尽快完善
|
||||
|
||||
## 输出要求
|
||||
请用3-5句话生成简历整体评价,包括:
|
||||
1. 背景概括(基于模块内容简要描述求职者背景)
|
||||
2. 优势总结(如果有值得肯定的地方)
|
||||
3. 主要问题(最需要改进的方面)
|
||||
4. 一句鼓励或行动建议
|
||||
|
||||
直接输出评价文本,不要输出JSON或其他格式标记。控制在300字以内。"""
|
||||
Reference in New Issue
Block a user