本篇文章源码已上传到atomgit项目地址:https://atomgit.com/air__Heaven/OfferLens

在这里插入图片描述

「薪资:面议」

我弟弟的 offer 上,「薪资」那一栏就这四个字。凌晨十二点多,他把他要签的那份转给我,问:哥,这家公司靠不靠谱。

我在职这些年,看过的 offer 不少。但这一份我看两遍也没底——不是因为它差,是因为整份文件里能拿来判断的,就四行:

薪资:面议 | 工作方式:弹性工作制 | 团队氛围:扁平化 | 发展空间:广阔

这四句读着都顺耳,也都验证不了,弟弟最在意的是「面议」到底是多少、「弹性」是七点走还是十点走、「扁平」是两层汇报还是五层 一个字都没写。

我开了二十多个标签页替他查。脉脉上说十点下班,牛客上说福利不错,匿名群里传去年裁过一轮,还有个学弟说「好像还行」。**这些说法来自匿名论坛、二手转述、三年前的老帖,彼此打架,也都没有出处。**查到最后我意识到:不是我搜不到,是搜到的每一条我都不敢拿给他。

为了回他,我把市面上能试的求职工具都试了一遍,它们有一个共同点——取证层是 prompt-driven 的。

这一行背后有三个我回答不了的问题。这条结论来自哪一次搜索、哪一页结果?同一个问题再跑一次,拿到的还是同一批页面吗?我能不能只看某几个网站、只要最近一年的结果?

三个都答不上来。搜索是在聊天框里发生的:搜什么词、拆成几段、翻几页、挑哪几条放进上下文,全是模型当场定的。最后给我一段总结,中间那几步我看不见,也没法复查。

所以我想自己做一个试试,也好下次有人拿这种四句话来问我,我不用再手忙脚乱。它的任务很单一,只管留下证据——它说的每一句话,都要能顺着线索走回那张网页,回答一个很朴素的问题:

这句话,出自哪一页、哪一天、是谁写的,我凭什么信?

顺着这个问题往下找,我认识了元宝搜索。
在这里插入图片描述

一、 先说说元宝搜索

元宝搜索是腾讯元宝提供的搜索服务。平时大家用的是元宝 App 里的那个搜索框,但它还开放了接口,可以像调用一个普通函数一样,直接从代码里发请求。返回的也不是一段写好的总结,而是一批结构化结果:标题、链接、摘要、发布时间。
在这里插入图片描述

区别就在这儿。聊天框里的搜索是模型替我做决定:搜什么词、拆成几段、翻到第几页、挑哪几条放进上下文。接口搜索不一样——请求是我发的,参数是我定的,结果原样回来。

我做的第一件事是验证它到底稳不稳。同样的检索词,让两边各来一遍,看结果有多少重合。

聊天框那边:第一轮 73 个网址,第二轮 68 个,重合 15 个。元宝搜索这边:两轮都是 10 条,重合 10 个。

我后来一直用这组数。两边差的不是谁搜得更多,是结果稳不稳。聊天框里的搜索,你今天看到的一批页面和下周看到的,可能是完全不同的东西——而且你不会知道它换过。判断是建立在这批材料上的,材料会悄悄变掉,这比搜不到更麻烦。

除了稳,元宝搜索的接口还有四个参数,正好对上我的需求。

  • Site:限定只在某一个网站里搜,或者一次性排除最多五个网站。

  • Industry:限定在政府、新闻、学术、财经这四个垂直领域里搜。

  • Freshness:限定时间范围,最近七天、三个月、两年,或者一个精确的日期区间。

  • Cnt:每次返回 10 到 50 条,一次拿够,少发几次请求。

对取证来说这四个参数很要紧:搜什么范围可以自己定,不用碰运气。查法定规定我把范围锁在政府网站,查公司近况只要最近一年的新闻,查员工评价就把招聘平台和社区站点列进去、把内容农场挡在外面。

还有一个小设计,我后来越用越依赖:每次调用,接口都会返回一个 RequestId。我把每次调用的参数、条数、耗时和这个编号一起写进一个日志文件,长这样:

{"ts":1793497073796,"query":"脉脉 字节跳动 前端开发工程师 远程",
 "params":{"Cnt":20,"Freshness":"m3"},
 "requestId":"aa36f6ca-031d-4b73-81eb-6f0f111d2855","nResults":17,"elapsedMs":204}

