【元宝搜索开发者征文大赛】我用元宝搜索做了一款开源求职取证 Agent
本篇文章源码已上传到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 上的区间对不上,这一条面试时必须问清楚。会知道有三件事公开信息根本查不到,只能等见到人才有答案。
这些都不会替弟弟做决定。但至少他不是凭感觉押的。
做下来我自己觉得,它的用处不在结论好不好看,在于结论能不能被复查。元宝搜索在这件事上帮了大忙:参数是你自己给的,它就按参数返回,不替你判断。没有它,上面那些结论全是空中楼阁。
接下来还有三件事要做:
-
抓取证据页正文。现在只比对摘要,很多关键数字藏在表格里,摘要里没有,会被误判成「查不到」。
-
公司实体消歧。「大连字节跳动数据科技有限公司」的劳动仲裁,不该算到字节总部头上。后面想用统一社会信用代码来区分,而不是靠名字匹配。
-
把法规原文缓存到本地。政府网站的原文本就带生效日期,缓存下来并标记何时该重新拉取,取证会快很多。
项目已经开源。如果你也想用用,可以拿去跑跑。
项目地址:https://atomgit.com/air__Heaven/OfferLens

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



所有评论(0)