LangChain 实践笔记:RAG 调优的基线、定位与测试

从四个评估分数拿到手却仍判断不了改动是否有效出发,讲清基线与阈值怎么定、七类故障如何沿链路逐层定位、控制变量测试怎么验证原因。

上一篇讲到RAG 评估的四个指标,得到这四个指标之后,仍然不能直接回答“这次改动是否有效”。

通常原因有三个:

  • 指标需要基线和阈值才能解释;
  • 低分只能指出异常所在的阶段,不能直接确定根因;
  • 修改方案必须通过控制变量测试验证,否则无法建立改动与结果之间的因果关系。

因此,一套可复用的 RAG 调优流程应该包含三步:

建立基线 --> 定位异常 --> 控制变量验证。

#一、先建立基线,再解释分数

同一个分数,在不同指标中的意义并不相同。

比如,可以为当前项目定义一组内部参考区间:

指标高中低
context_recall≥ 0.80.5 ~ 0.8< 0.5
context_precision≥ 0.70.4 ~ 0.7< 0.4
faithfulness≥ 0.90.7 ~ 0.9< 0.7
answer_relevancy≥ 0.90.7 ~ 0.9< 0.7

注:上述数值只作为示例,不是 Ragas 的官方阈值,也不是通用行业标准。

实际开发过程中阈值应由对应业务的风险决定。

高风险场景通常更关注:

  • faithfulness:是否存在无上下文依据的陈述;
  • context_recall:是否遗漏关键事实。

客服、咨询等场景更关注:

  • answer_relevancy:是否直接解决用户问题;
  • context_precision:是否引入过多无关上下文。

因此,阈值回答的不是“技术上能做到多少”,而是:业务允许系统以多大的概率出现哪一类错误。

比外部阈值更重要的是系统自身的历史基线。

需要固定:

  • 同一批评估问题;
  • 同一套参考答案;
  • 同一组指标;
  • 同一个 Judge(LLM-as-a-Judge);

上一个版本的结果就可以作为下一个版本的比较基准。

这里的目标不是要求新版本中四项指标全部上升,而是明确:

改动带来了哪些收益,又付出了哪些代价。

#二、四个指标先定位异常阶段

基础 RAG 可以粗分为两个阶段:检索 --> 生成。

#1.检索阶段

context_recall 衡量:应该被检索到的信息是否发生遗漏。

context_precision 衡量:与问题相关的检索结果是否排在靠前的位置。

#2.生成阶段

faithfulness 衡量:生成的答案是否有检索到的信息作为依据。

answer_relevancy 衡量:生成的答案是否直接回答用户问题。

因此,第一次读取评估结果时,可以先做阶段级定位:

现象优先检查
context_recall 低检索覆盖能力
context_precision 低检索排序与噪声
检索正常、faithfulness 低生成阶段是否引入没有依据的内容
faithfulness 高、answer_relevancy 低问题理解与回答策略

做到这里只能回答**问题主要发生在检索还是生成。**具体原因是什么,还需要继续往下查。

#三、两个检索指标冲突时,可增加辅助分数

context_recall 和 context_precision 经常存在取舍。

比如:

A
context_recall    = 0.6667
context_precision = 0.4511

B
context_recall    = 1.0000
context_precision = 0.3869

可以看到,B 的召回更高,但精确度更低。

这时,我们可以额外定义一个调和平均:

Retrieval F1 = 2PR / (P + R)

得到:

A:0.5381
B:0.5579

此时,B 的 Retrieval F1 更高,说明在这一比较规则下 B 略优,但是不能直接推出 B 的业务效果一定更好。

如果业务更重视召回或精确,应当分别读取原始指标,而不是依赖单一合并值。

#四、从指标继续定位到具体异常点

阶段定位之后,还需要沿 RAG 数据流继续向下排查。

基础 RAG 的四个环节:知识库、检索、上下文组装、生成。

对应有七类常见异常:

环节异常
知识库Missing Content
检索Missed Top Ranked
上下文组装Not in Context
生成Not Extracted
生成Wrong Format
生成Incorrect Specificity
生成Incomplete

这是一种工程排查分类,不代表所有 RAG 异常只能落在这七类中。

#五、第一层:知识源中是否存在答案

