你的 RAG 检索到底在哪个环节掉了链子?

RAG 系统 debug 比传统后端难得多——问题藏在切片、召回、重排、生成四个环节的任意一个里,而且每个环节都可能以反直觉的方式失败。这篇文章拆解最常见的检索失败模式,并给出一个可落地的观测与调试方案。

上周调一个 RAG 系统的 bug,查了三个小时。

现象:用户问"项目延期了怎么处理",系统返回了一个看起来很合理的答案,但引用的文档段落跟问题毫不相关——它引用了"项目立项流程"里的内容来回答延期问题。

第一反应是 embedding 模型不行。换了榜单上更高的模型,重跑一轮,结果一样。

第二反应是切片策略有问题。改了 chunk 大小和 overlap,重新索引,召回到是召回来了——但召回来的 Top5 没一个能用。

第三反应才找到根因:query embedding 和 doc embedding 之间存在系统性偏差——用户用的是口语化查询("延期了怎么办"),而文档中是正式表述("项目进度异常的应对措施")。向量距离足够远,正确的那一段排在第六位,被 top-k=5 截掉了。

但真正让我难受的不是这个 bug 本身,而是花了三个小时才定位到它。

RAG 系统的问题是:你看到的只有最终答案。如果答案错了,你无从判断是切片切坏了、检索没找到、rerank 排错了、还是 LLM 自己脑补了。每一步都是黑盒,每一步都可能出问题,但你没有任何中间指标告诉你哪一步出了问题。

这不是个例。在三个不同的 RAG 项目(包括正在做的一个多 Agent 知识库系统)里,我反复踩到同一类坑——检索质量看起来"还行",但就是差那么一口气。而每次调优都靠盲猜,效率极低。

后来想明白了:RAG 调试不能靠"改一个参数试试看效果"。需要一套结构化的方法,把检索链路拆成可观测的环节,每个环节都有明确的信号告诉你它好还是不好。

这篇文章就讲这套方法。

第一个误判:你以为问题出在 LLM,其实出在切片

几乎所有 RAG 性能问题的起点都在切片。

我见过一个团队,面对一份 50 页的技术规范文档,用了固定 512 token 的 RecursiveCharacterTextSplitter。结果呢?一段关于"部署环境要求"的内容被切成了两块——前半段讲硬件配置,后半段讲软件依赖。用户问"需要什么操作系统",系统只召回了前半段,回答里只提了 CPU 和内存,操作系统信息丢了。

这不是 embedding 的问题。是把一句话切成了两半,语义完整性被破坏了。

切片策略有三个常见陷阱:

陷阱一:固定长度切片。 512 token 是教程里最常见的默认值,但它不适用于任何真实文档。表格、代码块、列表——这些结构单元的长度差异巨大。固定长度对表格尤其致命:表头和数据行可能被分到不同的 chunk 里,检索时只有表头没有数据。

陷阱二:结构性信息丢失。 你把段落切出来,但没保留它属于哪个章节、哪个文档。检索返回的 chunk 是一段孤立文本,LLM 不知道它是"部署指南"里的内容还是"API 参考"里的内容。没有上下文锚点的 chunk,召回率天然低 20% 以上。

陷阱三:overlap 被当成万能药。 很多人把 chunk_overlap 调到 15-20% 就觉得够了。但 overlap 只是让边界处的几个 token 重复出现,解决不了"一个表格被切成两半"这类问题——表头和第一行数据之间可能隔了几十个 token,overlap 覆盖不到。

正确的做法是按文档结构切。Markdown 文档按标题层级切,PDF 按页面和段落边界切,代码按函数粒度切。对每个 chunk 保留它的章节标题、父文档 ID、层级深度作为 metadata。这样检索时可以用这些 metadata 做后过滤,生成的回答也能附带引用来源。

我用的一套基线参数:按结构边界切,chunk 大小 800 token(对技术文档),chunk 之间保留 10% overlap。每个 chunk 附带父章节标题和文档 ID 作为 metadata。在这之上做一层 parent-child 索引——小 chunk 用于检索,命中后展开到父 chunk 送入 LLM。

第二个误判:你觉得向量召回"差不多",其实差很多

很多人觉得向量检索是 RAG 的"已经解决的问题"。选个 embedding 模型,存到向量库,跑 similarity search——不就完了吗?

远远不够。

向量检索有两个系统性问题,不是调模型参数能解决的。

一个叫 query-doc 漂移。用户查询的语言风格和文档语料不一致是常态。用户说"上个月的项目怎么还没做完",文档里写的是"项目实施工期评估与管理规范"。这两者的 embedding 距离天然就远。你在评测集上看到 recall@5 有 85%,但那是用和文档风格相似的 query 测出来的。真实用户的问法五花八门,recall 掉到 60% 以下一点都不奇怪。

