主题
14.17-RAG评估指标
要点
- RAG 系统有十几个可调参数,没有指标就没有优化方向——改了一个参数,你不知道系统是变好了还是变差了
- 评估分两个独立维度:检索质量(找到了对的文件没有)和生成质量(回答得好不好)——两者互不替代
- 检索指标各有侧重:Recall@K 看覆盖率,MRR 看首位命中,NDCG 看排序质量,Hit Rate 看兜底能力
- 生成指标看三件事:忠实度(不编造)、相关性(不答偏)、完整性(不遗漏)
- LLM-as-Judge 是当前最实用的生成评估方案,但有位置偏差、自我偏好和提示敏感三个系统性局限
- 从零搭建评估体系只需五步:建数据集 → 跑检索 → 跑生成 → 算指标 → 集成到 CI
1. 改了一个参数,然后呢
RAG 系统的可调参数比你想象的多:chunk size、overlap、embedding 模型、topK、相似度阈值、rerank 策略、上下文拼接方式、Prompt 模板。每个参数都可能影响最终回答质量,但影响方向不确定。
举个例子。你把 chunk size 从 1000 改成 500,直觉上更细粒度的切分应该检索更精确。实际跑下来,回答质量下降了 15%——切得太小,上下文丢失,检索反而找不到完整信息。
如果你没有评估体系,这个发现不会发生。你只会觉得「更细粒度更好」,然后带着一个退化的系统上线。
没有指标就没有优化方向。 这一篇解决两个问题:用什么指标衡量 RAG 质量,以及怎么从零搭建评估体系。
RAG 评估分两个独立维度——检索质量和生成质量。检索阶段要回答「找到了正确的文档没有」,生成阶段要回答「回答得好不好」。两个维度需要分别评估,因为检索满分不代表生成满分,反之亦然。
2. 检索质量:四个指标各自回答什么问题
检索评估的核心是拿「实际检索到的文档」和「应该检索到的文档」做对比。后者需要你提前标注——这在第 5 章讲。现在先理解每个指标的直觉含义和适用场景。
2.1 Recall@K:找到了多少正确答案
Recall@K 回答的问题是:在所有相关文档中,你找到了几个?
Recall@K = |{相关文档} ∩ {检索到的 topK}| / |{相关文档}|假设用户问题有 3 篇相关文档 [A, B, C],检索返回 top5 [A, C, D, E, F]:
Recall@5 = |{A, C}| / |{A, B, C}| = 2/3 ≈ 0.67Recall@K 关注覆盖率。RAG 的检索阶段目标是尽量多找到相关文档——漏掉关键信息,后面的 LLM 再生成也补不回来。这是 RAG 场景最核心的检索指标。
typescript
function recallAtK(
retrieved: string[],
relevant: string[],
k: number
): number {
const topK = retrieved.slice(0, k)
const found = topK.filter((id) => relevant.includes(id)).length
return found / relevant.length
}2.2 MRR:正确答案排在第几位
MRR 回答的问题是:用户看到的第一个结果是对的吗?
它取每个查询中第一个正确结果的排名倒数,然后求平均。正确答案排第 1 位得 1.0,排第 3 位得 0.33,排不到得 0。
问题 1:第一个正确结果在第 1 位 → 1/1 = 1.0
问题 2:第一个正确结果在第 3 位 → 1/3 ≈ 0.33
问题 3:第一个正确结果在第 2 位 → 1/2 = 0.5
MRR = (1.0 + 0.33 + 0.5) / 3 ≈ 0.61MRR 适合用户只看前几个结果的场景——问答系统、搜索建议。如果你的用户习惯翻好几页,MRR 的信息量就不够,应该看 NDCG。
typescript
function mrr(queries: Array<{ retrieved: string[]; relevant: string[] }>): number {
let sum = 0
for (const { retrieved, relevant } of queries) {
const firstCorrect = retrieved.findIndex((id) => relevant.includes(id))
if (firstCorrect >= 0) {
sum += 1 / (firstCorrect + 1)
}
}
return sum / queries.length
}2.3 NDCG:排序质量的整体度量
NDCG 回答的问题是:相关文档是不是都排在前面?
和 Recall 只看「有没有找到」不同,NDCG 给每个位置加了对数衰减——排在第 1 位的相关文档贡献最大,越往后贡献越小。同时支持多级相关性(0/1/2),可以区分「部分相关」和「完全相关」。
DCG@K = Σ (rel_i / log2(i + 1))
NDCG@K = DCG@K / IDCG@K
其中 IDCG 是理想排序下的 DCG,用于归一化到 [0, 1]NDCG 适合对排序质量要求高的场景——搜索结果页、文档推荐。计算比 Recall 复杂,但在排序敏感的场景更有区分度。
typescript
function ndcgAtK(
retrieved: string[],
relevant: string[],
k: number
): number {
const topK = retrieved.slice(0, k)
let dcg = 0
for (let i = 0; i < topK.length; i++) {
const rel = relevant.includes(topK[i]) ? 1 : 0
dcg += rel / Math.log2(i + 2)
}
const idealRelevant = Math.min(relevant.length, k)
let idcg = 0
for (let i = 0; i < idealRelevant; i++) {
idcg += 1 / Math.log2(i + 2)
}
return idcg > 0 ? dcg / idcg : 0
}2.4 Hit Rate:能不能答上
Hit Rate 回答的问题是:有多少查询至少找到了一个相关文档?
它不关心找到了几个,只关心「有没有」。一个查询只要 topK 里有至少一个相关文档就算命中。
typescript
function hitRate(
queries: Array<{ retrieved: string[]; relevant: string[] }>
): number {
const hits = queries.filter(({ retrieved, relevant }) =>
retrieved.some((id) => relevant.includes(id))
).length
return hits / queries.length
}Hit Rate 衡量的是系统的兜底能力——有多少问题你根本答不上来。如果你的业务场景对「答错」的容忍度远低于「答不上」,Hit Rate 是最直接的指标。
2.5 怎么选
| 指标 | 核心问题 | 最佳场景 | 局限 |
|---|---|---|---|
| Recall@K | 找到了多少相关文档 | RAG 检索,关注覆盖率 | 不关心排序 |
| MRR | 第一个正确答案在哪 | 问答系统,用户只看前几个 | 忽略第二个及之后的正确结果 |
| NDCG@K | 整体排序质量 | 搜索排序,排序敏感 | 计算复杂,需要多级相关性标注 |
| Hit Rate | 有没有找到任何相关文档 | 兜底率评估 | 不区分找到 1 个还是 10 个 |
RAG 场景的推荐组合:Recall@K + Hit Rate。 Recall 看覆盖率,Hit Rate 看兜底能力。如果你的检索结果有排序要求(比如展示前 3 条),加上 MRR 或 NDCG。
指标选好了,检索质量可以度量了。但检索找到了正确的文档,不代表 LLM 就能生成好的回答——接下来看生成质量怎么评估。
3. 生成质量:忠实度、相关性、完整性
生成质量评估的是 LLM 拿到检索上下文后产出的回答。它和检索质量是独立的——检索 Recall 满分,LLM 仍然可能编造信息、答非所问或遗漏关键内容。
3.1 忠实度:回答有没有编造
忠实度(Faithfulness)回答:回答中的每个事实,都能在检索到的上下文中找到依据吗?
这是 RAG 场景最重要的生成指标。忠实度低意味着幻觉——LLM 在回答里加入了上下文中不存在的信息。对于企业知识库场景,幻觉的代价往往比「答不上来」更高。
评估忠实度最直接的方式是 LLM-as-Judge:用另一个 LLM 检查回答中的事实是否能从上下文中推导出来。
3.2 相关性:有没有答非所问
相关性(Relevance)回答:回答和用户的原始问题匹配吗?
LLM 有时候会「答偏」——问题问的是退款流程,回答讲的是退款政策历史。相关性评估不关心事实是否来自上下文,只关心回答是否直接回应了用户的问题。
3.3 完整性:有没有遗漏关键信息
完整性(Completeness)回答:回答覆盖了问题的所有方面吗?
一个问题可能有多个维度。比如「退款怎么申请,多久到账」包含两个子问题——完整性评估会检查回答是否两方面都覆盖了。
三个维度的代码实现思路相似,核心都是 LLM-as-Judge 打分,区别在于 Prompt 的评估标准。统一实现留到第 4 章。
三个维度正交:一个回答可以忠实但不完整(只说了一部分但没编造),相关但不忠实(回答了问题但编造了细节),完整但不相关(信息全面但答非所问)。 分开评估才能定位具体问题出在哪个环节。
这三个指标都依赖 LLM 来做评估。这个方案可行吗?有什么坑?
4. LLM-as-Judge:方案与局限
用 LLM 评估 LLM 的输出,是当前 RAG 评估最实用的方案。思路很直接:把上下文、问题和回答拼在一起,让一个评估模型按指定标准打分。
4.1 统一实现
三个生成指标可以用同一个评估函数实现,区别只在评估标准和 Prompt:
typescript
type EvalDimension = 'faithfulness' | 'relevance' | 'completeness'
const prompts: Record<EvalDimension, string> = {
faithfulness: `评估回答是否完全基于提供的上下文。
- 1.0:所有事实都能在上下文中找到依据
- 0.5:大部分有依据,但有些细节是推断的
- 0.0:大部分信息在上下文中没有依据`,
relevance: `评估回答和用户问题的相关性。
- 1.0:直接且完整地回答了问题
- 0.5:部分相关,但没有直接回答
- 0.0:完全无关或答非所问`,
completeness: `基于上下文,评估回答是否覆盖了问题的所有方面。
- 1.0:完整覆盖了问题涉及的所有方面
- 0.5:回答了部分方面
- 0.0:遗漏了关键信息`,
}
async function evaluateGeneration(
question: string,
answer: string,
contexts: string[],
dimension: EvalDimension
): Promise<number> {
const input = dimension === 'faithfulness'
? `上下文:\n${contexts.join('\n---\n')}\n\n回答:${answer}`
: dimension === 'completeness'
? `问题:${question}\n上下文:${contexts.join('\n')}\n回答:${answer}`
: `问题:${question}\n回答:${answer}`
const response = await llm.chat({
messages: [
{
role: 'system',
content: `${prompts[dimension]}\n\n${input}\n\n只输出 0-1 的数字评分。`,
},
],
})
return parseFloat(response.content)
}4.2 LLM-as-Judge 的三个系统性偏差
LLM 评估器不是客观裁判——它自己也是一个 LLM,继承了 LLM 的所有偏差。
位置偏差(Position Bias)。 当评估器比较多个候选回答时,它倾向于给排在前面或后面的回答更高分。中间位置的回答容易被低估。
自我偏好(Self-Enhancement Bias)。 评估器倾向于给与自己风格相似的回答更高分。如果你用 GPT-4 生成回答,又用 GPT-4 评估,评分会系统偏高——不是因为回答更好,而是因为风格一致。
提示敏感(Prompt Sensitivity)。 改一下评估 Prompt 的措辞,评分可能波动 0.1-0.2。评估器对评估标准的理解和你的意图不一定一致。
4.3 四种改进方向
偏差没法消除,但可以缓解:
- 使用不同的模型做生成和评估。 用 Claude 生成、GPT-4 评估,或反过来。打破自我偏好。
- 多次评估取平均。 同一个回答评估 3 次,取中位数。降低随机波动。
- 明确的评分标准加 few-shot 示例。 在评估 Prompt 里给 2-3 个标注好的示例,锚定评估器的理解。
- 人工抽检校准。 每 100 个自动评估结果抽 10 个人工复核,检查评估器和人工的一致性。如果偏差超过阈值,调整 Prompt 或换评估模型。
4.4 RAGAS:开箱即用的整合方案
RAGAS 是一个专门为 RAG 设计的评估框架,把忠实度、相关性、上下文精确率、上下文召回率整合在一起,不需要你自己写评估 Prompt:
typescript
import { evaluate } from 'ragas'
const results = await evaluate({
question: ['退款多久到账?', '如何修改密码?'],
answer: ['退款一般在 3-5 个工作日到账。', '在设置页面可以修改密码。'],
contexts: [
['退款一般在 3-5 个工作日到账,退回原支付账户。'],
['在「设置」>「账户安全」页面可以修改密码。'],
],
ground_truth: [
'退款一般在 3-5 个工作日到账。',
'在设置页面的账户安全选项中可以修改密码。',
],
metrics: ['faithfulness', 'answer_relevancy', 'context_precision', 'context_recall'],
})
// { faithfulness: 0.92, answer_relevancy: 0.88, context_precision: 0.85, context_recall: 0.78 }RAGAS 的价值在于标准化——你不用自己定义评估 Prompt,社区已经验证过。但它的底层仍然是 LLM-as-Judge,第 4.2 节的偏差同样存在。
有了评估方法和工具,下一步是准备评估的基础设施——标注数据。
5. 评估数据集:标注与生成
所有指标的计算都需要一个前提:你知道每个问题的「正确答案」是什么。对于检索指标,你需要知道每个问题应该检索到哪些 chunk;对于生成指标,你需要知道标准答案。
5.1 数据结构
typescript
type EvaluationCase = {
question: string
relevantChunkIds: string[] // 检索评估用:应该检索到的 chunk
groundTruthAnswer?: string // 生成评估用:标准答案
category?: string // 用于按类别分析薄弱环节
}
const evalDataset: EvaluationCase[] = [
{
question: '退款多久到账?',
relevantChunkIds: ['doc-refund-chunk-2'],
groundTruthAnswer: '退款一般在 3-5 个工作日到账。',
category: '退款',
},
{
question: '如何申请退款?',
relevantChunkIds: ['doc-refund-chunk-0', 'doc-refund-chunk-1'],
groundTruthAnswer: '在订单页面点击「申请退款」,填写退款原因后提交。',
category: '退款',
},
]5.2 两种构建方式
人工标注——最准确,但成本最高。适合核心业务场景、高频问题。标注人员需要同时理解业务和文档结构。
LLM 辅助生成——成本低,速度快,适合快速起量。让 LLM 根据文档内容生成 QA 对,然后人工抽检:
typescript
async function generateEvalDataset(documents: Document[]): Promise<EvaluationCase[]> {
const cases: EvaluationCase[] = []
for (const doc of documents) {
const response = await llm.chat({
messages: [{
role: 'system',
content: `根据以下文档内容,生成 3-5 个用户可能会问的问题及答案。格式:
Q: 问题
A: 答案
文档内容:
${doc.content}`,
}],
})
const qaPairs = parseQAPairs(response.content)
for (const qa of qaPairs) {
cases.push({
question: qa.question,
relevantChunkIds: [doc.id],
groundTruthAnswer: qa.answer,
})
}
}
return cases
}最低数量建议:50-100 个测试用例。 低于 50 个,评估结果的统计意义不足——10 个用例的平均分波动太大,无法可靠区分改进和退化。如果资源有限,宁可少做几个参数对比,也要保证评估集的规模。
5.3 标注质量检查
标注完之后做一轮交叉验证:随机抽 20% 的用例,让另一个人(或另一个 LLM)重新标注,计算标注一致性(Cohen's Kappa > 0.7 算合格)。一致性低说明标注标准不清晰,需要重新校准。
数据集建好了,接下来把所有东西串起来。
6. 从零搭建评估体系
不需要一步到位,按这五步走。
6.1 第一步:收集评估用例
从真实用户对话、客服工单或 FAQ 中提取 50-100 个 QA 对。按业务类别分组(退款、账户、产品使用……),每组至少 10 个。用 LLM 辅助生成补充用例,人工抽检标注质量。
6.2 第二步:跑检索,记录指标
对每个用例,记录检索到的 topK 文档 ID,计算 Recall@K 和 Hit Rate:
typescript
for (const testCase of dataset) {
const queryVector = await embed(testCase.question)
const results = await vectorDB.search(queryVector, { topK: 5 })
const retrievedIds = results.map((r) => r.id)
const recall = recallAtK(retrievedIds, testCase.relevantChunkIds, 5)
const hit = retrievedIds.some((id) => testCase.relevantChunkIds.includes(id))
}6.3 第三步:跑生成,记录指标
用检索到的上下文拼接 Prompt,让 LLM 生成回答,再评估忠实度和相关性:
typescript
const context = results.map((r) => r.content).join('\n---\n')
const answer = await generateAnswer(testCase.question, context)
const faithfulness = await evaluateGeneration(
testCase.question, answer, results.map((r) => r.content), 'faithfulness'
)
const relevance = await evaluateGeneration(
testCase.question, answer, [], 'relevance'
)6.4 第四步:看分布,不要只看平均
平均 Recall 0.8 可能是两种情况:80% 的问题 Recall 1.0 + 20% 的问题 Recall 0,或者所有问题都在 0.7-0.9 之间。前者说明有一类问题完全检索不到——需要按类别分析,找到薄弱环节。
typescript
const byCategory = groupBy(results, (r) => r.category)
for (const [category, cases] of Object.entries(byCategory)) {
const avgRecall = cases.reduce((sum, r) => sum + r.recall, 0) / cases.length
console.log(`${category}: n=${cases.length}, recall=${avgRecall.toFixed(2)}`)
}
// 退款: n=25, recall=0.92
// 账户: n=20, recall=0.65 ← 薄弱环节6.5 第五步:集成到 CI
评估不是一次性的——数据在更新,模型在更换,需要定期跑。集成到 CI/CD,设阈值告警:
yaml
# .github/workflows/rag-eval.yml
name: RAG Evaluation
on:
schedule:
- cron: '0 2 * * 1' # 每周一凌晨 2 点
workflow_dispatch:
jobs:
evaluate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install
- run: pnpm run rag:eval
- name: Check thresholds
run: |
node scripts/check-eval.js --metric recall --threshold 0.8
node scripts/check-eval.js --metric faithfulness --threshold 0.85阈值怎么定?先跑一轮基线,把当前分数记录下来。然后设阈值为基线的 90%——低于基线 90% 就告警,说明有退化。随着优化推进,逐步提高阈值。
7. 指标权衡与评估陷阱
7.1 指标之间的权衡
Recall 和延迟的权衡。 topK 越大,Recall 越高,但检索延迟和 LLM 上下文拼接成本也越高。topK=5 到 topK=10 可能只提升 3% 的 Recall,但延迟和成本几乎翻倍。找到边际收益递减的拐点。
Recall 和精确率的权衡。 检索更多文档提高了覆盖率,但也引入了更多噪声。噪声上下文会干扰 LLM 生成——模型需要在无关信息中定位关键内容,忠实度可能反而下降。
评估成本和频率的权衡。 每次完整评估需要大量 LLM 调用(生成回答 + 评估回答)。100 个用例 × 3 个评估维度 = 300 次 LLM 调用。如果每天跑一次,月度成本不低。折中方案:核心指标每天跑,完整评估每周跑。
7.2 五个常见陷阱
评估数据集太小。 10 个用例的评估没有统计意义。指标波动本身就是噪声,你无法区分「改进」和「随机波动」。最低 50 个用例。
过拟合评估集。 反复调参直到评估分数好看,但实际用户体验没改善。和机器学习过拟合一模一样的问题。解法:保留一个独立的测试集,调参过程中不看,只在最终验证时用一次。
只看平均分数。 前面说过,平均掩盖分布。一定要按类别看,找到薄弱类别。
LLM-as-Judge 当绝对标准。 评估器本身有偏差(第 4.2 节)。它给出 0.9 的忠实度分数不代表真的没有幻觉——只是评估器没检测到。人工抽检是必要的校准手段。
忽略延迟和成本。 检索质量提升了 5%,但延迟翻倍、成本翻三倍。这不是改进,是倒退。评估体系应该同时记录延迟和成本,做综合判断。
7.3 指标的边界条件
评估指标也有不适用的时候。
文档质量本身就差。 如果知识库里充斥着过时、矛盾或不完整的文档,检索指标再高也没用——找到了正确的 chunk,但 chunk 本身就是错的。评估指标度量的是 RAG 管道的技术质量,不是知识库的内容质量。
问题超出系统能力范围。 用户问了一个知识库根本没覆盖的问题,系统回答「我不知道」。这在忠实度上可能得高分(没编造),但在相关性上得低分(没回答问题)。标注数据集时应该包含「无法回答」类型的问题,并为评估标准做特殊处理。
指标之间的冲突无法完全消除。 没有一个数字能概括 RAG 系统的全部质量。评估体系的价值不在于给你一个满分答案,而在于让你知道改了一个参数之后,哪些方面变好了,哪些方面变差了,以及你是否接受这个取舍。
到这里,你已经知道怎么度量 RAG 系统的质量了。下一步是把这些组件串成 API——检索、生成、评估,怎么设计成可调用的接口,这是第 18 篇的内容。