分享一个 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_type是tool_manual、knowledge_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 不是把资料“找出来”就结束了;系统还要明确告诉模型:哪段资料可以回答什么、哪段资料只能作为辅助、冲突时究竟该听谁的。