AI在线 AI在线

RAG多岗位简历筛选系统实践:多租户架构设计模式与源码解读

我在8月底的时候,发过一篇基于 LlamaIndex LangChain 框架,开发的简历筛选助手的应用。 后续有星球成员提出希望能增加多个岗位的管理功能,正好接下来的校招活动可以用的上。 这篇在原项目的基础上,核心实现了多岗位并行管理(独立 JD、候选人池、向量索引隔离)和 HR 工作流(标签系统、分组展示、快速操作),同时进行了架构重构(分层设计、数据分库、模块化),并增强了大模型分析输出(四级推荐等级、结构化优劣势)和智能问答(按岗位过滤检索、流式输出)。

RAG多岗位简历筛选系统实践:多租户架构设计模式与源码解读

我在8月底的时候,发过一篇基于 LlamaIndex+LangChain 框架,开发的简历筛选助手的应用。后续有星球成员提出希望能增加多个岗位的管理功能,正好接下来的校招活动可以用的上。

这篇在原项目的基础上,核心实现了多岗位并行管理(独立 JD、候选人池、向量索引隔离)和 HR 工作流(标签系统、分组展示、快速操作),同时进行了架构重构(分层设计、数据分库、模块化),并增强了大模型分析输出(四级推荐等级、结构化优劣势)和智能问答(按岗位过滤检索、流式输出)。

这篇试图说清楚:

系统的实际效果演示、四层系统架构拆解、五点核心技术实现、三个二次开发场景指南,以及对端侧模型应用的一些感想。

1、视频效果展示

在开始讲具体实现之前,老规矩先来看看整个系统的架构设计。下面这张图展示了从用户界面到数据存储的完整数据流。

图片

2.1为啥要分四层

在上一版单岗位系统里,UI 代码、业务逻辑、AI 调用全都混在一起的,一开始写起来确实快,但这次升级到多岗位管理的时候,改动起来难免顾此失彼,所以这次也算是做了下系统重构。

前端交互层这部分依然采用了轻量化的 Streamlit,搭建了三个 Tab 页面:候选人概览、候选人详情和智能问答。这一层只管显示数据和响应用户操作,不关心数据从哪来以及怎么处理。

图片

业务服务层这部分是整个系统的核心处理逻辑所在。一份简历从上传到展示,需要经过:先提取文本,再调用大模型分析,然后存入数据库,最后建立向量索引。这些流程编排都在ResumeProcessor这个类里完成。还有一个PositionService,专门管理岗位的增删改查。这一层的好处是,如果以后想改业务流程,比如在大模型分析前加一个简历去重检查,只需要在这一层加几行代码,不用动其他地方。

核心引擎层这部分封装了所有大模型相关的能力。这一层最重要的是RAGEngine,包括了简历文本向量化、存入 ChromaDB,以及在用户提问时检索相关内容等功能。

数据存储层这部分用了三种存储方式:SQLite 存储岗位信息和候选人结构化数据,ChromaDB 存储向量索引,文件系统存储原始简历文件。所有复杂的 SQL 逻辑都封装在各个 Store 类里,这样以后如果要把 SQLite 换成 MySQL,只需要改 Store 类的实现,上层代码完全不用动。

2.2多岗位数据隔离

为了保证不同岗位的数据不会串台,我在三个层面做了隔离设计。

文件系统按岗位分目录

最简单直接的办法,就是把不同岗位的简历文件分开存。我在uploaded_resumes/目录下,给每个岗位创建一个子目录,目录名就是岗位 ID。比如"AI 产品经理"的岗位 ID 是position_001,这样做的好处是删除岗位的时候可以直接把整个目录删掉。

关系数据库用外键关联

在 SQLite 里,我给candidates表、ai_analysis表、hr_tags表都加上了position_id字段,并且设置了外键约束。每次查询候选人数据,SQL 语句里必须带上WHERE position_id = ?,从源头上避免跨岗位查询。

向量数据库按岗位分 collection

这是整个隔离机制里最关键的一环,ChromaDB 支持创建多个 collection(可以理解为不同的向量数据库),我给每个岗位创建一个独立的 collection。比如position_001对应的 collection 叫resumes_position_001,position_002对应的叫resumes_position_002。

