"""人岗匹配引擎:技能覆盖率召回 + 四维加权评分 + 差距分析 + 学习路径。 四维权重默认 必备 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, )