代码语言

知识点思维导图

26 个知识节点

RAG(10) - RAG 生产排障:坏案例与修复手册

读完后,你应能完成以下任务:

  • 绘制“RAG(10) - RAG 生产排障:坏案例与修复手册 / 先保存一条请求的完整证据链”的关键对象与数据流,解释“隐私字段和密钥不能进入 Trace;”,并用源码位置、日志或 Trace 标注证据。
  • 为“RAG(10) - RAG 生产排障:坏案例与修复手册 / 第一问:正确证据在哪一层丢了”设计正常与异常输入,验证“原文是否解析出来。 -> 正确段落是否形成完整 Chunk。 -> Chunk 是否写入正确索引版本。 -> 权限条件下是否应该可见。”,输出首个偏差位置与回归测试结果。
  • 实现“RAG(10) - RAG 生产排障:坏案例与修复手册 / 十二类高频坏案例”的最小代码或配置,检验“检查:去重是否使用稳定 chunk_id;”,输出命令、结果与 Diff,并说明不适用边界。

更新日期:2026/08/11

flowchart LR
    T[失败 Trace] --> P{原文解析正确?}
    P -->|否| P1[修解析/OCR]
    P -->|是| C{Chunk 完整?}
    C -->|否| C1[修切分]
    C -->|是| R{召回 Top K 命中?}
    R -->|否| R1[查索引/权限/Embedding/BM25]
    R -->|是| K{Rerank/Context 保留?}
    K -->|否| K1[修融合与预算]
    K -->|是| G[查生成与引用校验]

一、先保存一条请求的完整证据链

没有 Trace 的坏案例无法复现。每次请求至少记录:

  • trace_id、租户/权限摘要、问题原文和改写问题。
  • 索引版本、Embedding/Rerank/生成模型版本、Prompt 版本。
  • 每路候选的 chunk_id、原始分数、排名、过滤原因和耗时。
  • 融合与 Rerank 后的顺序、最终 Context 和引用映射。
  • 结构化回答、校验结果、Token、费用和总延迟。

隐私字段和密钥不能进入 Trace;正文可按策略脱敏或只保存稳定哈希与受控回放引用。

二、第一问:正确证据在哪一层丢了

按漏斗定位:

  1. 原文是否解析出来。
  2. 正确段落是否形成完整 Chunk。
  3. Chunk 是否写入正确索引版本。
  4. 权限条件下是否应该可见。
  5. 是否进入任一召回路 Top K。
  6. 是否在 RRF、去重或 Rerank 后掉出。
  7. 是否被 Context Packing 截掉。
  8. 已进入 Context 时,模型是否正确引用和回答。

前一层没有证据,修改后一层 Prompt 不会解决根因。

三、十二类高频坏案例

3.1 PDF 内容顺序错乱

现象:答案把双栏 PDF 左右两列拼在一起。

检查:渲染原页,对照解析块的坐标、阅读顺序和页码。

修复:使用布局感知解析;按坐标重建段落;扫描件走 OCR;给表格和正文使用不同解析策略。

3.2 Chunk 切断关键条件

现象:召回“可以退款”,却丢了下一句“定制商品除外”。

检查:查看命中块的前后邻居和标题路径。

修复:按条款/段落切分;使用父子块;命中子块后扩展相邻块;用条件完整性评测而不只看字符长度。

3.3 Chunk 太大导致主题稀释

现象:一整章向量只有宽泛主题,细节问题排不到前面。

修复:小块召回、父块补上下文;按标题层级切;比较不同粒度的 Recall@K 与引用定位精度。

3.4 Embedding 不一致

现象:重建后相似度异常或向量库报维度错误。

检查:索引/查询模型名、版本、维度、归一化和 query/document 前缀。

修复:版本化向量字段;全量并行重建后切换读别名;禁止新旧向量混写。

3.5 错误码只靠向量搜不到

现象:“E401”召回登录概念,却没有精确错误码文档。

修复:增加 ES/BM25、keyword 精确词字段和业务词典;错误码类 Query 动态提高稀疏路权重。

3.6 BM25 中文分词错误

现象:业务专有词被切碎,或者 term 查询全文字段始终无结果。

检查:调用 _analyze,确认索引与查询分析器;查看 Query DSL 和 explain