可能会有人问为啥不用 metadata 过滤呢?比如所有简历存在一个 collection 里,然后在 metadata 里标记position_id,检索的时候再过滤。这个方案看起来更简单,但有个致命问题是,实际使用的时候性能会随着候选人总数线性下降。假设系统里有 10 个岗位,每个岗位 100 个候选人,那全局就有 1000 个候选人的向量。每次检索都要在 1000 个向量里搜索,然后再用 metadata 过滤,这无疑会很慢。

而用 collection 隔离,每个岗位的检索只在自己那 100 个向量里进行,性能只跟该岗位的候选人数相关,跟其他岗位完全无关。这就是物理隔离优于逻辑过滤的典型场景。这点非常像我在做 ibm rag 冠军赛项目拆解中提到的“疑一文一库”的做法。

2.3RAG 引擎与业务逻辑的解耦

在上一版系统里,我把 RAG 的代码直接写在业务逻辑里,结果就是代码复用性很差。这次重构我专门抽出了一个通用的RAGEngine类,只提供三个核心能力:建索引、检索、问答。

业务层想用的时候,把数据准备好,调用对应的方法就行。比如ResumeProcessor在处理简历的时候,会调用RAGEngine.build_index()把简历文本向量化;在智能问答的时候,会调用RAGEngine.query_with_rag()执行 RAG 流程。

这种解耦带来的好处是,如果各位想换一个向量数据库,比如从 ChromaDB 换成 Milvus,只需要改RAGEngine的内部实现,业务层的代码一行都不用动。或者如果想把这套 RAG 引擎用到其他项目,比如做一个标准的企业知识库问答系统,直接复制core/rag_engine_v2.py这个文件过去,写一个新的 Processor 类就能跑起来。

3、核心技术实现拆解

前面讲完了架构设计,这部分从代码层面讲几个关键的技术实现,这部分会聚焦在私以为有一定工程借鉴价值的地方。

3.1简历完整向量化的考量

一个好用的 RAG 系统设计,一个绕不开问题是如何精准的切分文档。上一版系统里,因为目的是要演示完整的系统流程,所以演示的简历部分也是针对性的进行了设计,分块部分按照"核心技能"、"工作经历"等章节标题进行处理。但这显然不符合实际五花八门的简历格式情况。 这次重构,我做了一个看似偷懒实则更务实的做法,就是把每份简历作为一个完整的 node,不做分块处理。

复制

首先有个共识是,简历通常很短,一般 2-4 页,毛估 2000-4000 字左右,完全在主流嵌入模型的处理范围内。这次演示用的bge-m3模型支持 8192 tokens,处理一份完整简历应该说毫无压力。

其次,完整向量化也避免了信息碎片化。当 HR 问“张三有没有 RAG 项目经验”的时候,如果简历被切成 10 个 chunk,可能需要检索多个 chunk 才能拼凑出完整答案。而整份简历作为一个 node,一次检索就能拿到所有相关信息,LLM 可以看到完整上下文进行推理。

再者,实现逻辑简单也减少了 bug 风险。代码的核心逻辑就是把多页 PDF 合并成一个字符串,然后塞进一个 node,没有复杂的正则匹配、没有边界判断,代码清晰好维护。在实际使用中实测效果很好,检索准确率明显提升,而且及时是 8b 尺寸的量化模型推理过程也很稳定。

3.2Pydantic 驱动的结构化分析

如何让 LLM 稳定地输出结构化数据,这也是个绕不开的挑战。好的工程实践,无非也是一套围绕模型动态边界构建的解决方案。这次我用了在历史企业项目中常用的 Pydantic 模型 + 详细 Prompt 的组合,实现了相对可靠的结构化输出。先定义数据模型:

复制

Pydantic 的好处是,它不仅定义了数据结构,还内置了校验逻辑。比如recommendation_level必须是四个等级之一,key_strengths至少有 1 条最多 5 条,这些规则会自动执行。然后在 Prompt 中明确输出格式:

复制

Prompt 里不仅给出了 JSON 格式,还详细说明了每个字段的要求,以及提供了好例子和坏例子的对比。这种高指令性的 Prompt 实测可以明显提升 LLM 输出的质量和稳定性。最后在解析时做好容错处理:

复制

这套机制总结来说有三层容错:先尝试直接解析,不行就用正则提取 JSON 块,再不行就清理格式后解析,最后还有一个降级策略返回默认值。实际使用中,绝大部分情况第一次就能成功解析,少数格式异常的也能被后续逻辑兜住,系统健壮性非常高。

