主题
知识库检索模块是这个 RAG 项目的核心功能模块,我们是基于 Elasticsearch 实现的文档混合检索能力,将语义检索和关键词检索结果结合起来,为用户提供更高质量的搜索体验。
该模块依赖于文件上传与解析模块完成的向量化处理,直接使用存储在 Elasticsearch 中的向量数据进行检索。系统目前使用阿里云百炼的 API 生成文本向量,并将向量存储在 Elasticsearch 中。
混合检索流程
知识库检索解决的核心问题是:用户的问题和文档的表述往往不一致——纯关键词检索召回不了同义改写,纯向量检索又对专有名词、编号这类精确词不敏感。所以方案是向量与关键词各干各擅长的事,在一次 Elasticsearch 请求里完成,分三层。
第一层是向量召回:用户问题先过 embedding 模型变成查询向量,用 HNSW 做近似检索。这里有个关键参数——召回窗口设成了最终返回量的 30 倍,因为后面还有关键词过滤和重排会大量淘汰候选,召回阶段宁可多拿。
第二层是关键词过滤加权限过滤,在一个 bool 查询里。must 条件要求文档必须命中查询的关键词,把纯语义但不沾字的候选淘汰掉;权限过滤放在 filter 子句——三个条件的或:是我上传的、是公开的、或者组织标签在我的有效标签集合里。filter 不参与打分只做过滤,而且是在查询层生效——越权的文档根本不会进入候选集,这和‘检索完再在应用层过滤’有本质区别,既安全又不浪费打分开销。用户的有效组织标签是登录时按层级展开、缓存在 Redis 里的,和文件列表接口共用同一份缓存,保证‘列表看得见的,检索一定搜得到’。
第三层是 rescore 二阶段重排:对召回窗口内的结果,用 BM25 做关键词重打分,和向量分做线性融合——向量分权重 0.2、BM25 权重 1.0,关键词主导排序。设计意图一句话:向量负责‘找得全’,关键词负责‘排得准’,尤其专有名词、术语编号这类精确匹配,BM25 的权重才能顶到前面。
整个查询一次请求返回 topK 条,结果自带页码和锚点,文件名用一个批量的 IN 查询回填——这些元数据直接支撑聊天回答里的‘第 X 页’引用定位。
异常降级也分了层:embedding 服务挂了,自动退化成纯 BM25 加同样的权限过滤,检索还能用只是失去语义能力;ES 整体异常,再兜一层纯文本重试,最终失败返回空结果——聊天侧会基于空结果告诉用户没找到,而不是报 5xx。
为什么用 BM25 rescore 重排,不用 rerank 模型?
两个原因。一是成本和延迟:cross-encoder 类 rerank 模型要对每个候选做一次模型推理,topK 乘以候选量级的推理延迟和调用成本,对一次毫秒级的检索来说不成比例;ES rescore 是引擎内置的打分融合,零额外延迟。二是当前瓶颈不在精排:我们的问题更多出在召回质量而不是几十条候选的排序精度,rerank 是排序精度敏感场景的工具。当然边界也说清楚:如果未来知识库到十万级文档、且发现‘该命中的排在第五位而不是第一位’这类问题占比高,rerank 模型(比如 bge-reranker)就是下一个该上的东西——它是排序精度问题的对症药。
用户问题和文档零字面重叠、纯同义改写的情况,你的检索会怎样?
这是当前设计一个明确的取舍。链路上向量召回其实能抓到语义相近的块——召回窗口放大了 30 倍就是为这个——但第二层的 must 关键词条件会把不沾字的候选全部杀掉,所以纯同义改写的场景会漏。当时这么选的理由是:企业知识库场景里,查询和文档的术语重合度其实很高(产品名、制度名、编号),must 条件换来的是精确性的大幅提升,避免语义召回带进大量‘意思沾边但答非所问’的噪音。这是个可以调的旋钮:把 must 降级成 should、配合重排权重保排序,就能放开同义召回——但纯 should 会放大噪音,需要配 minimum_should_match 调优和评测集验证,我倾向于有评测数据再动这个参数。
简历写命中率从 63% 提升到 85%,怎么测的?
方法是控制变量的 recall@5 对比。
第一步,建评测集:从知识库的真实文档里挑了三四十份,每份文档手写一条用户视角的查询问题——不是复制文档里的句子,而是模拟真实提问的措辞,比如文档讲某个制度,问题就写成‘XX 怎么办理’这种自然问法。每条查询人工标注出应命中的文件和具体块作为标准答案。
第二步,定义基线和度量:度量用 recall@5——正确答案块出现在前五条结果里的比例。选前五是因为下游聊天场景构造上下文时实际引用的就是这个量级,排序在第五位之外对用户等于没召回。基线是纯 BM25 关键词检索——代码里本来就有这条降级路径,纯文本匹配加同样的权限过滤,这样对比时唯一变量就是向量召回和重排,权限条件两边完全一致,不会混入干扰因素。
第三步,跑对比:同一批查询分别跑基线和完整的混合检索,统计各自命中标准答案的比例。结果纯关键词是 63% 左右,混合检索到 85%——提升的主要来源正是纯关键词的两类失败:同义改写召回不了(向量补上),以及多个候选都沾关键词但精确度不够(BM25 重排把最相关的顶到前面)。
这个评测的局限我也清楚:集子只有几十条、单人标注没有仲裁、测的是召回不是精确率。它的结论强度足够支撑‘混合优于纯关键词’这个方向性判断,但如果要指导参数调优——比如调 must 和 should 的取舍——需要更大规模、多人标注的评测集,这是我知道的下一步。
为什么不用 FAISS(向量检索专用库)?
设计文档里 FAISS 是规划项,当前没有集成——这是评估后的决定。三条理由:一是规模远没到,全库向量在千级,ES 的 HNSW 毫秒级延迟绰绰有余,FAISS 的优势(亿级向量、内存索引)没有发挥空间;二是权限过滤会倒退,ES 的 kNN 支持 filter 感知的近似检索(图遍历时带上 userId/orgTag 条件),FAISS 是纯向量索引、不懂业务过滤,换过去权限过滤只能先召回再应用层筛,正确性和延迟都变差;三是双存储一致性成本,向量另存一份要补重建、增量同步、故障恢复一整套逻辑。演进阈值设在千万级向量、ES 延迟不达标时——第一步是抽象 VectorStore 接口让两种实现并存,且优先考虑支持标量过滤的向量库(如 Milvus)而不是裸 FAISS。