调试 RAG 时,一个很容易误导人的场景是:用户提了一个很具体的问题,检索召回的内容和问题并不匹配,但模型仍然给出了语言流畅、结构完整的回答。
如果只盯着最终输出,这个 case 看起来是成功的。但把召回片段单独拿出来对照就会发现,答案所依赖的内容根本没有出现在被检索到的证据里。“流畅”只是语言模型具备补全能力,并不代表检索层提供了正确证据。
所以在 RAG 排障里,第一原则是:先不要用最终回答判断检索质量。
先把检索结果单独拿出来评估
要判断问题是否出在检索层,最直接的办法是把答案生成临时拿掉。对一个 query 打印 top-k 召回结果,人工检查这几个问题:
- 正确内容是否出现在候选中?
- 它排在第几位?
- 如果有多个事实需要支撑,相关片段是否都进入了 top-k?
一次人工抽查只能发现问题,不能形成回归能力。建议顺手建一个很小的检索评估集:
- 从知识库中选择 20~50 条有代表性的查询;
- 为每条查询标注“哪一段文档才是正确答案的来源”;
- 对每条查询执行向量检索,看正确片段是否落入 top-k。
用不着复杂的指标,命中率就已经有足够的诊断价值:
- Hit@k:top-k 结果中是否包含正确 chunk;
- Recall@k:需要被召回的多段内容中,有多少段出现在 top-k。
如果 Hit@k 很低,说明正确内容根本没有进入候选。此时调整 prompt 或生成参数没有意义,问题出在检索上游。这个判断会直接影响后续排查方向。
def hit_at_k(query, gold_chunk_id, retriever, k=5):
result_ids = [r.chunk_id for r in retriever.search(query, top_k=k)]
return int(gold_chunk_id in result_ids)
chunk 切割是最值得优先排查的上游变量
chunk 切得不对,embedding 再好也很难召回到正确内容。因为向量检索是在 chunk 整体上计算相似度,而不是在整篇文档上。
固定字符长度切割最容易出问题。chunk 过小时,一个完整语义可能被拆到两个块中,query 命中的内容只包含残缺的上下文;chunk 过大时,一个块的内容太杂,embedding 会把全篇语义平均掉,导致块与某个窄 query 的相似度被稀释。
更隐蔽的是切割点和语义边界错位。例如切在一个段落的中间,或者把一个代码示例和它的说明文档分开。这些被硬切出来的块,即使被检索命中,也无法独立支撑回答。
调整方向可以从下面几点入手:
- 优先按文档结构切。标题、段落、表格、代码块都是值得保留的语义边界。固定长度切割可以作为兜底,但不应作为首选。
- 一个 chunk 尽量只包含一个可独立理解的主题。如果单节内容过长,在小节边界处继续切分,避免在句子中间断开。
- 重叠度仅用于弥补边界问题。例如让相邻 chunk 重叠少量上下文,可以避免切在关键信息中间。但重叠会带来重复召回,同一个来源可能在 top-k 里占多个位置,需要考虑去重。
这里不存在一组“最优 chunk_size 和 overlap”。它们要匹配你的文档长度、查询粒度、embedding 模型能力。正确做法是把切割参数做成配置,用上面的小评估集测几组组合,而不是让代码里硬编码一个固定值。
阈值调节要建立在分数分布上
很多人会直接给 embedding 相似度设置一个固定阈值,比如低于某个分数就不返回。这个做法本身没有错,但阈值不能随便拍。
不同 embedding 模型、不同文本规范化方式、不同相似度算法(cosine、dot product)产生的分数分布不同。别人博客里可用的 0.7,在另一个模型上可能完全不适合。阈值调太高会把相关样本误伤,调太低又会让大量噪声进入生成上下文。
更稳妥的做法是先把 top-k 和阈值拆开:
- top-k 决定“正确 chunk 有没有资格进入候选”;
- 阈值决定“没有任何内容明显相关时,要不要返回空结果”。
调阈值之前,先打一批真实 query 的分数分布,观察相关 chunk 和不相关 chunk 的得分区间是否明显分开。
- 如果两个分布能分开,可以在分界处选阈值;
- 如果两个分布严重重叠,阈值调来调去解决不了根因,问题可能仍在 chunk 粒度或检索策略上。
很多 RAG 项目会跳过这个观察,直接凭一次对话效果固定阈值。这是造成“检索结果与问题不相关,却仍然被送进上下文”的常见原因。
不要忽略生成阶段的兜底
即使检索层已经调好,也建议在生成阶段加一层限制。比如在 prompt 中要求模型:只有在检索证据足以回答问题时才回答;证据不足时直接说明未找到相关内容,而不是用自己的知识补全。还可以要求回答中标注出所依据的 chunk 编号或引用来源。
这层兜底解决的是“证据缺失时不要编造”的问题,但无法解决“错误证据进入上下文”的问题。因此不能把 prompt 调优当成检索修复的替代品。
最小排障流程
如果现在遇到的正是“回答流畅但检索结果不相关”,可以按下面的步骤收窄问题:
- 固定 embedding 与生成模型,保证每次只改一个变量。
- 对一小组测试问题执行检索-only 评估,记录 Hit@k 和 Recall@k。
- 对失败样本检查理想来源 chunk 是否被“切坏”了,比如内容被截断、标题丢失、跨段落拆分。
- 修改 chunk 切割方式后重新跑同样的评估集,对比命中率变化,而不是只靠人工看一两个例子。
- 命中率稳定后再调阈值与 top-k,判断依据是相关与不相关的分数分布,而不是个人感觉。
- 最后检查包含正确答案的片段是否真的进入了模型上下文。如果进入了但回答仍然不对,再去调整生成策略。
在 RAG 系统里,回答流畅属于语言模型的特性,信息是否有据才是系统可靠性的信号。与其反复猜测 prompt 哪里不对,不如先把“检索命中率”变成一个可回归的指标。chunk 切割负责让正确内容能被单独召回到,阈值与 top-k 负责让正确内容能进入生成器。这两层得到验证后,RAG 的可靠性才谈得上可度量。