3.3简历处理的五步流水线

一份简历从上传到最终可用,需要经历多个步骤。原版系统这些逻辑散落在各处,这次我用ResumeProcessor类把整个流程标准化成五步流水线:

复制

这个设计有几个好处:

首先,每一步都有明确的输入输出和成功标志。result['steps']字典记录了每一步的执行状态,方便调试和监控。如果处理失败,可以立刻看到是在哪一步出的问题。

其次,失败快速返回,避免无效计算。如果文本提取就失败了,后面的大模型分析、向量索引都不用做了,直接返回错误信息。这种 fail-fast 策略节省资源,也让错误信息更清晰。

最后,result字典不仅包含每一步的状态,还包含候选人 ID、分析结果、文件路径等信息,上层 UI 可以根据这些信息给用户精准的反馈。

这种流水线模式在企业级应用中非常常见,它把复杂流程拆解成清晰的步骤,每步职责单一,容易测试和维护。

3.4多岗位检索的元数据过滤机制

前面架构部分提到,我用 metadata 过滤实现多岗位隔离。这里展示一下具体的检索代码:

复制

LlamaIndex 的MetadataFilters功能非常强,支持多种操作符(EQ、GT、LT、IN等),可以组合多个条件。这里我只用了最简单的等值匹配,但如果以后需要更复杂的查询,比如"工作年限大于 5 年且有制造业背景",只需要添加更多的MetadataFilter就行。在向量化这一步,我把position_id、candidate_name等信息存入 node 的 metadata:

复制

检索的时候,ChromaDB 会先在全局向量空间找到语义最相似的 top-K 个结果,然后用 metadata 过滤器筛选出符合条件的 node。语义匹配 + 精确过滤的组合,既保证了检索质量,又实现了数据隔离。

需要注意的是,我在架构设计部分提到过 collection 隔离性能更好。但实际开发时我发现 LlamaIndex 对多 collection 管理不太友好,需要为每个岗位创建独立的 index 对象,代码会变得很复杂。所以我在单 collection + metadata 过滤和多 collection 隔离之间做了权衡,选择了前者。

这也是说明了理论最优方案不一定是工程最优方案。要考虑框架限制、开发成本、可维护性等多方面因素。目前这个方案在候选人数量不超过 200 的情况下性能完全够用,如果以后数据量真的上去了,再重构成多 collection 也不迟。

3.5基于 Session 的多轮对话记忆

智能问答如果只能单轮回答,用户体验无疑会很差。我用 Streamlit 的session_state实现了按岗位隔离的对话记忆。

复制

首先是按岗位 ID 隔离对话历史。不同岗位的问答互不干扰,HR 在"AI 产品经理"岗位的对话,不会影响到"算法工程师"岗位。其次,只显示最近 N 条消息。对话历史完整保存在session_state里,但页面上只显示最近 15 条,避免页面过长影响体验。

图片

最后,保存推理过程。除了最终答案,我还把 LLM 的 thinking 过程保存下来。这样用户可以在st.expander里查看 AI 的推理链路,提升可解释性。这个实现虽然简单,但在实际使用中效果很好。多轮对话让 HR 可以不断追问,获得更深入的候选人情况了解。

4、二次开发指南

在实际使用中,不同企业或者用户会有不同的定制需求。这部分我根据和一些从业者的沟通,介绍三个高频需求场景,作为二次开发的参考。

4.1批量导入简历

从招聘网站批量下载的简历通常打包在 zip 文件里,每次都要手动解压再上传,非常低效。如果能直接上传 zip 包,系统自动解压并批量处理,能节省掉不必要的手动操作。

复制

核心实现思路是在上传组件支持 zip 格式,解压后递归扫描所有简历文件,然后调用现有的批量处理逻辑即可。

4.2薪资期望范围提取与预算匹配

薪资是招聘决策中的关键因素,但人工查看每份简历判断是否在预算内,无疑会浪费大量时间在预算不匹配的候选人上。如果系统能自动提取薪资期望并与岗位预算对比,就可以在筛选阶段就过滤掉不合适的候选人。

复制

核心实现思路,是在大模型分析的 Pydantic 模型中增加薪资字段,在 Prompt 中增加提取指令,大模型会自动识别各种薪资表述("20-25K"、"年薪 30 万"等)并归一化为统一格式。

