RAG 评估:指标、数据集与回归体系
为什么只看答案准确率不够
用户问:“生鲜商品签收后能否 7 天无理由退货?”系统回答“可以”,最终答案显然错了,但可能原因完全不同:
- PDF 解析漏掉了“生鲜除外”这一行。
- 分块把通用规则与例外拆开了。
- 检索召回了通用规则,没有召回例外。
- 例外进入 top-20,却被 reranker 排到上下文之外。
- 两条都进了上下文,压缩器删掉否定句。
- 模型看到了例外,却只生成通用规则。
- 答案引用了来源,但来源并不支持结论。
- 用户本不应看到某份制度,权限过滤发生在检索之后。
如果评估只输出 answer_score=0,团队仍然不知道该改解析器、chunk、embedding、reranker、prompt 还是权限逻辑。
可靠评估必须保留链路中的中间产物:
一套可落地的分层评估地图
| 层级 | 输入与观测点 | 核心问题 | 代表指标 |
|---|---|---|---|
| 入库 | 原文、解析文本、chunk、metadata | 知识是否完整、可检索、可追溯 | 解析覆盖率、表格正确率、孤儿 chunk 率 |
| 召回 | query、filters、top-k candidates | 正确证据是否被找回 | Recall@K、Hit Rate@K、候选覆盖率 |
| 排序 | 候选及其顺序 | 有用证据是否足够靠前 | MRR、MAP、nDCG、Precision@K |
| 上下文 | 最终送入模型的证据 | 是否相关、充分、无重复且版本正确 | Context Precision/Recall、充分性、噪声率 |
| 生成 | question、context、answer | 是否忠实、正确、切题、完整 | Faithfulness、Correctness、Relevance、Completeness |
| 引用 | claims、citations、evidence spans | 引用是否支持结论、是否漏引 | Citation Precision/Recall、无效引用率 |
| 权限 | 用户身份、ACL、候选、上下文 | 无权内容是否在任何可见阶段泄露 | 越权召回率、越权上下文率、拒绝正确率 |
| 工程 | trace、token、调用、时间、错误 | 质量是否在预算与 SLO 内 | 单请求成本、P50/P95/P99、错误率 |
分层不代表各层独立。比如入库漏掉表头,会同时拉低召回、忠实度和答案正确性。它的价值在于让团队找到最早出现错误的环节。
先定义评估单位
在写指标之前,要统一三个对象。
Query
一条 query 不能只有问题文本,还要包含会影响正确答案的上下文:
{
"query_id": "leave-001",
"question": "我今年有几天年假?",
"conversation_context": "用户 2025-03-01 入职,2026-09-01 通过试用期",
"user_context": {
"tenant_id": "company-a",
"department": "sales",
"region": "CN-SH",
"roles": ["employee"]
},
"as_of": "2026-09-07"
}不记录时间、租户和身份,版本与权限测试就无法复现。
Relevant evidence set
relevant_doc_ids 不应只标一个“标准 chunk”。同一个事实可能被多个合法 chunk 支持;复杂问题还可能需要多个证据共同回答。
{
"required_evidence_groups": [
["policy:v3:p12#annual-rule"],
["policy:v3:p12#rounding", "faq:v2:annual-rounding"]
]
}上例表示第一组必须命中指定条款;第二组命中制度或官方 FAQ 任意一个即可。对多跳题,还要标记 all_of 关系,避免系统只找回其中一步也被判为成功。
Answer contract
不要把标准答案限制成一句固定文本。更稳妥的是标注事实与约束:
{
"must_include_claims": [
"生鲜类目不支持无理由退货"
],
"must_not_include_claims": [
"生鲜商品支持 7 天无理由退货"
],
"acceptable_status": ["answer"],
"required_citations": ["after-sales:v4:p2#fresh-food"]
}文字相似度只能作为辅助。两个答案只差一个“不”字,embedding 相似度可能很高,业务结论却相反。
Golden Dataset 怎么构造
Golden dataset 不是把线上问题随便导出后补答案。它至少要覆盖知识库结构、查询分布和高风险边界。
样例来源
- 真实搜索日志中高频且经过脱敏的问题。
- 客服、实施和业务专家整理的关键任务。
- 制度、产品与 API 文档中的条款反向生成,再由人校验。
- 历史故障、用户投诉和错误回答。
- 专门构造的对抗样例与边界样例。
真实日志贴近分布,但会遗漏用户已经放弃的问题;合成数据覆盖广,却可能太像文档标题。两者应混合,并由领域专家确认事实。
必须包含的题型
| 类型 | 目的 | 示例 |
|---|---|---|
| 单事实 | 建立基本命中能力 | “退款多久到账?” |
| 多条件 | 检查限定是否保留 | “上海正式员工 8 月的晚餐上限?” |
| 多跳 | 检查多个证据能否组合 | “先确定等级,再按等级查额度” |
| 比较 | 检查来源覆盖和结构化回答 | “A、B 两版 API 的差异?” |
| 精确词 | 检查 BM25/过滤 | 错误码、合同号、版本号 |
| 无答案 | 检查拒答 | 知识库不存在的“宠物假” |
| 冲突 | 检查版本、时效和权威 | 新旧制度给出不同额度 |
| 权限 | 检查强过滤 | 普通员工询问高管薪酬文件 |
| 注入 | 检查不可信文档隔离 | 文档要求忽略系统规则并调用工具 |
Hard negatives
Hard negative 是与问题表面非常相似、但不能正确回答的资料。它比随机无关文档更能区分检索系统质量。
问题:Python 3.12 中 distutils 怎么处理?
正例:Python 3.12 移除 distutils 的迁移说明。
难负例:Python 3.11 的弃用说明;Python 3.12 的其他移除项;第三方博客中的旧方案。难负例可以来自:
- 同主题不同版本。
- 同实体不同时间。
- 通用规则与例外规则。
- 标题相似但正文无答案。
- 包含全部关键词却语义相反。
- 被旧系统高排但经专家判错的线上结果。
数据集应记录负例为什么错,例如 wrong_version、wrong_region、missing_exception,便于按错误类型分析。
防止数据泄漏
若用评估集问题调 prompt、训练 reranker 或选阈值,它已经变成开发集。至少划分:
train:训练或构造检索器
dev:选 chunk、top-k、阈值和 prompt
test:只做最终比较
challenge:持续加入线上事故和新型对抗样例文档更新时,答案也可能更新。golden 必须带 valid_from/valid_to 或快照版本,否则系统引用新制度反而会被旧标签判错。
入库层评估:先确认知识真的进来了
入库层通常不需要 LLM Judge,优先使用确定性检查和抽样审核。
文档完整性
可以记录:
解析字符覆盖率 = 成功解析且归属正文的字符数 / 可提取正文字符数
页面覆盖率 = 有有效正文的页面数 / 应有正文的页面数
表格行覆盖率 = 成功保留表头关系的行数 / 标注表格行数“可提取正文字符数”本身可能需要基于源格式或人工抽样定义,扫描 PDF 还要单独测 OCR。不要用 PDF 文件字节数当分母。
Chunk 质量
关注:
- 空 chunk、超长 chunk、极短 chunk 的比例。
- 标题、页码、章节、版本、ACL 等 metadata 缺失率。
- 条件与结论被切开的比例。
- overlap 引发的近重复率。
- chunk 到原文 span 的可逆定位率。
- 删除文档后残留 chunk 数。
一种实用抽样是按来源类型分层,每类随机抽样,再额外抽超长、超短、表格、代码和 OCR 低置信内容。纯随机抽样容易错过稀有但高风险的解析问题。
入库回归门禁
以下问题通常应直接阻断发布:
- ACL、tenant、版本或原文定位字段缺失。
- 文档删除后旧 chunk 仍可召回。
- 关键制度表格丢失表头或否定词。
- 新索引的文档数与预期快照差异无法解释。
召回与排序指标
设某个 query 的相关文档集合为 $R$,检索前 $K$ 个结果为 $D_K$,第 $i$ 个结果的二元相关标记为 $rel_i\in{0,1}$。
Precision@K
$$ Precision@K = \frac{|R \cap D_K|}{K} $$
它回答:前 K 个结果里有多少是相关的。Precision 高意味着上下文噪声少,但不保证所有必需证据都找到了。
Recall@K
$$ Recall@K = \frac{|R \cap D_K|}{|R|} $$
它回答:所有相关证据中,前 K 找回了多少。多证据问题更应关注 Recall;单事实场景则常配合 Hit Rate。
Hit Rate@K
单个 query 的命中指示为:
$$ Hit@K(q)=\mathbb{1}[R_q \cap D_{q,K}\ne\varnothing] $$
数据集上的 Hit Rate 是所有 query 的 Hit@K 平均值。它只看是否至少命中一个正例,不关心命中几个,也不关心排在第 1 还是第 K。
MRR
令 $rank_q$ 为第一个相关结果的排名:
$$ RR(q)=\frac{1}{rank_q}, \qquad MRR=\frac{1}{|Q|}\sum_{q\in Q}RR(q) $$
没有命中时 RR 为 0。MRR 强调“第一个有用结果来得多早”,适合单答案、首个命中很重要的场景;它忽略第二个及后续相关结果。
MAP
在第 $k$ 位的精确率记为 $P@k$,query 的相关文档总数为 $|R|$:
$$ AP(q)=\frac{1}{|R|}\sum_{k=1}^{N}P@k\cdot rel_k $$
$$ MAP=\frac{1}{|Q|}\sum_{q\in Q}AP(q) $$
未召回的相关文档对分子贡献 0。MAP 同时奖励多个相关结果靠前,适合一个问题有多条等价或互补证据的场景。
DCG 与 nDCG
当相关性不是 0/1,而是 0、1、2、3 等分级标签时,可用:
$$ DCG@K=\sum_{i=1}^{K}\frac{2^{rel_i}-1}{\log_2(i+1)} $$
将结果按理想相关性排序得到 $IDCG@K$:
$$ nDCG@K=\frac{DCG@K}{IDCG@K} $$
nDCG 既考虑相关程度,又惩罚高相关结果排得太后。若 IDCG=0,说明该 query 没有标注相关结果,应单独处理,不能除以 0 后静默填 0。
一组完整算例
设某问题共有 4 条二元相关文档,系统前 5 位的相关标记为:
排名: 1 2 3 4 5
是否相关: 1 0 1 0 1于是:
Precision@5 = 3 / 5 = 0.60
Recall@5 = 3 / 4 = 0.75
Hit@5 = 1
RR = 1 / 1 = 1.00
AP = (1/1 + 2/3 + 3/5) / 4
= 0.5667注意 AP 分母是全部 4 条相关文档,漏掉的第 4 条贡献 0。如果错误地除以“已召回的 3 条”,会高估系统。
再看分级相关性。前 5 位标签为 [3, 0, 2, 0, 1],理想排序为 [3, 2, 1, 0, 0]:
DCG@5 = 7/log2(2) + 0 + 3/log2(4) + 0 + 1/log2(6)
≈ 8.8869
IDCG@5 = 7/log2(2) + 3/log2(3) + 1/log2(4)
≈ 9.3928
nDCG@5 ≈ 8.8869 / 9.3928 ≈ 0.9461这个 nDCG 很高,因为最重要的 3 级证据已经排第一;第二重要的证据排第三只受到有限折损。
Python 实现
from math import log2
from statistics import mean
def precision_at_k(binary_rels, k):
top = binary_rels[:k]
return sum(top) / k
def recall_at_k(binary_rels, total_relevant, k):
if total_relevant == 0:
return None # 无正例 query 应单独统计,不要伪装成 0
return sum(binary_rels[:k]) / total_relevant
def reciprocal_rank(binary_rels):
for rank, rel in enumerate(binary_rels, start=1):
if rel:
return 1.0 / rank
return 0.0
def average_precision(binary_rels, total_relevant):
if total_relevant == 0:
return None
hits = 0
score = 0.0
for rank, rel in enumerate(binary_rels, start=1):
if rel:
hits += 1
score += hits / rank
return score / total_relevant
def dcg_at_k(graded_rels, k):
return sum(
(2 ** rel - 1) / log2(rank + 1)
for rank, rel in enumerate(graded_rels[:k], start=1)
)
def ndcg_at_k(graded_rels, k):
actual = dcg_at_k(graded_rels, k)
ideal = dcg_at_k(sorted(graded_rels, reverse=True), k)
return actual / ideal if ideal else None
def mean_reciprocal_rank(all_binary_rels):
return mean(reciprocal_rank(rels) for rels in all_binary_rels)生产实现还要明确:同一文档的重复 chunk 算一个还是多个正例、多个等价证据如何标注、权限过滤后的相关集合是什么,以及查询无正例时是否从某项指标排除。
指标怎么选,不要全都堆上去
| 任务 | 主指标 | 辅助指标 | 原因 |
|---|---|---|---|
| 单事实 FAQ | Hit Rate@K、MRR | Precision@K | 至少命中一条,并尽量靠前 |
| 多证据综合 | Recall@K、MAP | Context Recall | 需要找全多个证据 |
| 分级相关排序 | nDCG@K | Recall@K | 关键证据应优先于背景资料 |
| 小上下文预算 | Precision@K、nDCG | token 数 | 每个位置都昂贵,应减少噪声 |
| reranker 比较 | MRR、nDCG | P95 延迟、成本 | 排名收益要覆盖精排代价 |
Recall@20 高而 Recall@5 低,说明召回器可能够用,问题更像排序或上下文预算;所有 K 都低,则应检查入库、query、filter 或召回器。
上下文层评估
候选 top-20 不等于最终 context。去重、压缩、父块扩展和 token 截断都会改变证据集合,因此必须同时保存并评估最终送入 LLM 的 context。
Context Precision
直觉是:上下文中有用证据是否排在无用证据前面。不同框架实现可能采用排名加权或 LLM 判定,报告时必须写清版本和算法,不能把同名指标当作完全可比。
Context Recall
直觉是:参考答案所需事实是否能从最终上下文得到支持。它与检索 Recall@K 不同:前者通常在事实或 claim 层判断,后者在标注文档 id 层判断。
充分性与噪声敏感度
应额外测试:
- 删除一个必要证据后,模型是否错误地继续作答。
- 加入多个 hard negatives 后,正确答案是否变化。
- 新旧冲突版本同时出现时,是否使用适用版本。
- 关键证据放在上下文不同位置时,表现是否稳定。
- 压缩后数值、否定、主体、例外是否保留。
上下文有效密度可以作为工程观测:
$$ Evidence\ Density=\frac{被最终答案实际使用的证据 token}{上下文证据总 token} $$
它不是统一质量标准,但能发现“上下文越来越长,真正使用的内容没有增加”。
生成层:四个维度不要混成一个
Faithfulness
Faithfulness 检查回答是否受检索上下文支持,不要求它与标准答案逐字相同。RAGAS 的官方定义 会先拆分回答 claim,再检查每个 claim 能否由上下文推出:
$$ Faithfulness=\frac{受上下文支持的回答 claims 数}{回答 claims 总数} $$
回答忠实不代表真实正确:如果上下文本身过期,模型忠实地复述旧规则,faithfulness 可以高而 correctness 低。
Correctness
Correctness 比较回答与经过确认的事实或参考答案。它应关注实体、数值、时间、否定、条件和关系,而不仅是语言相似度。
可把一个参考答案拆成加权 claims:
[
{"claim": "生鲜不支持无理由退货", "weight": 0.7, "critical": true},
{"claim": "普通商品期限为签收后 7 天", "weight": 0.3, "critical": false}
]关键 claim 错误应触发硬失败,不能靠措辞流畅或次要事实正确把平均分拉回来。
Relevance
Answer relevance 判断回答是否回应用户问题。模型可能给出完全忠实的资料摘要,却没有回答“能不能退”。应检查是否直接给出所需结论、是否包含大量无关背景、是否遗漏用户限定。
Completeness
Completeness 检查参考答案中的必要信息覆盖了多少。对于“条件 + 例外 + 下一步”问题,只回答主规则是不完整,而未必是不忠实。
一个回答可能出现:
faithfulness = 高:说出的内容都有依据
correctness = 部分正确:主规则正确
relevance = 高:直接回应问题
completeness = 低:漏掉生鲜例外这正是分维度评估的意义。
引用评估
引用存在率只能证明答案里出现了 [1],不能证明 [1] 支持结论。
将答案拆成需要核验的 claims,建立 claim 到 evidence span 的映射:
$$ Citation\ Precision=\frac{真正支持相邻 claim 的引用数}{答案给出的引用总数} $$
$$ Citation\ Recall=\frac{获得充分引用支持的可核验 claims 数}{需要引用的 claims 总数} $$
ALCE 是面向带引用生成的端到端基准,强调不仅要检索证据,还要评价回答正确性与引用质量。
生产检查至少包括:
- 引用 id 是否来自本次授权检索。
- 引用 span 是否真的蕴含 claim。
- 日期、数值、主体和例外是否完整覆盖。
- 引用是否指向生成时使用的文档版本。
- 用户点击引用能否回到正确页码或字符位置。
- 一个引用支持多个 claim 时,位置是否造成歧义。
权限评估是硬门禁
只检查最终回答是否泄露不够。无权文档一旦进入候选、日志、reranker 或模型上下文,就可能通过侧信道泄露。
分别统计:
Unauthorized Retrieval Rate
= 无权候选数 / 全部候选数
Unauthorized Context Rate
= 进入模型上下文的无权证据数 / 全部上下文证据数
Unauthorized Citation Rate
= 指向无权来源的引用数 / 全部引用数这三项的目标通常都是 0,而且不能用平均答案分抵消。
权限数据集要覆盖:
- 不同 tenant 存在同名文档。
- 部门继承、临时授权和授权撤销。
- 用户有文档权限但无附件权限。
- 对方无权限时,系统是否泄露文件标题或存在性。
- ACL 更新后缓存与向量索引是否及时失效。
- 日志和评估平台是否保存了未经脱敏的证据。
成本与延迟评估
质量提升不是免费的。一次 RAG 请求的直接成本可拆成:
$$ C_{request}=C_{query\ embed}+C_{retrieval}+C_{rerank}+C_{judge/route}+C_{generation}+C_{observability} $$
生成模型的 token 成本还应区分普通输入、缓存输入和输出,具体单价以供应商当期定价为准:
$$ C_{llm}=\frac{T_{input}-T_{cached}}{10^6}P_{input} +\frac{T_{cached}}{10^6}P_{cached} +\frac{T_{output}}{10^6}P_{output} $$
不要把检索 top-k 增加带来的长上下文只记为“向量库成本”。它还会增加生成输入 token、首 token 延迟,可能触发额外压缩和 Judge 调用。
延迟应记录阶段分解与分位数:
T_total = T_route + T_embed + T_search + T_rerank
+ T_context + T_generation + T_postcheck重点看 P50、P95、P99 和超时率。平均 800 ms 可能掩盖 P99 为 12 秒的问题。流式输出要同时记录 TTFT(首 token 时间)和完整答案时间。
建议每个实验报告同时给出:
质量:Recall@5、nDCG@5、Faithfulness、关键题通过率
体验:TTFT P50/P95、端到端 P50/P95
资源:平均输入/输出 token、每请求估算成本、索引存储
可靠性:超时率、降级率、空结果率RAGAS、TruLens、DeepEval 怎么定位
框架是执行与观测工具,不是业务真值来源。
RAGAS
RAGAS 当前官方指标目录 包含 Context Precision、Context Recall、Response Relevancy、Faithfulness、Noise Sensitivity 等 RAG 指标,也支持自定义指标和测试数据生成。
适合:快速建立 question/context/answer/reference 数据集上的离线评估,并使用现成的 RAG 指标范式。
注意:指标版本、Judge 模型、语言和 prompt 都会改变分数;某些指标需要 reference,某些不需要。报告中要固定这些配置。
TruLens
TruLens 的 RAG Triad 从三条关系观察系统:问题到上下文的 Context Relevance、上下文到回答的 Groundedness、问题到回答的 Answer Relevance。
适合:结合 tracing 观察一次调用记录,快速判断问题更像出在检索、grounding 还是回答切题性。
注意:Triad 不能替代版本正确性、权限、引用精度和业务关键条件等定制门禁。
DeepEval
DeepEval 的指标体系 支持端到端与组件级评估、RAG 指标、自定义 G-Eval/DAG 指标以及 CI 风格的阈值测试。
适合:把 LLM 应用评估组织成自动测试,为不同 test case 配置阈值,并与持续集成结合。
注意:官方也明确许多预置指标使用 LLM-as-a-Judge。阈值不是跨模型、跨任务通用的,需要用人工样本校准。
一个实际选择
| 需求 | 可优先考虑 |
|---|---|
| 快速跑标准 RAG 指标与合成测试集 | RAGAS |
| 重视 tracing 和三段关系诊断 | TruLens |
| 把评估写成测试并接 CI | DeepEval |
| 业务有严格规则、权限和版本语义 | 自定义确定性指标 + 任一框架编排 |
不必强行三套全接。先明确数据结构、指标定义和门禁,框架只是承载方式。
LLM-as-Judge 的偏差与控制
LLM Judge 很适合判断语义支持、完整性和回答质量,但它不是稳定、无偏的裁判。
常见偏差:
- 位置偏差:成对比较时偏好先出现或后出现的答案。
- 长度偏差:把更长、更像解释的答案误判为更好。
- 自我偏好:Judge 可能偏好与自身表达方式相近的答案。
- 措辞敏感:同义改写、顺序变化会导致评分波动。
- 领域不足:不了解企业制度、法律或专业术语。
- 证据污染:把候选回答里的指令当成评审要求。
- 宽严漂移:模型版本或服务配置变化后阈值失效。
控制方法:
- 先用确定性规则判断 id、数值、格式、权限和版本,再让 Judge 处理语义。
- 给 Judge 明确 rubric 和正反例,要求按原子 claim 打分。
- 成对比较时交换 A/B 顺序;不一致样例标记为不稳定。
- 使用低随机性设置,并保存模型、版本、prompt 和参数。
- 对高风险样例使用双 Judge 或人工复核,但不要误以为多数投票自动等于真相。
- 用人工标注集计算一致率、混淆矩阵,并校准阈值。
- 将检索资料包在不可信数据区,禁止其修改评分规则。
- 定期抽查高分失败与低分成功样例,寻找系统性偏差。
Judge 分数应携带理由与 claim 级结果,但理由也可能编造,不能把一段顺畅解释当作评分正确的证明。
离线评估执行结构
一条评估记录建议同时保存输入、每层 trace 和结果:
{
"case_id": "return-017",
"dataset_version": "2026-09-07",
"query": "生鲜商品能否 7 天无理由退货?",
"expected": {
"evidence_groups": [["policy:v4:p2#fresh-food"]],
"must_include": ["生鲜不支持无理由退货"],
"status": "answer"
},
"trace": {
"retrieved_ids": ["..."],
"reranked_ids": ["..."],
"context_ids": ["..."],
"answer": "...",
"citations": ["..."]
},
"metrics": {
"hit_at_5": 1,
"recall_at_5": 1.0,
"faithfulness": 1.0,
"critical_claim_pass": true,
"acl_violation": false
}
}一个简化的执行器:
def evaluate_case(case, rag_app, evaluators):
trace = rag_app.run_with_trace(
question=case["query"],
user_context=case["user_context"],
as_of=case["as_of"],
)
scores = {}
scores.update(evaluators.ingestion(case, trace))
scores.update(evaluators.retrieval(case, trace))
scores.update(evaluators.ranking(case, trace))
scores.update(evaluators.context(case, trace))
scores.update(evaluators.answer(case, trace))
scores.update(evaluators.citation(case, trace))
scores.update(evaluators.security(case, trace))
scores.update(evaluators.cost_latency(case, trace))
return {"case_id": case["case_id"], "trace": trace, "scores": scores}评估环境要固定知识库快照、embedding 模型、索引参数、reranker、生成模型、prompt、随机性和并发配置。否则两次结果差异无法归因。
Ablation:确认提升到底来自哪里
Ablation 是一次只移除或改变一个组件,观察质量、成本和延迟变化。
例如以 dense + BM25 + rerank + compression 为完整系统:
| 实验 | 改动 | 要回答的问题 |
|---|---|---|
| A | 完整系统 | 当前基线是什么 |
| B | 去掉 BM25 | 精确词命中是否下降 |
| C | 去掉 reranker | 排序收益值不值得延迟 |
| D | 去掉 compression | 压缩是否保真,是否只是省 token |
| E | top-k 从 20 改 50 | 更高召回是否被噪声抵消 |
| F | 关闭 query rewrite | 改写是否帮助口语问题、伤害精确版本 |
每组应使用同一数据快照和配置,只改一个因素。若同时换 embedding、chunk 和 prompt,即使总分上涨,也无法知道哪个变化有效。
报告不要只写平均分:
总体变化:Recall@5 +x,P95 +y ms
切片变化:口语查询改善,版本号查询退化
关键失败:2 条权限样例失败
结论:即使平均质量上升,也不允许上线x/y 填入实验测得的变化值。
回归门禁:平均分不能掩盖事故
门禁分三类。
硬门禁
- 任意越权证据进入上下文。
- 关键制度问题生成错误金额、日期或否定结论。
- 无答案问题编造确定答案。
- 引用不存在的 evidence id。
- 恶意文档触发副作用工具。
这类失败通常要求数量为 0。
统计门禁
- Recall@K、nDCG、Faithfulness 不低于已批准基线的容忍区间。
- P95 延迟和平均成本不超过预算。
- 各业务切片均达到最低样本数与阈值。
小数据集上的 0.02 波动可能只是随机性。可以使用 bootstrap 置信区间或配对检验,不要只比较两位小数。
人工复核门禁
用于 Judge 不稳定、业务高风险、冲突无法自动裁决或新题型覆盖不足的样例。人工复核不应替代自动 trace,它负责处理自动指标无法可靠决定的部分。
线上反馈与离线指标如何连接
线上可观察:
- 用户点赞、点踩与原因标签。
- 用户是否立即改写同一问题。
- 是否点击引用,点击后是否返回继续追问。
- 是否转人工、复制答案或完成下游任务。
- 空结果率、拒答率、二次检索率。
- 延迟、取消率、错误率和单请求成本。
这些都不是天然真值。用户可能给错误答案点赞,也可能因为正确拒答而点踩。引用点击率低可能是答案已经够清楚,也可能是链接不可用。
更稳妥的闭环:
线上信号发现问题
-> 抽样回放完整 trace
-> 领域专家确认错误与原因
-> 加入 challenge set
-> 离线复现并修复
-> 通过门禁后灰度上线
-> 比较同一用户与问题切片A/B 测试时要防止同一用户跨组污染、知识库版本不同和流量切片不均。高风险场景不应只凭点击率决定上线。
逐层错误归因
从最早可证伪的环节开始,不要先改 prompt。
典型信号:
| 现象 | 优先检查 |
|---|---|
| Recall@20 低 | 解析、query、filter、embedding、索引 |
| Recall@20 高但 Recall@5 低 | 融合和 reranker |
| Context Recall 低但候选 Recall 高 | token 预算、去重、压缩、父块选择 |
| Faithfulness 低但 context 充分 | prompt、模型、生成后校验 |
| Faithfulness 高但 correctness 低 | 过期/错误知识、版本裁决、golden |
| Correctness 高但 citation precision 低 | 引用生成与 claim 对齐 |
| 离线好、线上差 | 数据分布、权限、会话上下文、并发或知识新鲜度 |
一次完整评估案例
以包含 20 条问题的评估集为例,比较两套检索配置。
目标与配置
知识库:售后政策快照 v4
问题:20 条
切片:10 条常规、4 条精确 SKU、3 条无答案、2 条版本冲突、1 条权限
基线:dense top-20 -> rerank top-5 -> 生成
候选:dense + BM25 top-30 -> rerank top-5 -> claim 引用校验分层结果
假设得到以下评估结果:
| 指标 | 基线 | 候选 | 解释 |
|---|---|---|---|
| Recall@20 | 0.82 | 0.92 | 混合召回帮助精确 SKU |
| nDCG@5 | 0.74 | 0.84 | rerank 输入更完整 |
| Context Recall | 0.78 | 0.88 | 更多必要证据进入最终上下文 |
| Faithfulness | 0.90 | 0.94 | claim 校验减少无支持结论 |
| 关键题通过率 | 0.85 | 0.90 | 总体提升,但还要看硬失败 |
| Citation Precision | 0.76 | 0.93 | 无效引用减少 |
| P95 延迟 | 1.8 s | 2.4 s | 多路召回与校验增加延迟 |
不能直接上线的原因
逐条看失败样例发现:唯一的权限题中,候选系统在 BM25 分支先召回了无权文档,虽然最终 context 过滤掉了它。最终答案没有泄露,但 Unauthorized Retrieval Rate > 0,说明安全边界放得太晚。
结论应是:
候选系统在大多数质量指标上改善,但违反权限硬门禁,暂不上线。
先将 ACL 过滤下推到 BM25 与 dense 两个召回分支,再复跑全部权限和普通回归集。修复后的归因验证
修复 ACL 后,先验证:
- 两个召回分支都只产生授权候选。
- 缓存 key 包含权限版本,撤权后不复用旧结果。
- reranker、日志和 Judge 从未接触无权正文。
- 普通用户与管理员的合法召回结果分别保持预期。
- 再比较质量、P95 与成本,确认过滤下推没有意外漏召回。
这个案例说明,评估的目的不是宣布“候选总分更高”,而是做出可解释的发布决定。
生产评估清单
一次可以复现的评估报告至少写明:
- 数据集版本、构造方式、规模、切片和时间范围。
- 知识库快照、文档版本和 ACL 快照。
- chunk、embedding、索引、query rewrite、融合与 rerank 配置。
- 生成模型、prompt 版本、温度、输出约束。
- Judge 模型、rubric、框架和指标版本。
- 每层指标、置信区间、失败样例与错误分类。
- P50/P95/P99 延迟、token、费用估算和错误率。
- 硬门禁结果、回滚条件和已知盲区。
评估原始记录应可回放,但要遵守数据最小化。用户问题、检索正文和 Judge 输入可能含敏感信息,不能为了可观测性无期限保存全量明文。
常见误区
用一个综合分数排名所有方案
加权总分会掩盖越权、关键事实错误和无答案幻觉。总分可用于粗看,但硬门禁必须单独展示。
把 Recall@K 当答案质量
正确证据被召回只是必要条件。重排、压缩、生成和引用仍可能失败。
用最终答案反推召回质量
模型可能凭参数知识答对,掩盖检索失败;也可能因生成问题答错,冤枉检索器。必须直接评估 evidence id。
用随机负例让指标很好看
随机无关文档太容易区分。应加入同主题不同版本、相反结论和关键词高度重合的 hard negatives。
LLM Judge 分数保留三位小数就显得精确
小数位不代表测量稳定。应报告 Judge 配置、重复运行波动、与人工的一致性及置信区间。
只测平均延迟
尾延迟决定用户是否取消请求。至少看 P50/P95/P99、TTFT 和超时率,并按是否触发重试、压缩、Judge 分组。
评估集永不更新
知识、用户表达和攻击方式都在变化。线上失败应进入 challenge set,旧制度样例要绑定历史快照或更新标签。
面试中如何回答“你怎么评估 RAG”
一个有说服力的回答应沿着故障链路展开:
先固定知识库快照和 golden dataset
-> 入库层测解析、chunk、metadata 与删除传播
-> 召回层用 Recall/Hit Rate,排序层用 MRR/MAP/nDCG
-> 最终 context 测相关性、充分性、噪声和版本适用
-> 生成层拆 faithfulness/correctness/relevance/completeness
-> claim 级验证 citation precision/recall
-> 权限和注入设硬门禁
-> 同时报告成本与 P95
-> 用 ablation 证明组件贡献,用 CI 和线上反馈形成回归闭环如果面试官继续追问,就拿一个失败答案从原文、召回、上下文一路定位到生成,而不是只列工具名。
先看一个小场景
测试集里有一道题:“生鲜签收后能否七天无理由退货?”答案错了,可能是 OCR 漏行、chunk 拆开、reranker 排错,也可能是模型忽略了已召回的例外。只记录最终得分看不出原因;把每一层的中间结果留下,回归测试才知道下一版到底修好了哪一步。
读完后可以故意制造一次 OCR 漏行,再沿原文、召回、上下文和生成逐层排查,确认评测能指出具体故障位置。