“用户说答得不对,我拿同一个问题复现,结果又是对的。”这是 RAG 上线后最常见的对话,也最多被误读——它通常不说明用户记错,而说明这次回答的正确性不由同一条 query 决定。检索受索引版本、时间过滤、并发写入、采样参数影响,两次调用可能走在两份证据上。
处理 RAG 失效的第一步不是换模型、调温度,而是把“答错”拆成四层可分别验证的判断。四层各有独立判据与修法;修错层是这类系统最大的浪费。
1. 四层失效与各自的判据
| 层次 | 用户看到的样子 | 判定方法 | 第一责任环节 |
|---|---|---|---|
| 没检索到 | 缺关键信息,或直接说“资料未提及” | 正确段落是否在未截断的候选集里 | 切分、索引、改写、过滤 |
| 检索到但没用上 | 回避了明明存在的证据 | 证据在 prompt 里,输出却绕开 | 排序、截断、上下文排布 |
| 用了但理解错 | 引用了证据,结论反了 | 证据与结论并列人工比对 | 冲突消解、时间/单位解析 |
| 理解了但表达错 | 结论对,粒度或格式不对 | 结论正确、形式不合格 | 提示约束、输出校验 |
判定的前提是中间结果可回放。没有它,四层只能靠猜。每次请求至少落一条:
{
"trace_id": "req-...",
"query_rewritten": ["…", "…"],
"retrieved": [{"chunk_id": "d41#3", "score": 0.71, "rank": 1}],
"sent_to_model": ["d41#3", "d41#4"],
"truncated": false,
"answer": "…",
"citations": ["d41#3"]
}
价值最高的是 retrieved 与 sent_to_model 的差集:为空却答错 → 生成侧问题;非空 → 问题在被丢掉的那段。
2. “没检索到”的四种成因
| 成因 | 典型信号 | 判定 | 改法方向 |
|---|---|---|---|
| 切分粒度 | 答案跨相邻两块,两半都不完整 | 按当前切分器重切 gold 段落,看是否被切断 | 语义边界切、父块配小块 |
| 同义词/口语 | 用户用词与文档用词不同 | query 换成文档原文措辞重查,命中即证实 | 混合检索、查询扩展 |
| 改写不足 | 多轮对话里指代丢失 | 用改写后的 query 重查是否命中 | 多 query 并行、指代补全 |
| 过滤过严 | 候选整体为空或极小 | 关闭 filter 重查,命中即证实 | 过滤降级为排序加权 |
四个成因的验证成本都很低,各只需一次对照。陷阱是只看 top-k:正确块排第 23 位被截掉,与“没被索引”表现一样,修法却相反。
# 召回诊断:按问题类型分桶,区分"没索引"与"排太后"
for q in evalset:
cands = retrieve(q.query, k=50) # 远大于线上 k
rank = first_index(cands, q.gold_chunk_ids)
bucket(q.type).add(rank) # None 记为未召回
# rank=None 占比高 → 索引/切分问题
# rank 集中在 6–30 → 排序与截断问题
# rank 集中在 1–3 仍答错 → 生成侧问题
3. “检索到但没用上”的三种成因
| 成因 | 机制 | 典型信号 | 对策 |
|---|---|---|---|
| 上下文过长截断 | 拼接超预算,尾部整块丢弃 | truncated=true 且丢的是 gold |
按分数排序后截断;父块按需展开 |
| 排序靠后 | 相关但分数低,排在无效块之后 | gold 在上下文中的位置 > 前列 | 先重排再拼;限制无效块数量 |
| 模型忽略中段 | 长上下文对中段注意力弱 | gold 居正中时命中率明显下降 | 关键证据前置/后置,长度压到必要范围 |
后两种常被混为一谈,判别靠位置对照:同一批证据只改位置。
A:gold 放第 1 段 → 答对
B:gold 仍放第 1 段,前面垫 8 段无关内容 → 答错
结论:不是"没检索到",是排布问题。修法是把证据前置或压缩无关块。
4. “理解错”的三种成因
| 成因 | 表现 | 判定 | 对策 |
|---|---|---|---|
| 多文档冲突 | 两份文档说法相反,模型随机选边 | 单独喂冲突文档,看是否稳定选同边 | 显式给出优先级(版本/生效日期/权威级) |
| 时间信息缺失 | 用已废止的旧规则回答 | 检索结果是否带有效期字段 | 时间作为元数据参与过滤 |
| 表格解析错 | 行列错位、合计行当数据行 | 原文表格与解析结果并排看 | 表格单独成块并保留表头 |
冲突处理的关键是不把裁决权交给模型:它在两句相反的话之间选择,行为接近随机。优先级应在检索阶段带上。
{
"doc_id": "policy-2023",
"effective_from": "2023-07-01",
"effective_to": "2025-06-30",
"supersedes": ["policy-2019"],
"authority": "primary"
}
5. “表达错”:约束要分层
| 约束 | 放在哪里 | 失效方式 |
|---|---|---|
| 语气、结构、是否引用 | 系统提示 | 上下文变长后被稀释 |
| 字段、枚举、必填 | 结构化输出 schema | 模型绕过 schema 时解析报错 |
| 事实一致性 | 生成后引用比对 | 无校验则幻觉直接出门 |
只靠提示词约束表达是最脆的一层:文字会被长上下文稀释,schema 可机器校验,引用比对才抓得住“说得通但没依据”。
6. 否定型与多跳为什么特别难
| 问题类型 | 难点 | 表现 | 缓解 |
|---|---|---|---|
| 否定型(“X 是否不需要 Y”) | 相似度不度量命题真假,否定证据往往不存在 | 检索到一堆讲 Y 的文档,答成“需要” | 转枚举确认:取全集再逐条排除 |
| 多跳(A 的负责人所在部门) | 中间结果要当下一跳的 query | 只做一跳,答案看似合理实则错 | 拆子问题、显式中间结果、串行编排 |
“文档里没有”与“文档里说不需要”是两件事,相似度只度量接近程度,区分不了。否定型问题必须换范式。
7. 空结果与低分:明说不知道的判据
| 候选情况 | 建议动作 | 理由 |
|---|---|---|
| 无任何候选 | 明说没有相关资料,给出可查范围 | 强行作答的期望损失更大 |
| 有候选但最高分很低 | 先看覆盖:能否覆盖问题主实体 | 分数跨 query 不可比,覆盖可判 |
| 覆盖但相互冲突 | 并列呈现,标注来源与时间 | 替用户裁决等于隐藏不确定性 |
| 候选充分 | 作答并给引用 | — |
if candidates empty: → 明说缺失 + 建议查询路径
elif not covers(candidates, q): → 只答覆盖部分
elif conflicting(candidates): → 并列 + 标注生效日期
else: → 作答 + 引用
阈值别设绝对值:分数分布随 query 变化,“0.72 算不算高”没有全局答案。用覆盖度更稳。
8. 可观测信号
| 信号 | 定义 | 指向的层次 |
|---|---|---|
| 追问率 | 同会话内对同一问题连续追问 | 表达错、理解错 |
| 重述率 | 用户换措辞重问同一意图 | 没检索到(措辞不匹配) |
| 人工修正率 | 客服/运营改写答案的比例 | 全层,最可靠的坏味道 |
| 空结果率 | 判定为“无资料”的比例 | 偏高 → 索引覆盖问题 |
人工修正率是唯一同时覆盖四层的信号,代价是要有人真的在改。它上升时先查检索日志。
9. 评测集必须包含的四类负例
| 负例类型 | 构造方式 | 检验的能力 |
|---|---|---|
| 文档没有 | 问题超出语料范围 | 是否敢说不知道 |
| 文档冲突 | 同主题两份相反说法 | 是否按优先级裁决并标注 |
| 部分匹配 | 只覆盖问题的一半 | 是否只答覆盖部分 |
| 过时答案 | 旧版本仍可检索到 | 是否按生效日期过滤 |
只测“能答对的题”,测的是理想条件下的上限;线上问题绝大多数落在上表四类里。
10. 改一处的耦合
| 改动 | 预期收益 | 引入的失效 |
|---|---|---|
| 增大 top-k | 召回上升 | 上下文变长 → 中段忽略、截断丢尾 |
| 切小块 | 定位更准 | 跨块信息断裂 → 多跳更难 |
| 强过滤 | 噪声下降 | 边界样本误杀 → 空结果率上升 |
| 强化“必须引用” | 幻觉下降 | 无引用可用时编引用 |
| 加重排 | 排序更准 | 延迟上升、吞吐下降 |
这是基本约束:没有单向优化。调参前先写下“我要牺牲哪一项”。
11. 定位流程:先判层,再动环节
1 取一条失败样本,固定索引快照 + 温度 0,确认可复现
2 gold 在 top-50 内? 否 → 第 2 节(切分/同义词/改写/过滤)
3 在 sent_to_model 里? 否 → 第 3 节(截断/排序/预算)
4 在里面但答案绕开它? 是 → 位置对照实验 → 排布
5 引用了证据但结论反了? 是 → 第 4 节(冲突/时间/表格)
6 结论对、形式错 → 第 5 节(约束分层)
7 只有走到第 6 步才允许改提示词
顺序的意义在成本:前四步只读日志和做对照,第 6 步起才是改提示、改模型、重跑评测。
12. 排错:六种“像模型问题”的情况
| 现象 | 原因 | 处理 |
|---|---|---|
| 同一问题两次答案不同 | 索引在写入,或温度 > 0 | 固定快照复现,温度 0 再判 |
| 回答总是很笼统 | top-k 里全是概述块 | 检查切分是否把细节并入大段 |
| 答案对但引用错 | 引用由模型生成而非回填 | 引用从检索结果回填 |
| 中文专名检索不到 | 分词/嵌入对生僻词不敏感 | 补关键词通道,混合召回 |
| 长文档问答答非所问 | 上下文超预算被截断 | 先压缩再拼,别靠加预算 |
| 上线后质量缓慢下滑 | 语料在增,索引未重建 | 把索引版本纳入发布流程 |
13. 边界:什么时候 RAG 本就不该用
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 知识本身不在语料里 | 补数据源,或交给人 | 检索变不出不存在的事实 |
| 问题域极窄、答案固定 | 查表或写死规则 | 一次查表胜过一条链路 |
| 需要精确计算或严格事务 | 调用工具/接口 | 生成模型不保证数值正确 |
| 要求逐字引用、可审计 | 抽取式回答 + 人工签发 | 生成式改写不可审计 |
RAG 的价值是把“模型知道什么”换成“这次给了它什么”。当“给了什么”本身不可控,它能做的只是如实暴露。
14. 自测
- 用户说答错了,你复现却是对的——第一个要固定的变量是什么?
- 正确段落在 top-50 里但不在送进模型的上下文里,属于哪一层?
- 同一批证据只改位置就改变了对错,说明问题在哪一层?
- 为什么否定型问题不能靠提高 top-k 解决?
- 分数阈值为什么不适合跨 query 直接比较?替代判据是什么?
- 把 top-k 从 5 调到 20,最可能同时引入哪两种新失效?