4.3AI 生成面试问题清单

筛选出候选人后需要准备面试问题,要仔细读简历找出可以深挖的点。如果系统能根据简历和岗位要求自动生成针对性的面试问题,可以大幅提升面试准备效率。

复制

核心实现思路是设计一个专门的 Prompt,让 LLM 基于简历内容和分析结果,生成分类的面试问题(能力验证、项目深挖、短板确认等)。

5、写在最后

现在,当大家谈论大模型企业应用落地的时候,默认的潜台词都是企业主导、集中部署。而现实情况是,企业去落地一个面向于不同部门的大模型应用,是一个道阻且长的过程。但实际上像 HR、律师、会计这类专业工作者,每天也在做大量重复劳动。如果能把这套系统打包成一个开箱即用的桌面应用,内置嵌入模型、向量数据库、开源 LLM,完全本地运行,不依赖云服务,可以更加短平快的的给很多岗位带来提效或者解放双手。

这个项目中演示时使用的 qwen3:8b(Q4 量化版)5.2GB大小,在我的 24GB 内存 MacBook 上,首字响应时间大概 5 秒,后续 token 生成速度相对比较流畅。但这个性能对大部分 HR 的工作电脑来说是个不小的挑战。DeepSeek 年初开源的蒸馏后的小尺寸模型,在智力水平和电脑性能要求上都不够实用,但现在似乎重新来到了端侧模型应用重新爆发的临界点。一方面,1.5B-3B 的小模型配合垂直领域微调,完全可以胜任简历筛选这类专业场景。其次,更优秀的小尺寸开源模型更新的速度我想也会超出大家的预期。

从创业视角来看,这不仅是 2B 市场的机会,更是面向专业个人用户(像 Cursor 面向开发者那样)的增量市场。让专业工作者都能拥有自己的大模型应用助手,而不是一味的等待企业采购,这或许是接下来非常值得保持关注的长尾需求。

相关资讯

从 LangChain 到企业级应用:RAG 中 Fixed-Size Chunking 的最佳实践揭秘

众所周知,在构建 RAG(Retrieval-Augmented Generation,检索增强生成)系统的过程中,文档切块策略往往决定了模型检索质量的上限。 切得好,信息命中更精准,生成回答更有上下文逻辑;切得差,模型则容易“答非所问”。 在众多策略中,Fixed-Size Chunking(固定切块)可谓最简单直接,却也是最常被忽视的一种。
6/11/2025 8:40:00 AM
Luga Lee

蚂蚁集团 KAG:知识增强生成如何革新专业领域大模型应用?万字深入解读!

在大语言模型(LLMs)的应用浪潮中,检索增强生成(RAG)技术得到广泛运用,利用外部检索系统,显著提升大语言模型的时效性,并大幅减少模型幻觉。 为了进一步提升RAG在多跳任务、跨段落任务的性能,研究人员引入了知识图谱,包括GraphRAG、DALK、SUGRE、ToG 2.0 和 HippoRAG 等。 尽管 RAG 及其优化方案已经解决了因知识缺乏和更新导致的大部分幻觉问题,但生成的文本仍然缺乏连贯性和逻辑性,难以产生正确且有价值的答案,尤其是在法律、医学和科学等需要分析推理的专业领域:首先,真实场景的业务流程一般需要基于知识片段之间特定关系的推理来收集与回答问题相关的信息。
5/19/2025 2:50:00 PM
Goldma

RAG系列:基于 DeepSeek + Chroma + LangChain 开发一个简单 RAG 系统

创建 Next 项目首先,使用 npx create-next-app@latest 根据提示完成 Next 项目的创建:复制创建好项目之后,在 src/app 目录下新建 rag 目录,本次 demo 的代码都将放在这里。 知识库构建接下来,我们将构建知识库,主要目标是将准备好的 pdf 通过向量化存到向量数据库中,以便后续的检索。 由于本次 RAG 系统的开发都要依赖 LangChain 框架,所以我们先在项目中安装 LangChain 框架和核心依赖:复制文档加载LangChain 的 DocumentLoaders[1] 提供了种类丰富的文档加载器,可加载文件系统的文件也可以加载线上文件,包括 csv、docx、pdf、pptx、html、github、youtube等等。
5/22/2025 6:48:50 AM
赖祥燃
  • 1