重构简历优化
This commit is contained in:
@@ -44,14 +44,18 @@ class ResumeExtractorModel:
|
||||
PARSE = LLM.DOUBAO_PRO_32K.create(temperature=0)
|
||||
|
||||
|
||||
class ResumePolisherModel:
|
||||
"""简历段落润色模块"""
|
||||
# 段落润色:仅做格式/错字/表达优化,不改内容,低温度保证稳定
|
||||
POLISH = LLM.DEEPSEEK_V4_FLASH.create(temperature=0.2)
|
||||
|
||||
|
||||
class DiagnoserModel:
|
||||
"""简历诊断模块"""
|
||||
# 模块诊断:逐条分析经历记录的问题(错别字/无量化/弱相关等)
|
||||
MODULE = LLM.DEEPSEEK_V4_FLASH.create(temperature=0)
|
||||
# 整体评价:汇总所有诊断结果生成总结性评语
|
||||
SUMMARY = LLM.DEEPSEEK_V4_FLASH.create(temperature=0.3)
|
||||
# 内容润色:用户编辑后的文本做专业润色
|
||||
POLISH = LLM.DEEPSEEK_V4_FLASH.create(temperature=0.3)
|
||||
|
||||
|
||||
class BrowserPlugModel:
|
||||
|
||||
@@ -7,7 +7,7 @@ from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
from app.ai.model_config import DiagnoserModel
|
||||
from app.ai.resume_diagnoser.prompts import DIAGNOSE_MODULE_PROMPT, SUMMARY_PROMPT, POLISH_PROMPT
|
||||
from app.ai.resume_diagnoser.prompts import DIAGNOSE_MODULE_PROMPT, SUMMARY_PROMPT
|
||||
from app.core.logger import log
|
||||
from app.tool.json_helper import parse_llm_json
|
||||
|
||||
@@ -54,45 +54,6 @@ async def generate_summary(grade: str, urgent_total: int, important_total: int,
|
||||
return "简历诊断已完成,请查看各模块的详细诊断结果。"
|
||||
|
||||
|
||||
_polish_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", POLISH_PROMPT), ("human", "请开始优化。")])
|
||||
| DiagnoserModel.POLISH
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def polish_content(module_type: str, reference_content: list[dict] | str | None,
|
||||
user_content: list[str], is_summary: bool) -> list[str]:
|
||||
"""润色用户编辑后的文本"""
|
||||
ref_text = ""
|
||||
if reference_content:
|
||||
if isinstance(reference_content, list):
|
||||
ref_text = "\n".join(
|
||||
item.get("text", "") if isinstance(item, dict) else str(item)
|
||||
for item in reference_content
|
||||
)
|
||||
else:
|
||||
ref_text = str(reference_content)
|
||||
if not ref_text:
|
||||
ref_text = "无"
|
||||
|
||||
inp = {
|
||||
"module_type": module_type,
|
||||
"reference_content": ref_text,
|
||||
"user_content": "\n".join(user_content),
|
||||
"summary_constraint": "- 注意:此模块只能输出一个段落,数组只能有一个元素" if is_summary else "",
|
||||
}
|
||||
try:
|
||||
raw = await _polish_chain.ainvoke(inp)
|
||||
result = parse_llm_json(raw)
|
||||
if isinstance(result, list):
|
||||
return [str(item) for item in result]
|
||||
return [str(result)]
|
||||
except Exception as e:
|
||||
log.warning(f"AI润色失败: {e}")
|
||||
return user_content
|
||||
|
||||
|
||||
async def _safe_invoke(task: dict) -> dict:
|
||||
"""单条记录诊断,失败返回空结果"""
|
||||
module_type = task.get("module_type", "unknown")
|
||||
|
||||
@@ -82,26 +82,3 @@ SUMMARY_PROMPT = """你是一位资深简历顾问。请根据以下简历诊断
|
||||
4. 一句鼓励或行动建议
|
||||
|
||||
直接输出评价文本,不要输出JSON或其他格式标记。控制在200字以内。"""
|
||||
|
||||
POLISH_PROMPT = """你是一位资深简历顾问。请对用户提供的简历描述文本进行润色优化,让语言更精练、更专业。
|
||||
|
||||
## 模块类型
|
||||
{module_type}
|
||||
|
||||
## AI 之前的优化版本(仅供参考)
|
||||
{reference_content}
|
||||
|
||||
## 用户提交的文本(以此为主进行优化)
|
||||
{user_content}
|
||||
|
||||
## 优化要求
|
||||
- 以用户提交的文本为主体进行润色,AI之前的版本仅作参考
|
||||
- 让语言更精练、更专业,去除冗余表达
|
||||
- 尽量使用数据量化成果
|
||||
- 保持原意不变,不凭空捏造内容
|
||||
- 输出为 JSON 数组格式,每个元素是一个段落的纯文本
|
||||
{summary_constraint}
|
||||
|
||||
## 输出格式
|
||||
严格输出 JSON 数组,不要输出其他内容:
|
||||
["优化后的段落1", "优化后的段落2"]"""
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""简历段落润色 AI 引擎:仅做格式/错字/表达层面的优化"""
|
||||
|
||||
import json
|
||||
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
from app.ai.model_config import ResumePolisherModel
|
||||
from app.ai.resume_polisher.prompts import POLISH_PROMPT
|
||||
from app.core.logger import log
|
||||
from app.tool.json_helper import parse_llm_json
|
||||
|
||||
# 润色链(StrOutputParser 拿原始文本,再手动解析 JSON,避免 markdown 代码块导致解析失败)
|
||||
_polish_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", POLISH_PROMPT), ("human", "请开始润色。")])
|
||||
| ResumePolisherModel.POLISH
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def polish_paragraphs(content: list[str]) -> list[str]:
|
||||
"""对段落数组做表达层面的润色,返回与输入等长的数组;失败兜底原样返回"""
|
||||
if not content:
|
||||
return []
|
||||
|
||||
inp = {"content": json.dumps(content, ensure_ascii=False)}
|
||||
try:
|
||||
raw = await _polish_chain.ainvoke(inp)
|
||||
result = parse_llm_json(raw)
|
||||
if isinstance(result, list) and len(result) == len(content):
|
||||
return [str(item) for item in result]
|
||||
log.warning(f"AI润色返回结果不符合预期, 原样返回: {result}")
|
||||
return content
|
||||
except Exception as e:
|
||||
log.warning(f"AI润色失败: {e}")
|
||||
return content
|
||||
@@ -0,0 +1,21 @@
|
||||
"""简历段落润色 Prompt 模板"""
|
||||
|
||||
POLISH_PROMPT = """你是一位严谨的简历文字校对助手。请对用户提交的简历段落进行"表面润色",只做表达层面的优化。
|
||||
|
||||
## 优化范围(只允许做这些)
|
||||
- 修正错别字、标点、语法错误
|
||||
- 优化文本格式与排版(如多余空格、断句、全半角混用)
|
||||
- 让表达更通顺、专业,去除明显口语化和冗余措辞
|
||||
|
||||
## 严格禁止(绝对不能做)
|
||||
- 不得改变原意,不得增加或删除任何信息点
|
||||
- 不得编造、补充任何内容(尤其禁止凭空添加数字、量化成果、技能、成就)
|
||||
- 不得改变段落的数量和顺序
|
||||
|
||||
## 输入
|
||||
用户提交的段落数组(每个元素是一个段落):
|
||||
{content}
|
||||
|
||||
## 输出格式
|
||||
严格输出 JSON 数组,元素个数和顺序必须与输入完全一致,不要输出其他任何内容:
|
||||
["润色后的段落1", "润色后的段落2"]"""
|
||||
+15
-2
@@ -1,10 +1,11 @@
|
||||
"""简历上传解析接口"""
|
||||
|
||||
from fastapi import APIRouter, UploadFile, File
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.context import RequestContext
|
||||
from app.core.database import get_db
|
||||
from app.services.resume_parse_service import ResumeParseService
|
||||
from app.services.resume_service import ResumeService
|
||||
|
||||
router = APIRouter(prefix="/resume", tags=["简历"])
|
||||
|
||||
@@ -15,7 +16,7 @@ async def upload_resume(file: UploadFile = File(...)):
|
||||
user_id = RequestContext.user_id.get()
|
||||
content = await file.read()
|
||||
|
||||
service = ResumeParseService()
|
||||
service = ResumeService()
|
||||
# 文件解析 + AI 结构化(不占数据库连接)
|
||||
parsed = await service.parse_and_extract(file.filename, content)
|
||||
# 短事务:只做数据库写入
|
||||
@@ -23,3 +24,15 @@ async def upload_resume(file: UploadFile = File(...)):
|
||||
async for session in get_db():
|
||||
resume_id = await service.save_resume(session, user_id, file.filename, parsed)
|
||||
return {"resumeId": resume_id}
|
||||
|
||||
|
||||
class PolishParam(BaseModel):
|
||||
content: list[str] = Field(..., description="待润色的简历段落文本数组")
|
||||
|
||||
|
||||
@router.post("/polish", summary="AI润色简历段落")
|
||||
async def polish_resume(param: PolishParam):
|
||||
"""对前端提交的简历段落做表达层面的润色(格式/错字/表达),不改内容,返回等长数组"""
|
||||
service = ResumeService()
|
||||
result = await service.polish_paragraphs(param.content)
|
||||
return {"content": result}
|
||||
|
||||
@@ -5,7 +5,7 @@ import time
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.ai.resume_diagnoser.diagnoser import diagnose_all, generate_summary, polish_content
|
||||
from app.ai.resume_diagnoser.diagnoser import diagnose_all, generate_summary
|
||||
from app.core.auth import func_permission
|
||||
from app.core.context import RequestContext
|
||||
from app.core.database import get_db
|
||||
@@ -98,25 +98,3 @@ async def feedback_issue(issue_id: int, param: FeedbackParam):
|
||||
async for session in get_db():
|
||||
service = ResumeDiagnoseService(session)
|
||||
await service.update_feedback(issue_id, user_id, param.user_feedback)
|
||||
|
||||
|
||||
class PolishParam(BaseModel):
|
||||
content: list[str] = Field(..., description="用户编辑后的文本段落数组")
|
||||
|
||||
|
||||
@router.post("/issue/{issue_id}/polish", summary="AI润色用户编辑的文本")
|
||||
async def polish_issue_content(issue_id: int, param: PolishParam):
|
||||
"""基于诊断问题上下文,AI润色用户编辑后的文本"""
|
||||
user_id = RequestContext.user_id.get()
|
||||
|
||||
async for session in get_db():
|
||||
service = ResumeDiagnoseService(session)
|
||||
ctx = await service.get_issue_for_polish(issue_id, user_id)
|
||||
|
||||
result = await polish_content(
|
||||
module_type=ctx["module_label"],
|
||||
reference_content=ctx["optimized_content"],
|
||||
user_content=param.content,
|
||||
is_summary=ctx["is_summary"],
|
||||
)
|
||||
return {"content": result}
|
||||
|
||||
@@ -153,21 +153,6 @@ class ResumeDiagnoseService:
|
||||
issue.user_feedback = user_feedback
|
||||
await self.session.flush()
|
||||
|
||||
async def get_issue_for_polish(self, issue_id: int, user_id: int) -> dict:
|
||||
"""获取 issue 润色所需的上下文信息"""
|
||||
result = await self.session.execute(
|
||||
select(ResumeDiagnosisIssue).where(
|
||||
ResumeDiagnosisIssue.id == issue_id, ResumeDiagnosisIssue.user_id == user_id))
|
||||
issue = result.scalar_one_or_none()
|
||||
if issue is None:
|
||||
raise ValueError("诊断问题不存在")
|
||||
return {
|
||||
"module_type": issue.module_type,
|
||||
"module_label": _MODULE_LABELS.get(issue.module_type, issue.module_type),
|
||||
"optimized_content": issue.optimized_content,
|
||||
"is_summary": issue.module_type == "summary",
|
||||
}
|
||||
|
||||
|
||||
# ===== 工具函数 =====
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""简历解析 Service
|
||||
"""简历 Service
|
||||
|
||||
上传简历文件 → 解析为纯文本 → AI 两阶段并行结构化 → 写入数据库。
|
||||
依赖:file_parser(文件解析工具)、resume_extractor(AI两阶段并行提取)
|
||||
@@ -11,6 +11,7 @@ import shortuuid
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.ai.resume_extractor.extractor import extract_all
|
||||
from app.ai.resume_polisher.polisher import polish_paragraphs
|
||||
from app.core.logger import log
|
||||
from app.models.user_resume import UserResume
|
||||
from app.models.user_resume_competition import UserResumeCompetition
|
||||
@@ -22,7 +23,7 @@ from app.tool.file_parser import parse_to_text
|
||||
from app.tool.snowflake import next_id
|
||||
|
||||
|
||||
class ResumeParseService:
|
||||
class ResumeService:
|
||||
|
||||
async def parse_and_extract(self, filename: str, content: bytes) -> dict:
|
||||
"""文件解析 + AI 两阶段并行结构化,不涉及数据库操作"""
|
||||
@@ -37,6 +38,13 @@ class ResumeParseService:
|
||||
log.info("AI两阶段并行结构化提取完成")
|
||||
return parsed
|
||||
|
||||
async def polish_paragraphs(self, content: list[str]) -> list[str]:
|
||||
"""对简历段落做表达层面的润色(格式/错字/表达),不涉及数据库操作"""
|
||||
log.info(f"开始简历段落润色, 段落数={len(content)}")
|
||||
result = await polish_paragraphs(content)
|
||||
log.info("简历段落润色完成")
|
||||
return result
|
||||
|
||||
async def save_resume(self, session: AsyncSession, user_id: int, filename: str, parsed: dict) -> int:
|
||||
"""将解析结果写入主表 + 5张子表,返回简历ID"""
|
||||
resume_id = next_id()
|
||||
Reference in New Issue
Block a user