Jackの宝藏之地
首页项目归档照片墙音乐说说杂谈友链关于
封面

SmartRecruit

写作时间:2026-08-22 00:00:05

我花了一个月,给简历筛选做了一套 RAG 系统

一、缘起:被简历筛选逼疯之后

去年有段时间,我被拉去帮一个做招聘的朋友看简历。

他给我发来一堆 PDF,问:帮我筛一下,有没有懂 Python、3 年以上经验的?

我打开一看,好家伙,几十份简历。有的写精通 Django、Flask,五年后端开发,就是没提 Python;有的标题写着资深算法工程师,正文里全是业务项目,技术栈藏在第 3 页。用关键词搜 Python,前两种简历全被漏掉;搜工程师,又召回一堆无关的。

那一刻我意识到:简历筛选的本质是语义匹配,而传统工具都在做关键词匹配。

正好那段时间在学 RAG,脑子里冒出一个念头:能不能用检索增强生成,把看懂简历这件事交给 AI?于是就有了 SmartRecruit,一个基于 RAG 的智能简历推荐系统。

项目地址:https://github.com/jack86125/SmartRecruit-RAG-

二、第一个决策:向量模型选谁

项目启动第一天,我就面临选择:嵌入模型用哪个?

市面上的方案很多,OpenAI 的、智谱的、BGE 系列的。我最后选了 BGE-M3,理由很简单:中文效果好,简历场景中文是主力;一个模型能同时出稠密向量和稀疏向量,稠密管语义,稀疏管关键词,相当于买一送一,不用部署两套模型;而且本地部署,数据不出内网,简历这种敏感信息放在本地更安心。

现在回头看,第二点帮了大忙。后来做混合检索的时候,稀疏向量直接喂给 Milvus 的 hybrid_search,原生支持多路加权融合,少踩了很多坑。

三、第一版:天真地以为一个向量库就够了

第一版我很天真:简历全部向量化,扔进 Milvus,用户提问,向量检索,LLM 生成答案,完事。

跑起来才发现问题一堆。

用户问懂 NLP 的工程师,简历里写的是自然语言处理,向量能匹配上;但问会不会用 Flask,简历里明明写了 Flask,向量检索却经常把这条排到很后面。精确术语恰恰是向量检索的弱项。

简历全文几千字,直接切块向量化,语义被切得七零八落。

检索结果里还经常混进看着相关、其实不相关的简历,直接喂给 LLM,它就一本正经地胡说八道。

于是我明白了一件事:RAG 的瓶颈根本不在生成,在检索。

四、多路召回:把鸡蛋放进三个篮子

针对上面的问题,我把检索层整个重构了,思路是三个引擎各管一摊。

Milvus 管向量,稠密加稀疏,用 WeightedRanker 按 0.7 和 0.3 加权融合;Elasticsearch 管 BM25 关键词召回,专治 Flask 这种术语;MongoDB 存完整简历文本和元数据,是最终真相。

检索的时候三路同时打,取并集去重,再统一交给重排序。

一开始我觉得这样很重,三个数据库,就为了找个候选人?但跑完对比实验我服了:单路检索的召回率根本不够看,多路召回就是把召回率拉满的兜底方案,剩下的交给重排序去精。

这也是我整趟开发里最大的认知转变。以前觉得检索就是向量相似度排序,现在觉得检索是一个系统工程,召回、融合、精排,每一层都有自己的职责。

五、重排序:一开始完全忽略的环节

重排序是我第一版完全没考虑的东西。

当时我把向量检索的前几名直接喂给 LLM,结果它经常基于排在第 5、6 位的边缘简历生成推荐,理由还编得头头是道。后来加了 BGE-Reranker,这是一种 CrossEncoder 结构的模型,把问题和每条候选简历拼起来过一遍 Transformer,重新打分排序。

效果立竿见影,Top-K 的精度明显提升,幻觉式推荐少了很多。

