分享一个rag的线上事故

📅 2026/8/1 8:55:11 👁️ 阅读次数 📝 编程学习
分享一个rag的线上事故

分享一个 RAG 的线上事故:工具文档抢答,维修方向答错了

这是一次智能马桶维修 Agent 的真实复盘。问题不在模型“不会回答”,而在于召回内容来自不同知识源,却没有在进入 Prompt 前明确优先级和适用范围。

事故现象

一位维修师傅处理智能马桶盖板时,把固定螺丝的操作方向弄反,导致螺丝滑丝并引发投诉。

最初我们怀疑是盖板维修文档写错了。回查原始资料后发现,产品维修手册对这个盖板固定螺丝的说明是正确的;真正的问题出在 RAG 的多路检索与答案生成环节。

用户询问的是“智能马桶盖板的螺丝怎么处理”。系统同时召回了两类资料:

知识源内容实际适用范围
盖板维修手册盖板固定螺丝的安装/拆卸步骤与方向指定产品、指定盖板组件、指定操作视角
螺丝刀工具手册螺丝刀的通用操作说明通用工具使用,不描述该产品螺丝的工艺步骤

两段内容都出现了“顺时针”这个词。前者是产品零件的维修工艺,后者只是工具的一般操作表述。Agent 把两段材料平铺注入上下文后,没有识别它们的对象和适用范围不同,最终将工具说明误当成了该螺丝的维修依据,输出了有歧义的操作指令。

这类问题的危险点在于:表面上看,召回“没有错”;真正出错的是模型在多个来源之间做了不该做的自由拼接。

排查过程:先看答案,再看证据链

事故发生后,不应该只检查最终答案。我们沿着一次请求的证据链回溯:

用户问题 → Query Rewrite / Multi-Query → 产品维修手册、工具手册等多路召回 → 融合、重排 → 注入 Prompt → Agent 生成维修步骤

重点记录每个候选 Chunk 的:

{"chunk_id":"cover_manual_5_2","document_type":"product_repair_manual","product_id":"STC-1000","component":"盖板","knowledge_domain":"维修工艺","source_rank":1,"score":0.91}

同一轮里,工具手册也会以类似结构返回,只是它的document_typetool_manualknowledge_domain工具使用,并且通常没有具体产品与组件约束。

复盘时我们发现:检索阶段已经能找到正确的盖板维修手册,但 Prompt 里只按相关性分数拼接了文本,没有将“它是哪个产品的哪个组件、能回答什么问题”一并告诉模型。工具手册因此获得了与产品维修手册近似的发言权。

根因:相关性不等于可执行性

RAG 常见的融合链路是:向量检索、BM25、RRF 融合、CrossEncoder 重排。它们主要回答的是:

这段文本和用户问题是否相关?

但维修类问题还必须回答另一件事:

这段文本能否作为当前产品、当前部件、当前工序的操作依据?

工具手册对“螺丝刀”“顺时针”当然相关;但它并不等于某型号盖板固定螺丝的工艺规范。把两者混成同一层证据,会让模型把通用说明补全成具体动作,风险会直接落到现场操作上。

修复:在 Prompt 注入前做知识源治理

这次修复没有依赖“换一个更大的模型”,而是在 RAG 结果进入 Prompt 前补上优先级与适用范围。

1. 先按 product_id 过滤

产品维修类问题从请求开始就带着product_id。第一次召回与二次召回都先限定为当前产品范围,避免混入其他型号的维修规范。

检索过滤:product_id = STC-1000

注意:这里的核心并不是在二次召回后再排除其他产品;而是在整个检索链路的入口就固定产品范围。

2. 再做组件与知识域适用性过滤

问题目标是“盖板固定螺丝”,优先保留component=盖板knowledge_domain=维修工艺的 Chunk。工具文档不是全部丢弃,而是只在用户明确问“螺丝刀怎么使用”“用什么工具”时作为辅助证据。

问题:盖板螺丝如何拆装 优先知识域:产品维修工艺 可辅助知识域:工具使用、安全规范 不作为方向依据:通用工具操作说明

