skip to content
tlj 的工程笔记
← 项目

HR 简历筛选系统

面试作品

给做外包招聘的 HR 朋友用的简历筛选工具:AI 负责语义理解、代码负责严格匹配。一个'短 JD 把真没经验的人排到前面'的 bug,逼我加了一层 LLM 布尔判断,也让我想清楚 embedding 相似度什么时候不够用。

时间
2026-05
角色
独立完成(架构 / 算法 / 部署)
成果
硬条件精确匹配 + 语义偏好分离;用 LLM 布尔判断层修掉短 JD 排序错乱,把区分度从差 1 分拉到差 27 分

给几个做外包/猎头招聘的 HR 朋友用的简历筛选辅助工具。他们帮多家客户招聘,JD 和规则经常变。核心思路一句话:AI 负责语义理解(抽取信息),代码负责严格匹配(打分排序),分三阶段——JD 结构化、简历结构化、规则匹配。

一个决策:LLM 只做它稳的事

需求里有个诱惑:让 LLM 直接从 JD 派生出每个条件的权重、再让 HR 拖滑块微调。我一度顺着「减少你工作量」切到了这个方案,然后自己把它推翻了——理由是:原始需求文档自己就写了「JD + 简历直接丢大模型不稳定」,那「JD → 权重」同样是不稳定的业务判断、没有事实依据;而且 HR 拖滑块是伪体验(HR 根本不知道该拖 30 还是 50)。最终定的是:LLM 只做信息抽取这种稳定任务,权重由模板均分。规则引擎分两类——硬条件(字符串/数值精确匹配,pass/fail)和语义偏好(embedding 相似度),分开处理,最终分按硬条件过/不过自然落到 [60,100] 和 [0,40] 两段。

核心 bug:短 JD 把真没经验的人排到了前面

测试时用一份只有一句话的短 JD(「有手机或平板的项目经验」做 stress test),结果排序彻底错了:3 个真有手机/平板经验的 sensor 工程师,反而被 3 个真没经验的咨询顾问压在下面

根因拆下来很清楚:JD 太短 → 只有一个语义偏好、它占了 100% 权重 → 所有人硬条件都过、都拿 60 分 → 剩下 40 分全靠那一个 cosine 相似度。而 embedding 对「手机或平板的项目经验」这种宽泛 query 区分度不够,6 个人的相似度都挤在 0.62-0.74,咨询顾问简历里的泛行业词正好被算出偏高相似度。embedding 相似度在 query 宽泛、候选同质时会失灵——这是它的固有局限,不是参数没调好。

我没有用治标的办法(调阈值、或把短 JD 硬拆成 3-5 维),而是加了一层 LLM 布尔判断:把简历和每条偏好给 LLM,逐条输出 yes/no(强制 JSON)。打分公式改成 match = 0.3*cosine + 0.7*(1 if 判yes else 0)——LLM 布尔占 70%、cosine 保留 30% 作 API 失败的兜底 + 同样判 yes 的人之间做差异化。修复后那 3 个 sensor 从排 1/5/6 变成 1/2/3,分段差距从 5 分扩大到 27 分。另一份 13 条偏好的 JD,区分度也从「第一名第二名差 1 分」变成「差 20 分」。

部署踩的两个坑

几个务实的取舍

小结

这个项目最有价值的认知是 embedding 相似度的边界——它在 query 宽泛、候选同质时区分度不够,这时候得让 LLM 做个明确的布尔判断把信号拉开。以及那个「worker 假死」的排查:看着像死锁,其实是 SDK 阻塞 + 超时设太短,先分清「死」和「慢」再动手。