跳到主要内容

RAG 召回摘要问题排查

🔍 “只召回摘要/标题”不是单一故障,而是索引内容、检索粒度、返回字段、上下文压缩四个环节之一改变了原文。排查时不要先调模型或 topK,应先沿数据流逐层打印证据。

先建立正确的数据流

原始文档 → 解析结果 → 原文 chunk → embedding / 稀疏索引 → 候选集 → 重排 → 最终上下文 → 生成答案

每一层都抽样记录 doc_idchunk_id、正文前 200 字和字符区间。在哪一层正文首次变成标题或摘要,问题就在哪一层。

30 分钟最小诊断法

  1. 选 3 个答案明确、可定位到原文的问题。
  2. 对每个问题保存召回前 20 条候选,不要只看最终 5 条。
  3. 分别检查 indexed_textreturned_textprompt_context
  4. 临时关闭重排、压缩、查询改写与摘要链,一次只恢复一个组件。
  5. 记录 Recall@K:正确证据是否进入候选集;若进入但没被使用,再检查重排和生成。

1) 先确认:你到底把什么写进了向量库?

最常见情况:embedding 的输入用的是 summary / title,或者只把摘要字段入库了,所以召回结果当然也只像“摘要”。

  • 检查 embedding 代码:embedding 的 text_to_embed 是否来自摘要字段?
  • 检查入库字段:向量库里每条记录是否有 content_chunk(原文片段)以及 doc_id / source / offset 等元信息?
  • 快速验证:随便抽一条向量库记录,看 text/payload 是否就是摘要。

修复原则

  • embedding 必须来自原文 chunk(摘要可以作为 metadata,或辅助 BM25/重排特征,但不要替代 chunk 原文)。
  • 召回返回的 text 应该是可直接拼进 prompt 的原文片段,不是 summary。

2) Chunk 策略:别把 chunk 做成“自动摘要”

如果你在切分时做了“每段先总结成一句话再入库”,那召回自然像摘要。

推荐做法(经验值):

  • chunk size:中文 300–800 字(或 500–1500 tokens)
  • overlap:10%–20%(例如 80–150 字)
  • chunk 里保留:原文 + 小量结构(标题层级/小节名)

另外,务必把这些写进 metadata:

  • document_title / section_title / heading_path
  • chunk_index
  • source_url
  • start_char/end_char 或页码

这样召回后你能“回填上下文”,不至于只靠一段话。

3) 召回时返回字段:不要只取 summary 字段

很多人 query 时只取了 summary(比如为了省 token),导致看起来“召回都是摘要”。

  • 检查 query 返回:是否有 chunk_text?还是只取了 summary
  • 如果你做了两段式:先召回 doc,再取 doc.summary ——那你需要在第二段再召回 doc 下的 chunks,或者直接召回 chunks。

正确链路(常见可行):

  1. query → topK chunks(向量/BM25/混合)
  2. 可选:cross-encoder rerank top 50 → top 5–10
  3. 把 top chunks 原文拼 prompt(带来源)

4) 你看到“像摘要”,也可能是重排/压缩器在作祟

如果你用了:

  • LLM-based rerank
  • contextual compression(压缩检索结果)
  • “回答前先总结证据”的链

这些会把 chunk 压成摘要再喂给下游。

排查方法:

  • 在进入 LLM 前,打印/日志输出最终 prompt 里的“证据块”到底是什么。
  • 关掉压缩器,直接把原文 chunk 喂给生成,看是否恢复。

5) 混合检索 + 重排,能显著减少“泛泛摘要”

如果你的语料结构化强、标题多,纯向量容易召回“概述性段落”。

建议:

  • Hybrid:BM25(关键词) + 向量(语义)一起召回,再合并去重
  • Rerank:cross-encoder(例如 bge-reranker、cohere rerank 等)对 top 50 做重排
  • MMR:让 topK 更“多样”,减少都落在概述段落

6) 如果你确实需要摘要:让摘要作为“补充视图”,不是召回主体

可以在每个 chunk 或 doc 旁边存:

  • doc_summary(文档级)
  • chunk_summary(可选,用于浏览/展示) 但回答时:
  • 证据引用:用 chunk_text
  • 用户展示:可以额外显示 summary 作为“预览”

建议的日志结构

{
"query": "原始问题",
"rewritten_queries": ["改写后的问题"],
"candidates": [
{
"chunk_id": "doc-7#p3#c2",
"dense_score": 0.72,
"bm25_score": 8.4,
"rerank_score": 0.91,
"text_preview": "原文前 200 字"
}
],
"selected_chunk_ids": ["doc-7#p3#c2"],
"prompt_context_chars": 4830
}

线上不要默认记录完整敏感正文,可保存脱敏预览、哈希和原文引用。

用指标定位,而不是凭感觉

  • Recall@K 低:索引、query、过滤或召回有问题。
  • Recall@K 高但 MRR 低:需要融合或重排。
  • 排名正常但 Context Precision 低:候选噪声过多,组装策略有问题。
  • 证据充分但 Faithfulness 低:生成 Prompt、引用约束或模型有问题。

易被忽略的原因

  • metadata 过滤条件过严,正确文档在召回前已被排除。
  • 文档解析失败,只保存了目录、页眉或 OCR 摘要。
  • parent-child retrieval 先找到父摘要,但没有回填子 chunk。
  • topK 按“文档”去重后只剩每篇文档的标题块。
  • 上下文窗口预算按字符错误估算,原文在尾部被截断。
  • reranker 输入长度限制导致正文被截掉,只看到了标题。
  • 索引版本与查询服务版本不一致。

回归检查清单

  • 随机抽样索引记录,确认是原文而非摘要
  • 正确证据在 Top-20 中可见
  • 返回字段与 Prompt 使用字段一致
  • 标题只作为上下文,不替代正文
  • 关闭每个增强组件后有对照结果
  • 记录 Recall@K、MRR、Context Precision 和 Faithfulness
  • 每个线上失败问题进入固定测试集