3. 明确知识源优先级

同一问题下,来源优先级要变成结构化规则,而不是让模型自行猜测:

产品专属维修手册 > 产品安装说明书 > 配件手册 > 工具使用手册 > 通用检修流程 / 行业规范

优先级不是全局真理。它应随问题类型变化:问“工具型号或操作规范”时,工具手册自然应升到前面;问“某型号盖板螺丝的拆装方向”时,产品维修手册必须拥有最高优先级。

4. 用结构化上下文约束 Agent

不要只把 Chunk 原文堆到 Prompt 里。建议按“主依据”和“辅助资料”分区,并把适用范围写清楚:

【主维修依据|优先遵循】 来源:STC-1000 盖板维修手册,第 5.2 节 适用范围:STC-1000,盖板组件,固定螺丝 用途:确定拆装顺序、方向、扭矩和注意事项 【辅助资料|不得覆盖主维修依据】 来源:螺丝刀工具使用手册 适用范围:通用工具 用途:仅回答工具选择、安全操作和握持方式 回答规则: 1. 对具体零件的方向、顺序、扭矩等操作,必须以主维修依据为准。 2. 辅助资料与主维修依据的对象或范围不一致时,不得用于推导具体维修动作。 3. 主维修依据不足时,明确说明缺失信息;不得用通用工具说明补造产品工艺。 4. 输出时标注引用的文档与章节。

这段规则的作用不是让 LLM 背诵优先级,而是限制它对证据的使用方式:通用文档可以补“怎么选螺丝刀”,不能覆盖“这个螺丝该怎样拆装”。

二次检索也要回到同一套规则

维修手册经常有跨章节引用,例如:

如果按压泄阀按钮被卡住,请检查章节 2 的杠杆方向是否正确。

第一次召回可能拿到了故障现象和当前操作步骤,但缺少“章节 2”的杠杆说明。此时可以让模型做上下文完整性评估,生成补充 Query,再进行二次召回。

首次召回 → RRF + CrossEncoder → 检查是否缺少被引用章节或前置步骤 → 生成补充 Query → 仍以 product_id 作为检索入口过滤 → 与首次候选按 chunk_id 合并去重 → 按文档类型、组件、知识域过滤 → 再次重排 → 按来源优先级注入 Prompt

二次召回不能直接拼入上下文。即使已经在入口用product_id限定了同一产品,它仍可能召回词面相近、但属于不同组件或知识域的说明;也可能与首次结果的工艺步骤不一致。因此仍需要去重、适用性过滤和重排。

最终回答增加“可追溯性”

高风险维修指令不应该只返回一段自然语言。接口可以把答案与其主依据一并返回:

{"answer":"请按 STC-1000 盖板维修手册第 5.2 节执行固定螺丝操作;不要以通用螺丝刀说明替代该部件的方向与顺序。","references":[{"chunk_id":"cover_manual_5_2","document_type":"product_repair_manual","chapter":"5.2 盖板固定","priority":"primary"}],"supporting_references":[{"chunk_id":"screwdriver_manual_1_3","document_type":"tool_manual","priority":"supporting"}]}

这样现场人员、质检和研发都能看见:答案究竟依据哪一本手册、哪一节内容生成;一旦出现争议,也能快速定位是文档问题、召回问题,还是生成阶段的问题。

复盘结论

这次事故让我重新确认了一点:在设备维修 RAG 中,召回 TopK 不是“资料越多越好”。

真正需要治理的是证据的边界:

  • 相关的内容不一定能指导当前维修动作;
  • 产品专属工艺不能被通用工具说明覆盖;
  • 多路召回和二次召回都要保留产品、组件、知识域与文档类型;
  • Prompt 注入必须把主依据、辅助资料和禁止覆盖规则表达出来;
  • 最终答案必须带上可追溯的文档与章节引用。

对高风险操作而言,RAG 不是把资料“找出来”就结束了;系统还要明确告诉模型:哪段资料可以回答什么、哪段资料只能作为辅助、冲突时究竟该听谁的。