首先需要确认:原始知识源中是否存在回答问题所需的信息。

如果原始资料本身没有相关内容,调节:Top-k、Embedding、Retriever、Prompt 都无法从根本上解决问题。

但“检索结果中没有”不能直接推出“知识库没有”。

还应依次检查:

原始文件
  ↓
Loader 输出
  ↓
切分后的 Document
  ↓
索引

可能的问题包括:

  • Loader 没有正确解析;
  • 文档切分破坏了完整语义;
  • 索引未正确建立;
  • metadata filter 排除了目标文档。

只有确认原始资料中本身不存在相关内容,才能归为真正的 Missing Content。

这类问题修复的是数据源,而不是模型参数。

#六、第二层:相关内容是否进入Top-K

如果知识库中存在答案,下一步检查 Retriever 是否将对应 chunk 排入前 K。

常见原因包括三类。

#1.查询与文档表达差异

例如:

用户:能不能退?
文档:支持七天无理由退货。

语义相同,但表达差异可能降低检索得分。

#2.Top-K 截断

答案所在的 chunk 排名第 7,但系统只返回前 5 个。

#3.切分造成信息断裂

完整答案跨在两个 chunk 之间,导致单个 chunk 的语义完整度下降。

排查时可以先适度提高 K。

如果相关 chunk 随 K 增大进入返回结果,可以确认原配置存在 cutoff 问题。

如果 K 增大后仍然无法检索到,说明:问题不只是 K 太小。

此时需要继续检查:

  • Query rewrite;
  • Embedding;
  • 检索策略;
  • 分块方式;
  • 索引状态。

不能直接据此判断知识库缺少内容。

#七、第三层:检索结果是否进入最终上下文

Retriever 返回相关 chunk,并不代表模型一定看到了它。

检索结果到模型输入之间,还可能经过:

  • 截断;
  • Context Compression;
  • 去重;
  • 自定义过滤;
  • token budget 控制;
  • Prompt 组装。

例如 Retriever 返回 10 个 chunk,但最终只允许放入 6 个。

如果答案位于第 8 个 chunk,检索本身已经成功,但模型仍然无法使用这条信息。

因此,RAG 排障至少需要保留两个观测点:

  1. Retriever 原始返回结果;
  2. 最终发送给模型的完整 Context。

Retriever 输出的是候选上下文,最终 Prompt 才是模型实际看到的上下文。

#八、第四层:上下文中存在,模型是否正确使用

如果最终的 Context 中已经存在答案,而模型依然回答错误,则进入生成阶段。

Not Extracted :回答所需的证据已经在 Context 中存在,但是模型没有正确提取或使用。

常见原因包括:

  • 上下文噪声过多;
  • 相关信息位置不突出;
  • 多份文档内容互相冲突;
  • 旧版本资料与新版本资料同时存在。

这时可以考虑:

  • 减少 Context;
  • 增加 reranker;
  • 清理旧文档;
  • 使用版本 metadata;
  • 提高相关内容的排序位置。

需要特别区分:回答中出现 Context 未支持的额外陈述,不属于 Not Extracted。

这种情况更接近 faithfulness 下降,即生成了缺乏上下文支持的内容。

#九、输出阶段:格式、粒度与完整性

如果模型已经获得正确内容,仍可能在输出阶段失败。

异常表现处理方式
Wrong Format要 JSON,却返回自然语言Structured Output、Schema 校验
Incorrect Specificity要参数值,却返回概述约束回答粒度
Incomplete回答正确但遗漏子问题问题拆分、逐项生成

结构化输出建议至少包含两层控制:生成约束、Schema 校验。

必要时再增加失败重试。

不能只依赖 Prompt 约束格式,因为模型输出仍可能不满足下游程序要求。

对于复合问题,例如:

A、B、C 分别有什么区别?

如果完整性长期偏低,可以拆成多个子问题后分别生成,再统一合并。

#十、定位只是提出假设,测试才能验证原因

异常定位得到的是:哪一段最值得检查。

它不是根因结论。

真正验证某个因素是否导致问题,需要控制变量测试。

基本原则是:一次测试只改变一个研究变量。

至少固定三个方面。

#1.评估数据固定

使用同一批问题和参考答案。

