第 11 章:质量评估与回归测试——让 RAG 可量化、可迭代
1. 开篇:告别“玄学”调优
在前面的章节中,你已经搭建起了 RAG 的骨架。你输入一个问题,CC 返回了一段看起来不错的答案。这时候,新手和专家的区别就出现了:
- 新手会说:“我觉得效果不错,上线吧。”
- 专家会问:“Recall@10 是多少?忠实度(Faithfulness)有几个点?针对代码片段的检索,噪音比例如何?”
RAG 系统本质上是一个由多个概率组件串联起来的非确定性系统。
- 你的 Chunk 切分也许切断了函数定义的上下文。
- 你的 Embedding 模型也许无法区分
java.util.List 和 java.awt.List。
- 你的重排(Rerank)策略也许把旧的文档排到了前面。
如果没有一套量化的“度量衡”,你的每一次优化(更换模型、调整 Prompt、修改切分长度)都只是在盲人摸象——你可能修复了一个 Bug,却引入了两个更严重的退化。
本章将带你构建一套完整的评估流水线(Evaluation Pipeline),涵盖从黄金数据集的构建到自动化回归测试的全过程。我们的目标是:让 RAG 的改进像软件工程一样,有测试覆盖,有指标对比,心里有底。
2. 评估的维度:RAG 三元组与端到端指标
业界公认的 RAG 评估框架被称为 “RAG 三元组” (RAG Triad),它将评估拆解为三个独立的环节。此外,对于 CC 这种交互式 Agent,我们还需要关注端到端(End-to-End) 的体验指标。
2.1 RAG 三元组详解
我们将整个流程切分为两刀,形成三个评估面:
[ 用户 Query ]
/ \
(A) (C)
索相关性 / \ 回答相关性
(Retrieval) \ (Answer Relevance)
/ \
v v
[ 检索到的 Context ] --(B)--> [ 生成的 Answer ]
忠实度 (Faithfulness)
(A) 检索相关性 (Context Relevance)
- 核心问题:我要找的“菜”买对了吗?
- 定义:检索出的 Top-K 文档中,包含正确答案的信息比例是多少?噪音比例是多少?
- 为什么重要:如果第一步就没捞到相关文档,CC 再聪明也只能“巧妇难为无米之炊”;如果捞到的全是垃圾,CC 就会产生幻觉。
(B) 忠实度 (Faithfulness / Groundedness)
- 核心问题:厨师是按菜谱做的,还是瞎编的?
- 定义:生成的 Answer 是否完全、严格地基于 Context 中的信息?
- CC 特有的关注点:对于代码库 RAG,如果 CC 生成了一个 Context 里没有的函数参数,这就是严重的幻觉(导致代码跑不通)。忠实度要求:不知为不知。
(C) 回答相关性 (Answer Relevance)
- 核心问题:这道菜是客人点的吗?
- 定义:生成的 Answer 是否直接回答了用户的原始 Query?
- 陷阱:有时候 CC 会非常忠实地复述 Context,但完全答非所问(例如用户问“怎么安装”,CC 回答了“历史背景”)。
2.2 端到端指标 (End-to-End Metrics)
除了内容质量,作为外挂服务,以下指标决定了 CC 的“手感”:
- Latency (延迟):从收到 Query 到返回 Context 给 CC 需要多久?(目标:< 500ms,否则 CC 会超时或用户体验极差)。
- Hit Rate (命中率):系统成功返回非空结果的比例。
3. 构建“黄金数据集” (Golden Dataset)
评估的前提是有“标准答案”。在 RAG 领域,这被称为黄金数据集。
3.1 数据集结构
一个标准的 RAG 测试用例(Test Case)应包含以下字段:
| 字段 |
说明 |
示例 |
| Query |
用户的提问 |
“Authorization模块在哪里初始化的?” |
| Ground Truth (GT) Contexts |
必须被检索到的文档/Chunk ID |
["src/auth/init.ts", "docs/setup.md#L50"] |
| Ground Truth Answer |
(可选) 标准参考答案 |
“在 src/auth/init.ts 的 initAuth 函数中…” |
| Negative Contexts |
(进阶) 易混淆的错误文档 ID |
["src/auth/init_test.ts"] (测试文件常是干扰项) |
3.2 如何构建?(三种策略)
策略 A:人工标注(最精准,最贵)
由熟悉项目代码的资深开发者,查看文档/代码,编写问题并标记对应的 Chunk。
策略 B:合成数据(Synthetic Data Generation,推荐)
利用 LLM 逆向生成。这是目前扩充数据集的主流方法。
- 流程:
- 随机抽取一个 Chunk(例如一段代码或 API 文档)。
- 构造 Prompt 喂给 GPT-4/Claude 3.5:“假设你是一开发者,你想了解这段代码的功能,你会问什么问题?请生成 3 个不同难度的问题(简单查询、逻辑推理、代码意图)。”
- 记录
{生成的问题, 该Chunk ID}。
- 优点:一晚上能生成 1000 条。
- 缺点:LLM 倾向于使用 Chunk 中的原词提问,导致检索难度偏低。
策略 C:用户日志挖掘(最真实)
直接从 CC 的历史对话日志中提取用户真实的 Query。
- 难点:需要人工回填 Ground Truth(这道题的标准答案是哪个文档)。
4. 检索阶段评估详解
这是最好量化、也是优化 RAG 的第一战场。
4.1 核心指标公式与人话解释
1. Recall@K (召回率)
- 公式:$\frac{\text{检索出的 Top K 中包含的相关文档数}}{\text{数据库中所有的相关文档总数}}$
- 解读:假设正确答案分散在 3 个文档里。
Recall@10 = 0.66 意味着前 10 个结果里捞到了 2 个,漏了 1 个。
- 对于 CC,我们通常关注 Recall@20 或 Recall@50,因为 CC 窗口大,只要捞进来就能读,漏了就彻底没戏。
2. MRR (Mean Reciprocal Rank, 平均倒数排名)
- 公式:$\frac{1}{N} \sum_{i=1}^{N} \frac{1}{\text{rank}_i}$
- 解读:第一个正确答案排得越靠前分越高。
- 排第 1 名得 1 分。
- 排第 2 名得 0.5 分。
- 排第 10 名得 0.1 分。
- 意义:CC 虽然能读长文,但也有“Lost in the Middle”效应。正确答案排第一,CC 采纳的概率最高。
3. NDCG (Normalized Discounted Cumulative Gain, 归一化折损累计增益)
- 解读:这是 MRR 的升级版。MRR 只看“第一个对的”,NDCG 关注“所有对的文档是否都排在前面”。
- 场景:如果一个问题需要 3 个文档拼凑才能回答,MRR 只要有一个排前面就满分,但 NDCG 会惩罚那些把第 2、3 个证据排在最后的系统。
4.2 常见检索失败模式
在看指标时,要注意区分以下情况:
- 语义不匹配:Embedding 模型不懂行话(比如搜“ORM”,Embedding 不知道它是“Object Relational Mapping”)。
- 粒度错位:搜“鉴权流程”,召回了 50 个细碎的函数片段,却漏了总结性的架构文档。
5. 生成阶段评估:LLM-as-a-Judge
检索之后的评估很难用数学公式,我们主要依靠“用大模型评估大模型” (LLM-as-a-Judge)。
5.1 裁判的 Prompt 设计
你需要写一个特殊的 Prompt,让 GPT-4 或 Claude 3.5 Sonnet 扮演裁判。
示例 Prompt (裁判指令):
你是一个 RAG 系统评估专家。
请阅读以下的 [用户问题]、[检索上下文] 和 [系统回答]。
请从以下两个维度打分(1-5分)并给出理由:
- 忠实度:系统回答中的每一条信息是否都能在上下文中找到依据?如果有任何未提及的信息被包含在回答中,打 1 分。
- 有用性:回答是否解决了用户的问题?
输出格式 JSON: { "faithfulness": 5, "relevance": 4, "reason": "..." }
5.2 针对 CC 代码场景的特殊指标
普通的 RAG 只要通顺即可,但给 CC 用的 RAG 必须严谨:
- 代码可执行性预判:如果是生成代码示例,裁判需要检查引用的函数名是否真的存在于 Context 中(防止幻觉造库)。
- 引用准确率 (Citation Precision):CC 经常需要输出
[File: line 10-20]。评估器需要正则提取这个引用,去检查原文件该行是否真的是相关内容。
6. 自动化回归测试流水线 (The Pipeline)
不要在本地跑脚本,要把评估集成到代码仓库的 CI/CD 或日常维护流程中。
6.1 工具链推荐
- Promptfoo:极简的 CLI 工具,通过 YAML 配置文件定义测试用例,支持 LLM 评分。非常适合 RAG 评估。
- Ragas:专门的 RAG 评估 Python 库,内置了计算 Context Recall, Faithfulness 等指标的算法。
- LangSmith / Langfuse:可视化平台,便于查看每个 Case 的详细 Trace。
6.2 典型的回归流程
每当你修改了 Prompt、Embedding 模型或切分逻辑时,执行以下步骤:
- Freeze Data:锁定当前的测试数据集(版本 v1)。
- Run Baseline:在旧版系统上跑一遍,记录 Recall@20 和 Faithfulness 分数。
- Run Experiment:在修改后的系统上跑一遍。
- Diff:
- Recall 提升了 5%? -> 好事。
- Latency 增加了 200ms? -> 权衡一下。
- Faithfulness 下降了? -> 绝对不行,这意味幻觉增加了,必须回滚。
- Bad Case 分析:专门把那些“旧版答对、新版答错”的 Case 捞出来人工分析。
7. 本章小结
- 拒绝盲测:没有评估指标的 RAG 优化是伪科学。
- 黄金数据集:这是资产。通过“合成数据”可以低成本启动,但最终要引入人工校验的高质量测试集。
- 分段评估:先看“检索准不准”(Recall/MRR),再看“回答真不真”(Faithfulness)。
- LLM 裁判:用最强的模型(裁判)去评估你的 RAG 系统(选手),是目前最可行的质量控制手段。
- 回归红线:在代码场景下,忠实度(不瞎编) 的优先级高于 召回率。CC 说“我不知道”比“瞎写一行代码”要安全得多。
8. 练习题
基础题
Q1. 关于 Recall@K 的理解
在调试 CC 的外挂 RAG 时,你发现 Recall@5 只有 40%,但 Recall@50 达到了 95%。这说明了什么问题?
A. Embedding 模型完全失效。
B. 重排(Rerank)做得不好,相关文档被排到了后面。
C. 切分粒度太细了。
答案
**答案:B**
**解析**:Recall@50 很高说明**召回(Retrieval)** 阶段成功把文档捞进来了(在候选池里)。但 Recall@5 很低说明这些正确的文档没有排在前面。这通常意味着你需要引入或优化 **Reranker(重排器)**,或者你的向量相似度在区分“真正相关”和“表面相似”时不够敏锐。
Q2. 忠实度(Faithfulness)陷阱
你构建了一个测试用例:
- Context: “API
login 在 v2.0 中已被废弃,请使用 auth_user。”
- CC Answer: “你应该使用
auth_user 进行登录,因为 login 已经废弃了。”
裁判模型给出了 1 分(满分 5 分)的忠实度评分,声称“Context 中没有提到‘因为’这个因果关系,属于过度推断”。
请问这是系统的错,还是裁判的错?如何修正?
答案
**答案:裁判过于严苛(Prompt 问题)**
**解析**:这是一个常见的 LLM-as-a-Judge 陷阱。Context 虽然只陈述了事实,但语义包含了替代关系。
**修正**:需要优化裁判的 Prompt,允许进行“合理的逻辑推断(Logical entailment)”,只要不引入 Context 之外的**事实性信息**(如“`login` 是因为安全漏洞被废弃的”——这 Context 里没说,如果 CC 说了就是幻觉)。
Q3. 幻觉与知识截止
如果用户问:“React 19 有什么新特性?”
你的知识库里只有 React 18 的文档。
理想的 RAG 系统应该输出什么?
A. 基于 React 18 的文档,尝试预测 React 19 的特性。
B. 调用 CC 自身的训练知识回答(如果 CC 知道的话)。
C. 明确回答“检索到的文档中不包含 React 19 的信息”。
答案
**答案:C(对于外挂 RAG 而言)**
**解析**:虽然 B 看起来对用户友好,但在 RAG 系统评估中,这被视为 **"External Hallucination"(外部幻觉)** 或 **"Leakage"(知识泄露)**。
作为外挂 RAG,核心职责是**基于给定的私有知识**回答。如果允许模型用自身知识,你将无法判断它下一次胡编乱造时是在引用文档还是在瞎编。可以通过 Prompt 设置 fallback 策略:“如果你知道答案但文档里没有,请明确说明‘文档中未提及,但根据我的常识...’”。
挑战题
Q4. 代码检索的“Hard Negative”
在构建代码检索的测试集时,直接随机抽取无关 Chunk 作为负样本是不够的。请设计一种生成“Hard Negative”(高难度干扰项)的策略,专门用于测试 RAG 对代码的辨识能力。
提示与答案
**提示**:
* 想想单元测试文件和实现文件的关系。
* 想想重载函数或不同版本的 API。
**答案策略**:
1. **同名函数/类**:选取不同模块下的同名函数(例如 `User.save()` 和 `File.save()`)。
2. **测试代码**:选取针对目标函数的**单元测试代码**作为干扰项。测试代码通常包含大量相同的关键词,向量相似度极高,但用户想要的是“实现”,而不是“测试用例”。
3. **旧版本代码**:如果仓库有 `v1/api.ts` 和 `v2/api.ts`,选取旧版本作为干扰项,测试 RAG 是否能识别路径权重或最版本。
**意义**:只有通过了 Hard Negative 测试的 RAG,才能在复杂的工程代码库中精准工作。
Q5. 评估“多跳推理”(Multi-hop Reasoning)
有些问题无法通过单一文档回答,例如:“对比模块 A 和模块 B 的错误处理机制有什么不同?”
这需要检索模块 A 的文档 + 模块 B 的文档,然后综合。
在 Recall 指标上,这类问题应该如何计算?单纯的 Recall = 1 还是 0 够用吗?
提示与答案
**提示**:
* 如果只找回了模块 A,答案能写出来吗?
* 全有或全无(All or Nothing)。
**答案策略**:
对于多跳问题,传统的 Recall 计算(找回了 2 个中的 1 个 = 50%)是**误导性**的。因为只拿到一半信息,CC 根本无法完成“对比”任务,回答质量是 0,而不是 50%。
**修正算法**:使用 **Coverage Score(覆盖分)**。
定义 Ground Truth 为集合 $G = \{d_A, d_B\}$。
只有当检结果 $R$ 满足 $G \subseteq R$ (即A和B都在结果里)时,该 Case 判为成功。否则视为失败。
这对 RAG 的召回策略(如 Query Rewrite 或迭代检索)提出了极高要求。
Q6. 上下文长度与精度的“倒U型曲线”
在 CC RAG 中,你发现随着 Top-K 的增加(喂给 CC 的文档变多),回答的准确率呈现先上升后下降的趋势。请解释原因,并提出一种缓解“下降”阶段的技术手段。
提示与答案
**原因**:
1. **信息过载/注意力分散**:无关的 Chunk 引入了噪音实体,干扰了模型的注意力机制。
2. **Lost in the Middle**:关键信息被淹没在长 Context 的中间,模型忽略了它。
**缓解手段**:
**重排(Reranking)** 是最有效的手段。
不要直接把检索出的 Top 50 喂给 CC。
流程:检索 Top 100 -> 用高精度 Rerank 模型给这 100 个打分 -> 截取分数最高的 Top 20 -> 喂给 CC。
这样既保证了 Recall(分母大),又保证了 Context 密度(只给最相关的)。
9. 常见陷阱与错误 (Gotchas)
1. 训练集污染 (Data Leakage)
- 错误:你直接拿 Embedding 模型训练过的数据(比如公开的 StackOverflow 数据)来做测试。
- 后果:分数虚高。当遇到真正的企业内部私有黑话时,效果断崖式下跌。
- 原则:测试集必须包含 Embedding 模型从未见过的私有业务数据。
2. 只有“正向”测试,没有“负向”测试
- 错误:测试集里的问题全都是“能回答”的。
- 后果:CC 学会了“强行回答”。当用户问一个无关问题时,CC 会把不相关的文档强行拼凑成答案。
- 修正:测试集中必须包含 10-20% 的不可回答问题(Unanswerable Queries),并要求 RAG 系统输出空的 Context,或要求 CC 回答“不知道”。
3. 忽视了引用(Citation)的“虚假链接”
- 错误只评估答案文本,不检查文件路径。
- 后果:CC 回答得头头是道,给出的路径是
src/utils/helper.ts,但你打开项目发现根本没这个文件,或者这个文件里没这个函数。
- 策略:评估脚本必须包含一步 Path Validation——检查 CC 引用的文件路径在当前 Git 树中是否存在。
4. 这里的 CC 指的是 Claude Code
- 特有坑点:Claude 模型倾向于过度礼貌和冗长。在评估“回答相关性”时,如果裁判模型(Judge)偏好简洁,可能会给 CC 的长篇大论打低分。
- 对策:在 Prompt 中明确约束 CC 的输出风格(如“简洁、直接、无废话”),并确保裁判模型的评分标准与该风格一致。