智能RAG问答系统KnowLink——知识库检索(面试)

张开发
2026/4/11 19:12:16 15 分钟阅读

分享文章

智能RAG问答系统KnowLink——知识库检索(面试)
1. 用户交互与参数封装用户在前端输入查询语句query指定返回数量topK点击搜索前端将参数封装为 JSON发送到后端/hybridSearch接口补充前端携带 JWT 或用户身份信息用于后续权限校验。2. 控制器接收与权限前置校验SearchController接收请求解析出query(搜索内容)、topK、userId核心参数hybridSearch(RequestParam String query, RequestParam(defaultValue 10) int topK, RequestAttribute(value userId, required false) String userId)根据用户ID判断是普通搜索还是带权限的搜索若是有ID使用带权限的搜索调用searchWithPermission方法前置权限过滤if (userId ! null) { // 如果有用户ID使用带权限的搜索 results hybridSearchService.searchWithPermission(query, userId, topK); }从 MySQL/Redis 加载当前用户的 数据库 ID、组织标签orgTags确定该用户能检索的文档范围公开 / 本人 / 同组织生成查询向量语义向量转换如果向量生成失败仅使用纯文本搜索带权限// 如果向量生成失败仅使用文本匹配 if (queryVector null) { logger.warn(向量生成失败仅使用文本匹配进行搜索); return textOnlySearchWithPermission(query, userDbId, userEffectiveTags, topK); }根据生成的向量与查询文本执行混合检索如果混合检索发生异常尝试使用纯文本搜索带权限catch (Exception e) { logger.error(带权限的搜索失败, e); // 发生异常时尝试使用纯文本搜索作为后备方案 try { logger.info(尝试使用纯文本搜索作为后备方案); return textOnlySearchWithPermission(query, getUserDbId(userId), getUserEffectiveOrgTags(userId), topK); } catch (Exception fallbackError) { logger.error(后备搜索也失败, fallbackError); return Collections.emptyList(); } }如果用户没有提供ID代表用户是匿名用户 没有权限过滤这种情况下只能搜索公开的文档调用的是search方法else { // 如果没有用户ID使用普通搜索仅公开内容 results hybridSearchService.search(query, topK); }3. 语义向量化转换HybridSearchService调用 EmbeddingClient对接云向量 API 或本地模型private ListFloat embedToVectorList(String text) { try { Listfloat[] vecs embeddingClient.embed(List.of(text)); if (vecs null || vecs.isEmpty()) { logger.warn(生成的向量为空); return null; } float[] raw vecs.get(0); ListFloat list new ArrayList(raw.length); for (float v : raw) { list.add(v); } return list; } catch (Exception e) { logger.error(生成向量失败, e); return null; } }将用户的自然语言查询如 “怎么配置 Kafka”转换为 高维语义向量优化若 Redis 缓存中存在该 query 的向量直接读取缓存减少 API 调用开销。4. 构建 ES 混合检索请求核心逻辑语义检索KNN 向量匹配在 ES 向量索引中查找与 query 向量最相似的 Top-N 文档计算向量相似度得分Cosine 相似度。关键词检索BM25在 ES 倒排索引中匹配关键词分词匹配计算 BM25 相关性得分确保关键词精确匹配的文档不被遗漏。权限过滤Filter 上下文不参与打分但强制过滤term: 匹配orgTag同组织term: 匹配userId本人私有文档term: 匹配isPublic公开文档确保检索结果绝对不包含无权限数据实现 RBAC ABAC 的数据隔离。5. Elasticsearch 执行与重排序RescoreES 同时执行上述三个查询逻辑关键步骤Rescore 重排序由于语义检索和关键词检索的打分体系不同直接融合会失真使用rescore机制先取语义检索的 Top-50 结果再用 BM25 的得分对这 50 个结果进行加权重排最终返回最相关的 Top-K 结果。补充ES 内部使用function_score或script_score实现多得分加权融合。6. 结果封装与返回后端对 ES 返回的原始结果进行二次处理计算最终综合得分语义权重 关键词权重提取文档摘要、标题、来源等信息封装为统一的SearchResultDTO对象将结果返回给前端用户直观看到检索列表。混合检索详细流程第一阶段粗筛召回阶段KNN 关键词匹配 权限过滤一起执行、一起过滤先做 KNN 向量检索从海量数据中捞回recallK个语义相关的文档。同时叠加 关键词 must 匹配过滤不含关键词的。注意此时确实会计算一个BM25分但是因为这段查询主要被用作 KNN 的过滤器配合filter权限过滤ES 在这个阶段的核心任务是“筛选”Pass/Fail。它要找出“包含关键词”且“有权限”的文档。在这个阶段结束时文档列表是按KNN 向量分数排序的。第一阶段的 BM25 分数虽然存在但它并没有决定文档的顺序也没有被“携带”进最终结果作为主导因素。3. 同时叠加 权限 filter过滤没权限看的文档。→ 最终得到有权限 含关键词 语义相关 的候选集合int recallK topK * 30; // KNN 召回窗口 也是下一轮的候选池 s.knn(kn - kn .field(vector) .queryVector(queryVector) .k(recallK) .numCandidates(recallK) ); // 必须命中关键词 权限过滤 s.query(q - q.bool(b - b // 必须命中关键词 // 这确保了只有包含查询关键词的文档才会被考虑 .must(mst - mst.match(m - m.field(textContent).query(query))) // 权限过滤 // 确保用户只能搜索其有权限访问的文档 // 包括自己的文档、公开文档、所属组织的文档 .filter(f - f.bool(bf - bf // 条件1: 用户可访问自己的文档 .should(s1 - s1.term(t - t.field(userId).value(userDbId))) // 条件2: 公开文档 .should(s2 - s2.term(t - t.field(public).value(true))) // 条件3: 组织标签 .should(s3 - { if (userEffectiveTags.isEmpty()) { return s3.matchNone(mn - mn); } else if (userEffectiveTags.size() 1) { return s3.term(t - t.field(orgTag).value(userEffectiveTags.get(0))); } else { return s3.bool(inner - { userEffectiveTags.forEach(tag - inner.should(sh2 - sh2.term(t - t.field(orgTag).value(tag)))); return inner; }); } }) )) ));第二阶段精排重排序阶段Rescore只对第一阶段筛选出来的候选这里是通过截取窗口windowSize来确定取前recallK个文档ES 会再次执行你在rescore.query里定义的查询match查询并再计算一遍BM25 分数。这就是为什么你在rescore里还要再写一遍.field(textContent).query(query)的原因。线性融合拿出该文档在第一阶段算好的 KNN 分数。拿出该文档在第二阶段刚刚算好的 新 BM25 分数。套用公式最终分 (KNN分 × 0.2) (新BM25分 × 1.0)。重新排序从重新排好序的列表中只取前topK个返回给用户。//最终分数 KNN分数 × 0.2 BM25分数 × 1.0。 s.rescore(r - r .windowSize(recallK) .query(rq - rq .queryWeight(0.2d) // 保留部分 KNN 分 .rescoreQueryWeight(1.0d) // BM25 主导 .query(rqq - rqq.match(m - m .field(textContent) .query(query) .operator(Operator.And) )) ) ); s.size(topK); return s; }, EsDocument.class);示例第一步阶段 1 —— 粗筛真正执行顺序① ES 先执行 KNN 向量检索对全库计算向量相似度召回 TOP 300 条最相似的给这 300 条每条算出一个 KNN 分数这 300 条 它们的 KNN 分数全部保留② 在这 300 条里执行关键词 mustBM25只在这 300 条里算 BM25过滤掉不包含关键词的假设剩下 200 条③ 在这 200 条里执行权限过滤过滤掉没权限的假设剩下 150 条第二步阶段 2 —— Rescore 重排序最关键s.rescore(r - r .windowSize(recallK) // 300 .queryWeight(0.2) // KNN 分数权重 .rescoreQueryWeight(1.0) // BM25 分数权重 )ES 在这里干了 3 件事取出阶段 1 过滤后的 150 条它们都来自那 300 条对这 150 条重新计算 BM25 分数第二次 BM25更精准直接使用阶段 1 算好的 KNN 分数然后套公式最终分数 【原来的 KNN 分数】 × 0.2 【新算的 BM25 分数】 × 1.0核心流程类考察整体逻辑1. 你们知识库的混合检索整体流程是什么分几个阶段我们做的是KNN 向量语义检索 BM25 关键词检索的混合检索方案整体分两个核心阶段全程叠加权限过滤第一阶段是粗筛召回先通过 KNN 向量检索从全库召回语义相关的候选集数量是最终返回的 30 倍同时在这批候选集里做关键词 must 匹配 权限过滤得到「语义相关 含关键词 有权限」的干净候选第二阶段是精排重排序对粗筛后的候选集重新计算 BM25 分数将第一阶段 KNN 算出的语义分数和重排的 BM25 分数加权融合最终分数 KNN 分数 ×0.2BM25 分数 ×1.0重新排序后返回指定数量的结果。2. 为什么要做混合检索而不是纯向量或纯关键词检索纯向量检索KNN对自然语言的语义理解好能匹配同义词、模糊表达但对精确关键词如专有名词、技术术语匹配度低容易遗漏核心结果纯 BM25 关键词检索精准匹配关键词但无法理解语义用户 query 表述不同就可能查不到相关结果混合检索结合两者优势既保证语义层面的相关性又确保关键词的精准命中大幅提升检索准确率和用户体验。KNN 与 BM25 细节类考察底层理解1. 整个检索过程中 BM25 用了几次分别在什么阶段作用是什么一共用了两次都是在 KNN 召回的候选集上计算不会对全库操作第一次在粗筛阶段作为 must 条件做关键词匹配作用是过滤—— 把候选集中不含查询关键词的文档剔除缩小候选范围第二次在重排序阶段作用是精细算分—— 对过滤后的候选集重新计算 BM25 分数为后续分数融合、重新排序提供依据。2. KNN 分数和重排的 BM25 分数分别对多少条数据计算是不是同一批属于同一批候选集的不同子集核心是 KNN 分数先算、全程复用KNN 分数是对第一阶段 KNN 召回的全部候选集如 300 条计算的一旦算出就跟着文档走后续过滤不会修改重排的 BM25 分数是对粗筛后关键词 权限过滤的候选集如 150 条计算的这 150 条是 300 条里过滤后的子集简单说BM25 计算的范围是 KNN 候选集的子集本质还是同一批数据。3. 第一阶段算出的 KNN 分数后续有用吗为什么不重新计算有用直接参与最终分数的加权融合这也是核心设计之一一方面KNN 向量检索是全库计算耗时相对较高重新计算会增加检索延迟第一阶段算好后直接复用能提升性能另一方面KNN 分数反映的是文档与查询的语义相关性这个相关性不会因关键词 / 权限过滤发生变化无需重新计算。4. 为什么要给 KNN 和 BM25 设置 0.2 和 1.0 的权重而不是其他比例核心是贴合知识库的使用场景—— 我们的知识库以技术文档、专业内容为主用户检索时对关键词的精准度要求更高比如查「kafka 配置」必须匹配 kafka、配置这些核心词0.2 的 KNN 权重是为了保留语义相关性避免纯关键词检索的机械性1.0 的 BM25 权重让关键词匹配主导最终排序确保核心结果不遗漏这个比例是通过实际业务测试调优后的结果能平衡语义和精准性。Rescore 重排序类考察 ES 实操1. 为什么要做 Rescore 重排序而不是直接融合 KNN 和 BM25 分数核心原因是KNN 和 BM25 的打分体系不同直接融合会导致分数失真KNN 的语义分数是相似度得分范围一般 0-1BM25 的关键词分数是相关性得分范围可能 0-10两者量纲不一致直接相加会让其中一个分数的作用被覆盖Rescore 重排序是先缩小候选集再精细算分融合既保证了性能又能让两种分数在同一范围内有效融合让最终排序更合理。2. Rescore 的 windowSize 为什么设置成 KNN 召回的数量而不是更小windowSize 是重排序的候选范围设置成 KNN 召回的数量如 300 条核心是为了不遗漏潜在的相关结果如果 windowSize 更小可能会把粗筛后部分语义相关、关键词匹配度稍低的文档排除在重排序之外导致最终结果不全而 KNN 召回的数量本身就是经过粗筛的数量可控在这个范围内重排序不会带来明显的性能损耗还能保证结果的完整性。权限过滤类考察工程化思维1. 权限过滤是在哪个阶段做的为什么不在检索后再过滤权限过滤在粗筛阶段和 KNN 召回、关键词匹配同时执行且是Filter 上下文过滤原因有两个一是性能优化—— 在候选集阶段过滤能大幅减少后续重排序的计算量避免对无权限的文档做无效处理二是数据安全——Filter 上下文是强制过滤能确保后续所有步骤都不会接触到无权限的文档从源头保证数据隔离如果在检索后再过滤不仅会浪费计算资源还可能出现无权限数据的临时加载存在安全风险。2. 权限过滤的规则是什么怎么保证多租户 / 多组织的数据隔离我们的权限过滤基于 **「本人文档 公开文档 同组织标签文档」** 三大规则在 ES 的 Filter 上下文里用 should 组合实现匹配当前用户 ID确保能访问自己上传的文档匹配公开标识确保能访问所有公开文档匹配用户的有效组织标签确保能访问同组织的文档多标签时做或逻辑同时我们会在 ES 文档中直接存储userId、isPublic、orgTag三个字段过滤时直接基于字段匹配无需关联数据库既保证了隔离性又提升了过滤效率。性能优化类考察优化思路1. 你们是怎么优化混合检索的性能的为什么 KNN 召回要设置成最终结果的 30 倍核心优化思路是 **「粗筛尽可能快、精排尽可能准」**主要做了三点KNN 召回控制数量设置成最终结果的 30 倍是平衡「结果完整性」和「性能」的最优值 —— 数量太少会遗漏相关结果太多会增加后续过滤和重排序的计算量30 倍是经过压测后的工程实践值所有计算基于候选集BM25、权限过滤都在 KNN 召回的候选集上执行不触达全库大幅减少计算量Filter 上下文做权限过滤Filter 上下文会被 ES 缓存且不参与打分相比 must 上下文过滤速度更快还能提升重复查询的性能。2. 向量检索的性能瓶颈是什么你们怎么解决的向量检索的核心瓶颈是全库的向量相似度计算尤其是数据量增大后纯暴力计算耗时会急剧增加我们的解决方式一是通过 KNN 做近似最近邻检索替代暴力计算大幅提升召回速度二是控制召回数量只召回语义最相关的候选集减少后续处理三是对 ES 向量索引做优化使用 HNSW 索引结构进一步提升 KNN 召回的性能。异常与边界类考察鲁棒性1. 如果用户的查询词没有明显的关键词混合检索还能生效吗能生效因为我们的核心是 **「KNN 做基础召回BM25 做精准强化」**即使没有明显关键词KNN 向量检索依然能基于语义召回相关文档而粗筛阶段的 BM25 must 匹配会尽可能匹配文档中的相关词汇不会因为 query 关键词少而过滤掉核心结果同时重排序阶段的 BM25 会对召回的文档做精细打分依然能保证结果的相关性排序。2. 当知识库数据量很大时如千万级混合检索的性能会受影响吗怎么应对基础影响主要在 KNN 召回阶段后续过滤和重排序因为基于候选集受影响较小我们的应对方案一是优化 ES 向量索引使用更高效的索引结构如 HNSW提升 KNN 召回速度二是做数据分片按组织标签 / 文档类型对 ES 索引做分片KNN 召回时只在对应分片执行减少检索范围三是增加 ES 节点通过水平扩容提升检索能力保证大数量下的性能稳定。

更多文章