上一篇讲到RAG 评估的四个指标,得到这四个指标之后,仍然不能直接回答“这次改动是否有效”。
通常原因有三个:
- 指标需要基线和阈值才能解释;
- 低分只能指出异常所在的阶段,不能直接确定根因;
- 修改方案必须通过控制变量测试验证,否则无法建立改动与结果之间的因果关系。
因此,一套可复用的 RAG 调优流程应该包含三步:
建立基线 --> 定位异常 --> 控制变量验证。
#一、先建立基线,再解释分数
同一个分数,在不同指标中的意义并不相同。
比如,可以为当前项目定义一组内部参考区间:
| 指标 | 高 | 中 | 低 |
|---|---|---|---|
context_recall | ≥ 0.8 | 0.5 ~ 0.8 | < 0.5 |
context_precision | ≥ 0.7 | 0.4 ~ 0.7 | < 0.4 |
faithfulness | ≥ 0.9 | 0.7 ~ 0.9 | < 0.7 |
answer_relevancy | ≥ 0.9 | 0.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 排障至少需要保留两个观测点:
Retriever原始返回结果;- 最终发送给模型的完整
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
结果:
| 指标 | 128 | 256 | 512 |
|---|---|---|---|
faithfulness | 0.9650 | 0.9697 | 0.9570 |
answer_relevancy | 0.8994 | 0.8851 | 0.9301 |
context_recall | 0.8519 | 0.8889 | 0.8889 |
context_precision | 0.9044 | 0.7775 | 0.7680 |
Retrieval F1 | 0.8773 | 0.8295 | 0.8241 |
这组测试数据中,差异最明显的是 context_precision:
128:0.9044
512:0.7680
较大的 chunk 获得了略高的召回:
0.8889 vs 0.8519
但 Precision 的下降更明显。
因此,可以看出在当前语料、当前模型、当前评估集和当前检索配置下,128 的检索综合表现更好。
#十二、测试条件变化后,结论也会变化
如果保持 chunk_size 不变,只调整 RecursiveCharacterTextSplitter 的 separators 配置策略:
| 指标 | 128 | 256 | 512 |
|---|---|---|---|
faithfulness | 0.7857 | 0.7315 | 0.6000 |
answer_relevancy | 0.9101 | 0.9136 | 0.8851 |
context_recall | 0.8333 | 1.0000 | 1.0000 |
context_precision | 0.3301 | 0.3869 | 0.3213 |
根据上边的表格可以看出,分隔符不一定比 chunk_size 更重要。
它只说明:测试条件发生变化后,指标结果不能与上一组测试直接混合解释。
因此,测试记录至少要能回答三个问题:改了什么、什么没改、结果变成了什么。否则即使保留了结果,也无法复现测试条件。
#十三、形成一条固定的异常定位路径
当一条 RAG 回答错误时,可以按数据流从上游到下游依次检查:
- 原始知识文档中是否存在答案
- Loader / Splitter 是否正确输出了包含答案的 chunk
- 索引中是否存在目标 chunk
- Retriever 是否把相关 chunk 排进前 k
- 相关 chunk 是否进入最终的 Context
- 相关 chunk 是否进入最终的 Context
- 最终输出的格式、粒度和完整性是否符合要求
对应的处理方向分别是:
| 层级 | 主要处理对象 |
|---|---|
| 知识文档缺失 | 补充数据 |
| 解析 / 切分失败 | 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 或某一组阈值,而是这套从评估到定位、再到测试验证的过程。