添加适配评分

This commit is contained in:
zk
2026-06-24 18:14:57 +08:00
parent dc02969f3a
commit 00deef31ad
7 changed files with 153 additions and 4 deletions
+39
View File
@@ -0,0 +1,39 @@
"""求职助手 - 岗位匹配评分 AI 引擎
根据简历与岗位在技能/学历/经验上的要求评估匹配度,输出 0-100 分。
依赖:job_agent/prompts、model_config.JobAgentModel.MATCH_SCORE、parse_llm_json
"""
import time
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from app.ai.job_agent.prompts import RESUME_JOB_MATCH_SCORE_PROMPT
from app.ai.model_config import JobAgentModel
from app.core.logger import log
from app.tool.json_helper import parse_llm_json
_score_chain = (
ChatPromptTemplate.from_messages([("system", RESUME_JOB_MATCH_SCORE_PROMPT), ("human", "请开始评估。")])
| JobAgentModel.MATCH_SCORE
| StrOutputParser()
)
async def score_match(job_title: str, job_skill_tags: str, job_description: str, resume_text: str) -> int:
"""评估简历与岗位匹配度,返回 0-100 整数分,失败返回 0"""
t0 = time.monotonic()
try:
raw = await _score_chain.ainvoke({
"job_title": job_title, "job_skill_tags": job_skill_tags or "",
"job_description": job_description or "", "resume_text": resume_text or "暂无简历信息",
})
parsed = parse_llm_json(raw)
score = int(parsed.get("score", 0))
score = max(0, min(100, score)) # 兜底夹取到 0-100
log.info(f"岗位匹配评分完成 score={score} ({round(time.monotonic() - t0, 2)}s)")
return score
except Exception as e:
log.error(f"岗位匹配评分失败: {e} ({round(time.monotonic() - t0, 2)}s)")
return 0
+34
View File
@@ -38,3 +38,37 @@ RESUME_EXPERIENCE_OPTIMIZE_PROMPT = """你是一个求职投递助手。用户
3. 适当使用岗位要求中的关键词润色描述,但不要编造内容 3. 适当使用岗位要求中的关键词润色描述,但不要编造内容
4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除 4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)""" 5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
# ===== 岗位匹配评分 Prompt =====
RESUME_JOB_MATCH_SCORE_PROMPT = """你是一名资深招聘评估专家,负责客观评估候选人简历与目标岗位的匹配程度。请从【技能】【学历】【经验】三个维度逐项分析,再综合给出一个 0-100 的匹配分数。
【候选人简历】
{resume_text}
【目标岗位】
岗位名称:{job_title}
技能标签:{job_skill_tags}
岗位职责与要求:{job_description}
【评估维度与要点】
1. 技能匹配(建议权重约 50%):候选人掌握的技能/工具/技术栈与岗位技能标签、JD 中要求的硬技能的覆盖程度;核心技能命中比软技能更重要,关注关键技能是否缺失。
2. 学历匹配(建议权重约 20%):学历层次、院校、专业与岗位学历门槛及专业相关度;满足门槛即可得高分,明显高于要求不额外加分,低于门槛要扣分。
3. 经验匹配(建议权重约 30%):工作/实习/项目/竞赛经历与岗位职责的相关性、年限、岗位层级、业务领域贴合度;优先看相关经历的深度与成果,而非堆数量。
【打分档位(用于校准总分)】
- 90-100:高度匹配,核心技能基本全覆盖、学历达标、有强相关经验,可直接进入面试
- 75-89:较匹配,主要要求满足,存在少量可弥补的差距
- 60-74:基本匹配,满足部分核心要求,但有明显短板
- 40-59:匹配度偏低,多数关键要求不满足
- 0-39:基本不匹配或简历信息严重不足
【规则】
1. 严格基于简历已有信息评估,简历中缺失或未提及的能力一律按"不具备"处理,不得臆测或脑补
2. 客观打分,避免一律给高分;不相关的经历/技能不计入加分
3. 岗位信息或简历信息明显不足时,应给保守分而非高分
4. 先在心里完成三维度分析与加权,最终只输出一个综合整数分(0-100),越高代表越匹配
严格只返回 JSON,不要任何分析过程或其他内容:
{{"score":85}}"""
+2
View File
@@ -24,6 +24,8 @@ class SkillGapModel:
class JobAgentModel: class JobAgentModel:
"""求职助手Agent模块""" """求职助手Agent模块"""
# 岗位匹配评分:评估简历与岗位在技能/学历/经验上的匹配度,输出0-100分(评分要稳定,温度设0)
MATCH_SCORE = LLM.DEEPSEEK_V4_FLASH.create(temperature=0)
# 岗位简历-summary优化:针对具体岗位JD优化个人概述 # 岗位简历-summary优化:针对具体岗位JD优化个人概述
SUMMARY = LLM.DEEPSEEK_V4_FLASH.create(temperature=0.3) SUMMARY = LLM.DEEPSEEK_V4_FLASH.create(temperature=0.3)
# 岗位简历-经历优化:针对具体岗位JD优化单条经历描述 # 岗位简历-经历优化:针对具体岗位JD优化单条经历描述
+11 -1
View File
@@ -5,12 +5,22 @@ from fastapi import APIRouter, Depends
from app.core.auth import func_permission from app.core.auth import func_permission
from app.core.context import RequestContext from app.core.context import RequestContext
from app.core.database import get_db from app.core.database import get_db
from app.schemas.job_agent_chat import OptimizeResumeParam from app.schemas.job_agent_chat import OptimizeResumeParam, MatchScoreParam
from app.services.job_agent_chat_service import JobAgentChatService from app.services.job_agent_chat_service import JobAgentChatService
router = APIRouter(prefix="/job-agent", tags=["求职助手agent"]) router = APIRouter(prefix="/job-agent", tags=["求职助手agent"])
@router.post("/match-score", summary="岗位匹配度评分")
async def match_score(param: MatchScoreParam):
"""根据定制简历和岗位,从技能/学历/经验维度评估匹配度,返回 0-100 分"""
user_id = RequestContext.user_id.get()
async for session in get_db():
service = JobAgentChatService(session)
result = await service.score_match(user_id, param.customize_resume_id, param.job_id)
return result
@router.post("/optimize-resume", summary="针对岗位优化简历") @router.post("/optimize-resume", summary="针对岗位优化简历")
async def optimize_resume(param: OptimizeResumeParam, _: None = Depends(func_permission("resume_custom"))): async def optimize_resume(param: OptimizeResumeParam, _: None = Depends(func_permission("resume_custom"))):
"""根据目标岗位,AI并发优化简历(summary + 5张子表经历),存Redis并返回""" """根据目标岗位,AI并发优化简历(summary + 5张子表经历),存Redis并返回"""
+5
View File
@@ -10,3 +10,8 @@ from pydantic import BaseModel, Field
class OptimizeResumeParam(BaseModel): class OptimizeResumeParam(BaseModel):
resume_id: int = Field(..., alias="resumeId", description="简历ID") resume_id: int = Field(..., alias="resumeId", description="简历ID")
job_id: int = Field(..., alias="jobId", description="岗位ID") job_id: int = Field(..., alias="jobId", description="岗位ID")
class MatchScoreParam(BaseModel):
customize_resume_id: int = Field(..., alias="customizeResumeId", description="定制简历ID")
job_id: int = Field(..., alias="jobId", description="岗位ID")
+9
View File
@@ -92,6 +92,15 @@ async def save(user_id: int, job_id: int, cr: CustomizeResume, resume_id: int |
session.add(UserJobCustomizeResume(id=next_id(), user_id=user_id, job_id=job_id, resume_id=resume_id, resume_name=resume_name, content=content)) session.add(UserJobCustomizeResume(id=next_id(), user_id=user_id, job_id=job_id, resume_id=resume_id, resume_name=resume_name, content=content))
async def get_by_id(user_id: int, customize_resume_id: int) -> CustomizeResume | None:
"""按定制简历ID查询,校验归属当前用户,查不到返回 None"""
async for session in get_db():
result = await session.execute(select(UserJobCustomizeResume).where(UserJobCustomizeResume.id == customize_resume_id, UserJobCustomizeResume.user_id == user_id))
record = result.scalar_one_or_none()
return CustomizeResume.model_validate(record.content) if record else None
return None
async def get(user_id: int, job_id: int) -> dict | None: async def get(user_id: int, job_id: int) -> dict | None:
"""查询定制简历,查不到则加载默认简历构建返回 """查询定制简历,查不到则加载默认简历构建返回
+53 -3
View File
@@ -1,8 +1,8 @@
"""求职助手 Agent 岗位简历优化 Service """求职助手 Agent 岗位简历优化 Service
主要功能:针对岗位并发优化简历。 主要功能:针对岗位并发优化简历;岗位匹配度评分
依赖:resume_loader(简历统一查询)、customize_resume_store(定制简历存取+构建)、job_agent.resume_optimizer(岗位简历优化) 依赖:resume_loader(简历统一查询)、customize_resume_store(定制简历存取+构建)、job_agent.resume_optimizer(岗位简历优化)、job_agent.match_scorer(匹配评分)
使用表:bg_user_resume + 5张子表(通过 resume_loader 查询)、bg_job(查岗位) 使用表:bg_user_resume + 5张子表(通过 resume_loader 查询)、bg_job(查岗位)、bg_user_job_customize_resume(定制简历)
""" """
import asyncio import asyncio
@@ -13,6 +13,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.job_agent.resume_optimizer import optimize_summary, optimize_experience_record from app.ai.job_agent.resume_optimizer import optimize_summary, optimize_experience_record
from app.ai.job_agent.match_scorer import score_match as ai_score_match
from app.core.logger import log from app.core.logger import log
from app.models.job import Job from app.models.job import Job
from app.schemas.customize_resume import CustomizeResume, Education, Work, Internship, Project, Competition from app.schemas.customize_resume import CustomizeResume, Education, Work, Internship, Project, Competition
@@ -25,6 +26,55 @@ class JobAgentChatService:
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
async def score_match(self, user_id: int, customize_resume_id: int, job_id: int) -> int:
"""岗位匹配评分:查定制简历 + 岗位 → 序列化简历 → 调AI评分,返回 0-100 整数"""
cr = await customize_resume_store.get_by_id(user_id, customize_resume_id)
if cr is None:
raise ValueError("定制简历不存在")
job = await self._get_job(job_id)
resume_text = self._build_resume_text_from_cr(cr)
skill_tags = "".join(job.skill_tags) if job.skill_tags else ""
job_desc = f"{job.description or ''}\n{job.requirement or ''}"
return await ai_score_match(job.title or "", skill_tags, job_desc, resume_text)
@staticmethod
def _build_resume_text_from_cr(cr: CustomizeResume) -> str:
"""将定制简历序列化为文本供 AI 评估使用"""
r = cr.resume
parts: list[str] = []
if r.name:
parts.append(f"姓名:{r.name}")
if r.skills:
parts.append(f"技能:{''.join(r.skills)}")
if r.certificates:
parts.append(f"证书:{''.join(r.certificates)}")
if r.summary:
parts.append(f"个人概述:{r.summary}")
if cr.education:
parts.append("教育经历:")
for e in cr.education:
parts.append(f" - {e.school} {e.major} {e.degree} {e.study_type}".rstrip())
if cr.work:
parts.append("工作经历:")
for w in cr.work:
parts.append(f" - {w.company_name} {w.position}".rstrip())
parts.extend(f" {p.text}" for p in w.description if p.text)
if cr.internship:
parts.append("实习经历:")
for i in cr.internship:
parts.append(f" - {i.company_name} {i.position}".rstrip())
parts.extend(f" {p.text}" for p in i.description if p.text)
if cr.project:
parts.append("项目经历:")
for p in cr.project:
parts.append(f" - {p.project_name} {p.role}".rstrip())
parts.extend(f" {seg.text}" for seg in p.description if seg.text)
if cr.competition:
parts.append("竞赛经历:")
for c in cr.competition:
parts.append(f" - {c.competition_name} {c.award}".rstrip())
return "\n".join(parts) if parts else "暂无简历信息"
async def optimize_resume(self, user_id: int, resume_id: int, job_id: int) -> dict: async def optimize_resume(self, user_id: int, resume_id: int, job_id: int) -> dict:
"""针对岗位优化简历:查简历+岗位 → 构建定制简历 → 按单条记录并发AI优化 → 存数据库 → 返回""" """针对岗位优化简历:查简历+岗位 → 构建定制简历 → 按单条记录并发AI优化 → 存数据库 → 返回"""
# 1. 查简历 + 岗位 # 1. 查简历 + 岗位