有了它,半年后我还能回答:某个结论来自哪一天的哪一次调用,当时返回了 17 条,用了 204 毫秒,依据的是其中哪一条。聊天框里的搜索给不了这个东西,聊天记录一关,过程就没了。

在这里插入图片描述

图 1 审计流水:一次取证 68 条记录(20 条命中缓存),每次检索一行,RequestId 可回放到具体调用

实测下来,它单次调用平均 226 毫秒,返回的结果里 75%以上都落在目标网站内。

二、于是有了 OfferLens

最后我开发了一个叫 OfferLens的项目,开源,一个求职取证台。整个项目不依赖任何第三方包,装好 Node 就能跑。

它做的事其实不复杂:把一份 offer 拆成十三个必须回答的问题,每个问题用几组检索式去公开信息里找证据,找完再分级、判断、汇总。

这十三个问题覆盖了求职时最常踩坑的地方:薪资范围、实际工时、公司经营状况、裁员历史、社保缴纳、岗位是否真实、团队稳定性等等。一次完整取证会发出 56 到 68 次搜索请求,全部跑完大约三秒。整个流程是这样的:

输入:公司 / 岗位 / 城市 / 目标薪资 / 我最看重什么
   │
   ├─ 13 个信号 × 2~3 条检索式 × 2 种模式 → 56~68 次搜索
   │        ↓
   ├─ 合并去重 → 信源分级 → 确认搜到的就是这家公司 → 过滤无关内容
   │        ↓
   ├─ 核对数字(这个数真的出现在证据里吗)
   │        ↓
   ├─ 判断:证据够不够硬 → 按目标加权 → 给出结论
   │        ↓
   └─ 报告:结论 + 图表 + 每个问题的证据链 + 面试要问的清单

搜索只是第一步。后面去重、分级、过滤、核对都是普通代码,同样的输入跑两次,结果一样。

举个例子,这是「实际工时」这个问题在代码里的样子:

{
  id: 'work-hours',
  label: '实际工时',
  queries: [
    { q: '{company} 加班',       intent: '实际下班时间' },
    { q: '{company} 弹性工作制', intent: '考勤制度' },
    { q: '{company} 双休 单休',  intent: '休息日' },
  ],
  need: '来自两个以上独立网站的员工说法,或企业自己公开的考勤制度',
  skipIf: '只有一个来源提到(孤证,不能当规律)',
  askIf: '标准上下班时间、有没有大小周',
}

每个问题配两三组检索式,每组只带一个词。这不是随手写的,后面「踩坑」一节会讲为什么。need 和 askIf 这两个字段也很关键:前者说清楚什么才算证据,后者直接变成面试时要问的话。

**每条检索式还会用两种方式各跑一遍。**第一种是 Site 定向,比如限定知乎,加上语义关键词;第二种是把平台名写进查询词,比如「知乎 某公司 加班」。前一种搜出来的结果很干净,实测站外内容为零;后一种召回更稳,但二十条里有十四条来自站外。两种结果取并集,再交给后面的规则过滤。

在这里插入图片描述

图 2 检索式矩阵:13 个信号、28 条检索式,由 lib/signals.mjs 直接渲染

为什么不只用干净的那一种?因为实测发现,Site 对一部分中文网站会直接把结果收窄到零。这个后面细说。

2.1、三道判断,各管各的

搜索回来的东西不能直接拼成一段话发给弟弟——那还是摘要,不是证据。我在中间加了三道判断,三道之间互不推导。

第一道看证据够不够硬。每个问题有一个状态:查到了可核验的、没过期的来源,算已证实;有直接证据但关键细节靠猜,算部分证实;证据缺失、互相矛盾或者过期了,算查不清。

这里有一条我写进代码的规矩:没查到,不等于没问题。很多工具搜了一圈没发现异常,就给这个维度打个绿灯;但「没人提过」和「确实没事」是两件事。所以只要没有一手来源,这个维度就记「查不清」,不记「已证实」。

第二道看信源算几等。官方和政府网站、企业自己挂的招聘页和薪资页,算一等;主流媒体报道、员工的个人说法,算二等,并且标注「未经独立核实」;薪资报告站、排行榜、来路不明的网站,算三等,默认降权。

等级不是按网站定的,是按「这条信息在这个问题下算什么」定的。同一个招聘网站,企业自己挂的薪资算一等,员工写的加班评价只能算二等。

