知识点思维导图
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;正文可按策略脱敏或只保存稳定哈希与受控回放引用。
二、第一问:正确证据在哪一层丢了
按漏斗定位:
- 原文是否解析出来。
- 正确段落是否形成完整 Chunk。
- Chunk 是否写入正确索引版本。
- 权限条件下是否应该可见。
- 是否进入任一召回路 Top K。
- 是否在 RRF、去重或 Rerank 后掉出。
- 是否被 Context Packing 截掉。
- 已进入 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>
学完自测
选择所有正确答案;提交后逐项核对判断依据。