修复:维护业务词典;精确值使用 keyword;自然语言使用 match/multi_match;词典变更按需求重建索引。

3.7 多路都能召回,融合后正确项掉出

检查:去重是否使用稳定 chunk_id;RRF rank_window_size 是否小于各路候选;同一文档的重复块是否挤占排名。

修复:修正主键和窗口;融合后做父文档去重;用标注集调窗口,不直接相加原始分数。

3.8 Rerank 把正确证据排低

检查:候选传给模型时是否被截断;Rerank 模型是否支持目标语言和领域;问题改写是否丢失实体。

修复:保留标题和关键 metadata;替换或微调模型;混合使用精确匹配保护;对 Rerank 单独评估 nDCG/MRR。

3.9 正确证据在 Context,模型仍编造

检查:回答中的每个事实是否有引用;文档中是否包含指令注入;Context 是否重复冲突。

修复:结构化输出;引用 ID 由程序校验;资料区标记为非可信数据;证据不足强制拒答;高风险字段做规则校验。

3.10 文档已更新,仍回答旧内容

检查:源版本、索引别名、双库 ID 差集、缓存键中的知识库版本。

修复:稳定文档 ID + 增量删除;双索引对账;发布后原子切读别名;缓存键携带索引版本并主动失效。

3.11 权限泄漏

现象:无权限用户看不到正文,但能从答案、标题、高亮或命中数量推断机密文档。

修复:权限过滤下推到每条召回;缓存键包含权限摘要;生成前只允许可见证据;建立跨租户自动化攻击用例。

3.12 延迟突然升高

检查:把 P95 拆成查询改写、各路检索、Rerank、生成;查看并发、候选数、超时和重试。

修复:召回并行但限制并发;设置分层超时;Rerank 只处理小候选集;单路故障降级;缓存只复用权限和版本完全一致的结果。

四、自动检查 Trace 的最小代码

# requirements.txt
# 检查脚本仅使用 Python 3.10+ 标准库,无第三方依赖。

它只做契约检查,不能代替语义忠实度评测,但可以快速拦截索引混用、空 Context 和引用幻觉。

五、建立坏案例回归集

每次线上反馈转成一条版本化样本:问题、用户权限、期望证据、可接受答案要点、必须拒答条件和故障标签。修复后先跑该样本,再跑全量集防止局部优化破坏其他问题。

回归集应覆盖:精确码、同义问法、多跳、表格、时间敏感、冲突文档、不可回答、恶意文档、跨租户和长问题。每次索引、Embedding、Rerank、Prompt 或模型升级都运行同一套集。

六、错误预算与降级

  • ES 超时:可降级向量路,但明确记录稀疏召回缺失。
  • 向量库超时:可降级 BM25,口语化问题可能召回下降。
  • Rerank 超时:使用 RRF 顺序,不阻断全部请求。
  • 生成模型超时:返回证据列表或可重试状态,不伪造空答案。
  • 两路均无可信证据:拒答或追问,不能用模型常识替代企业知识。

验收清单

  • 任一坏案例都能在 Trace 中定位“正确证据最后出现在哪一层”。
  • 修复有对应回归样本和指标前后对比。
  • 权限过滤、删除传播和 Prompt Injection 有独立安全测试。
  • 索引/模型/Prompt 版本可回放,升级可以回滚。
  • P95 和成本按阶段拆分,降级结果对用户可解释。

七、总结

  • 先保存一条请求的完整证据链:隐私字段和密钥不能进入 Trace;
  • 第一问:正确证据在哪一层丢了:原文是否解析出来。 -> 正确段落是否形成完整 Chunk。 -> Chunk 是否写入正确索引版本。 -> 权限条件下是否应该可见。
  • 十二类高频坏案例:检查:去重是否使用稳定 chunk_id;
  • 自动检查 Trace 的最小代码:它只做契约检查,不能代替语义忠实度评测,但可以快速拦截索引混用、空 Context 和引用幻觉。
  • 建立坏案例回归集:每次线上反馈转成一条版本化样本:问题、用户权限、期望证据、可接受答案要点、必须拒答条件和故障标签。
  • 错误预算与降级:两路均无可信证据:拒答或追问,不能用模型常识替代企业知识。