否则测试前后分数没有直接可比性。

#2.Judge 固定

保持:判分模型、评估指标实现、评估参数一致。

否则 Judge 自身变化可能导致整体分数漂移。

#3.其它链路配置固定

比如测试 chunk_size 时,应尽量固定:

  • Embedding;
  • Retriever;
  • Top-K;
  • Prompt;
  • Chat Model;
  • 向量库。

如果同时修改多个变量,即使指标上升,也无法判断变化由哪个因素造成。

一组测试应只回答一个明确问题。

#十一、chunk_size 控制变量测试

下面是一组实际分块测试。

研究变量是分块尺度。

chunk_overlap 始终设置为当前 chunk_size 的 20%,因此严格来说,测试控制的是一组“chunk 尺度配置”,而不是只改变单个数值参数。

其它主要条件保持一致:

  • 相同评估集;
  • 相同模型;
  • 相同向量库;
  • 相同 Retriever;
  • Top-K = 10;
  • 不使用重排和混合检索。

三档配置:128 | 256 | 512

结果:

指标128256512
faithfulness0.96500.96970.9570
answer_relevancy0.89940.88510.9301
context_recall0.85190.88890.8889
context_precision0.90440.77750.7680
Retrieval F10.87730.82950.8241

这组测试数据中,差异最明显的是 context_precision:

128:0.9044
512:0.7680

较大的 chunk 获得了略高的召回:

0.8889 vs 0.8519

但 Precision 的下降更明显。

因此,可以看出在当前语料、当前模型、当前评估集和当前检索配置下,128 的检索综合表现更好。

#十二、测试条件变化后,结论也会变化

如果保持 chunk_size 不变,只调整 RecursiveCharacterTextSplitter 的 separators 配置策略:

指标128256512
faithfulness0.78570.73150.6000
answer_relevancy0.91010.91360.8851
context_recall0.83331.00001.0000
context_precision0.33010.38690.3213

根据上边的表格可以看出,分隔符不一定比 chunk_size 更重要。

它只说明:测试条件发生变化后,指标结果不能与上一组测试直接混合解释。

因此,测试记录至少要能回答三个问题:改了什么、什么没改、结果变成了什么。否则即使保留了结果,也无法复现测试条件。

#十三、形成一条固定的异常定位路径

当一条 RAG 回答错误时,可以按数据流从上游到下游依次检查:

  1. 原始知识文档中是否存在答案
  2. Loader / Splitter 是否正确输出了包含答案的 chunk
  3. 索引中是否存在目标 chunk
  4. Retriever 是否把相关 chunk 排进前 k
  5. 相关 chunk 是否进入最终的 Context
  6. 相关 chunk 是否进入最终的 Context
  7. 最终输出的格式、粒度和完整性是否符合要求

对应的处理方向分别是:

层级主要处理对象
知识文档缺失补充数据
解析 / 切分失败Loader、Splitter
检索失败Embedding、Retriever、Query、Top-K
Context 丢失Compression、过滤、Prompt 组装
生成未使用Context 质量、Prompt、文档冲突
输出不合格Structured Output、Schema、任务拆分

只有确认上一层没有问题,才继续检查下一层。

#十四、总结

RAG 调优可以归纳为一个闭环:

#1. 评估指标发现异常

四个核心指标分别观察:

  • 检索是否遗漏;
  • 排序是否有效;
  • 回答是否有依据;
  • 回答是否对题。

#2. 异常路径缩小问题范围

沿着:

知识库
→ 检索
→ Context
→ 生成
→ 输出

逐层确认信息在哪一步丢失或被错误处理。

#3. 控制变量测试验证根因

定位得到的是假设。

只有在:固定评估集、固定 Judge、固定其它配置的条件下,只改变一个研究变量并重新评估,才能判断修改是否真正产生了预期影响。

因此,RAG 调优不应该是反复修改参数并观察几条答案。

更可靠的流程是:

建立基线
→ 发现异常
→ 定位阶段
→ 提出假设
→ 控制变量测试
→ 重跑评估
→ 与基线比较

真正可复用的不是某一个 chunk_size、某一个 Top-K 或某一组阈值,而是这套从评估到定位、再到测试验证的过程。