跳到博客正文

投出去的简历,在到人手里之前先过一遍机器。这句话大部分人听过,但很少有文章说清楚机器具体 做了什么。搜「AI 简历筛选」出来的基本是厂商内容,通篇「准确率 XX%」「匹配效率提升 XX%」, 往上溯源一层就断了。读完你还是不知道自己该改哪儿。

这篇不推荐工具,只拆机制。

先说一件我们不知道的事:国内每家招聘平台、每套企业自建的招聘系统,用的是什么解析引擎、 什么匹配模型,都不公开。下面讲的是这类系统的通用工作方式,不是任何一家的具体实现。拿不准 的地方我会直说。

机器处理一份简历分三段:解析、匹配、排序。三段的坏法不一样,你能做的事也不一样。

简历进入招聘系统后依次经过解析、匹配、排序三个阶段的流程示意图:解析阶段把 PDF 拆成结构化字段,匹配阶段计算简历与岗位描述的贴合度,排序阶段生成一份按分数排列的候选人列表

解析:把 PDF 变成字段,这一步就会失败

解析要做的事,是把一个人类可读的文件变成一张数据库表:姓名、手机、最高学历、毕业院校、 每段工作的公司名和起止时间、技能词表。

这件事有四层,每层都能单独坏掉。

第一层是把字符抽出来。如果 PDF 的文本层本身是坏的,后面三层拿到的就是一堆碎片。中文 简历在这一层的翻车方式很隐蔽:排版看着完全正常,复制出来汉字之间全是空格。成因和自查方法 我们单独写过一篇,见中文简历 PDF 的文本层真相。这一层 没过,后面全都不用谈。

第二层是切块、定阅读顺序。解析器把页面上的文字块按顺序连成一条流。绝大多数解析器按 线性顺序读:从上往下,遇到同一水平线上的东西就从左往右。

所以双栏排版是高风险的。左边一栏技能、右边一栏工作经历,解析器很可能横着读过去,把两栏 交替串在一起。下面这行是我构造的示例,不是真实提取结果,但失真的方式就是这样:

Python 2021.03 - 2023.07  SQL  某某科技  Docker  后端开发工程师

技能词和公司名、职位名混成一行之后,「后端开发工程师」再也不会作为一个独立的职位字段存在。 同类风险还有:用表格排的整段经历、文本框、分栏符。

第三层是认出每一块是什么。这一层大量依赖你写的章节标题。「工作经历」「实习经历」 「教育背景」「项目经历」这类通用说法,任何解析器都认得。「我的旅程」「职业足迹」「一些 作品」认不出来,这一块要么被扔进「其他」,要么跟着上一个认得出来的标题走,变成你教育背景 的一部分。

我知道有人嫌通用标题土。它是土,但它是这一层唯一的路标。

第四层是从块里抽字段。中文特有的麻烦集中在这里:

  • 日期格式。2021.03-2023.072021年3月至2023年7月21.3—23.7,同一份简历里混着用, 抽出来的工作年限就可能是错的。而工作年限经常是硬过滤条件。
  • 分词。中文没有空格,切词要靠词典和模型。「机器学习」被切成「机器」和「学习」是这类系统 的老问题,专有名词、新框架名、公司简称都容易切错。
  • 实体识别。「某某科技(上海)有限公司」和它的品牌简称,在系统眼里可能是两家公司。

最麻烦的一点:解析失败通常不报错。 它不会告诉招聘方「这份简历没读懂」,它交出的是一张 看起来填满了的表,只是字段和内容对不上。你收到的反馈是「暂不合适」,或者什么也没有。

多少比例的简历在这一层出问题?我们没有可靠的公开数据。厂商材料里的数字溯源不到,我们不转述。 但机制上很清楚:上面四层任何一层出错,后面两段都在处理一份错的数据。

匹配:语义匹配和关键词匹配差在哪

解析完成之后,系统手上有两份结构化数据:你的简历,和这个岗位的 JD。匹配就是算这两者有多贴。

关键词匹配是老办法。把 JD 和简历都切成词,建索引,算重合。它的问题是字面依赖:你写 「后端开发」,JD 写「服务端研发」,字面不重合。要救只能靠人工维护的同义词词典,而词典永远 补不全。

语义匹配是现在的主流方向。把文本转成向量,在向量空间里算距离;或者更直接,让大模型 读完两段文本给一个分和一段理由。这时「服务端研发」和「后端开发」离得很近,不需要词典。

实际系统通常混着用,而且有先后:

  1. 硬过滤在最前面。学历层次、工作年限、城市、是否应届,这类字段是布尔判断,不参与打分。不满足,直接不进池子。
  2. 然后是召回。用关键词从候选库里捞出一批,几百到几千人不等。
  3. 最后才是打分排序。语义模型只在这个池子里工作。

