导读:RAG 出了问题,大多数情况下问题出在检索而非生成。但大多数人第一反应是调 prompt 或者换模型。这篇文章沿着 RAG 的数据管线,从 chunking 到 reranker,逐层拆解每个环节的验证方法和调优空间,核心原则是:先修上游,再动下游,每次改动都要用对指标。
从一次沉默的失败开始。
你的 RAG 系统上线了,用户问了一个你确定知识库里有答案的问题。系统返回了一段流畅的没有错误的回答,但仔细看,答案并不正确。它引用了正确的文档片段,但忽略了文档里最关键的一句话。它把产品 A 的定价策略安到了产品 B 头上。它甚至在某段回复里自相矛盾,前半句说"不支持",后半句说"请按以下步骤操作"。
你第一反应是调 prompt。第二反应是换生成模型。第三反应是给 LLM 加 system prompt 要求"请仔细阅读上下文"。但这些操作大多不痛不痒,因为问题不在生成端,而在检索端。
RAG 失败案例中,绝大多数是检索失败穿了生成的外衣。而检索失败的根源,往往在文档入库的那一刻就已经埋下了。
这篇文章沿着 RAG 的数据管线梳理一遍:从文档入库的 chunking,到向量化,到检索策略,到排序,再到 query 改写。
文档入库——Chunking 是第一块多米诺骨牌
很多人把 RAG 优化的精力放在 embedding 模型选型和向量数据库调参上,但一个常被忽略的事实是 :Chunking 策略对召回率的影响,经常超过 embedding 模型本身。
NVIDIA 在 2025 年发布过一个 chunking 基准测试(NVIDIA Developer Blog),结果很能说明问题:在保持 embedding 模型和检索方式不变的前提下,最优和最差 chunking 策略之间可以拉开约 9% 的 recall 差距。你花大价钱换一个 MTEB 排名更高的 embedding 模型,效果可能还不如换个 chunking 方式来得直接。
三个典型失败模式
Chunking 的核心矛盾在于颗粒度。小 chunk 带来精确的语义匹配,但容易丢失上下文。大 chunk 保留完整语境,但语义信号被稀释,检索时召回的段落可能包含大量无关信息。
以下是三种典型失败场景:
•chunk 太小:一个完整的技术概念被切在相邻两个 chunk 里,检索命中了下半段,但上半段的关键定义没能进入上下文。LLM 看到了操作步骤,却看不到适用条件,于是给出了错误的操作指导。•chunk 太大:一个 2000 token 的 chunk 覆盖了三个不同的技术要点,检索时因为开头那句匹配上了,结果 LLM 把 chunk 里的 A 公司数据当成了 B 公司的。•切割边界错误:最典型的情况是,像"如果合同中有例外条款,承包商不承担责任"这种句子,切割点刚好落在"不"字之前,下一段从"承担责任"开始。检索时匹配到了"承担责任",却看不到前面的"不"字。
基线策略与验证方法
不要上来就追求最优 chunking,先用一个可靠的基线跑通全流程,再通过指标决定是否值得优化。
对于大多数结构化文档(技术文档、产品手册、FAQ),RecursiveCharacterTextSplitter 以 512 token 为块大小、20% 重叠率是一个不错的起点。跑通这个基线,拿到 Recall@K 的基准值,你才有判断依据:换了 chunking 策略之后,召回率到底是升了还是降了。
验证方法很简单:准备一组已知正确答案的 query 文档对,分别用两种 chunking 策略跑 retrieval,对比 Recall@K。如果差距在 1-2% 以内,不值得为 chunking 优化花太多时间。如果差距超过 5%,那 chunking 就是你的第一优先级。
关于 Semantic Chunking 的实话
Semantic Chunking(基于 embedding 相似度识别语义边界来切分)听起来很合理,但实际调参时,合适的 breakpoint_percentile_threshold 最终定在 92。
即便调好了,它在结构化文档上的表现也没有明显优于层级化 chunking(按 Markdown 标题和段落结构切分)。结构化文档优先用层级化切分,非结构化文档再考虑 semantic chunking。
向量化——Embedding 不是银弹
Embedding 模型把文本映射到高维语义空间,相近的语义在向量空间里距离更近。这个设计在语义检索场景下非常有效,但它有两个嵌入到模型骨子里的盲区。
盲区一:语义不等于事实
"该药物对患者有效"和"该药物对患者无效",在语义上是相反的,但在一些 embedding 模型中,它们的向量距离却非常近。因为它们共享了几乎全部词汇和句法结构,差别只是一个"不"字。
在 768 维的向量空间里,一个字的语义差异在某些模型中可能编码不足。这意味着:如果你的知识库里有大量包含否定结构的句子("不支持""不兼容""不适用"),纯向量检索在这类 query 上的召回率可能系统性偏低,建议在你的数据集上单独验证这个盲区。
盲区二:精确标识符的丢失
"错误代码 E4392"这个 query,如果用纯向量检索,大概率返回的是"错误代码 E4391"和"错误代码 E4393"的文档,因为它们在语义上高度相似。但用户要的就是 E4392 那个精确条目。向量检索天然不擅长处理这类精确匹配需求。
选型思路
MTEB 榜单上 embedding 模型上百个,但做 RAG 时只看 Retrieval 子榜单就够了,不要被分类、聚类、STS 这些子任务的分数干扰。看 score-per-parameter 比只看绝对排名更有参考价值。一个 0.6B 参数的模型如果在 Retrieval 子榜上排第 8,性价比远高于一个 8B 参数排第 2 的模型。
什么时候值得微调
如果你的领域有大量专业术语(医疗、法律、金融),通用 embedding 模型可能对这些术语的语义理解不够好。Databricks 的一项实验显示(Databricks Blog,2025),用金融数据(FinanceBench)微调 gte-large-en-v1.5 模型,Recall@10 从 0.293 提升到了 0.552,提升幅度接近 88%。
如果领域术语密集到这个程度,微调就值得认真考虑。
但如果你的文档是通用技术内容(API 文档、产品说明、FAQ),开箱即用的 embedding 模型通常已经够用。先跑基线,Recall@K 不理想再考虑微调。
验证方法
分别在 embedding 更换前后跑 Hit Rate 和 Recall@K,同时单独追踪精确匹配类 query 的召回率。如果精确匹配 query 的召回率明显低于语义类 query,说明你的检索策略需要补充 BM25 这类精确匹配能力。
检索策略——Hybrid Search 是召回率的高杠杆手段
纯向量检索的问题在上面已经说清楚了:语义相似但事实相反,精确标识符无法匹配。这两个问题的解决方案,指向一个古老但有效的技术:BM25。
BM25 基于词频和逆文档频率做精确匹配,对"错误代码 E4392"这种 query 的召回非常准确。但它的问题也很明显:对同义词、近义词、语义改写完全无感。所以 BM25 和向量检索是天然的互补关系。
RRF 融合与 k 值的陷阱
Hybrid Search 最常见的做法是分别用 BM25 和向量检索各跑一遍,然后用 RRF(Reciprocal Rank Fusion,倒数排名融合)合并结果。RRF 的公式很简单,核心逻辑只有一行:
def reciprocal_rank_fusion(dense_results, bm25_results, k=60):
scores = {}
for results in [dense_results, bm25_results]:
for rank, doc_id in enumerate(results, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores.keys(), key=lambda d: scores[d], reverse=True)
RRF 的核心参数是 k,控制排名位置的权重衰减速度。k 越大,排名差异越被"抹平";k 越小,靠前位置的优势越明显。
但 k 的默认值(通常是 60)在小数据集上可能出问题。 如果你的知识库只有 80 份文档,k=60 意味着前 60 名的位置权重几乎拉不开差距,BM25 和向量检索的排序差异被抹平了,融合结果几乎等于随机排序。对于小数据集,可以把 k 降到 10 左右,让排名靠前的结果有足够的权重优势。
权重分配是最难的部分
Hybrid Search 中 BM25 和 Embedding 的权重分配是调优中最棘手的一个环节。核心难点在于:BM25 的分数和向量余弦相似度处于不同的量纲。BM25 的分数范围取决于文档集合的大小和词频分布,可能从 0 到 20 不等。余弦相似度始终在 0 到 1 之间。你无法用一个固定的加权比例把它们融合在一起,因为它们的数值分布根本不在一个量级上。
一个常见的做法是先做分数归一化(Min-Max 或 Z-score),但归一化本身会引入新问题:当某个 query 的 BM25 分数范围很窄时,归一化反而会放大噪声。
推荐的做法是:
1先用 BM25 召回 top-N,然后用 cross-encoder 精排。这是一种更干净的融合方式,不需要处理分数对齐问题。2如果坚持用加权融合,先验证 Recall@K 在融合后是否有提升,并确保 P95 延迟在预算内。如果向量检索在 BM25 的 top-N 结果之外没有贡献新的相关文档,那说明你的向量检索在增加延迟但没有提升召回率,可以考虑去掉。
关于 Sub-page Chunk 的注意点
如果每个 chunk 很小(比如 200 token),BM25 在这些小 chunk 上的信号会变得非常嘈杂,因为词频统计在小文本上不稳健。一个可行的方案是:在文档级别(page-level)做 BM25 索引,在 chunk 级别做向量索引,两者互补。这样 BM25 在完整的文档上做精确匹配(词频统计可靠),向量检索在细粒度 chunk 上做语义匹配。
验证方法
对比 Recall@50 和 MRR(Mean Reciprocal Rank)在纯向量检索和 Hybrid Search 之间的差异。如果 Hybrid Search 的 MRR 没有提升,说明 BM25 没有带来新的有效排序信息,这时需要检查一下:是 BM25 分数噪声太大,还是 k 值不适合你的数据规模。
排序——Reranker 不是万能药
Reranker 用 cross-encoder 架构对检索结果逐对打分,理论上比 embedding 的 bi-encoder 更精确。在大多数 RAG 架构中,reranker 是 Stage 1(廉价高召回)和 Stage 2(昂贵高精度)的分界点。
一个典型的 reranker 可以为 nDCG@10 带来 5-15 个百分点的提升。但前提是 Stage 1 的召回率足够高。
一个真实的翻车案例
有团队分享过这样的案例(dev.to/mossforge):花了 $400/月 购买 Cohere Rerank 的 API。上线后 nDCG@10 从 0.72 降到了 0.68,同时延迟增加了 250ms。
排查后发现根因很简单:Stage 1 的 Recall@50 只有 0.61。也就是说,100 个正确答案里,有 39 个根本没进入 reranker 的候选池。reranker 只是在一个已经漏掉大量正确结果的候选集上做了排序优化。
如果候选集本身漏掉了大量正确答案,reranker 再精确也无济于事。
他们后来做了两件事:把 chunking 从简单的按字符切分换成了 semantic chunking(200-500 token),Recall@50 从 0.61 跳到 0.78。然后 reranker 才开始发挥作用,nDCG@10 最终到了 0.87。而且因为不需要频繁调用 Cohere API,月度成本降到了 $30。
什么时候不该用 Reranker
一个经验法则是:如果 Recall@50 低于 0.8–0.9 这个区间,暂不建议优先考虑 reranker。先修上游。
这个范围不是拍脑袋的,而是基于一个普遍观察:当召回率明显偏低时,reranker 对最终结果的影响是震荡的,有时提升有时下降,且每次提升都会伴随一个"本应有但没召回"的失败案例。只有召回率接近 0.9 之后,reranker 的增益才是稳定正向的。具体拐点因数据集而异,建议在自己的数据上验证。
如果你确实需要 reranker,并且对成本敏感,开源的 BGE Reranker v2-m3 和 Qwen3 Reranker(都是 Apache 2.0 协议)在性价比上表现不错,在大多数场景下可以替代商业 API。
验证方法
对比加入 reranker 前后的 nDCG@10。但在测试之前,先确认 Recall@50 是否进入 0.8–0.9 的拐点区间。如果没有,先去优化上游,回来再测 reranker。
Query 改写——锦上添花,而非雪中送炭
Query 改写是 RAG 管线里的最后一个可调环节,也是投入产出比相对不确定的环节。
常见的改写技术按成本排序:
•HyDE(Hypothetical Document Embedding):让 LLM 根据 query 生成一段假设的答案文档,然后用这段文档做检索。成本是 1 次 LLM 调用。•Step-back Prompting:先让 LLM 生成一个更宽泛的 query(从"如何调优 RAG 的 chunking 参数"退到"RAG 调优的常用方法有哪些"),然后同时检索原始 query 和 step-back query。也是 1 次 LLM 调用。•Multi-query + RRF:生成 3-5 个不同角度的 query,分别检索后合并。成本是 3-5 次 LLM 调用。
HyDE 在短 query 和术语密集的技术场景下效果最好。比如用户只输入了"chunking 参数",但经过 HyDE 生成一段假设文档后,检索到的内容更相关。
但有一个来自实践的重要观察 :复杂的 query expansion 不一定比简单的 rephrasing 效果更好。 在某个案例中,团队尝试了多种复杂的 query 改写方案,最终发现简单的同义改写就足够了,复杂的方案并没有带来额外的提升。
同时,2026 年 3 月的一项行业研究发现(arXiv:2603.02153[1]),multi-query 生成 + RRF 的增益在经过 reranker 和截断后,在实际部署中几乎消失了。
先不加 query 改写,跑通基线。如果基线 MRR 已经不错,query 改写带来的收益有限。只有当你发现某些特定类型的 query 召回率系统性偏低(比如用户输入极短、关键词模糊),才值得有针对性地引入改写策略。
自适应触发
不要对每个 query 都做改写。当初始检索返回高置信度结果时(比如 top-1 的 cosine similarity 超过 0.9),直接返回即可。改写只在低置信度场景下触发。这能明显降低延迟和成本。
验证方法
对比加入 query 改写前后的 MRR,同时严格监控延迟预算。如果 MRR 提升不到 2%,就不值得为这个复杂度买单。
调优的本质
把前面所有讨论归结为三个原则。
第一,先修上游,再动下游。 Chunking 的问题不要用 embedding 来弥补,recall 的问题不要用 reranker 来掩盖。每个环节的指标就是它的健康检查:Recall@K 告诉你检索够不够好,nDCG 告诉你排序够不够好,MRR 告诉你 query 理解够不够好。
跳过这些检查直接往下游走,你只是在推迟问题而不是解决问题。当然,这是通用原则而非铁律——如果你有明确的指标指向某个下游环节才是瓶颈,直接修复它也没问题。
第二,测量,不要猜测。 每次改动都要有对应的指标来验证。换 chunking 策略、换 embedding 模型、加 hybrid search、加 reranker、加 query 改写,每个改动都要有 before/after 的数据。没有数据支撑的优化,很可能是在做无用功。
第三,简单方案往往胜过复杂方案。 Hybrid Search 权重分配往往是调优中最耗时的环节,但实际上,当上游的 chunking 和 embedding 都调对之后,一个简单的 BM25 + 向量检索 + RRF 就已经够用了。复杂方案增加的是理解成本和维护负担,而不是确定性。
RAG 调优不是找到一个"最优解"然后停下来。它更像是在搭建一个反馈循环,让你和你的系统每次都能比上一次做得更好一点。那个从 0.61 的 Recall@50 走到 0.87 的 nDCG@10 的团队,不是因为找到了某个神奇的参数组合,而是因为他们建立了一套发现问题、定位问题、验证修复的流程。
这个流程,比任何单个优化技巧都值钱。
- RAGAS: arXiv:2309.15217
- ARES: arXiv:2311.09476
- BEIR: arXiv:2104.08663
- UDCG: arXiv:2510.21440
- RAG Evaluation Survey: arXiv:2504.14891
- NVIDIA Chunking Benchmark: developer.nvidia.com/blog/findin…
- "I Spent $400/Month on a Reranker That Made My RAG Worse": dev.to/mossforge/i…
- Databricks Embedding Fine-tuning: databricks.com/blog/improv…
- MTEB Leaderboard: huggingface.co/spaces/mteb…