面试知识库

主题 3 · 检索召回优化#

定位:RAG 管线第三层——向量/关键词/混合检索 + 路径分流 + Agentic 循环,决定了检索的覆盖面和精准度


一、通用知识#

1.1 核心概念与原理#

Dense Retrieval(稠密向量检索)原理

面试怎么讲:

“Dense Retrieval 的核心思路是把 query 和 document 各自通过一个 Bi-Encoder(通常是 BERT 级别的 Transformer)编码成固定维度的向量,然后用 ANN(近似最近邻)算法在向量空间里找距离最近的 top-K。优势是理解同义词和语义相似——‘automobile’ 和 ‘car’ 虽然词形完全不同,但向量距离很近。劣势是对精确匹配很差——‘iPhone 15 Pro Max’ 这种型号名、API 参数名、专有名词,稍有偏差向量就漂走了,BM25 反而精准。所以实际系统里 Dense Retrieval 从来不是单路用,一定要和 Sparse Retrieval 混合。”

技术细节:

  • 编码阶段:query 和 document 独立过 Transformer,无交叉注意力(区别于 Cross-Encoder)
  • document 向量可以离线预计算,存入向量数据库(Milvus / Pinecone / Weaviate)
  • 线上只需算一次 query 向量 → ANN 搜索 → 返回 top-K,时间复杂度近似 O(1)
  • 典型 embedding 模型:text-embedding-v3(阿里, 1024 维)、text-embedding-3-large(OpenAI, 3072 维)、BGE-M3(开源, 1024 维)
  • 距离度量:COSINE(归一化后等价于内积)、L2(欧几里得)、IP(内积)

Sparse Retrieval / BM25 原理

面试怎么讲:

“BM25 是基于 TF-IDF 思想的改进公式——它解决了两个 TF-IDF 的问题:一是词频饱和(一个词出现 100 次不应该比 10 次高 10 倍,边际收益递减),二是文档长度归一化(长文档天然词频高,不归一化会系统性偏向长文档)。”

TF-IDF 基础:

  • TF(t, d) = 词 t 在文档 d 中的出现频率
  • IDF(t) = log(N / df(t)),N 是文档总数,df(t) 是包含词 t 的文档数
  • 直觉:一个词在当前文档里频繁出现(TF 高)但在整个语料库里很少见(IDF 高),说明它对当前文档有强区分力

BM25 改进公式(必须能写出来):

score(q, d) = Σ_{qi ∈ q} IDF(qi) · [TF(qi, d) · (k1 + 1)] / [TF(qi, d) + k1 · (1 - b + b · |d| / avgdl)]
plaintext

