Mika Flowers 在 dev.to 上把两封邮件的时间戳并排贴出来。我盯着看了会儿,第一反应不是生气,是熟悉。
那种熟悉,跟 review 一段 agent 生成的代码差不多——文件名读对了,模式匹配上了,活交了,diff 干净,逻辑自洽,就是没读过上下文。
时间线大概这个形状:
09:14:07 Application submitted
09:15:11 Thanks for applying. We've received your application.
09:16:03 After careful consideration, we've decided to move forward with other candidates.
"After careful consideration"。两分钟。
两分钟够干什么?打开简历——可能。扫一眼求职信——如果你手快。再把她专门写的、把自身经历逐条对应到岗位职责的那段读一遍,跟 JD 交叉比对,然后落一个"审慎"的判断?
不够。谁都知道不够。
她投的是 Junior Solutions Engineer。初级。不是那种开头先写"需要 5 年以上经验"把你劝退的岗,就是初级。JD 列得很杂:Python、SQL、数据工具、自动化、AI 产品、B2B 软件、合法工作权利,外加一条——STEM、金融或者其他"强分析类"领域的学位。
她没学位,还是投了。理由朴素得有点天真:JD 里写的事,她基本都真干过。画外音:这句话在筛选器眼里约等于零。
她干过的到底是什么
给一个家电客户搭内部库存系统。客户之前员工在 WooCommerce 上一条条手工建商品,拍照、定价、写描述、改库存,一批货能耗掉一整天。她把整条流程包了下来:一份家电清单加上传的照片进去,出来的是结构化商品条目,自动对齐库存、生成标题和描述,促销价和 MSRP 并排摆着,员工发布前逐条过目、随时能改。
真正让我坐直的是后面这块:多设备同步,以及防冲突。几个人同时动同一批库存,不互相覆盖,也不重复做已经做完的条目。
这活儿的关键词形状非常差。"库存同步"不是 JD 关键词,"WooCommerce"大概率也不在列表里。真要命的是,她做的东西压根没法被关键词概括——"几个人同时改同一批数据还能不打架",这句话里没有一个词是招聘系统认得的。可这段经历里塞着真实世界最难的两样东西:并发状态,和会来抱怨的真实用户。API 集成、数据库、部署、同步问题排查、听客户反馈改需求,这一整套她都趟过。
跟着教程做的练习项目,和交付给真实业务、真的被人用的软件,中间隔的不是熟练度,是"有人会因为它出问题而损失钱"。做过一次,你就再也回不去写玩具了。
第一层到底在筛什么
评论区有人总结得很准:第一道筛选看的不是技能,是简历上的信号。学位、学校、认证、头衔、年限,或者精确的关键词。写成代码大概长这样:
def screen(resume, jd):
score = 0
if has_degree(resume.education, field="STEM"):
score += 30
for kw in jd.keywords:
if kw.lower() in resume.text.lower():
score += 5
return "PASS" if score >= THRESHOLD else "REJECT"
注意这个函数里没有一行在问"这个人做过什么"。
有人会说,这不是人干的,是 ATS 在人工介入前就触发了。有人会说,招聘方一天过几百份简历,一轮筛选不到一分钟,这是行业常态。这两条我都信,因为它们不冲突——正因为是常态,才更说明问题不在某个招聘专员身上。
我也不觉得这是个"写得不够好的函数"。它不是 bug,它在完成设计目标——只不过那个目标从来不是"找到最强的候选人"。企业可能收到上千份申请,你让 HR 一个个去翻 GitHub 主页,不现实。筛掉噪音是它的正职。
问题在另一件事上。招人真正的目标函数,是最坏情况下谁背锅。
招进来一个名校学位的人,结果他不行——没人问你当初为什么招他,简历摆在那儿,流程走完了。破格招进来一个没学位但实战很强的人,结果他不行——问题立刻变成"你当初为什么要破格",那是你个人判断的失误。
学位在这套系统里不是能力证明,是免责符号。
顺着推一步:一个不申诉、不解释、不委屈、不需要被安抚的软件,是完美的背锅对象。评论区有人说,AI 越强,越方便把录用结果的解释权推给筛选系统,录用的人不合适时,还留着"是算法筛出来的"这层余地。
所以那封署了真人名字的拒信才最扎人。署名本来是想显得"这件事有人负责",实际效果正好相反——签上名字,就等于有人看过了。而那个名字后面,恰恰是整条链条上唯一没读过材料的位置。
我自己也写过这种东西
给客户搭内部工具时,我给 AI 放权是分阶段的:先让它读、让它生成、让它给方案,只读不写;确认过几轮再让它写代码;碰生产数据、碰迁移、碰删改,一律加 dry-run 加回滚,绝不"全自动"。上次一次数据库迁移翻车,就是没守住这条线的代价。核心不是不信任模型,是判断权不能交出去,尤其是不可逆的操作。出了事,责任还是我的。
结果我们给一个活人做的第一道筛选,全自动放权,还署了个真人名字上去。
挺讽刺的。
有人建议她优化简历、往 ATS 关键词上靠。我信一半。技术上完全没错,这是最高效的解法——我甚至觉得,换我处在必须尽快有收入的位置上,我也会先这么干。但我不写这种建议。你一旦照着关键词列表重写自己的履历,等于默认了上面那个函数是对的:你花两年做的并发同步和冲突处理,最后被压缩成"熟悉 Python、SQL、Git",而跟你竞争的那个人,可能只是把 JD 原封不动贴进了简历里。
这不是正义不正义的问题,是这套写法会让真正干活的人去适应不干活的系统。
我凭什么这么说
这里我得踩一下自己。我不用靠一份 9-to-5 的 offer 过日子,所以我能站在旁边说"你该专注做真实项目、开发开源、慢慢积累客户",说得轻巧。对很多必须尽快有收入的人来说,这建议的代价高得不现实——先过筛子活下来,才是第一位的。这个我承认,有点站着说话不腰疼。
她本人的态度反而比我干净。她说最沮丧的不是被拒,是自己做过的那些工作几乎没有机会替她说话。她也没打算不投了,说不愿意替公司提前把自己筛掉。评论区还有人分享:发了十几篇文章、投了七份提案一封没回,里面一家公司的投稿表单连 Python 这个选项都懒得给。
业界年年讲 skill-based hiring,讲得跟真的一样。但只要第一层还是"有没有学位",这句话就只是贴在墙上的标语,不是流程的一部分。也别指望它自己变好——改 ATS 的参数没用,除非改成本归属,让那个拒绝的人真的为拒绝这个动作负责。
我自己能守的很简单:以后不管给客户搭系统,还是哪天轮到我自己去招人,第一层的自动化只做排序,不做拒绝。拒绝权必须留给一个真读过材料的人——哪怕慢一点,贵一点。
AUTO_REJECT = False
就这一行。
