No.342
RAG 效果不好,怎么一步步排查?
一句话回答
先别急着换模型或调参数,拿几个具体的失败案例,沿着链路逐段看中间结果:知识库里有没有答案 → 解析出来的文本对不对 → 切块后是否完整 → 召回结果里有没有正确的片段 → 重排后它排第几 → 进入 Prompt 的上下文是什么 → 模型有没有忠实地使用它。找到第一个出错的环节再改,改完用评测集确认没有让其他问题变差。
详细解析
准备工作
- 能看到中间结果:每次请求都记录改写后的查询、召回列表(ID、分数、排名)、重排分数、最终的 Prompt 和回答(见 链路追踪)。没有这些,排查只能靠猜
- 收集一批失败案例:从用户点踩、人工抽检里收集,按现象分类,不要只看一个例子就下结论
- 确认标准答案:对每个失败案例,人工找出知识库里能回答它的文档和原文片段。如果根本没有,那是知识库缺内容,不是检索的问题
沿链路逐段检查
- 入库:文档有没有同步进来、是不是最新版本、有没有被权限或元数据过滤掉
- 解析:直接看解析后的纯文本。PDF 多栏排版的顺序错乱、表格变成一堆对不上的数字、页眉页脚混进正文、扫描件没做 OCR 导致内容为空,都很常见
- 切分:找到包含答案的块,看它是否完整:答案有没有被切到两块里,块里是不是只剩"该功能"这类没有主语的描述,表头和数据有没有被分开
- 召回:看召回的前几十条里有没有包含答案的块。没有的话,用答案的原文本身去检索:能找到,说明问题和文档的表达差异大,考虑查询改写、混合检索;还是找不到,就检查 Embedding 模型和索引。有的 Embedding 模型要求查询和文档加上不同的前缀或指令,漏加也会影响效果
- 重排:召回里有,但重排后没进前 K 条,可能是重排模型不适合这个领域或语言、块太长被截断,或者 K 太小
- 上下文组装:最终放进 Prompt 的是不是重排后的前 K 条,有没有因为 token 预算被截掉;有没有新旧版本互相矛盾的资料同时出现
- 生成:上下文里明明有答案,模型却答错了。可能是 Prompt 没要求只根据资料回答、没允许说"不知道";上下文太长,答案在中间被忽略(见 上下文窗口);问题需要综合多块信息,模型的推理能力不够;输出被长度上限截断
常见现象和原因
| 现象 | 常见原因 | 先查什么 |
|---|---|---|
| 回答"没有找到相关信息",但知识库里明明有 | 召回没找到:表达差异大、专有名词、过滤条件太严、文档没入库 | 召回结果;用原文检索 |
| 问型号、编号,回答的是相近的另一个 | 纯向量检索对精确词不敏感 | 加关键词检索(见 混合检索) |
| 多轮对话里一追问就答偏 | 用带指代的原句去检索 | 改写日志(见 查询改写) |
| 表格类问题数字答错 | 表格解析错乱,或者表头和数据被切开 | 解析后的文本、切块 |
| 新旧政策混在一起回答 | 旧版本文档没下线,新旧块同时被召回 | 文档版本、更新时间的元数据 |
| 回答的内容在资料里找不到 | Prompt 没有约束,资料不足时模型自由发挥 | 忠实度评分、Prompt |
| 简单问题对,比较类、汇总类问题错 | 一次检索找不全多个实体的信息 | 子问题拆分;汇总类问题见 GraphRAG |
| 只在某个语言或领域效果差 | Embedding 模型不适合这个语言或领域 | 在评测集上对比几个模型 |
修完之后
把排查过的失败案例加进评测集。每次只改一个变量,跑完整的评测集,和改动前逐条对比,重点看有没有原来对、现在错的样本(见 RAG 系统怎么评估)。
代码示例
把前几步写成脚本,对每个失败案例自动定位第一个出问题的环节。chunkStore、hybridSearch、rerank 是项目里的检索函数,过滤条件要和出错的那次请求一致:
TypeScript
async function diagnose(c: { question: string; docId: string; evidence: string }, topK = 5) {
const norm = (s: string) => s.replace(/\s+/g, '') // 忽略解析带来的空白差异
const hasEvidence = (text: string) => norm(text).includes(norm(c.evidence))
const docChunks = await chunkStore.listByDoc(c.docId)
if (docChunks.length === 0) return '入库:文档没有被索引'
if (!docChunks.some((ch) => hasEvidence(ch.text))) return '解析或切分:没有一块包含完整的证据原文'
const recalled = await hybridSearch(c.question, 50)
const recallRank = recalled.findIndex((ch) => hasEvidence(ch.text)) + 1
if (recallRank === 0) {
const byEvidence = await hybridSearch(c.evidence, 50) // 用证据原文本身去检索
return byEvidence.some((ch) => hasEvidence(ch.text))
? '召回:问题和文档的表达差异大,考虑查询改写、混合检索'
: '索引:用原文都检索不到,检查 Embedding 模型和索引配置'
}
const reranked = await rerank(c.question, recalled)
const finalRank = reranked.slice(0, topK).findIndex((ch) => hasEvidence(ch.text)) + 1
if (finalRank === 0) return `重排:召回时排第 ${recallRank},重排后没进前 ${topK}`
return `检索正常(最终排第 ${finalRank}):检查上下文组装、Prompt 和生成`
}
面试官可能追问
换一个更强的模型,能解决问题吗?
只能解决最后一环的问题。如果正确的资料根本没进上下文,换多强的模型也答不对,反而可能"编"得更像真的。所以要先确认检索没问题,再考虑换生成模型。
检索结果里有正确答案,模型还是答错了,怎么办?
减少噪声:降低 K、提高重排阈值,去掉不相关和重复的块;把最相关的块放在开头或结尾;Prompt 里要求先找出相关的原文、再基于原文回答;检查是否有互相矛盾的资料。问题本身需要多步推理时,再考虑换更强的模型。
怎么判断是不是 Embedding 模型的问题?
只换 Embedding 模型、其他保持不变,在评测集上对比几个候选模型的 Recall@k。同时检查几项容易忽略的配置:模型是否支持中文和领域术语,查询和文档是否按模型要求加了前缀,文本是否超过模型的最大输入长度被截断,换模型后是否重新向量化了全部文档。
易错点
- 一上来就换模型、调参数,不看中间结果
- 忽略解析环节,大量问题其实出在 PDF 解析和表格上
- 一次改多个变量,改完不跑评测集,不知道是哪个改动起了作用
- 修好眼前的案例就上线,没检查其他案例有没有变差
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。