第三道才轮到结论:这份工作值不值得去。这一层上游没有,它给的是分数。我想让它跟着目标走——同一个 offer,目标不同,答案就该不同。

「查不清」是一个正经结论,不是程序出错。它的意思是:公开信息撑不起这个判断,别搜了,该找活人聊了。报告会顺带告诉你该找谁、问什么。

2.2、数字对了,出处也可能错

求职场景里最高发的错误,是数字引对了、出处引错了。JD 上写 25 到 40K,公司官网挂的是 20 到 30K;某张对比表里写着 98%,点进去发现那是另一家公司另一年的报告。

普通的关键词检索只能告诉你「这一页提到了薪资」,告诉不了你「这一页的薪资是不是这条招聘信息的薪资」。所以我在中间加了一步数值核对:先把各种写法统一成同一个量纲,25-40K、2.5 万、25000 都算同一类;再按数字边界切开比较,让 80 不会匹配到 8000;最后检查数字附近有没有同时出现主体、指标和限定词。

一个数字要能叫「可核验的当前来源」,得连过两关:它确实出现在原文里、上下文也对得上;而且那条证据本身得是一手来源。这一步就是我和「把搜索结果丢给模型总结」的区别。

三、它跑起来是什么样

拿「拼某多 · 后端开发工程师 · 上海 · 目标 40K」试一次。56 次搜索,三秒左右,出来的报告是这样的:

  • 结论是「建议慎重」:去重后有 41 个独立来源提到实际工时,需要当面确认是否和招聘信息里的说法一致。

  • 证据覆盖度 85%,拆开看是薪资 89%、风险 92%、成长 80%、匹配 67%。

  • 一共 405 条证据,分三层:一等 76 条、二等 119 条、三等 210 条,用一张环形图展示。

  • 十三个问题逐条列出证据链,每条证据标着等级和来源数。

  • 红旗一条,待确认三条。

  • 一份面试必问清单,具体到怎么开口,比如「你的期望在这个区间的哪个位置」「标准上下班时间是几点,有没有大小周」。

我本来以为「建议慎重」这个结论最有用,拿弟弟的 offer 试了两次才发现不是。真正派上用场的是最后那份清单——他拿着去面试,不是去听对方介绍公司,而是拿着几个具体问题去核对。对方答得含糊,他心里就有数了。

在这里插入图片描述

图 3 核查报告(上):结论「建议慎重」、证据分层、四维覆盖度与红旗

在这里插入图片描述

图 4 证据分层与四维覆盖度(图 3 的局部放大):405 条证据,一等 76 / 二等 119 / 三等 210

在这里插入图片描述

图 5 「实际工时」的证据链:56 条证据,按内容去重后 41 个独立来源

我最初想解决的其实就这一件事:求职的时候信息全在对方手里,你只能凭感觉押注。取证不能保证他押对,但至少能让他知道自己押的是什么。

四、踩过的三个坑

下面这些坑都是实测踩出来的,官方文档里没有。

4.1、坑一:检索词堆得越多,越搜不到这家公司

我最初给「公司经营与近期动向」写的检索式是:{company} 裁员 欠薪 仲裁。跑回来 60 条结果,几乎全是劳动仲裁科普页:如何申请劳动仲裁、劳动争议调解仲裁法全文、工资被拖欠怎么办。每一页看着都相关,但没有一页在讲这家公司。

我一开始怀疑是自己的过滤规则写错了。单独做了个小实验才发现,问题出在词上:像「裁员」「劝退」「加班」「打卡」「双休」这类描述公司事件的词,搜出来的结果里九成以上真的提到了这家公司;而「欠薪」「仲裁」「劳动争议」「经济补偿」这类法律术语,命中率不到两成。

原因不难理解:搜索引擎按整句语义打分,「欠薪 仲裁」这种强领域词的权重压过了公司名。规矩就这么定下来了——一个信号只放一个事件词。我还把它写成了一条自动测试,谁往检索式里多塞了词,测试就报错。

test('每条检索式最多两个内容词', () => {
  for (const s of SIGNALS) {
    for (const tpl of s.queries) {
      const words = tpl.q.replace(/\{company\}/g, '').replace(/\{role\}/g, '').trim().split(/\s+/);
      assert.ok(words.length <= 2, `${s.id}: "${tpl.q}" 堆了 ${words.length} 个词`);
    }
  }
});

