mark392a-ux/redrob-ranker
GitHub: mark392a-ux/redrob-ranker
面向十万级候选人库的混合式智能排名系统,通过 TF-IDF 与规则评分结合蜜罐检测,在无 GPU 无网络环境下快速筛选出最佳匹配候选人。
Stars: 0 | Forks: 0
# Redrob 智能候选人排名 — 赛道 1 提交
**仓库:**
https://github.com/mark392a-ux/redrob-ranker/
**在线交互式沙盒:**
https://pantheon00-redrob-ranker.hf.space/
**团队:**
Bharat_Vani(个人)
由 Bharat Vani 团队提交。
## 概述
给定 100,000 份简历和一份职位描述,此项目会挑选出最佳的 100 个匹配项——不是通过统计关键词,而是通过检查某人真实的职业历史是否与该职位相匹配,同时在此过程中捕捉虚假/不可能的资料,并用通俗易懂的语言解释每个人入选的*原因*。运行大约需要一分钟,无需网络连接,无需 GPU。
## 项目简介
这是一个用于 Redrob“智能候选人发现与排名挑战赛”(赛道 1,数据与 AI 挑战赛)的混合排名器。它对针对高级 AI 工程师 JD 的所有 10 万名候选人进行评分,并输出排名前 100 的候选人,同时为每位候选人提供 1-2 句推理说明。
## 结果一览
| 指标 | 数值 |
|---|---|
| 人才库规模 | 100,000 名候选人 |
| 运行时间(CPU,无 GPU/网络) | ~55-70s |
| 峰值内存 | ~3.5 GB |
| 标记的蜜罐 | 61(目标:~80) |
| 前 100 名构成 | 明确由高度相关的 AI/ML 职位主导(NLP 工程师、ML 工程师、搜索工程师、推荐系统工程师等) |
| 验证器 | 顺利通过 `validate_submission.py` |
## 快速开始
```
pip install -r requirements.txt
python rank.py --candidates /path/to/candidates.jsonl --out Bharat_Vani.csv
python validate_submission.py Bharat_Vani.csv # validator from the hackathon bundle
```
也支持直接接受 gzip 压缩格式:`--candidates candidates.jsonl.gz`。
在 `test_data/sample_candidates.jsonl` 中包含了一个包含 50 名候选人的小型测试夹具,方便进行快速的冒烟测试:
```
python rank.py --candidates test_data/sample_candidates.jsonl --out test_out.csv --top 20
```
## 蜜罐标记类型 — 每次检查能捕捉什么
| 标记 | 检查内容 | 作为蜜罐信号的原因 |
|---|---|---|
| `skill_zero_usage` | 3 个及以上的技能被标记为专家/高级,且 `duration_months == 0` | 没有人能对使用了零个月的技能达到“专家”水平。阈值 (≥3) 是通过经验调整的:真实人才库的分布呈现清晰的双峰特征——候选人要么有 0 个这样的技能,要么正好有 3-5 个,介于两者之间的极少 |
| `yoe_mismatch` | 声明的 `years_of_experience` 与 `career_history` 时长总和相差 2 年及以上 | 一份声称有 12 年经验但其列出工作总和仅为 4 年的资料是说不通的 |
| `date_math_mismatch` | 某条职业经历的开始/结束日期无法支撑其自身声明的 `duration_months`(相差 3 个月及以上) | 记录仅通过自身的字段就产生了自相矛盾 |
| `education_timeline_impossible` | 声称的工作经验年限超过了毕业以来的年限(+2 年缓冲期) | 不可能在 3 年前刚毕业的情况下拥有 15 年的工作经验 |
| `overlapping_roles` | 两条职业经历的日期范围存在重叠 | 无法同时担任两份全职工作 |
| `multiple_current_roles` | 存在多个 `is_current: true` 的条目 | 与上述问题相同,表现形式不同 |
| `education_date_order` | 某条教育经历的 `end_year` 早于其 `start_year` | 自身就存在矛盾 |
任何一项*硬*检查(日期计算、职位重叠、多个当前职位、教育日期顺序、技能零使用)都足以单独标记一份资料。两项*软*检查(经验年限不匹配、教育时间线)需要与其他标记同时出现,因为仅凭其中一项原则上可能只是描述了一段不同寻常但真实的职业生涯。有关具体逻辑请参见 `src/honeypot.py`,有关塑造这些阈值的经验调查请参见 `diagnose_skills.py` / `diagnose_extra.py`。
## 架构
```
candidates.jsonl
│
▼
honeypot.py ──── flags & drops structurally-impossible profiles
│ (date-math errors, overlapping roles, expert-skill-with-
│ zero-usage, yoe/career-history mismatch, etc.)
▼
text_features.py ── TF-IDF + cosine similarity, JD vs candidate text,
│ fit once over the whole surviving pool
▼
rule_features.py ── title match, skills trust (proficiency × duration ×
│ endorsements), production-vs-research signal,
│ experience-band fit, location, education tier,
│ JD-specific disqualifier penalties
▼
behavioral.py ───── multiplier (0.4-1.0) from redrob_signals: recency,
│ recruiter response rate, interview completion,
│ notice period, open-to-work
▼
scorer.py ───────── fit_score = weighted_sum(rule features) - penalties
│ final_score = fit_score × behavioral_multiplier
▼
reasoning.py ─────── template-built explanation from the same components
│ the scorer used (no LLM call — see below)
▼
rank.py ──────────── sorts, takes top 100, ties broken by candidate_id
ascending, writes submission.csv
```
## 为什么做出这些设计选择(用于方法总结 / 面试)
**使用 TF-IDF 而不是密集(dense)嵌入模型。** 零网络依赖,零模型下载步骤,并且无需任何预计算编排即可满足“排名期间无网络”的限制。IDF 加权意味着罕见的 JD 特定术语(例如“qdrant”、“ndcg”)的权重远高于普通词汇,这无需 400MB 的模型文件就能完成大部分“不仅仅是统计关键词”的工作。`text_features.py` 是日后若要替换为密集嵌入唯一需要更改的模块——离线拟合一个本地的 sentence-transformers 模型,将其缓存,然后用预计算的嵌入矩阵 + 余弦相似度替换 `build_relevance_scorer` 的内部逻辑。(如果你这样做,请确保模型下载发生在计时运行*之前*——在排名时下载属于“排名期间使用网络”,会导致你在第 3 阶段被取消资格。)
**为什么职位匹配的权重独立于技能/文本相关性。** 这是应对该数据集中关键词堆砌陷阱的决定性信号:一个列出了十个 AI 相关技能的“市场经理”或“HR 经理”在这里得分依然很低,因为他们实际的角色历史不匹配;而一个具有相关职业描述但技能列表稀疏的“高级后端工程师”会获得高分。
**为什么技能通过 duration_months 和认可度进行加权,而不仅仅是看是否存在。** 一项被列为“专家”但使用时长为 0 个月且认可度为 0 的技能几乎不应该算数——这是一种直接、可解释的防御措施,用于对抗技能数组中懒惰的关键词堆砌,且独立于蜜罐过滤器。
**为什么蜜罐检测是硬性预过滤,而不是评分惩罚。** 规范规定,在第 3 阶段,前 100 名中蜜罐率超过 10% 的提交将被取消资格——这是一个二元悬崖,而不是渐进的梯度。因此,被标记为结构上不可能的候选人会在评分前被移除,而不是仅仅被降低权重。
**为什么推理列是通过模板生成的,而不是由 LLM 生成的。** 推理文本是由计时的、无网络的排名步骤生成的 CSV 的一部分——在那里为每个候选人调用托管的 LLM 不仅会打破在 10 万规模下的 5 分钟时间预算,还会违反网络限制。使用评分器提取的相同特征来构建它,也意味着它在结构上不可能凭空捏造(幻觉)候选人并不具备的技能,这正是第 4 阶段“无幻觉”检查所要排查的内容。
## 测试
```
pytest tests/ -v
```
19 个测试覆盖了 `honeypot.py`(每种检查类型,既包括正向的“它是否会在不可能的资料上触发”,也包括负向的“它是否会在干净的资料上保持沉默”——一个过于敏感的蜜罐过滤器会取消真实候选人的资格,这与捕捉虚假资料同等重要)以及 `scorer.py`(权重和的健全性、核心的防关键词堆砌保证、取消资格的惩罚以及得分边界)。
## 在线沙盒
https://pantheon00-redrob-ranker.hf.space/ — 运行与此仓库完全相同的 `src/` 模块(同步的副本,而不是重新实现)。上传 `.json`/`.jsonl` 候选人文件(根据黑客松“小样本”沙盒要求,上限为 500 名候选人,以实现快速交互式演示),或使用内置的 50 名候选人样本。它展示了蜜罐筛选结果、标记类型细分、包含完整推理文本的排名表,以及演示运行输出的 CSV 下载。
## 10 万级规模的性能
在合成的 10 万行压力测试上测量(见提交历史):~70 秒挂钟时间,~3.5GB 峰值内存。轻松满足 5 分钟 / 16GB 的预算限制,如果日后添加更重的特征,还有充足的余量。
## 已知局限 / 下一步计划
- TF-IDF 能捕捉词法及近乎词法的重叠;它会遗漏那些描述“构建了一个从点击中学习用户偏好的系统”却从不提及“推荐”或“排名”的候选人。密集嵌入模型可以填补这一空白——参见上面的替换说明。
- `config.py` 中的取消资格关键词列表是从 JD 明确的语言中提取的起点;一旦你对实际的 10 万级人才库的词汇(公司名称、职位变体、技能命名约定)有了深入了解,就应该对它们进行针对性调整。
- 蜜罐启发式方法经过了结构性验证(日期计算、内部一致性),并针对真实的 10 万级人才库进行了确认:标记了 61 名候选人(40 名 yoe_mismatch,35 名 date_math_mismatch,21 名 skill_zero_usage,18 名 education_timeline_impossible——这些存在重叠,因为一个候选人可能触发多次检查)。通过 `diagnose_extra.py` 测试了另外四个假设,并故意*没有*将它们转化为过滤器,因为它们在真实人才库中的基础发生率太高,不可能是 10 万人中约 80 个的蜜罐信号:薪资 min>max(18,865 名候选人,18.9%),技能时长超过总职业历史(跨分布共 13,581 名候选人),技能时长超过声明的 years_of_experience(13,449 名候选人),以及 last_active_date 早于 signup_date(7,496 名候选人,7.5%)。这些显然只是合成资料生成方式上的噪声,而不是故意的陷阱——根据这些进行过滤会取消大量合法候选人的资格,而不是捕捉蜜罐。`multiple_current_roles` 和 `overlapping_roles` 在整个数据集中始终保持为 0,这表明那些特定的蜜罐子类型(如果存在的话)并没有体现在该数据集的构建中。
- 剩余的差距(捕捉到了约 80 个中的 61 个)被作为一个实际的停止点接受,而不是进一步追赶:实际的取消资格风险在于*前 100 名中*的蜜罐率,而不是整个数据库的总检测率,而且这里的前 100 名已经明确由具有连贯职业叙事的高度相关职位所主导——建议在提交前对最终的前 100 名进行人工抽查,以对照已知的蜜罐模式,作为最后一道安全网。
标签:TF-IDF, 云计算, 人工智能, 安全规则引擎, 异常检测, 用户模式Hook绕过, 简历解析, 规则引擎, 逆向工具