从 bad case 挖掘优化 RAG
RAG 的真正优化方向不在论文里,而在你用户的 bad case 里——那些答错的、找不到的、答非所问的。本文讲清一套 bad case 驱动的 RAG 优化闭环:如何收集(用户反馈/低分/重试)、如何分类(检索没找到/找到了没用对/生成幻觉/格式错)、针对性修复,以及把它们沉淀进 eval 集防回归。
本文要点
- 主题聚焦:大模型、RAG
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
从 bad case 挖掘优化 RAG
做过线上 RAG 的人都有一个共同体会:论文里的 benchmark 永远在涨,自己系统上线后却是一地鸡毛。用户问的东西,要么答得驴唇不对马嘴,要么明明文档里有就是找不到,要么信心满满地编了一个不存在的 API。这些"错"就是 bad case。
但坏消息是,大多数团队对 bad case 的处理方式是:用户吐槽 → 工程师拍脑袋改一改 prompt → 上线 → 下一个问题来了再改一改。没有沉淀、没有分类、没有回归。结果就是系统在反复横跳,今天修好的问题下周又被一次"优化"带崩。
这篇文章讲一套我在线上系统里跑通了的闭环:从 bad case 的收集、分类、修复,到沉淀进 eval 集。不是花架子,是每一层都能落地的工程动作。
一、bad case 才是 RAG 优化的金矿
先泼一盆冷水:公开 benchmark 对你的系统几乎没有指导意义。
原因很简单。MS MARCO、BEIR、MTEB 这些榜单里的 query 是"标准检索问题",文档集是干净的、英文的、领域无关的。而你的系统面对的是:中文长文档、用户用半句话提问、query 里夹杂错别字、需要引用表格里的某一行、答案是"是/否"而不是段落摘要。这两者之间隔着一整个真实世界。
更关键的是,benchmark 测的是"平均分",而你线上出问题的永远是"长尾"。一个 95 分的系统,可能因为对某一类问法全军覆没,用户体验直接崩盘。平均分掩盖了灾难。
所以真正能指导你优化的,是线上真实用户的 bad case。它们是带场景的、带意图的、带你的数据特征的。每一个 bad case 都是一份免费的、精准标注的优化指引。论文告诉你"rerank 平均涨 3 个点",bad case 告诉你"用户问 X 类问题时 60% 召回不到,这才是你要解决的问题"。
一句话:benchmark 决定你的上限,bad case 决定你的下限。先把下限拉起来,平均分才有意义。
二、收集:把用户的痛埋下来
bad case 优化的第一步不是修,是收。没有成体系的收集,谈优化就是玄学。我建议至少铺这几条埋点。
第一,显式反馈。 每条回答下面挂 👍/👎,或者一个 1-5 星评分。这是最直接的信号。但要注意,用户点 👎 的概率远低于实际不满率——大部分人不爽就直接关页面了。所以显式反馈是"强信号但低召回",用来抓重点 case,不能作为唯一口径。
第二,CSAT 低分会话。 把 1-2 分的会话整体捞出来,看用户到底问了什么、系统答了什么、上下文是什么。一个会话往往能拆出多个 bad case。
第三,重试行为。 这是被低估的信号。如果用户问完一个问题,3 秒后用相似但不同的措辞又问了一遍——几乎可以确定上一条没让他满意。重试率高 = 召回或理解出了问题。把"同一 session 内 10 秒内 query 相似度 > 0.8"标记成一条疑似 bad case,自动入库。
第四,人工抽检。 每天从线上随机抽 50-100 条,人工打分。这是黄金标准,也是你校准上面三种自动信号基准。没有人工抽检,你都不知道 👎 召回率有多低。
埋点记录什么? 关键是不要只记 query 和 answer。一定要把完整的中间链路都存下来:query 原文、改写后的 query、召回到的 chunk 列表(含分数)、rerank 后的排序、送给 LLM 的最终 context、LLM 的 raw output、后处理后的回答。没有这些,你拿到一个 bad case 只能瞎猜是哪一环出了问题;有了这些,定位是分钟级的。
我自己的做法是每次请求落一条结构化日志,存进对象存储 + 建一个查询表。bad case 挖掘的效率,90% 取决于你日志全不全。
三、分类诊断:四类 bad case 各不同
收集来了不等于能修。一个 bad case 到底是哪一环的问题?这是整个闭环里最关键的一步,也是最容易被跳过的一步。我把它分成四类,每一类的修复方向完全不同。
① 检索没找到(recall 问题)
症状:文档里明明有答案,但召回的 top-k chunk 里压根没包含。这是最常见的一类。
判断方法:人工在知识库里搜一下,能找到相关内容,但系统的检索结果里没有。根因通常是:切分粒度不对(切太碎丢了上下文、切太粗相关信号被稀释)、embedding 模型对你的领域不友好、query 改写把关键词丢了、或者就是混合检索里字面权重太低。
② 找到了没用对(precision / 利用问题)
症状:召回的 chunk 里其实有正确的那一段,但排在 top-k 之外,或者排进来了但 LLM 没用上它,反而用了排名靠后但不相关的那段。
判断方法:看召回列表,正确 chunk 在第几名?如果在前 5 但 LLM 没用,是 prompt 或 context 太长被淹没的问题;如果在第 6-20 名,是 rerank 或排序分数的问题。这两者修复方向完全不同——前者改 prompt 和 context 组织,后者上 rerank 或调权重。
③ 生成幻觉(faithfulness 问题)
症状:检索结果是对的,但 LLM 没忍住,自己编了一段文档里没有的内容。典型表现是编造 API 名、编造参数、编造数字。
判断方法:把 LLM 的回答逐句对照 context,凡是 context 里没有支撑的陈述都是幻觉。根因是 prompt 约束不够(没强调"只能基于给定 context 回答")、context 里混入了无关内容诱导发散、或者模型本身遵循能力弱。
④ 格式 / 风格错(prompt / 后处理问题)
症状:事实没错,但回答太啰嗦、没按要求的格式(比如要表格给了段落)、引用编号没对上、中英文混杂、口吻不对。这类不影响正确性但严重影响可用性。
判断方法:看回答是否符合 prompt 里写明的输出规范。根因通常就是 prompt 没说清楚,或者后处理逻辑有 bug。
把这四类分清楚,你的优化才不会"用大炮打蚊子"。我见过太多团队一遇到 bad case 就无脑加 rerank、换更大的 LLM、加更多 chunk——结果大部分问题是切分和 prompt 的事,跟 rerank 一毛钱关系没有。
四、针对性修复:别无脑加 rerank
分类清楚后,每一类有对应的修复手段。下面给的是方向,不是银弹——每个修复都要用数据验证。
针对 recall 问题(检索没找到):
- 调切分:把 chunk 调小或调大,或者换成按语义切分(段落/标题边界)。长文档优先试 overlap 切分,避免边界丢信息。
- 调 query:加一步 query 改写或扩展(同义词、子问题拆解),尤其是用户用口语化提问时。
- 调检索:混合检索(向量 + BM25),字面匹配对专有名词、型号、代码片段极其关键,纯向量很容易漏。
- 加召回广度:top-k 从 5 调到 10 看看,但别盲目调大,会拖慢且引入噪声。
针对 precision 问题(找到了没用对):
- 上 rerank:这是 rerank 真正该上场的地方——召回够但排序不准。cross-encoder rerank 通常能把正确 chunk 从第 8 名顶到前 2 名。
- 减 context 噪声:top-k 太大反而稀释信号,LLM 容易被无关 chunk 带跑。实验最优 k,通常 3-5 比 10 好。
- 调 prompt 里 context 的呈现:把最相关的放最前面(lost-in-the-middle 现象),或者明确标注"以下按相关性排序"。
针对 faithfulness 问题(幻觉):
- 强约束 prompt:"只能基于下面 context 回答;context 里没有的内容必须回答'文档中未提及';禁止编造 API、参数、数字。"
- 加引用要求:每句话必须标注来源 chunk 编号,逼 LLM 回到 context。
- 加一道 faithfulness 校验:用另一个 LLM(或同一个)做一次"回答是否可由 context 支持"的自检,不过则重生成或降级回答"无法确认"。
- 清理 context:确认是不是无关 chunk 诱导了发散,如果是,回到 precision 那一步。
针对格式问题:
- prompt 里给 few-shot 示例,明确期望的输出结构。这是最有效的。
- 加结构化输出(JSON schema、function calling),把"风格"变成"字段"。
- 后处理兜底:正则修正引用编号、统一标点、trim 冗余寒暄。
注意一个反模式:不要拿一个 case 修一个 case。 改 prompt 之前先问自己,这个改动对其他 case 是正向还是负向?有没有更通用的解?下一节就是怎么把这事做扎实。
五、沉淀进 eval 集:防回归
修完一个 bad case 不算完事。如果你只是改完上线,两周后大概率会发现:同类问题又冒出来了,或者你的改动把别的 case 带崩了。原因是没有 eval 集兜底。
做法:每个被确认的 bad case,都标注成一条 eval 用例。 一条 eval 用例至少包含:
query:用户原始问题(或等价改写)expected_facts:期望回答必须包含的事实点(从文档里人工提取)expected_chunks:期望被召回的 chunk id(用于测检索)faithfulness:回答不能编造的边界category:属于上面四类里的哪一类
把这些跑成自动化 eval:每次改 prompt、切分、检索参数、换模型,都跑一遍全量 eval 集。这样有两个好处——第一,防回归,你改 A 不会悄悄把 B 改崩;第二,能量化收益,你新加的 rerank 到底把多少条 case 救回来了,一目了然。
eval 集要分层:核心集(必须全过的 hard case,几十条)、常规集(覆盖主流场景,几百条)、长尾集(那些边界 case,几千条)。 CI 里跑核心集,每周跑常规集,每月跑长尾集。一个不断长大的 eval 集就是你 RAG 系统最值钱的资产,比任何 prompt 模板都值钱。
还有一点容易被忽略:eval 集里也要有"不应该回答"的 negative case。 比如用户问的是知识库之外的内容,期望系统回答"超出范围"而不是硬编一段。这类 case 不加进去,你的系统就会养成"什么都敢答"的坏习惯。
六、闭环:线上 bad case → 分类 → 修复 → eval 验证 → 上线
把上面几步串起来,就是一个可持续迭代的闭环:
线上 bad case(反馈/低分/重试/抽检)
↓
日志回放,定位中间链路
↓
四类分类(recall / precision / faithfulness / format)
↓
针对性修复(切分/检索/rerank/prompt/后处理)
↓
全量 eval 集回归 + A/B 灰度
↓
上线 → 持续监控 → 新的 bad case 进入下一轮
这个闭环的核心价值不在于"修一个 case",而在于每一次循环都让系统在真实分布上更稳一点。跑半年下来,你的 eval 集有上千条带标注的 case,你的分类直觉被数据反复校准过,新人来了照着这套 SOP 就能上手——这才是工程化的 RAG 优化。
A/B 灰度不能省。任何改动上线前,先灰度 5%-10% 流量,对比核心指标(回答准确率、👍 率、重试率、幻觉率)。没有灰度数据的"优化"都是赌博。
七、别被个例带偏
最后一条是给资深工程师的提醒:bad case 驱动不等于 case-by-case 优化。
你会遇到一些非常具体、非常刁钻的 case,修起来很诱人——加个特例规则、塞个 if、hardcode 一段 prompt。但这种"点修复"长期看是技术债,会让系统越来越脆。正确做法是看趋势:同一类 bad case 反复出现 3 次以上,才值得动通用解;偶发的、孤立的 case,记录进 eval 集观察,不要急着改架构。
另一个极端也要避免:不要因为一两个 case 就换模型、换框架。换大模型确实能涨分,但成本、延迟、依赖全上来,而且很多时候瓶颈根本不在模型,在检索和切分。先榨干现有架构的优化空间,再谈换。
优化的艺术,是在"通用解"和"特例解"之间找平衡,而天平的砝码是频次和影响面。
小结
- bad case 是 RAG 优化最值钱的输入,比任何 benchmark 都贴近真实;benchmark 看上限,bad case 抓下限。
- 收集要铺网:显式反馈 + CSAT 低分 + 重试行为 + 人工抽检,且一定要埋点记录完整的中间链路(改写、召回、rerank、context、raw output)。
- 分类是关键中的关键:recall(检索没找到)/ precision(找到了没用对)/ faithfulness(幻觉)/ format(格式),四类修复方向完全不同。
- 修复要对症:recall 调切分和混合检索,precision 上 rerank 和减噪,faithfulness 强约束 prompt + 自检,format 给 few-shot 和结构化输出。别无脑加 rerank。
- 每个 bad case 沉淀进 eval 集,带期望标注,跑回归防退化;eval 集是 RAG 系统最值钱的资产。
- 闭环跑起来:收集 → 分类 → 修复 → eval 验证 → 灰度上线,持续迭代;个例看趋势,别被带偏。
金句:RAG 优化的尽头不是更大的模型、更长的 context,而是一个不断长大的、带标注的 eval 集——它才是你和"系统又变蠢了"之间唯一的防线。
Related