其中:

  • IDF(qi) = log(1 + (N - df(qi) + 0.5) / (df(qi) + 0.5))(Lucene 变体,+0.5 平滑防止零除)
  • k1:TF 饱和速度控制参数(经典默认值 1.2,DocMind 取 1.5 偏向长文档中的高频信号)
    • k1 越大 → 饱和越慢 → 高词频 chunk 得分越高(长文档更有利)
    • k1 = 0 → TF 项退化为常数 1,所有包含该词的文档同分
  • b:文档长度归一化强度(经典默认值 0.75
    • b = 1 → 完全归一化(长短文档公平竞争)
    • b = 0 → 忽略长度差异(长文档系统性占优)
  • |d|:当前文档长度,avgdl:语料库平均文档长度

面试关键话术:

“k1 和 b 这两个参数面试必须能说清楚。k1 控制词频的饱和曲线——想象一个 log 函数,k1 越大曲线越平缓,同一个词出现 20 次和 10 次的差距越大。b 控制长度惩罚——b=0.75 意味着长文档要’打折’,防止一篇 5000 字的文档仅仅因为长就比 500 字的相关段落得分高。”

为什么 Hybrid > 单路——Dense 和 Sparse 的互补性

面试怎么讲:

“Dense 和 Sparse 检索的错误模式是正交的:Dense 对同义词/释义理解好但对精确匹配差(‘RFC 7231’ 这种编号向量几乎无法区分),Sparse 对精确匹配好但对同义词差(‘automobile’ 和 ‘car’ BM25 认为毫不相关)。混合检索让两路互补——Anthropic 2024 年的 Contextual Retrieval 报告量化了这一点:hybrid(contextual embeddings + contextual BM25)相比纯向量检索将检索失败率降低了 67%。这不是微调,是架构级的收益。”

互补矩阵:

场景DenseSparseHybrid
同义词(automobile→car)
精确匹配(RFC 7231)
长尾专有名词(DashScope)
模糊意图(“怎么部署”)
中文多义词(“银行”→河岸/金融机构)

RRF(Reciprocal Rank Fusion)

公式:

RRFscore(d) = Σ_{r ∈ R} 1 / (k + rank_r(d))
plaintext
  • R:多路检索结果的集合
  • rank_r(d):文档 d 在检索源 r 中的排名(从 1 开始)
  • k = 60:Cormack et al. 2009 RRF 原论文推荐值,控制排名靠后的文档的影响力衰减速度

面试怎么讲:

“RRF 的核心洞察是:不同检索源的分数量纲不可比——cosine 范围 01,BM25 范围 0+∞,Web Tavily 有自己的分数标准。直接加权平均毫无意义。RRF 的做法是只看排名不看分数:每个文档在每路检索里的排名加一个常数 k 后取倒数再求和。这样 rank-1 的文档贡献 1/(k+1),rank-2 贡献 1/(k+2),衰减很快,自然地把多路一致排在前面的文档推到最前。k=60 让 rank-1 和 rank-2 的差距比较小(1/61 vs 1/62),不会过度放大首位优势。”

Weighted RRF 变体:

WRRFscore(d) = Σ_{r ∈ R} w_r / (k + rank_r(d))
plaintext
  • 给不同检索源不同权重 w_r
  • 适用场景:明确知道某路检索质量更高时(如已知 BM25 对精确查询更好时,给 BM25 更高权重)

向量索引类型对比

索引类型原理构建速度查询速度内存占用精度适用规模
HNSW基于图的近似搜索,分层可导航小世界图慢(需建图)快(O(log N))大(存图结构)高(>95%)< 100 万向量
IVF分桶(K-means 聚类)+ 局部搜索中等中等中等取决于 nprobe> 100 万向量
PQ向量切分子空间 + 量化压缩极小(压缩 8-32x)有损> 1000 万向量(降内存)
IVF_PQIVF 分桶 + PQ 压缩(组合)中等中等> 1000 万向量
ScaNNGoogle 出品,各项均衡中等中等通用

HNSW 详解(面试高频):

“HNSW 全称 Hierarchical Navigable Small World,灵感来自六度分隔理论——在图上任意两点的最短路径不超过 O(log N) 步。它构建多层图,顶层稀疏(跳跃式导航,快速缩小搜索范围),底层稠密(精确搜索)。查询时从顶层入口开始贪心搜索,逐层下降,最终在底层找到最近邻。关键参数:M(每层最大邻居数,越大精度越高但内存越大)、ef(搜索队列大小,越大精度越高但越慢)。我们 Milvus 配置 ef=64,是精度和速度的平衡点。”

选型建议:

  • < 100 万向量:HNSW(精度高、查询快,内存开销可接受)
  • 100 万 ~ 1000 万:IVF_HNSW 或 IVF_FLAT + nprobe 调优
  • 1000 万:IVF_PQ(必须压缩降内存,精度换空间)

检索指标(每个给出公式和适用场景)

指标公式衡量什么RAG 场景意义
Recall@K|相关文档 ∩ top-K| / |全部相关文档|找全了没有核心指标——漏召就是漏答案
MRR1/|Q| · Σ 1/rank_i(rank_i 是第一个相关结果的排名)第一个相关结果排第几LLM 对首位 chunk 利用率最高(Lost in the Middle)
NDCG@KDCG@K / IDCG@K,DCG = Σ rel_i / log2(i+1)排序质量(考虑位置权重)适合多级相关性(高度相关/部分相关/不相关)
MAPmean(AP_per_query),AP = Σ (P@k · rel(k)) / |相关|综合精确率和召回率多查询整体评估
Hit Rate@K有至少一个相关文档进入 top-K 的查询占比是否至少命中了一条最基本的”能不能用”指标

面试怎么讲:

“RAG 场景里 Recall@K 是第一优先级——因为你漏掉的 chunk 不可能出现在最终答案里,LLM 不是搜索引擎,它不会凭空补全没给它的信息。Precision 在 RAG 里重要性低于传统搜索,因为 rerank + 压缩会在后续环节过滤掉不相关的——但 Recall 一旦丢了就没有挽回余地。MRR 在 RAG 里意义特殊——不只是排序好不好的问题,而是直接影响 Lost in the Middle:首位 chunk 被 LLM 充分利用,第五位 chunk 可能被忽略。”

Agentic Retrieval 模式演进

阶段模式特点代表
Gen 1单次检索(Naive RAG)query → top-K → generate,一次召回定生死LangChain 早期
Gen 2迭代检索(Iterative)generate → 发现信息不足 → 再检索 → 补充 → generateITER-RETGEN
Gen 3自适应检索(Adaptive-RAG)根据查询复杂度选不同策略(直答/单次/多跳)Self-RAG, Adaptive-RAG
Gen 4工具调用循环(Agentic)模型自驱决定何时检索、检索什么、调哪个工具、何时停止Anthropic agentic search, Azure agentic retrieval, ChatGPT Deep Research

面试怎么讲:

“Agentic 和传统 RAG 的本质区别是控制反转——传统 RAG 的检索策略是代码写死的(拆几个子问题、跳几跳、查不查 Web 全是 if-else),Agentic 是把检索策略交给 LLM 自己决定。你给模型一组检索工具(向量搜索、关键词搜索、Web 搜索、记忆召回),它自己决定调哪个、参数是什么、什么时候够了停下来。好处是泛化能力强——代码不需要穷举所有查询模式,模型会自适应。代价是 LLM 决策增加延迟和成本(每轮决策一次 LLM 调用),而且不可控(模型可能空转、可能跑满预算)。所以实际工程里要加确定性护栏——迭代上限、空转检测、异常回退。“

1.2 业界主流方案对比(表格)#

维度Naive RAGAdvanced RAG (hybrid+rerank)Modular RAGAgentic RAG
检索方式单路向量 top-K多路混合 + RRF + Cross-Encoder 重排可插拔模块(检索/重排/压缩/验证任意组合)模型自驱工具调用循环
查询处理原始 query 直接检索query 改写 + HyDE + 分类路由每个模块独立处理模型自行分解/改写/多跳
迭代能力有限(CRAG 补搜一次)可配置原生多轮,模型自决停止
复杂查询差(无法处理多焦点/多跳)中等(规则驱动拆解)好(但需手写编排逻辑)强(模型自适应策略)
延迟最低(1 次检索 + 1 次 LLM)中等(检索 + rerank + LLM)取决于管线长度最高(每轮多一次 LLM 决策)
可控性高(确定性)中等低(需护栏约束)
工程复杂度中等高(模块间接口设计)高(循环控制 + 异常处理 + 兜底)
适用场景Demo / 简单事实题生产系统多数场景需高度定制的场景复杂/多跳/对比/时效性查询
代表系统LangChain 早期RAGFlow, QAnythingLlamaIndex, HaystackAnthropic, Azure, DocMind

1.3 关键论文与技术要点#

论文/工作年份核心贡献面试一句话
RRF (Cormack et al.)SIGIR 2009只看排名不看分数的融合方法,k=60”解决多路召回分数不可比的标准方法”
Contextual Retrieval (Anthropic)2024给每条 chunk 加上下文前缀后再 embedding + BM25 混合”hybrid 把检索失败率降了 67%——单路向量的天花板在那”
Adaptive-RAG (Jeong et al.)2024根据查询复杂度自适应选策略(直答/单次/多跳)“不是所有查询都需要走全管线,简单题走轻路径省资源”
CRAG (Yan et al.)arXiv 2024检索结果三档置信度评估 + 自适应策略”HIGH 直通、LOW 兜底、AMBIGUOUS 补搜”
Self-RAG (Asai et al.)ICLR 2024模型通过特殊 token 控制何时检索、生成后自我评估”模型学会了在生成过程中’觉得不够了再去查‘“
HyDE (Gao et al.)2022先让 LLM 生成假设文档,用其 embedding 做检索”解决短 query 与长文档的语义密度鸿沟”
DPR (Karpukhin et al.)EMNLP 2020奠基性的双塔 Bi-Encoder 检索框架”Dense Retrieval 的开山之作,证明可以替代 BM25”
HNSW (Malkov & Yashunin)2018分层可导航小世界图,向量近似搜索主流索引”几乎所有向量库默认索引,O(log N) 查询”
BM25 (Robertson et al.)1994/2009TF 饱和 + 文档长度归一化”搜索引擎的基础公式,面试必须能写出来”
ColBERTv2 (Santhanam et al.)NAACL 2022late interaction 架构,token 级匹配”Bi-Encoder 和 Cross-Encoder 之间的折中”

1.4 常见面试问答#

Q1: BM25 公式写一下,k1 和 b 什么意思?

A: BM25 公式是 score(q,d) = Σ IDF(qi) · TF(qi,d)·(k1+1) / (TF(qi,d) + k1·(1 - b + b·|d|/avgdl))。k1 控制词频饱和速度——k1 越大,高频词的边际收益递减越慢,长文档里出现 20 次的词比出现 10 次的更有优势;经典值 1.2。b 控制文档长度归一化强度——b=0.75 意味着长文档的 TF 要按长度比例打折,防止长文档仅因为词多就系统性占优;b=0 忽略长度,b=1 完全归一化。这两个参数是 BM25 相对于原始 TF-IDF 的核心改进。我们系统里 k1 取 1.5(比经典值略高,因为企业文档平均较长,需要更多地利用词频信号),b 取 0.75(经典值)。

Q2: HNSW 原理?和 IVF 的区别?

A: HNSW 是基于图的近似搜索——构建多层图,顶层稀疏用于快速导航缩小范围,底层稠密用于精确搜索。查询时从顶层贪心走到候选区域,逐层下降在底层找最近邻,时间复杂度 O(log N)。核心参数:M(每层邻居数,控制图密度)、ef(搜索队列大小,控制精度-速度权衡)。IVF 是基于分桶的——先用 K-means 把向量空间分成 N 个桶,查询时只搜最近的 nprobe 个桶。主要区别:HNSW 内存大(要存图结构)但精度高且查询快,适合 < 100 万向量;IVF 内存小、支持增量更新,nprobe 灵活调精度-速度权衡,适合 > 100 万向量。我们 Milvus 用 HNSW(企业知识库数据量在百万级以内),ef=64。

Q3: 为什么不直接用 cosine similarity 排序?

A: 两个原因。第一,cosine similarity 只衡量 query 向量和 document 向量的角度距离,而 Bi-Encoder 编码时 query 和 document 之间没有交叉注意力——“这份文件的第三章讲了什么”和”这份文件的第三章讲了系统架构”,cosine 可能分不出谁更相关。Cross-Encoder 把 query 和 document 拼在一起过 Transformer,有完整的 token 级交叉注意力,语义判断精度远高于余弦距离。第二,如果做了多路混合检索,向量路的 cosine、BM25 路的 TF-IDF 分数、Web 路的 Tavily 分数量纲完全不同——直接按 cosine 排只用了一路的信息,浪费了混合检索的优势。需要先 RRF 融合再 Cross-Encoder 统一打分。

Q4: Recall 和 Precision 的权衡?RAG 里哪个更重要?

A: RAG 场景里 Recall 更重要——漏掉的 chunk 不会出现在最终答案里,LLM 不会凭空补全。而 Precision 低(混入了不相关的 chunk)的影响可以在后续环节缓解:Cross-Encoder rerank 会把不相关的 chunk 排到最后,token 预算压缩会截掉低分的,LLM 自身也有一定的噪声容忍能力。所以检索阶段应该宁可多召、不可漏召——先用较大的 top-K(我们向量和 BM25 各取 50)保证 Recall,再通过 RRF → rerank → MMR → 压缩逐步提 Precision。这就是漏斗模型:上宽下窄,每层过滤一部分噪声。

Q5: top-K 怎么选?选多了和选少了分别有什么问题?

A: top-K 是 Recall 和 Precision/Cost 的权衡。选少了(比如 top-5):Recall 不够,可能漏掉相关文档,尤其是多焦点问题(“对比 A 和 B”需要同时召回 A 和 B 的内容);选多了(比如 top-100):三个问题——一是后续 rerank 的计算量 O(N) 增加,延迟上升;二是更多噪声混入候选集;三是如果直接喂 LLM 会触发 Lost in the Middle。经验法则:初次召回 top-K 要大(我们每路 50),给后续 rerank 足够的候选池;RRF 融合后保留 30 条进 rerank;rerank 后取 top-8 进 MMR;最终压缩到 5-6 条进 LLM。这是逐级缩小的漏斗。

Q6: 向量数据库怎么选型?Milvus vs Pinecone vs Weaviate?

A: 选型看三个维度:部署方式、规模、生态。Milvus:开源自部署,支持 HNSW/IVF/PQ 多种索引,Scale-out 架构(etcd+MinIO+Pulsar 分布式),适合企业私有部署对数据主权有要求的场景,但运维复杂度高。Pinecone:纯 SaaS,零运维,自动扩缩容,按 pod/存储量计费,适合快速原型和中小规模(<1000 万向量),但数据不在自己手里、大规模成本高。Weaviate:开源,内置 Hybrid Search(向量+BM25 原生融合,不需要单独跑 BM25),支持 GraphQL API,适合需要原生混合检索的场景。我们选 Milvus 因为:一是企业知识库需要私有部署;二是项目已有 Docker Compose 基础设施(etcd+MinIO);三是 Milvus 的 COSINE 度量 + HNSW 索引对我们百万级以内的数据量性价比最高。

Q7: Hybrid fusion 除了 RRF 还有什么方法?

A: 主流有四种:一是 RRF(Reciprocal Rank Fusion),只看排名不看分数,最简单最鲁棒,适合分数量纲不可比的场景(我们用这个);二是 线性加权融合(Weighted Sum),先对各路分数 min-max 归一化到 0~1,再加权求和——前提是归一化后分数可比,实际中不同分布很难完美对齐;三是 CombMNZ,乘以每条文档被多少路检索到的次数——被两路都检索到的文档权重翻倍,强调”多路一致性”;四是 学习排序(Learn-to-Rank),把各路分数当特征、训相关性模型来融合——精度最高但需要标注数据且模型维护成本大。实际工程里 RRF 是默认首选,因为不需要任何调参(k=60 就够了),也不需要对分数做归一化。

Q8: Agentic 检索和传统 RAG 的核心区别?什么时候该用 Agentic?

A: 核心区别是检索策略的控制权。传统 RAG 的检索逻辑是代码写死的——拆几个子问题、跳几跳、查不查 Web 全是规则或 if-else。Agentic 把这些决策交给 LLM——给它一组检索工具(向量搜索、Web 搜索、记忆召回),它自己决定调哪个、参数是什么、调几轮、什么时候够了停。该用 Agentic 的场景:一是多焦点问题(“对比 A 和 B”),模型自行分两次搜索分别查 A 和 B;二是多跳问题(“X 的负责人在哪一年入职”),模型先查 X 的负责人是谁,再用名字查入职年份;三是不确定需不需要联网的,模型 KB 搜完觉得不够自己决定补 Web。不该用的场景:简单事实题(“XXX 是什么?”),一次检索就够了,多走一轮 LLM 决策纯粹浪费延迟和成本。所以实际系统应该分流——简单题走规则路径,复杂题走 Agentic。


二、DocMind 实践#

30 秒口述版#

“DocMind 的检索召回做了两件关键事:路径分流混合检索 + Agentic 循环。路径决策阶段,routePath 根据分类器的 complexity/isAmbiguous 信号把查询分成三路:选中文档直读、简单题走规则规划器一次性检索、复杂题走 Agentic 循环。大部分日常简单问题走规则路径,决策开销 < 1ms,只有对比/多跳/歧义题才进 Agentic——节省了 LLM 决策成本。检索层面,一次性路径走 Vector + BM25 双路召回 → RRF(k=60) 融合,Agentic 路径给模型 4 个读工具(searchDocs/keywordSearch/webSearch/recall_memory)+ executeCode 沙箱计算工具(默认关),白名单硬排除写入类工具,最多 4 轮迭代,有空转护栏(连续两轮零新增 chunk 提前终止),异常时回退一次性检索。两条路径殊途同归走统一收尾管道:去重 → Cross-Encoder rerank → MMR → 压缩 → CRAG 评分。基于 52 条手工构建的评测集对照测试,混合检索上线后 Recall@5 提升约 10-15 个百分点(BM25 对精确匹配贡献最大),MRR 提升约 17 个百分点。“

详细展开(Situation → Action → Result)#

背景/痛点(Situation)#

DocMind 在 V1(纯向量检索)阶段面临三个核心痛点:

  1. 单路向量检索的语义漂移问题:对精确匹配极差——用户搜”RFC 7231”、搜”DashScope API 的 top_p 参数”、搜具体的规章条款号,向量检索经常返回语义相关但文本不匹配的 chunk,漏掉真正包含精确信息的段落。抽样测试(52 条 query x 7 类覆盖)发现纯向量检索的 Recall@5 不到 70%,精确查询场景尤其严重。

  2. 复杂查询处理能力缺失:多焦点问题(“对比 A 和 B”)、多跳问题(“X 的负责人在哪一年入职”)、需要联网补充的时效性问题——单次 top-K 检索根本无法覆盖。早期尝试过手写拆解/多跳分支,但每种查询模式一套 if-else,维护成本高且覆盖不全。

  3. 所有查询走同一路径的资源浪费:简单事实题(“什么是向量数据库?“)和复杂对比题走同一条全管线,简单题不需要的 LLM 决策步骤白白消耗延迟和 token。

做了什么(Action)#

1. 三模式路径决策(PathDecision + routePath)

在 query understanding 之后、检索执行之前增加一层纯规则路径决策,把查询分流到最匹配的执行路径:

  • SELECTED_DOC:用户选中了具体文档(1~N 篇)且命中文档/摘要意图关键词 → 跳过搜索,直接读取 chunk(DocumentDirectReader
  • RULE_PLANNER:SIMPLE 复杂度且 isAmbiguous=false → 规则规划器(RetrievalPlanner)选工具集,走一次性检索。始终包含 doc_search,精确查询或歧义时追加 keyword_search,时效性追加 web_search,memory 始终追加(成本仅 1 次 embedding + 1 次 ANN < 100ms)
  • AGENTIC:COMPLEX/MEDIUM/歧义/多焦点 → Agentic 检索循环,模型自驱

路径决策是纯规则 if-else,开销 < 1ms(vs 原先所有查询走 LLM 决策约 300ms)。分类器的 complexity 字段经过确定性后处理升级——检测到多焦点信号(“对比/分别/vs/多问号”)时自动升到 >= MEDIUM,确保这类查询不会走 SIMPLE 路径漏掉焦点。

2. 混合检索 + RRF 融合

一次性检索路径的 RetrievalWorker 实现了 Vector + BM25 双路并行召回:

  • Vector 路VectorRetriever 调 Spring AI EmbeddingModel 把 query 向量化,向 Milvus 做 COSINE ANN 搜索,top-50。支持 HyDE——对 FUZZY 查询先让 LLM 生成假设文档,用假设文档的 embedding 替代原始 query embedding 做检索,缓解短 query 与长 chunk 的语义密度鸿沟
  • BM25 路BM25Retriever 优先用 MySQL FULLTEXT(n-gram)预召回 400 条候选,失败时降级到应用内中文分词(ChineseTextTokenizer)+ 手动 BM25 打分(k1=1.5, b=0.75)。BM25 路用原始 query(保持关键词精准性),不用 HyDE 文本
  • RRF 融合RRFFusion.fuse() 对两路结果按 dedupeKey 去重,双路命中的标记为 HYBRID,RRF 常数 k 从 sys_ai_config 动态读取(默认 60),支持加权 RRF

3. Agentic 检索循环(AgenticSearchOrchestrator)

用 Spring AI 的 ToolCallingManager 实现真 LLM 工具调用循环,替代此前手写的拆解/多跳/CRAG-web 三条分支:

  • 手动控环:设置 internalToolExecutionEnabled(false) 关闭 Spring AI 的内部自动工具执行循环(该循环无迭代上限,模型不停调工具会跑到天荒地老),改为自己写 for 循环控制
  • 工具白名单硬排除:只暴露 4 个只读工具(searchDocskeywordSearchwebSearchrecall_memory)+ executeCode 沙箱计算工具(sandbox.enabled 默认关),通过 whitelistedToolCallbacks() 过滤而非 toolNames 追加(Spring AI 的 toolNames 是追加语义取并集,无法排除 store_memory),防止 LLM 误写记忆
  • kbIds 权限收窄AgentToolContext.activate(kbIds) 将用户勾选的知识库范围注入 ThreadLocal,工具层(DocSearchTool)强制用 context kbIds 覆盖 LLM 传入的参数,LLM 无法越权访问其它知识库
  • 空转护栏:每轮结束后检查新增 chunk 数(roundYield = currentTotal - chunksSeen),如果本轮零增益且已累积到资料 → 提前终止。命中场景:KB 首轮已空,模型换个措辞又搜一遍——徒增 1 次 LLM 决策 + embedding + 大上下文重发。仅靠 system prompt 约束不够(模型常跑满预算),代码侧硬兜底
  • 迭代上限agentic.max_iterations 默认 4 轮,从 sys_ai_config 动态配置
  • 温度锁零temperature(0.0) 保证检索规划的确定性
  • 不暴露轮数上限:system prompt 里不告诉模型”你最多 4 轮”——告知预算会把模型锚定到用满 N 轮(trace 实测每次都跑满),只引导”尽量少、搜空即止”

4. 异常回退机制

DocMindAgent.agenticWithFallback() 包裹整个 agentic 循环:循环异常或空结果时回退一次性检索——调 RetrievalPlanner 重新选工具集,走 SupervisorAgent.oneShotRetrieval。保证请求永不因 agentic 失败而中断。

5. 统一收尾管道

两条检索路径殊途同归到相同的后处理管道:

一次性路径: Vector+BM25 → RRF → mergeForRerank(fused, webPart) → rerank → MMR → compress → CRAG
Agentic 路径: 多轮工具调用并集 → copyAll 防御拷贝 → dedupe → rerank → MMR → compress → CRAG
plaintext

统一管道保证两条路径的输出质量一致,避免 agentic 路径因为跳过了某个后处理步骤而输出质量下降。

量化结果(Result)#

以下数据基于 52 条手工构建的评测集(覆盖事实/概念/对比/推理/操作/多跳/超范围 7 类),通过脚本驱动各组件对照测试得到,用于验证组件级增量收益,非自动化评测平台产出。

  • Recall@5 提升约 10-15 个百分点:混合检索(Vector + BM25)堵住了纯向量检索的精确匹配盲区,事实类查询(含关键词/条款号/参数名)提升幅度最大,约 20 个百分点
  • MRR 提升约 17 个百分点:RRF 融合 + 路径分流(复杂题走 agentic 多轮补全)联合提升,Cross-Encoder 重排的贡献大于 RRF 融合本身
  • 路径决策开销 < 1ms:纯规则 if-else,vs 此前所有查询过 LLM 路由约 300ms
  • 大部分简单查询走规则路径:日常简单事实题不进 agentic 循环,节省 LLM 决策成本
  • Agentic 空转减少:空转护栏上线后,trace 观察到简单题通常 1-2 轮即停,不再跑满 4 轮上限

代码锚点(表格)#

类/方法路径职责
PathDecision (record)agent/PathDecision.java路径决策结果:Mode 三值枚举 (SELECTED_DOC / RULE_PLANNER / AGENTIC)
DocMindAgent.routePath()agent/DocMindAgent.java :1117三模式路径决策:直读/规则/agentic 分流逻辑
DocMindAgent.agenticWithFallback()agent/DocMindAgent.java :841Agentic 循环 + 异常/空结果回退一次性检索
RetrievalPlanner.plan()service/rag/RetrievalPlanner.java :61纯规则工具集选择:doc_search 始终 + 条件追加 keyword/web/memory
AgenticSearchOrchestrator.search()agent/supervisor/AgenticSearchOrchestrator.java :100Agentic 循环主入口:手动控环 + 空转护栏 + finalize 收尾
AgenticSearchOrchestrator.whitelistedToolCallbacks()同上 :173工具白名单过滤:4 读工具 + executeCode 沙箱计算
AgenticSearchOrchestrator.finalize()同上 :182Agentic 统一收尾:dedupe → rerank → MMR → compress → CRAG
SupervisorAgent.oneShotRetrieval()agent/supervisor/SupervisorAgent.java :75一次性检索编排:dispatch Workers → RRF → rerank → MMR → compress → CRAG
SupervisorAgent.dispatchWorkers()同上 :305并行派发 Worker(CompletableFuture + 超时降级)
RetrievalWorker.execute()agent/worker/RetrievalWorker.java :45Vector+BM25 双路召回 → RRF → rerank → MMR
VectorRetriever.retrieve()service/rag/VectorRetriever.java :46Milvus COSINE ANN 搜索,ef=64
BM25Retriever.retrieve()service/rag/BM25Retriever.java :56MySQL FULLTEXT 预召回 + 应用内 BM25 打分(k1=1.5, b=0.75)
BM25Retriever.computeBm25()同上 :196手动 BM25 公式计算(含短语覆盖加分)
RRFFusion.fuse()service/rag/RRFFusion.java :46加权 RRF 融合,dedupeKey 去重,HYBRID 标记
DocSearchTool.searchDocs()mcp/DocSearchTool.java :42语义检索 MCP 工具,内部调用时 kbIds 由 AgentToolContext 强制覆盖

量化数据(表格)#

指标数值基线说明
Recall@5 增量约 +10~15 ppV1 纯向量(不到 70%)混合检索(52 条评测集对照)
事实类 Recall@5 增量约 +20 ppBM25 对精确匹配贡献最大
MRR 增量约 +17 ppV1 纯向量路径分流 + 混合检索联合(52 条评测集对照)
路径决策耗时< 1ms~300ms(LLM 路由)纯规则 if-else
规则路径占比大部分简单查询SIMPLE 非歧义日常问题
Vector top-K50retrieval.main.vector_top_k
BM25 top-K50retrieval.main.bm25_top_k
RRF 常数 k60rag.rrf_k_constant 动态配置
RRF 融合后保留30rag.rrf_top_n
BM25 k11.5经典 1.2偏向企业长文档
BM25 b0.75经典 0.75标准长度归一化
Agentic 最大迭代4 轮agentic.max_iterations
Agentic 工具白名单4 读 + executeCodesearchDocs / keywordSearch / webSearch / recall_memory + executeCode(沙箱计算,默认关)
Agentic 迭代表现(护栏后)简单题 1-2 轮即停护栏前常跑满 4 轮空转护栏 + 不暴露预算
Worker 超时15sretrieval.worker_timeout_ms
Milvus ef64HNSW 搜索队列大小
Milvus metricCOSINE归一化后等价于内积
Embedding 维度1024text-embedding-v3

三、追问应对#

面试官想听到的信号#

  • 理解 Dense 和 Sparse 的互补性,不是”加了 BM25 就完了”,而是知道哪些查询模式从混合中受益
  • 能写出 BM25 公式并解释 k1/b 的含义
  • 清楚 RRF 的核心洞察——分数不可比、只看排名——不是”随便选了个融合算法”
  • 理解 Agentic 的价值和代价,知道什么时候不该用 Agentic(简单题走规则更优)
  • 路径分流不是为了”炫技”,而是为了工程上的资源效率——大部分简单题省掉 LLM 决策
  • Agentic 循环的工程细节:手动控环(为什么不用 Spring AI 内部循环)、白名单硬排除(为什么不用 toolNames)、不暴露轮数上限(防锚定)、空转护栏
  • 异常回退是一等公民——agentic 挂了不是报错,是无缝降级到一次性检索

追问预判与应答#

Q1: 为什么 BM25 的 k1 取 1.5 而不是经典的 1.2?

A: 经典 1.2 是基于 Web 搜索场景的经验值——网页平均比较短(几百字),词频差异不大。我们的企业文档平均比较长(PDF 切片 400~800 字),同一个关键词在高度相关的长文档里可能出现十几次,k1=1.2 饱和太快导致 10 次和 15 次几乎没区别。调高到 1.5 让高频信号能拉开差距。这个值是通过 A/B 评测确定的——1.5 比 1.2 在精确查询上的 Recall 高了约 3 个百分点。

Q2: RRF 的 k=60 有什么讲究?为什么不调?

A: k=60 是 Cormack 2009 年 RRF 原论文推荐的,作者实验了 k=1 到 k=1000,60 在多个 TREC 数据集上表现最稳定。k 的作用是控制排名靠后文档的影响力衰减——k 越小(比如 1),rank-1 和 rank-2 的差距越大(1/2 vs 1/3),过度放大首位优势;k 越大(比如 1000),所有排名的贡献都差不多(1/1001 vs 1/1002),失去区分度。60 是一个平衡点。实际上 RRF 对 k 不太敏感——40 到 80 之间结果差异很小,不值得花时间调参,这也是 RRF 受欢迎的原因之一:几乎零调参。

Q3: Agentic 循环手动控环 vs Spring AI 内部自动循环,为什么要自己写?

A: Spring AI 的 ToolCallingManager 内部有自动工具执行循环——模型请求调工具,自动执行后把结果拼回 messages,再调模型,直到模型不再请求工具。问题是这个内部循环没有迭代上限。如果模型进入空转模式(换个措辞重复搜索同一来源),它会无限循环下去——每轮一次 LLM 调用 + 一次工具执行,token 和延迟累积。所以我们设 internalToolExecutionEnabled(false) 关掉内部循环,自己写 for 循环,好处:一是有硬上限(4 轮);二是每轮结束可以检查增益(空转护栏);三是可以发 SSE 事件让前端显示进度;四是可以加 OpenTelemetry span 做可观测。代价就是自己要管 message history 的拼接和 prompt 重建。

Q4: 白名单为什么用 filter 而不是 Spring AI 的 toolNames?

A: 这是 Spring AI 的一个设计特性——toolNames追加语义(和 toolCallbacks 取并集),不是限制语义。也就是说你设了 toolNames("searchDocs", "webSearch", "recall_memory"),如果 toolCallbacks 里已经包含了 store_memory,它不会被排除——toolNames 只是额外声明”这些也要暴露”。唯一能真正排除工具的方式是不把它放进 toolCallbacks。所以我们从 ToolCallbacks.from(docSearchTool, webSearchTool, memoryTool) 生成全量回调后,用 filter(ALLOWED_TOOLS::contains) 只保留白名单内的——store_memorykb_meta 的回调根本不进 toolCallbacks,模型看不到它们。

Q5: 空转护栏的判断逻辑?误判怎么办?

A: 护栏逻辑是:每轮结束后统计 AgentToolContext 里的 chunk 总数(chunks 只增不减),计算本轮新增量 roundYield = current - chunksSeen。如果 roundYield == 0(本轮零新增)且 chunksSeen > 0(此前已有累积资料)→ 提前终止。关键的 && chunksSeen > 0 条件防止首轮就搜空时误判——首轮搜空是正常的(可能 query 需要改写再搜),只有”已经有资料了还搜不出新的”才是空转。误判风险:理论上存在”第二轮搜的和第一轮完全相同的 chunk”的情况(dedupeKey 相同),但实际中几乎不会发生——模型每轮通常用不同的 query 搜不同的内容。而且这个护栏只是提前终止,不会丢弃已有资料——最坏情况是少搜了一轮,收尾管道会对已有资料正常 rerank。护栏可以通过 agentic.early_stop_on_empty_round 配置开关关闭。

Q6: HyDE 什么时候用?对 Recall 提升多少?

A: HyDE(Hypothetical Document Embeddings)在 specificity=FUZZY 时触发——即用户问题很模糊(“怎么部署?“而不是”Docker Compose 部署步骤”)。原理是让 LLM 先生成一个假设性的回答文档,用这个假设文档的 embedding 替代原始 query 的 embedding 做向量检索。好处是解决了”短 query 与长 chunk 的语义密度鸿沟”——5 个字的 query 向量信息密度太低,和 400 字的 chunk 向量距离判断不准。HyDE 路只用于向量检索;BM25 路仍用原始 query(保持关键词精准性)。Recall 在 FUZZY 类查询上提升约 10-15%,但对 PRECISE 类查询无效甚至略有下降(假设文档引入噪声),所以只在 FUZZY 时开。

Q7: Vector 和 BM25 各取 top-50 会不会太多?后面怎么收窄?

A: 初次召回 top-50 是有意为之——这是漏斗模型的最宽入口,目的是保 Recall。两路各 50 条,去重后通常 60-80 条候选。然后逐级收窄:RRF 融合后保留 top-30 → Cross-Encoder rerank 取 top-8 → MMR 去重保留 6-8 条 → token 预算压缩最终 5-6 条进 LLM。每层都是在前一层基础上精选,Recall 在最上层保住,Precision 在每层逐步提升。如果初次召回只取 top-10,RRF 融合后可能只有 15 条候选,rerank 从 15 条里选 8 条——池子太浅,容易漏掉被单路排名靠后但双路都命中的好 chunk。

Q8: 路径分流的 complexity 升级逻辑——检测到多焦点信号就升级,会不会有误判?

A: 确定性后处理规则是:原始 query 里出现”对比/分别/vs/多个问号”这些多焦点模式词时,complexity 自动升到 >= MEDIUM——即使分类器输出的是 SIMPLE。误判风险确实存在——比如”分别介绍一下向量数据库”其实不是多焦点,但被”分别”触发了升级。后果是这个查询走了 Agentic 而不是一次性检索——多花了一轮 LLM 决策(约 300-500ms)和对应 token,但最终结果不会变差(Agentic 首轮搜完发现不需要再搜就停了)。这是一个”宁可多走 Agentic 不可漏掉多焦点”的保守策略——漏掉真正的多焦点题会导致答案只覆盖一个焦点,用户体验明显变差;多走一轮 Agentic 只是延迟增加 300ms,体验影响小得多。

Q9: 如果 Milvus 向量库整库为空或不可用,检索怎么降级?

A: 两层兜底。第一层,VectorRetriever.retrieve() 内部有 try-catch——Milvus 不可用时返回空 list 而不是抛异常,BM25 路不受影响,单路 BM25 结果走 RRF(另一路为空,退化成 BM25 按排名取 top)进后续管道。第二层,如果向量和 BM25 都返回空(极端场景),走 CRAG 评分 → compressed.isEmpty()needsFallback=truePromptAssembler.assembleFallback() 组装 0-chunk 兜底模板,LLM 基于自身知识作答并声明”未找到知识库内容”。另外,我们此前修复过 Milvus 向量丢失事故(整库为空但应用层不知道),加了启动时一致性自检——应用启动时检查 Milvus collection 的 entity count,如果为 0 但 DB 中有 chunk 记录,打 ERROR 日志并触发重建流程。

Q10: 一次性检索路径的 Worker 是并行的吗?超时怎么处理?

A: 是并行的。SupervisorAgent.dispatchWorkers()CompletableFuture.supplyAsync() 把每个 Worker(RetrievalWorker / WebWorker / MemoryWorker)提交到 ragRetrievalExecutor 线程池,然后 CompletableFuture.allOf().join() 等全部完成。超时用 orTimeout(15s) + exceptionally(fallback) 包裹——单个 Worker 超时不阻塞整体,降级为 WorkerResult.failure(),其它 Worker 的结果正常参与融合。这是部分降级而非整体失败的设计。

反击引导#

  • 引向 rerank 和截断:“路径分流和混合检索解决的是’找全’的问题。但找全不等于找对——多路召回的分数不可比,需要 Cross-Encoder 统一标尺做精排,这是我在 rerank 优化那块重点做的事。”
  • 引向可信生成:“检索质量再高,最终还是要看 LLM 有没有忠实地引用。我们做了结构化引用解析和 coverage-based 置信度评分,可以聊一下。”
  • 引向工程降级:“Agentic 循环的异常回退只是降级策略的一个点。整条 RAG 管线每个环节都有降级——分类器挂了走 fallback classification(isAmbiguous=true 兜底)、reranker 挂了走关键词排序、Milvus 挂了走纯 BM25——可以展开聊一下全链路降级设计。”
  • 引向可观测:“Agentic 循环的一个工程挑战是可观测性——多轮工具调用的 trace 怎么串联、SSE 怎么逐轮推进度、Langfuse span 怎么记录每轮决策——我在可观测那块重点做了 StageEmitter 统一三套埋点。”