RAG上线评估指标体系:六大核心指标与压测实战全解析
导读:你的 RAG 系统跑通了,能回答问题了------但这够吗?召回率有没有90%?引用的来源对不对?该拒答的问题有没有硬答?上线前不评估,上线后用户骂。本文从 RAG 系统上线的六大核心指标出发,拆解每个指标的计算方式、达标标准和评测方法,附标准问答对设计思路和批量压测脚本,帮你建立从开发到上线的完整质量保障体系。
适合读者:
- RAG 系统开发完成、准备上线评估的开发者
- 需要设计 RAG 质量评估体系的工程师
- 准备 RAG 面试、需要回答"RAG怎么评估"的同学
- 需要向领导汇报"系统质量达标"的技术负责人
阅读收益:
- 掌握六大核心指标:Recall@K、MRR、引用准确率、拒答准确率、事实准确率、幻觉率
- 理解每个指标的达标标准和计算方式
- 学会设计标准问答对(含应答应拒答)
- 掌握批量压测脚本的编写方法
- 获得可直接落地的上线评估检查清单
目录
- 为什么需要系统化的评估体系
- 六大核心指标详解
- 指标一:Recall@K(召回率)
- 指标二:MRR(平均倒数排名)
- [指标三:Citation Precision(引用准确率)](#指标三:Citation Precision(引用准确率))
- [指标四:Refusal Accuracy(拒答准确率)](#指标四:Refusal Accuracy(拒答准确率))
- [指标五:Factual Accuracy(事实准确率)](#指标五:Factual Accuracy(事实准确率))
- [指标六:Hallucination Rate(幻觉率)](#指标六:Hallucination Rate(幻觉率))
- 标准问答对设计
- 批量压测脚本
- 上线评估检查清单
- 面试速答版
- 总结与延伸
- 文末互动
1. 为什么需要系统化的评估体系
1.1 直觉评估的陷阱
错误做法:
"我问了10个问题,看起来都答对了,可以上线了"
问题:
- 10个问题太少,不能代表真实分布
- "看起来对"不等于"事实对"
- 没测拒答场景(不该答的问题硬答了没发现)
- 没测边界场景(极端query的表现)
- 没量化指标,无法跟踪优化效果
1.2 系统化评估的价值
系统化评估 = 可量化 + 可对比 + 可回归
可量化:召回率85% vs 90%,清楚知道差多少
可对比:vector模式 vs hybrid模式,哪个更好
可回归:优化后重跑评测,确保没退化
2. 六大核心指标详解
| 指标 | 全称 | 含义 | 达标标准 | 计算方式 |
|---|---|---|---|---|
| Recall@K | 召回率 | 相关文档是否被召回 | >= 0.90 | 召回的相关文档 / 全部相关文档 |
| MRR | 平均倒数排名 | 正确答案排多靠前 | >= 0.75 | 1/排名 的平均值 |
| Citation Precision | 引用准确率 | 引用的来源对不对 | >= 0.95 | 正确引用 / 总引用 |
| Refusal Accuracy | 拒答准确率 | 不该答的是否拒答 | >= 0.95 | 正确拒答 / 应拒答总数 |
| Factual Accuracy | 事实准确率 | 回答中的事实对不对 | >= 0.90 | 正确事实 / 总事实 |
| Hallucination Rate | 幻觉率 | 编造事实的比例 | <= 0.10 | 幻觉事实 / 总事实 |
记忆口诀:召回九成排名七成五,引用拒答各九成五,事实九成幻觉一成。
3. 指标一:Recall@K(召回率)
3.1 定义
Recall@K = 召回的相关文档数 / 全部相关文档数
示例:
知识库中有5条关于"年假"的文档
检索Top-10召回了其中4条
Recall@10 = 4/5 = 0.80
3.2 为什么重要
Recall低 = 相关资料没被召回 → LLM看不到正确资料 → 可能瞎编
场景:
用户问"年假几天"
知识库里有"员工手册v2.4:年假10天"
但向量检索没召回这条 → LLM不知道 → 瞎编"年假5天"
3.3 计算方法
python
def recall_at_k(
retrieved_ids: list[str],
relevant_ids: list[str],
k: int,
) -> float:
"""计算Recall@K"""
retrieved_top_k = set(retrieved_ids[:k])
relevant = set(relevant_ids)
recalled = len(retrieved_top_k & relevant)
total_relevant = len(relevant)
return recalled / total_relevant if total_relevant > 0 else 0.0
4. 指标二:MRR(平均倒数排名)
4.1 定义
MRR = 平均倒数排名
对每条查询:
找到第一个相关文档的排名位置 rank
倒数 = 1/rank
MRR = 所有查询的倒数之和 / 查询总数
示例:
查询1:第一个相关文档排第2 → 1/2 = 0.5
查询2:第一个相关文档排第1 → 1/1 = 1.0
查询3:没有相关文档 → 0
MRR = (0.5 + 1.0 + 0) / 3 = 0.5
4.2 为什么重要
MRR衡量"正确答案排多靠前":
MRR=1.0 → 正确答案总是排第1
MRR=0.5 → 正确答案平均排第2
MRR=0.1 → 正确答案平均排第10
即使Recall@K很高(召回了),但如果正确答案排在第10,
而Top-5只送前5条给LLM,LLM还是看不到正确答案。
5. 指标三:Citation Precision(引用准确率)
5.1 定义
Citation Precision = 正确引用 / 总引用数
"正确引用" = 引用编号指向的文档确实包含该事实
示例:
回答:"年假10天[1]"
文档[1]内容:"员工每年享有10天带薪年假"
→ 引用正确
回答:"年假10天[1]"
文档[1]内容:"公积金提取需要身份证"
→ 引用错误!文档[1]根本没提年假
5.2 为什么重要
引用准确 = 可追溯 = 可验证 = 可信
引用错误 = 假溯源 = 用户发现被骗 = 系统不可信
场景:
模型回答"根据员工手册v2.4,年假10天[1]"
用户去查员工手册v2.4 → 发现根本没这句话
→ 用户信任崩塌
6. 指标四:Refusal Accuracy(拒答准确率)
6.1 定义
Refusal Accuracy = 正确拒答数 / 应该拒答的总数
"应该拒答" = 知识库中没有相关资料的问题
"正确拒答" = 系统如实说明"没有找到相关资料"
错误类型:
假阳性(False Positive):该答的拒答了
假阴性(False Negative):不该答的硬答了
拒答准确率关注:假阴性率要低(不该答的别硬答)
6.2 为什么重要
答错比不答更可怕:
不答 → 用户知道系统不知道 → 可以去问别人
硬答 → 用户拿到错误信息 → 可能做错误决策
场景:
用户问"公司年终奖怎么发"
知识库里没有年终奖政策
系统硬答:"根据公司规定,年终奖为2个月工资..."
→ 编的!用户信以为真,年底发现根本没有
7. 指标五:Factual Accuracy(事实准确率)
7.1 定义
Factual Accuracy = 正确事实数 / 回答中总事实数
"事实" = 回答中提到的具体信息(数字、日期、人名、政策条款等)
示例:
回答:"年假10天,需要入职满1年才能享受,申请需提前3天"
事实1:年假10天 ✓
事实2:入职满1年 ✓
事实3:提前3天申请 ✗(实际是提前1天)
Factual Accuracy = 2/3 = 0.67
7.2 评测方法
python
def check_factual_accuracy(answer: str, ground_truth: dict) -> float:
"""事实准确率人工/半自动评测"""
# 1. 提取回答中的关键事实
facts = extract_facts(answer)
# facts = ["年假10天", "入职满1年", "提前3天"]
# 2. 与标准答案比对
correct = 0
for fact in facts:
if fact in ground_truth["correct_facts"]:
correct += 1
elif fact in ground_truth["wrong_facts"]:
pass # 错误事实
else:
# 不确定,标记需人工审核
pass
return correct / len(facts) if facts else 1.0
8. 指标六:Hallucination Rate(幻觉率)
8.1 定义
Hallucination Rate = 幻觉事实数 / 回答中总事实数
幻觉事实 = 回答中编造的事实(资料中没有的)
Factual Accuracy + Hallucination Rate <= 1.0
(因为有些事实"不确定",可能资料有但没检索到)
8.2 为什么单独监控幻觉率
Factual Accuracy = 0.90 看起来不错
但 Hallucination Rate = 0.15 很危险
意味着:
90%的事实是对的
但15%的事实是编造的
用户更关心"有没有编造的",而不是"大部分对不对"
9. 标准问答对设计
9.1 设计原则
标准问答对需要覆盖:
1. 常见应回答问题(60%)
- 高频问题:年假、报销、请假
- 边界问题:数字精确匹配、多条件组合
2. 应该拒答的问题(30%)
- 知识库外的问题:公司CEO是谁
- 模糊问题:"那个政策"
- 无关问题:"今天天气怎么样"
3. 陷阱问题(10%)
- 诱导幻觉:"听说我们公司年终奖是5个月?"
- 矛盾信息:测试模型是否会强行合成
- 超长问题:测试上下文处理能力
总数:50-100条
9.2 问答对示例
python
"""标准问答对设计示例"""
from dataclasses import dataclass
from typing import Literal
@dataclass
class QAItem:
query: str # 用户问题
category: Literal[ # 问题类别
"normal", # 正常应回答
"refusal", # 应该拒答
"trap", # 陷阱问题
]
relevant_doc_ids: list[str] # 应该召回的文档ID(normal用)
should_refuse: bool # 是否应该拒答
expected_facts: list[str] # 回答中应包含的事实
ground_truth: str | None # 标准答案(用于人工评判)
# 正常问题
test_cases = [
QAItem(
query="年假几天",
category="normal",
relevant_doc_ids=["chunk_annual_leave_v24"],
should_refuse=False,
expected_facts=["10天", "带薪"],
ground_truth="根据员工手册v2.4,正式员工每年享有10天带薪年假。",
),
QAItem(
query="怎么申请公积金提取",
category="normal",
relevant_doc_ids=["chunk_gjj_01", "chunk_gjj_02"],
should_refuse=False,
expected_facts=["身份证", "公积金卡", "申请表"],
ground_truth=None,
),
# 应该拒答的问题
QAItem(
query="公司CEO是谁",
category="refusal",
relevant_doc_ids=[],
should_refuse=True,
expected_facts=[],
ground_truth=None,
),
QAItem(
query="今天天气怎么样",
category="refusal",
relevant_doc_ids=[],
should_refuse=True,
expected_facts=[],
ground_truth=None,
),
# 陷阱问题
QAItem(
query="听说我们公司年终奖是5个月工资,是真的吗?",
category="trap",
relevant_doc_ids=[],
should_refuse=True,
expected_facts=[],
ground_truth=None,
),
]
10. 批量压测脚本
python
"""RAG系统批量压测脚本"""
import asyncio
import json
from dataclasses import dataclass, asdict
from datetime import datetime
@dataclass
class EvaluationResult:
query: str
category: str
answer: str
retrieved_docs: list[str]
recall: float
mrr: float
refused: bool
should_refuse: bool
citation_correct: float
factual_score: float
class RAGEvaluator:
def __init__(self, rag_service):
self.rag = rag_service
async def evaluate_single(self, qa: QAItem) -> EvaluationResult:
"""评测单个问答对"""
# 1. 执行RAG查询
result = await self.rag.ask(qa.query)
# 2. 计算Recall@K
retrieved_ids = [d.metadata["chunk_id"] for d in result.retrieved_docs]
recall = self._recall_at_k(retrieved_ids, qa.relevant_doc_ids, k=10)
# 3. 计算MRR
mrr = self._mrr(retrieved_ids, qa.relevant_doc_ids)
# 4. 检查拒答
refused = result.refused
# 5. 引用准确率(简化版:检查引用编号是否有效)
citation_precision = self._check_citations(
result.answer, retrieved_ids
)
# 6. 事实准确率(需要人工标注或自动化提取)
factual_score = self._check_facts(
result.answer, qa.expected_facts
)
return EvaluationResult(
query=qa.query,
category=qa.category,
answer=result.answer,
retrieved_docs=retrieved_ids,
recall=recall,
mrr=mrr,
refused=refused,
should_refuse=qa.should_refuse,
citation_correct=citation_precision,
factual_score=factual_score,
)
async def evaluate_batch(
self,
test_cases: list[QAItem],
) -> dict:
"""批量评测,输出汇总报告"""
results = []
for qa in test_cases:
result = await self.evaluate_single(qa)
results.append(result)
# 汇总统计
normal_results = [r for r in results if r.category == "normal"]
refusal_results = [r for r in results if r.category == "refusal"]
metrics = {
"total": len(results),
"recall_at_10": sum(r.recall for r in normal_results) / len(normal_results),
"mrr": sum(r.mrr for r in normal_results) / len(normal_results),
"citation_precision": sum(r.citation_correct for r in results) / len(results),
"refusal_accuracy": sum(
1 for r in refusal_results if r.refused == r.should_refuse
) / len(refusal_results) if refusal_results else 0,
"factual_accuracy": sum(r.factual_score for r in normal_results) / len(normal_results),
}
# 计算幻觉率(1 - 事实准确率,近似)
metrics["hallucination_rate"] = 1 - metrics["factual_accuracy"]
return {
"metrics": metrics,
"details": [asdict(r) for r in results],
"timestamp": datetime.now().isoformat(),
}
def _recall_at_k(self, retrieved, relevant, k):
if not relevant:
return 1.0
recalled = len(set(retrieved[:k]) & set(relevant))
return recalled / len(relevant)
def _mrr(self, retrieved, relevant):
for i, doc_id in enumerate(retrieved):
if doc_id in relevant:
return 1.0 / (i + 1)
return 0.0
def _check_citations(self, answer, retrieved_ids):
import re
citations = re.findall(r'\[(\d+)\]', answer)
if not citations:
return 1.0 # 没有引用,不算错
valid = sum(1 for c in citations if int(c) <= len(retrieved_ids))
return valid / len(citations)
def _check_facts(self, answer, expected_facts):
if not expected_facts:
return 1.0
found = sum(1 for fact in expected_facts if fact in answer)
return found / len(expected_facts)
# 使用示例
async def main():
evaluator = RAGEvaluator(rag_service)
report = await evaluator.evaluate_batch(test_cases)
print("=" * 50)
print("RAG系统评测报告")
print("=" * 50)
for metric, value in report["metrics"].items():
print(f"{metric}: {value:.2%}")
# 保存报告
with open("rag_evaluation_report.json", "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
asyncio.run(main())
11. 上线评估检查清单
| 序号 | 检查项 | 达标标准 | 验证方法 |
|---|---|---|---|
| 1 | Recall@10 | >= 90% | 标准问答对批量跑 |
| 2 | MRR | >= 0.75 | 标准问答对批量跑 |
| 3 | 引用准确率 | >= 95% | 抽样检查引用与原文一致性 |
| 4 | 拒答准确率 | >= 95% | 测试应拒答问题 |
| 5 | 事实准确率 | >= 90% | 人工/半自动验证 |
| 6 | 幻觉率 | <= 10% | 1 - 事实准确率 |
| 7 | 标准问答对数量 | >= 50条 | 检查测试集规模 |
| 8 | 覆盖度 | 常见+拒答+陷阱 | 检查测试集分布 |
| 9 | 检索延迟 P95 | < 100ms | 压测工具 |
| 10 | 端到端延迟 P95 | < 3s | 监控面板 |
12. 面试速答版
RAG系统上线需要关注六大指标:Recall@K(召回率,要90%+)、MRR(正确答案排多靠前,要0.75+)、引用准确率(引用的来源对不对,要95%+)、拒答准确率(不该答的是否拒答,要95%+)、事实准确率(回答中的事实对不对,要90%+)、幻觉率(编造事实的比例,要<10%)。评估方法是准备50-100条标准问答对,包含应回答、应拒答、陷阱问题三类,跑批量脚本出指标,对比不同检索模式效果,定期回归测试确保优化不引入退化。记忆口诀:召回九成排名七成五,引用拒答各九成五,事实九成幻觉一成。
13. 总结与延伸
13.1 核心知识点回顾
六大指标:
Recall@K:召回率,>=90%
MRR:平均倒数排名,>=0.75
Citation Precision:引用准确率,>=95%
Refusal Accuracy:拒答准确率,>=95%
Factual Accuracy:事实准确率,>=90%
Hallucination Rate:幻觉率,<=10%
评估方法:
准备50-100条标准问答对(60%正常+30%拒答+10%陷阱)
批量跑评测脚本
对比不同检索模式
定期回归测试
记忆口诀:
召回九成排名七成五,引用拒答各九成五,事实九成幻觉一成
13.2 延伸方向
- 自动化评测:用LLM做评判员,自动判断回答质量
- A/B测试:线上灰度发布,对比不同版本的指标变化
- 用户反馈闭环:收集用户点赞/纠错反馈,动态更新评测集
- 多维度评测:除六大指标外,增加可读性、完整性、友好度等维度
14. 文末互动
你的 RAG 系统上线前做过系统化的评估吗?用了哪些指标------只看召回率,还是也测了拒答准确率和幻觉率?评论区聊聊你的评估经验。
思考题:如果"事实准确率"需要人工逐条核对,50条问答对可能需要2-3小时。你有什么办法可以半自动化地评测事实准确率,减少人工成本?欢迎在评论区讨论。
本文聚焦 RAG 系统上线的六大评估指标和压测实战。如果觉得有帮助,欢迎点赞收藏,后续会更新自动化评测和用户反馈闭环的进阶内容。