4.2、坑二:Site 定向对一部分中文网站,结果是零

我拿十个中文招聘和社区网站做了测试,其中八个的结果是这样:

网站Site 定向平台名写进查询结果
脉脉10 条10 条两种都行
牛客9 条10 条两种都行
BOSS 直聘8 条10 条两种都行
猎聘9 条10 条两种都行
知乎0 条10 条Site 搜不到
看准网0 条10 条Site 搜不到
贴吧0 条10 条Site 搜不到
小红书0 条10 条Site 搜不到

在这里插入图片描述

图 6 接口探针 P22~P33:11 条全部通过,64 次真实调用;P22 就是上表 Site 过度收窄的原始记录

有点反直觉:这些网站本身是能被搜到的,换个写法就能拿到具体页面。问题出在 Site 的匹配规则比文档说的严,而文档一个字都没提。这也是我两种都跑的原因——只用一种,有一半网站永远搜不到。

4.3、坑三:41 个网站,可能只是同一篇文章

「实际工时」第一次判出「41 个独立来源」时,我觉得这个数大得反常。查下去才发现,中文社区之间大量互相搬运同一篇帖子,41 个网站,可能只是一篇文章被转了 41 次。

后来我改成先比对正文再计数。判断方法很土:把正文前 80 个字拿出来当指纹,一样就算重复;正文太短、判不出来的,退回用网址区分。

const body = String(e.excerpt || '').replace(/\s+/g, '');
const fp = body.length >= 30 ? body.slice(0, 80) : `u:${e.url}`;

这里我宁可放宽一点:把两篇不同的帖子误判成同一篇,比把同一篇数了两遍要糟得多。

4.4、还有五个小坑

#出了什么问题原因怎么改的
4一等信源一条都没有分级表还在用旧的信号编号统一到一份数据源
5政府政策原文全被过滤掉检索式里没有公司名,却还在做公司名匹配没有公司名时跳过这一步
6「合肥钎焊工招聘」被判成拼多多后端岗的一等证据相关性词表里有个单字母 K关键词不允许单字符
7腾讯新闻、中青网被判成三等主流媒体没有单独分级主流媒体单列二等
8结论永远到不了「值得接」否决项里的信号即使证据充分,也被塞进必问清单否决项的语义是「出问题才否决」

五、两种搜索,摊开对比

指标聊天框里的搜索元宝搜索接口
同一个问题跑两轮的结果重合度12%100%
事后能不能回放某次判断不能,没有编号能,每次调用带 RequestId
结果落在目标网站内的比例无法统计75%
能不能限定网站、行业、时间不能能,Site / Industry / Freshness / Cnt
单次耗时无法准确计时平均 226 毫秒

在这里插入图片描述

图 7 复现实验:同一批检索词独立跑两轮,聊天框搜索重合度 12%,元宝搜索 100%

成本也交代一下:一次完整取证 56 到 68 次调用,就几毛钱【现在极速版是14块钱1000次】,活动期免费额度还送几千次,够跑几十家公司;加上本地缓存,同一家公司重复查不会重复计费。

六、写在最后

要是那晚手上有 OfferLens,我能给弟弟讲清楚三件事:

会知道这家公司有 41 个独立来源提到加班,不只是匿名群里那一句。会知道官网挂的薪资和 JD 上的区间对不上,这一条面试时必须问清楚。会知道有三件事公开信息根本查不到,只能等见到人才有答案。

这些都不会替弟弟做决定。但至少他不是凭感觉押的。

做下来我自己觉得,它的用处不在结论好不好看,在于结论能不能被复查。元宝搜索在这件事上帮了大忙:参数是你自己给的,它就按参数返回,不替你判断。没有它,上面那些结论全是空中楼阁。

接下来还有三件事要做:

  1. 抓取证据页正文。现在只比对摘要,很多关键数字藏在表格里,摘要里没有,会被误判成「查不到」。

  2. 公司实体消歧。「大连字节跳动数据科技有限公司」的劳动仲裁,不该算到字节总部头上。后面想用统一社会信用代码来区分,而不是靠名字匹配。

  3. 把法规原文缓存到本地。政府网站的原文本就带生效日期,缓存下来并标记何时该重新拉取,取证会快很多。

项目已经开源。如果你也想用用,可以拿去跑跑。

项目地址:https://atomgit.com/air__Heaven/OfferLens
在这里插入图片描述

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