Files
CareerNexus/app/match/engine.py
T
RoyirinRosmontis a10442d78c CareerNexus 职联:岗位能力图谱构建与动态演化分析平台(挑战杯 XH-202621)
- 全链路:多源JD接入/清洗交叉验证/结构化抽取(星火+离线双通道)/时序知识图谱/新岗位发现/演化分析/人岗匹配/学习路径
- WebUI 五视图:驾驶舱/图谱工作台/岗位演化/匹配(单人+批量)/学习路径
- 设置中心(config.json)/Token鉴权/SQLite状态库/语义向量通道(BGE-M3本地或联网API)
- 71 个 pytest 用例;数据目录仅保留结构(.gitkeep),数据由 scripts/generate_dataset.py 复现
2026-08-03 13:15:35 +08:00

287 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""人岗匹配引擎:技能覆盖率召回 + 四维加权评分 + 差距分析 + 学习路径。
四维权重默认 必备 50% / 加分 15% / 经验 20% / 场景 15%,可通过 config 覆盖。
学习路径:缺失技能按图谱/词表中的前置关系拓扑排序,分阶段输出。
"""
from __future__ import annotations
import logging
from app.extract.base import load_lexicon
from app.models import JobDefinition, MatchResult, ResumeProfile
logger = logging.getLogger(__name__)
DEFAULT_WEIGHTS = {
"required": 0.50,
"preferred": 0.15,
"experience": 0.20,
"scenario": 0.15,
}
def _coverage(have: set[str], want: list[str]) -> tuple[list[str], list[str], float]:
matched = [s for s in want if s in have]
missing = [s for s in want if s not in have]
cov = len(matched) / len(want) if want else 1.0
return matched, missing, cov
def topological_stages(missing: list[str], lexicon: dict) -> list[dict]:
"""把缺失技能按前置关系拓扑排序分阶段(Kahn 分层)。
前置关系取自技能词表 prereqs;前置技能若已掌握或不在缺失集中则视为已满足。
每个阶段附带结构化验收标准(产出物 / 量化验收项 / 自测问答),按技能类别生成。
"""
prereq = {s["name"]: [p for p in s.get("prereqs", []) if p in missing]
for s in lexicon["skills"] if s["name"] in missing}
cat_of = {s["name"]: s.get("category", "") for s in lexicon["skills"]}
remaining = set(missing)
stages: list[dict] = []
stage_no = 1
while remaining:
ready = sorted(s for s in remaining if not [p for p in prereq.get(s, []) if p in remaining])
if not ready: # 有环兜底:按字典序放出,保证终止
ready = sorted(remaining)
stages.append({"stage": stage_no, "skills": ready,
"goal": f"第 {stage_no} 阶段:掌握 {'、'.join(ready)}",
"acceptance": build_acceptance(ready, cat_of)})
remaining -= set(ready)
stage_no += 1
return stages
# ---------------------------------------------------------------- 验收标准模板
# 按技能类别给出差异化的验收过程:产出物、量化验收项、自测问答。
# {skills} 占位为本阶段技能名(顿号连接),{s1}/{s2} 为前两个技能。
_ACCEPTANCE: dict[str, dict] = {
"编程语言": {
"deliverable": "用 {skills} 独立完成一个 300 行以上、可运行的小工具或 CLI 程序,源码提交仓库并附 README",
"criteria": ["覆盖核心语法与常用标准库,代码可一次性运行无报错",
"编写不少于 5 个单元测试用例并全部通过",
"README 说明设计取舍与至少 2 个踩坑记录",
"代码符合该语言主流风格规范(通过对应 linter 检查)"],
"self_check": ["{s1} 与一门相近语言在类型与内存模型上的核心差异是什么?",
"{s1} 最常见的 3 个性能陷阱是什么,如何规避?",
"如果把这个工具的数据规模放大 100 倍,瓶颈会先出现在哪里?"],
},
"后端框架": {
"deliverable": "基于 {skills} 实现一个不少于 3 个接口的 REST 服务,含鉴权与参数校验",
"criteria": ["分层清晰(路由/服务/数据访问分离),无不合理耦合",
"关键接口有自动化测试,覆盖率不低于 60%",
"完成一次简单压测并记录 QPS 与耗时分布",
"能手绘并讲解一次请求的完整生命周期"],
"self_check": ["{s1} 的依赖注入/中间件机制是如何工作的?",
"生产环境下 {s1} 服务如何做优雅停机与连接管理?",
"哪些场景下你会放弃 {s1} 而选择更轻的框架?"],
},
"前端": {
"deliverable": "用 {skills} 完成一个可运行的单页应用(不少于 2 个页面),公开访问或录屏演示",
"criteria": ["组件拆分合理、状态管理方案有明确选型理由",
"主流浏览器无 console 报错,页面响应式适配两种宽度",
"完成一次构建优化(分包/懒加载),记录优化前后体积",
"核心交互有至少 3 个组件级测试"],
"self_check": ["{s1} 的渲染/更新机制是怎样的,什么操作会触发不必要的重渲染?",
"状态提升与全局状态各自适合什么场景?",
"首屏白屏时间可以从哪几个环节优化?"],
},
"数据库": {
"deliverable": "围绕 {skills} 设计并实现一个业务库表方案与查询集,附索引设计说明",
"criteria": ["给出 ER 图与范式/反范式取舍的书面说明",
"用 EXPLAIN 分析并优化至少 2 条慢查询,记录前后对比",
"通过实验验证事务隔离级别的差异(附复现 SQL)",
"完成一次备份与恢复演练"],
"self_check": ["什么情况下索引会失效?各举一个例子",
"{s1} 的 redo/undo 或持久化机制保证了什么?",
"千万级数据量下,分页查询如何优化?"],
},
"中间件": {
"deliverable": "本地搭建 {skills} 环境并跑通生产/消费(或读写)完整 demo",
"criteria": ["给出关键配置项(持久化/高可用/过期策略)的逐项说明",
"演练消息丢失、重复消费或缓存穿透场景并给出防护方案",
"接入监控并解读 3 个核心指标",
"整理一份与同类中间件的选型对比表"],
"self_check": ["{s1} 在什么条件下会丢数据?如何避免?",
"集群模式下数据是如何分布与迁移的?",
"什么业务场景下不该使用 {s1}?"],
},
"大数据": {
"deliverable": "用 {skills} 跑通一个批处理或流处理作业,输入输出可复现",
"criteria": ["说明分区与并行度策略及其依据",
"制造并解决一次数据倾斜,记录处理前后耗时",
"给出作业的容错与重跑方案",
"输出结果有数据质量校验(行数/空值/分布)"],
"self_check": ["{s1} 的 shuffle 过程发生了什么,为什么昂贵?",
"批处理与流处理的语义差异体现在哪里?",
"如何判断该作业该垂直扩容还是水平扩容?"],
},
"AI算法": {
"deliverable": "基于 {skills} 复现一个 baseline 模型或算法 demo,附实验记录",
"criteria": ["数据集划分规范(训练/验证/测试),杜绝数据泄漏",
"输出完整指标报告(P/R/F1 或 Loss/Acc 曲线)",
"完成至少 1 组消融或调参对比并记录结论",
"能讲清模型的适用边界与失败案例"],
"self_check": ["如何判断模型过拟合?你用了哪些手段缓解?",
"指标不变差的前提下,推理成本可以怎么降?",
"{s1} 与传统方法相比,优势和代价各是什么?"],
},
"云原生": {
"deliverable": "将一个应用容器化并部署到 {skills} 环境,完成一次滚动更新与回滚",
"criteria": ["Dockerfile 使用多阶段构建,镜像体积有优化记录",
"编排配置包含资源限额、健康探针与副本策略",
"演练滚动更新与回滚全过程并截图/录屏",
"日志与监控接入,能定位一次人为注入的故障"],
"self_check": ["三种探针的区别是什么,配错会导致什么后果?",
"有状态服务上 {s1} 要注意什么?",
"镜像分层缓存如何影响 CI 构建速度?"],
},
"运维": {
"deliverable": "用 {skills} 实现部署/巡检/告警中的至少一个自动化工具并投入使用演示",
"criteria": ["工具覆盖部署、监控、告警三个环节中的至少两个",
"完成一次故障注入演练(如杀进程/打满磁盘)并记录处置时长",
"编写运行手册(部署步骤、回滚步骤、常见告警处置)",
"脚本具备幂等性,重复执行不产生副作用"],
"self_check": ["负载飙升时你的排查顺序是什么?",
"监控覆盖率和告警疲劳之间怎么平衡?",
"{s1} 自动化过程中最需要防的人为风险是什么?"],
},
"测试": {
"deliverable": "为一个真实模块编写测试计划与自动化用例集,并在 CI 中跑通",
"criteria": ["用例设计覆盖等价类、边界值与异常路径,附设计说明",
"自动化用例不少于 10 条,CI 触发全绿",
"提交至少 2 份规范缺陷报告(复现步骤/期望/实际/环境)",
"输出覆盖率报告并说明未覆盖部分的风险"],
"self_check": ["如何为一个你从未见过的系统设计测试策略?",
"自动化测试的投入产出比怎么评估?",
"线上问题复盘时,测试环节应该如何改进?"],
},
"数据分析": {
"deliverable": "围绕 {skills} 完成一份端到端数据分析报告(数据获取→清洗→分析→结论)",
"criteria": ["清洗与处理过程代码化、可一键复现",
"给出不少于 3 个有业务含义的洞察,每个都有数据支撑",
"图表选型恰当,坐标轴与口径标注完整",
"明确写出结论的适用范围与可能的偏差来源"],
"self_check": ["什么是辛普森悖论?如何在分析中规避?",
"相关关系和因果关系的差异对结论有什么影响?",
"如果数据缺失 30%,你的处理策略是什么?"],
},
"产品": {
"deliverable": "围绕 {skills} 输出一份完整 PRD 或实验方案(含原型或流程图)",
"criteria": ["明确定义北极星指标与至少 2 个护栏指标",
"实验类方案需给出样本量估算、周期与显著性判定规则",
"包含灰度发布与回滚预案",
"预设结果判定规则:什么数据下继续/迭代/放弃"],
"self_check": ["A/A 测试的目的是什么?不做会有什么风险?",
"p 值和置信区间分别说明什么,常见误用有哪些?",
"指标提升但用户体验下降时,决策依据是什么?"],
},
"嵌入式": {
"deliverable": "基于 {skills} 完成一个可烧录运行的固件 demo",
"criteria": ["完成交叉编译与烧录,功能演示正常",
"给出关键中断/时序的分析记录",
"测量并记录功耗或资源占用数据",
"代码有看门狗与异常恢复处理"],
"self_check": ["裸机与 RTOS 的核心差异是什么,何时必须上 RTOS?",
"如何定位一次偶发的硬件通信失败?",
"Flash/RAM 受限时的优化手段有哪些?"],
},
"物联网": {
"deliverable": "用 {skills} 搭建一条设备数据采集→上报→可视化的端到端链路",
"criteria": ["说明通信协议选型理由(如 MQTT/CoAP/HTTP)",
"实现断线重连与本地缓存补传",
"完成弱网/断电场景下的鲁棒性演练",
"数据可视化看板可实时刷新"],
"self_check": ["弱网环境下消息的可靠性与时延如何权衡?",
"设备身份认证与防伪造怎么做?",
"海量设备接入时,架构的瓶颈会先出现在哪?"],
},
}
_DEFAULT_ACCEPTANCE = {
"deliverable": "独立完成一个综合运用 {skills} 的迷你项目,源码与说明文档齐全",
"criteria": ["项目可运行、核心流程演示无阻塞",
"能完整讲解 {s1} 的核心原理与常见坑",
"整理不少于 5 条学习笔记或踩坑记录",
"向他人做一次 10 分钟分享并通过提问环节"],
"self_check": ["{s1} 解决了什么问题?没有它时人们怎么做?",
"学习 {s1} 过程中最反直觉的一点是什么?",
"如何把本阶段技能组合应用到目标岗位的真实场景中?"],
}
def build_acceptance(skills: list[str], cat_of: dict[str, str]) -> dict:
"""按阶段内技能的主导类别选择模板,生成结构化验收标准。"""
cats = [cat_of.get(s, "") for s in skills if cat_of.get(s)]
if cats:
# 主导类别(并列时取阶段内先出现的技能类别)
cat = max(cats, key=lambda c: (cats.count(c), -cats.index(c)))
else:
cat = ""
tpl = _ACCEPTANCE.get(cat, _DEFAULT_ACCEPTANCE)
s1 = skills[0]
s2 = skills[1] if len(skills) > 1 else skills[0]
joined = "、".join(skills)
def fmt(text: str) -> str:
return text.format(skills=joined, s1=s1, s2=s2)
return {
"category": cat or "通用",
"deliverable": fmt(tpl["deliverable"]),
"criteria": [fmt(c) for c in tpl["criteria"]],
"self_check": [fmt(q) for q in tpl["self_check"]],
}
class MatchEngine:
"""人岗匹配引擎。"""
def __init__(self, lexicon: dict | None = None, weights: dict | None = None,
threshold: float = 60.0):
self.lexicon = lexicon or load_lexicon()
self.weights = {**DEFAULT_WEIGHTS, **(weights or {})}
self.threshold = threshold
self.category_of = {s["name"]: s.get("category", "") for s in self.lexicon["skills"]}
def match(self, profile: ResumeProfile, job: JobDefinition) -> MatchResult:
have = set(profile.skills)
m_req, miss_req, cov_req = _coverage(have, job.required_skills)
m_pref, miss_pref, cov_pref = _coverage(have, job.preferred_skills)
exp_score = min(1.0, profile.years / max(1, job.expected_years))
scen_matched = [s for s in job.scenarios if s in set(profile.scenarios)]
cov_scen = len(scen_matched) / len(job.scenarios) if job.scenarios else 1.0
dims = {
"required": round(100 * cov_req, 2),
"preferred": round(100 * cov_pref, 2),
"experience": round(100 * exp_score, 2),
"scenario": round(100 * cov_scen, 2),
}
total = round(100 * (
self.weights["required"] * cov_req
+ self.weights["preferred"] * cov_pref
+ self.weights["experience"] * exp_score
+ self.weights["scenario"] * cov_scen
), 2)
# 可迁移技能:与缺失必备技能同类别、且候选人已掌握的技能
miss_cats = {self.category_of.get(s, "") for s in miss_req} - {""}
transferable = sorted(s for s in have
if self.category_of.get(s, "") in miss_cats
and s not in job.required_skills)
path = topological_stages(miss_req + miss_pref, self.lexicon)
return MatchResult(
job_id=job.job_id,
job_title=job.title,
total_score=total,
dimension_scores=dims,
matched_required=m_req,
missing_required=miss_req,
matched_preferred=m_pref,
missing_preferred=miss_pref,
transferable_skills=transferable,
learning_path=path,
is_match=total >= self.threshold,
)