From 00deef31ad23c6cf9d89ffd49949b9bd142d67ff Mon Sep 17 00:00:00 2001 From: zk Date: Wed, 24 Jun 2026 18:14:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=80=82=E9=85=8D=E8=AF=84?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ai/job_agent/match_scorer.py | 39 ++++++++++++++++++ app/ai/job_agent/prompts.py | 34 ++++++++++++++++ app/ai/model_config.py | 2 + app/api/job_agent_chat.py | 12 +++++- app/schemas/job_agent_chat.py | 5 +++ app/services/customize_resume_store.py | 9 +++++ app/services/job_agent_chat_service.py | 56 ++++++++++++++++++++++++-- 7 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 app/ai/job_agent/match_scorer.py diff --git a/app/ai/job_agent/match_scorer.py b/app/ai/job_agent/match_scorer.py new file mode 100644 index 0000000..d65419d --- /dev/null +++ b/app/ai/job_agent/match_scorer.py @@ -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 diff --git a/app/ai/job_agent/prompts.py b/app/ai/job_agent/prompts.py index ea06066..e63856c 100644 --- a/app/ai/job_agent/prompts.py +++ b/app/ai/job_agent/prompts.py @@ -38,3 +38,37 @@ RESUME_EXPERIENCE_OPTIMIZE_PROMPT = """你是一个求职投递助手。用户 3. 适当使用岗位要求中的关键词润色描述,但不要编造内容 4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除 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}}""" diff --git a/app/ai/model_config.py b/app/ai/model_config.py index a7ed515..dbc5897 100644 --- a/app/ai/model_config.py +++ b/app/ai/model_config.py @@ -24,6 +24,8 @@ class SkillGapModel: class JobAgentModel: """求职助手Agent模块""" + # 岗位匹配评分:评估简历与岗位在技能/学历/经验上的匹配度,输出0-100分(评分要稳定,温度设0) + MATCH_SCORE = LLM.DEEPSEEK_V4_FLASH.create(temperature=0) # 岗位简历-summary优化:针对具体岗位JD优化个人概述 SUMMARY = LLM.DEEPSEEK_V4_FLASH.create(temperature=0.3) # 岗位简历-经历优化:针对具体岗位JD优化单条经历描述 diff --git a/app/api/job_agent_chat.py b/app/api/job_agent_chat.py index 8e07ee1..f7d6541 100644 --- a/app/api/job_agent_chat.py +++ b/app/api/job_agent_chat.py @@ -5,12 +5,22 @@ from fastapi import APIRouter, Depends from app.core.auth import func_permission from app.core.context import RequestContext 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 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="针对岗位优化简历") async def optimize_resume(param: OptimizeResumeParam, _: None = Depends(func_permission("resume_custom"))): """根据目标岗位,AI并发优化简历(summary + 5张子表经历),存Redis并返回""" diff --git a/app/schemas/job_agent_chat.py b/app/schemas/job_agent_chat.py index f96ed88..35201a0 100644 --- a/app/schemas/job_agent_chat.py +++ b/app/schemas/job_agent_chat.py @@ -10,3 +10,8 @@ from pydantic import BaseModel, Field class OptimizeResumeParam(BaseModel): resume_id: int = Field(..., alias="resumeId", 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") diff --git a/app/services/customize_resume_store.py b/app/services/customize_resume_store.py index 7b25479..aa2b34e 100644 --- a/app/services/customize_resume_store.py +++ b/app/services/customize_resume_store.py @@ -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)) +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: """查询定制简历,查不到则加载默认简历构建返回 diff --git a/app/services/job_agent_chat_service.py b/app/services/job_agent_chat_service.py index 21c98d3..182fe47 100644 --- a/app/services/job_agent_chat_service.py +++ b/app/services/job_agent_chat_service.py @@ -1,8 +1,8 @@ """求职助手 Agent 岗位简历优化 Service -主要功能:针对岗位并发优化简历。 -依赖:resume_loader(简历统一查询)、customize_resume_store(定制简历存取+构建)、job_agent.resume_optimizer(岗位简历优化) -使用表:bg_user_resume + 5张子表(通过 resume_loader 查询)、bg_job(查岗位) +主要功能:针对岗位并发优化简历;岗位匹配度评分。 +依赖:resume_loader(简历统一查询)、customize_resume_store(定制简历存取+构建)、job_agent.resume_optimizer(岗位简历优化)、job_agent.match_scorer(匹配评分) +使用表:bg_user_resume + 5张子表(通过 resume_loader 查询)、bg_job(查岗位)、bg_user_job_customize_resume(定制简历) """ import asyncio @@ -13,6 +13,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession 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.models.job import Job from app.schemas.customize_resume import CustomizeResume, Education, Work, Internship, Project, Competition @@ -25,6 +26,55 @@ class JobAgentChatService: def __init__(self, session: AsyncSession): 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: """针对岗位优化简历:查简历+岗位 → 构建定制简历 → 按单条记录并发AI优化 → 存数据库 → 返回""" # 1. 查简历 + 岗位