这里有个认知想纠正一下:很多人以为 RAG 就是向量检索加 LLM,其实中间还隔着一层精排。向量检索负责别漏,重排序负责别错,LLM 负责说人话,三层各干各的活,缺一层效果都打折。

六、意图识别:让系统听懂人话

简历检索跑通之后,我又遇到一个更软的问题:用户说话太随意了。

帮我找一个懂 Python 的,这是招聘需求;把年龄范围改成 25 到 35,这是修正上一轮条件;第一个候选人会什么技能,这是追问;这个系统支持什么格式,这是问系统本身;你好今天天气不错,这是纯闲聊。

如果所有输入都走简历检索,系统会疯掉。于是我加了一层意图识别,先判断用户到底想干嘛,再分流处理。这层看起来简单,其实特别重要,它决定了整个对话系统的情商。

接着是参数提取,把三十岁左右、五年以上经验这种模糊表达,转成 age_min 28、age_max 32、experience_min 5 这样的结构化过滤条件,直接进 Milvus 的标量字段过滤。这一步让硬筛选和语义检索各司其职,准确性上了一个台阶。

七、多轮追问:最难也最有趣的部分

推荐完候选人,用户一定会追问。怎么让系统记住上一轮推荐了谁,还听得懂第一个、他们这种指代?

我折腾了很久,最后方案其实很朴素:把上一轮的候选人列表 JSON 塞进下一轮对话的上下文,让 LLM 自己判断追问是筛选式,比如这些候选人里哪些有博士学位;还是问答式,比如介绍一下第二个候选人的项目经验。

不复杂,但效果出奇地好。这也让我意识到,很多智能其实来自对上下文的显式管理,而不是模型本身有多聪明。

八、踩过的坑,每一个都值得记录

第一个坑是版本地狱。启动时报错,TypeError: XLMRobertaModel 初始化时收到了一个意外的 dtype 参数,查了半天是 transformers 和 FlagEmbedding 版本冲突,升级这两个包才解决。LLM 生态的版本兼容性,真是第一生产力杀手。

第二个坑是模型文件传不上 GitHub。BGE-M3 的权重有好几个 G,仓库根本放不下。最后只能留一个下载脚本,模型按需从 HuggingFace 拉取,国内用户还得配 hf-mirror 镜像,不然连都连不上。

第三个坑是中文编码。有的 txt 简历是 GBK 编码,按 UTF-8 读直接乱码。最后写了逐级回退,UTF-8 不行试 GBK,再不行试 Latin-1,总有一个能解码。

第四个坑是评估缺失。项目做到一半我才发现,我根本说不清系统到底好不好。于是补上了基于 Ragas 的评估模块,用忠实度、答案相关性、上下文精确率和召回率四个指标量化质量。有了评估,每次改动是变好还是变坏一目了然。没有评估的 RAG 项目,都是盲人摸象。

九、几点感悟

这趟折腾下来,最大的感受有几点。

RAG 的难点从来不在接一个大模型,而在检索质量,检索的每一层都值得认真打磨。混合检索不是炫技,是刚需,语义检索和关键词检索是互补的,单用任何一个都有硬伤。上下文管理决定体验,多轮对话和指代消解,靠的是显式管理上下文,而不是指望模型记住。评估要尽早做,没有量化指标,你永远不知道系统是 60 分还是 90 分。最后,版本兼容性是 LLM 开发的头号敌人,依赖锁死、环境隔离,能省掉无数头发。

项目已经开源,代码、文档、24 个测试脚本都在仓库里,如果你也在做 RAG 相关的项目,欢迎来交流,踩坑经验管够。

‍

评论 (0)

评论加载中...

avatar

JACK

在编程、AI、Agent开发、旅游等领域深耕的男大.最近处于找实习和Vibe coding中

RECOMMENDED

数花智算面试

2026-08-19 21:57:53

发财发财

2026-08-14 01:28:58

DeeepSeek harness

2026-08-20 03:59:44