7.1 可运行实验:增量建库、删除传播与索引版本

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>AA-07 在线实验</title>
  <style>
    :root{color-scheme:dark;font-family:Inter,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:#0f1211;color:#e7ece9;font-size:13px}.shell{padding:16px}.top{display:flex;justify-content:space-between;gap:16px;margin-bottom:14px}h1{margin:3px 0;font-size:18px}.id,.value{color:#68e0b5;font-family:ui-monospace,monospace}.summary{margin:4px 0;color:#a5afa9}.run{border:0;border-radius:6px;background:#68e0b5;color:#07110d;padding:8px 14px;font-weight:700}.grid{display:grid;grid-template-columns:minmax(220px,.8fr) minmax(0,1.8fr);gap:12px}.panel{border:1px solid #29322e;background:#141817;padding:12px}.control{display:grid;gap:5px;margin-bottom:11px}.head{display:flex;justify-content:space-between;gap:8px}select,input{width:100%;accent-color:#68e0b5;background:#0d100f;color:#e7ece9}.toggle{display:flex;justify-content:space-between;border-top:1px solid #29322e;padding-top:9px}.toggle input{width:18px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.metric{border:1px solid #29322e;padding:8px}.metric b{display:block;color:#68e0b5;font-size:16px}.stages{display:flex;gap:6px;overflow:auto;margin:10px 0}.stage{border:1px solid #8a6230;padding:7px;min-width:90px}.stage.ok{border-color:#367a61}.stage.fail{border-color:#8b4545}table{width:100%;border-collapse:collapse}td{border-top:1px solid #29322e;padding:7px}.diagnosis{margin-top:9px;border-left:3px solid #68e0b5;background:#101412;padding:9px;line-height:1.5}.danger{border-color:#ef7f7f}@media(max-width:680px){.top,.grid{display:grid;grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,1fr)}}
  </style>
</head>
<body>
  <main class="shell">
    <header class="top"><div><div class="id">AA-07 · DETERMINISTIC LAB</div><h1 id="title"></h1><p class="summary" id="summary"></p></div><button class="run" id="run">运行实验</button></header>
    <section class="grid"><div class="panel"><div id="controls"></div><label class="toggle"><span>注入典型故障</span><input id="failure" type="checkbox"></label></div><div class="panel"><div class="metrics" id="metrics"></div><div class="stages" id="stages"></div><table><tbody id="rows"></tbody></table><div class="diagnosis" id="diagnosis"></div></div></section>
  </main>
  <script>
    const scenario = { title: '增量建库、删除传播与索引版本', summary: '执行新增、更新或删除,观察 Chunk Diff、蓝绿索引和回滚窗口。', controls: [
    { key: 'operation', label: '文档操作', type: 'select', value: 'update', options: [['create', '新增'], ['update', '更新 v3 → v4'], ['delete', '删除']] },
    { key: 'changed', label: '变化 Chunk', type: 'range', min: 1, max: 30, value: 6, suffix: ' 个' },
    { key: 'strategy', label: '索引发布', type: 'select', value: 'bluegreen', options: [['inplace', '原地更新'], ['bluegreen', '蓝绿切换'], ['dual', '双写一段时间']] }
  ] };
    const controls = document.querySelector('#controls');
    const failure = document.querySelector('#failure');
    document.querySelector('#title').textContent = scenario.title;
    document.querySelector('#summary').textContent = scenario.summary;
    function renderControl(control) {
      const label = document.createElement('label'); label.className = 'control';
      const head = document.createElement('span'); head.className = 'head'; head.innerHTML = '<span>' + control.label + '</span><span class="value" data-value="' + control.key + '"></span>'; label.appendChild(head);
      const input = document.createElement(control.type === 'select' ? 'select' : 'input'); input.dataset.key = control.key;
      if (control.type === 'select') control.options.forEach(option => { const item = document.createElement('option'); item.value = option[0]; item.textContent = option[1]; item.selected = option[0] === control.value; input.appendChild(item); });
      else { input.type = 'range'; input.min = control.min; input.max = control.max; input.step = control.step || 1; input.value = control.value; }
      input.addEventListener('input', updateValues); label.appendChild(input); return label;
    }
    function updateValues() { scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); document.querySelector('[data-value="' + control.key + '"]').textContent = control.type === 'select' ? input.options[input.selectedIndex].text : input.value + (control.suffix || ''); }); }
    function readValues() { const values = {}; scenario.controls.forEach(control => { const input = controls.querySelector('[data-key="' + control.key + '"]'); values[control.key] = control.type === 'range' ? Number(input.value) : input.value; }); values.failure = failure.checked; return values; }
    function stage(name, state, detail) { return { name, state, detail }; }
    const aiStage = stage;
    function clamp(value, minimum, maximum) { return Math.min(maximum, Math.max(minimum, value)); }
    function simulate(values) { const fail = values.failure;
      /** 本次需要重新计算 Embedding 的 Chunk 数。 */
      const reembedded = values.operation === 'delete' ? 0 : values.changed;
      /** 未正确传播删除时残留的旧向量数量。 */
      const stale = values.operation === 'delete' || values.operation === 'update' ? (fail ? Math.max(1, Math.floor(values.changed / 2)) : 0) : 0;
      /** 蓝绿发布需要同时保留的索引版本数。 */
      const versions = values.strategy === 'inplace' ? 1 : 2;
      return { metrics: [[reembedded, '重算 Embedding'], [stale, '残留旧向量'], [versions, '并存索引版本'], [stale ? 'ROLLBACK' : 'PROMOTE', '发布决策']], stages: [aiStage('Checksum', 'ok', 'changed'), aiStage('Chunk Diff', 'ok', '+' + values.changed), aiStage('Embed', 'ok', reembedded), aiStage('Tombstone', stale ? 'fail' : 'ok', stale), aiStage('离线校验', stale ? 'fail' : 'ok', 'retrieval + ACL'), aiStage('切换 Alias', stale ? 'warn' : 'ok', values.strategy), aiStage('清缓存', fail ? 'fail' : 'ok', 'v3')], rows: [['更新策略', values.operation + ' 只处理变化 Chunk,未变化向量复用'], ['索引切换', values.strategy === 'inplace' ? '无法原子回滚,线上可能读到中间态' : '校验新索引后原子切换 active alias'], ['删除传播', stale ? '旧规定仍可召回,必须清向量、关键词索引和缓存' : 'Tombstone 已传播到全部存储层']], diagnosis: stale ? '增量任务未清除旧证据,不能发布新索引。' : '变化集、版本、删除传播和回滚窗口均可追踪。', danger: stale > 0 };
     }
    function render() { const result = simulate(readValues()); document.querySelector('#metrics').innerHTML = result.metrics.map(item => '<div class="metric"><b>' + item[0] + '</b><span>' + item[1] + '</span></div>').join(''); document.querySelector('#stages').innerHTML = result.stages.map(item => '<div class="stage ' + item.state + '"><b>' + item.name + '</b><div>' + item.detail + '</div></div>').join(''); document.querySelector('#rows').innerHTML = result.rows.map(item => '<tr><td>' + item[0] + '</td><td>' + item[1] + '</td></tr>').join(''); const diagnosis = document.querySelector('#diagnosis'); diagnosis.textContent = result.diagnosis; diagnosis.className = 'diagnosis' + (result.danger ? ' danger' : ''); }
    scenario.controls.forEach(control => controls.appendChild(renderControl(control))); updateValues(); document.querySelector('#run').addEventListener('click', render); render();
  </script>
</body>
</html>

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“RAG 生产排障:坏案例与修复手册”中,需要同时满足“先保存一条请求的完整证据链”与“第一问:正确证据在哪一层丢了”。给定正文约束“正文可按策略脱敏或只保存稳定哈希与受控回放引用。”,哪些判断保持了原有处理机制?多选
2“RAG 生产排障:坏案例与修复手册”出现偏差:“在“RAG 生产排障:坏案例与修复手册 / PDF 内容顺序错乱”中,即使不满足“答案把双栏 PDF 左右两列拼在一起”,结果与副作用仍会保持不变。”已成为实际行为。围绕“PDF 内容顺序错乱”与“Chunk 切断关键条件”,哪些判断能定位被改变的职责或边界?多选
3评审“RAG 生产排障:坏案例与修复手册”方案时,验收条件包含“比较不同粒度的 Recall@K 与引用定位精度。”。关于“Chunk 太大导致主题稀释”与“Embedding 不一致”的哪些决策符合正文机制?多选