另一个叫语义近邻噪音。embedding 模型擅长找"语义相似"的内容,但语义相似不代表答案就在那里。用户问"退款流程",文档里有"退款流程"、"退货政策"、"售后处理流程"——三者在向量空间里靠得很近,但只有第一个包含退款步骤。如果你只做向量召回,Top5 里大概率混进两三个相关但不精确的片段。

解决方案是混合检索。向量检索做语义层面的召回,BM25 做关键词层面的召回,两者互补。实践中权重设在 6:4 左右效果最好——因为大多数 query 的精确信息(产品名、条款编号、日期)只能靠 BM25 命中。

def hybrid_retrieve(query, alpha=0.6):
    vec_results = vector_search(query, k=20)
    bm25_results = bm25_search(query, k=20)

    fused = {}
    for doc, score in vec_results:
        fused[doc.id] = alpha * score
    for doc, score in bm25_results:
        fused[doc.id] = fused.get(doc.id, 0) + (1 - alpha) * score

    return sorted(fused.items(), key=lambda x: x[1], reverse=True)[:5]

这是投入产出比最高的优化之一。代码量不到 30 行,recall 提升 15-30%。

第三个误判:你以为 top-k 够了,其实需要一个 reranker

加了混合检索之后,Top20 的召回率基本能到 80% 以上。但问题变了——Top20 里可能有 8 个有用,12 个是噪音。你只取 Top5 送给 LLM,但 Top5 里可能只混进了 2 个真正相关的,其他 3 个是"语义相近但答案不对"的干扰项。

向量检索和 BM25 本质上都是粗排。它们的 score 只能告诉你"这个词跟 query 有点像",做不到细粒度的相关性判断。

这个差距需要一个 reranker 来补。

reranker(通常是 Cross-Encoder)把 query 和每个候选文档拼在一起,用一个 transformer 模型做精确匹配。这个过程比向量检索慢一个数量级(单次推理 15-80ms vs 向量检索的 3-5ms),但它能区分"这篇文章里提到了退款"和"这篇文章的第一段就是退款操作步骤"——后者才是真正的答案来源。

两步走的方案:先用向量+BM25 混合检索召回 Top20(百毫秒级),再用 reranker 从 Top20 里精排选出 Top5(百毫秒级)。总耗时 200ms 以内,但答案精确率能从 40% 跳到 75% 以上。

Reranker 的选型有两档。追求质量用 bge-reranker-v2-m3(300ms,准确率高),追求速度用 bge-reranker-v2-base(80ms,质量略低)。线上场景我倾向 base 版——80ms 的代价换接近一倍的精确率提升,这笔买卖划算。

真正的解法:把 RAG 链路变成可观测的

前面三个问题各自有一个对应的解决方案。但最根本的问题不是"怎么修",而是"怎么知道你该修哪个"。

RAG 系统的调试困境在于:你看到的只有最终答案,但答案的质量取决于四个环节——切片、检索、rerank、生成。每个环节出问题,最终答案都会变差。但它们的"变差"在表象上几乎一样——都是"回答不准确"。

所以需要给每个环节安一个"仪表盘",让你能一眼看出哪一步偏了。

我用了四组指标:

切片质量: chunk 的平均长度、长度标准差、语义完整性比例(用启发式规则判断,例如 chunk 是否以句号/段落结束)。理想范围:平均长度 400-800 token,标准差不超过平均长度的 50%,语义完整性 90%+。

检索质量: recall@k(人工标注评测集上算)、query-doc embedding 距离分布。离线算,每次切片策略或 embedding 模型变更后跑一轮。recall@5 低于 70% 说明检索层有问题。

rerank 质量: MRR(Mean Reciprocal Rank)、NDCG@5。同样离线算。MRR 低于 0.7 说明 reranker 选型可能有问题。

生成质量: faithfulness score(答案在原文中的可追溯比例)、answer relevancy。用 Ragas 或者自建规则算。faithfulness 低于 0.8 说明 LLM 在编内容。

这四个指标跑一遍,你就能判断当前的问题出在哪个环节。recall@5 低 → 修检索。MRR 低 → 换 reranker。faithfulness 低 → 修 prompt 或加事实校验。

这套方法的关键不是工具,是习惯——每次改动一个参数前,先跑一轮各环节的指标。改完后重跑,看哪个指标变化了。只有这样才能积累出"什么参数在什么场景下有效"的经验,而不是每次靠蒙。

最后说一句

RAG 不是"检索 + LLM = 答案"这么简单。它是一条精密但脆弱的链路,链路上的每个环节都可能成为瓶颈。不对这条链路做可观测性改造,你永远不知道你的 RAG 系统到底在哪个环节掉了链子。

而调了三个月 RAG 之后,我得出的结论是:大部分 RAG 项目的问题根本不是 LLM 能力不够,是工程上从来没有认真观测过检索质量。把这件事做了,效果提升比你换一个更大的模型更明显,成本也更低。

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个