这个顺序有一个直接后果:一个字段填错,比一整段经历写砸更致命。 经历写砸只是分低, 字段错是根本不进池子。

还有一点值得单独说:语义匹配的输入是句子,不是词。 技能栏里孤零零一个「Kubernetes」, 和工作经历里一句「把三个服务的部署从手工脚本改成 Kubernetes 滚动发布」,在向量空间里离 JD 的距离不一样。后者带着动作、对象和场景,前者只是一个标签。

排序:你不是在过线,你是在排队

这一段是最多人误解的。

机器筛选没有「及格线」。招聘方打开一个岗位,看到的是一个按分数排好的列表,他从上往下看。 看到第几个停下来,取决于他这周要约几个人、有多少时间。

所以同一份简历,投一个二十人竞争的岗位可能排第三,投一个八百人的岗位可能排两百开外。你 没有被判定为「不合格」。你只是排在后面,而后面的人不会被点开。

这件事改变了优化的目标。「达标」在排队模型里没有意义,你要比同一批投递者里的其他人更贴 这个 JD。 一份通用简历投所有岗位,结果是在每个池子里都处于中游。中游不会被点开。

顺带一个推论,没有数据支撑,只是从排队这个模型推出来的:岗位刚放出来的时候池子浅,同样的 分数排位靠前。投递时机可能比大多数人以为的更重要。

关键词堆砌为什么会反噬

常见的堆法有三种:技能栏塞四十个词;把 JD 整段复制到简历末尾;用白色字体把关键词写在页面 上,渲染看不见但文本层里有。

第三种先说,因为它性质不同。文本层里有、人眼看不见,这正好是机器能读而你读不到的那一层。 把它当技巧是个误判:一旦被看到,这是造假,不是优化。而且这类文本在排版流里往往和正文位置 错乱,解析出来的结果可能比不塞更糟。

前两种不是造假,但在语义匹配面前同样不划算,原因有三个。

一是稀释。语义模型看的是整份文本的表示。四十个技能词里有三十五个没有任何句子承载,整份 简历给出的画像就从「一个后端工程师」变成「一个什么都沾一点的人」。你在向量空间里被拉向了 中心,而中心离任何一个具体 JD 都不近。

二是比较对象。排序阶段和你比的是同一个池子里的其他人。他们的关键词后面接着经历,你的 关键词后面是逗号。

三是机器之后还有人在读。技能栏写「精通 Kubernetes」,工作经历里一个字没提,面试第一个 问题就是这个。这一关的代价比排名靠后高得多。

我的看法是:技能栏最该做的动作是删,不是加。 删到剩下的每一个词你都能接一句「我在 哪个项目里用它做了什么」为止。

真正能改善匹配的三件事

第一,用 JD 的词,但要放在它该在的地方。 不是把 JD 的词抄进技能栏,是把你真做过的那 件事,换成 JD 的说法重写一遍。

下面这组改写是构造的示例,里面的数字都是编的:

改前 改后
负责公司内部系统的接口开发 承接订单中心 12 个对外接口的服务端开发与联调,对接 3 个业务方
熟悉高并发 大促期间把订单查询接口的响应从秒级压到百毫秒级,做法是加二级缓存和读写分离

右边那栏里,「服务端开发」「联调」「缓存」「读写分离」都是 JD 里会出现的词,但它们是被一 句真事带出来的,不是列出来的。关键词匹配能命中它们,语义匹配能理解它们的上下文,人读到也 知道你干了什么。三段都吃这一口。

第二,让结构可解析。 单栏、通用章节标题、统一日期格式、文字不做成图片。这些和内容无关, 是模板属性,定一次就固定下来。如果你现在用的是双栏排版,换掉的代价通常只是从 模板库里挑一个单栏的重排一遍。

第三,给每个能力配一件可验证的事。 对象、量级、结果,有一个算一个。这条同时服务于 语义匹配和后面读简历的人。

三件事里只有第二件能靠工具兜底。在不繁简历的简历列表里新建一份, 日期和章节这些结构化字段由编辑器统一管着,导出的时候不用你自己对格式。第一和第三件是内容, 谁也替不了你写。

机器过了之后,人还要再读一遍

排序只负责把你送到招聘方眼前。决定要不要约你的,还是那个从上往下翻的人,他花在你身上的 时间是几十秒。

机器和人的偏好不完全一致。机器喜欢结构稳定、字段对得上,人喜欢一眼看出你是干什么的。两者 不冲突,但要查的东西完全不一样,所以自检最好分成两遍做:一遍用机器的眼睛,一遍用人的眼睛。 我们把这两遍拆成了二十项,写在 投递前的 20 分钟自检里。

今天能做的一件事:打开你最想投的那个岗位的 JD,把里面出现的名词抄下来,一个一个回到简历 里找。找得到的,看看用的是不是同一个说法;找不到的,想想你有没有真做过。做过但没写,是最 可惜的一种。