Recall、MRR、NDCG 与忠实性
正在读取学习记录…
RAG评测把“看上去回答不错”拆成几个可核对的问题:该找的资料找到了多少,排在前面的是不是相关资料,答案中的说法有没有依据,引用是否支持它旁边的说法。
Recall是召回率,计算相关资料被找回的比例。MRR是平均倒数排名,关注第一份相关资料出现得早不早。NDCG是归一化折损累计增益,它按相关程度和位置衡量整份排序。忠实性关注答案有没有遵循提供的证据,引用正确率检查被标注的来源是否确实支持对应说法。
固定一组可以逐项手算的资料
问题是“退货期限和生鲜例外是什么”。应找回的两条资料是d1“签收7天内可退货”和d2“生鲜商品除外”,无关的d3讲退款到账。固定返回顺序为d3、d1、d2,先观察前两条。
答案有三项说法:期限7天引用d1,生鲜除外引用d2,任何订单都能退引用d1。前两项有支持,第三项扩大了条件,d1不支持它。支持判断用已人工标注的对应关系表示,方便手算;真实评测应对原文逐项核对并保留标注依据。
下面先用“按引用核对的证据支持比例”练习忠实性检查:每项说法至少有一条标注引用支持它,就计为有依据;再除以说法总数。完整的忠实性评测还会检查未标引用但受上下文支持的说法,因此这里的计算规则需要与评测目标一起说明。
跟着固定样本运行
先在 PowerShell 创建并进入学习目录:
mkdir "F:\学习\l5-rag-eval" -Force
cd "F:\学习\l5-rag-eval"保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。
新建 F:\学习\l5-rag-eval,将下面完整代码保存为 rag_eval_demo.py。
先在 PowerShell 创建并进入学习目录:
mkdir "F:\学习\l5-rag-eval" -Force
cd "F:\学习\l5-rag-eval"保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。
"""计算固定检索排名与逐项标注的引用支持比例。"""
import math
def retrieval_metrics(ranked, relevant, k):
if k <= 0 or not relevant:
raise ValueError("k必须为正,相关资料集合不能为空")
# 去重后按顺序截取,防止相同资料被计算多次。
unique = list(dict.fromkeys(ranked))
top = unique[:k]
hits = sum(doc in relevant for doc in top)
recall = hits / len(relevant)
first = next((i for i, doc in enumerate(top, 1) if doc in relevant), None)
reciprocal_rank = 1 / first if first else 0
# 此处相关性只有0或1;第i位的折损因子是log2(i+1)。
dcg = sum(1 / math.log2(i + 1)
for i, doc in enumerate(top, 1) if doc in relevant)
ideal = sum(1 / math.log2(i + 1)
for i in range(1, min(k, len(relevant)) + 1))
return recall, reciprocal_rank, dcg / ideal
def answer_metrics(claims, supports):
# 每项是(说法标识, 引用标识列表);一项可有多份引用。
supported_claims = 0
correct_links, all_links = 0, 0
for claim, citations in claims:
valid_sources = supports.get(claim, set())
supported_claims += bool(set(citations) & valid_sources)
for citation in citations:
all_links += 1
correct_links += citation in valid_sources
faithfulness = supported_claims / len(claims) if claims else None
correctness = correct_links / all_links if all_links else None
return supported_claims, len(claims), correct_links, all_links, faithfulness, correctness
if __name__ == "__main__":
ranking = ["d3", "d1", "d2"]
relevant = {"d1", "d2"}
for k in (2, 3):
recall, rr, ndcg = retrieval_metrics(ranking, relevant, k)
print(f"前{k}条:Recall={recall:.3f},RR={rr:.3f},NDCG={ndcg:.3f}")
claims = [("期限7天", ["d1"]), ("生鲜除外", ["d2"]), ("任何订单都能退", ["d1"])]
supports = {"期限7天": {"d1"}, "生鲜除外": {"d2"}}
supported, total, correct, links, faith, precision = answer_metrics(claims, supports)
print(f"按引用核对的证据支持比例:{supported}/{total}={faith:.3f}")
print(f"引用正确率:{correct}/{links}={precision:.3f}")在 PowerShell 中进入保存目录并运行:
cd "F:\学习\l5-rag-eval"
ls
python -X utf8 rag_eval_demo.py输出为:
前2条:Recall=0.500,RR=0.500,NDCG=0.387
前3条:Recall=1.000,RR=0.500,NDCG=0.693
按引用核对的证据支持比例:2/3=0.667
引用正确率:2/3=0.667前2条只有d1相关,相关资料总共2条,所以Recall@2=1/2。第一条相关资料排第2,倒数排名RR@2=1/2。前3条把d2也找回来,召回率升到1;第一份相关资料仍排第2,所以RR保持0.5。
NDCG@2的实际得分是1/log2(3)≈0.631,理想排序为d1、d2,理想得分为1/log2(2)+1/log2(3)≈1.631;相除得到0.387。@3的实际得分再加1/log2(4)=0.5,理想得分仍为1.631,相除得到0.693。
三项说法中2项有标注引用支持,证据支持比例为2/3。三条引用连接中2条正确,引用正确率也为2/3。这次数字相同是样本安排的结果;一项说法带多条引用时,两个分母会不同。
从一条问题扩展到一组评测
RR衡量一条问题的首个相关位置;MRR将多条问题的RR加总,再除以问题数。例子只含一条问题,因此这组的MRR等于0.5。这里将RR限定在前k条;评测报告需要说明这个截断规则。
NDCG也可以使用多级相关程度,例如完全回答为2、部分相关为1、无关为0。常见增益函数是2的相关等级次方减1,再按位置折损。当前程序使用二元标注,相关资料的增益均为1,因此可以逐项手算。
忠实性、引用与答案正确性如何区分
一个答案可以忠实复述过期资料,却在当前业务中答错。答案正确性还需要对照问题、时间和当前规则。引用存在也不意味着引用正确,来源要支持它相邻的具体说法。
程序将说法和引用的支持关系作为已标注输入,用引用中的有效来源检查证据支持。这种做法能清楚观察计数,真实评测还应检查没有引用的说法是否被提供的上下文支持,并另算引用覆盖率。覆盖率的分母是需要引用的说法数量,正确率的分母是实际给出的引用连接数量。
无引用时,引用正确率没有分母,程序返回None;不能将它显示为100%。没有相关资料的检索问题也需要单独标注为“应拒答”,再检查是否会编造答案,不能直接套用本例的召回公式。
改一份排序看结果
把主程序排序改为 ["d1", "d2", "d3"]。前2条与前3条的Recall、RR、NDCG都变为1.000,答案两项指标保持0.667。排序改善不会自动纠正答案里的“任何订单都能退”,两段流程需要分别评测。
| 现象 | 检查位置 |
|---|---|
| Recall高但相关资料排得靠后 | 首个相关位置和NDCG |
| 检索全中但答案扩大规则 | 逐项说法的证据支持 |
| 引用数量很多仍答错 | 引用是否支持相邻说法 |
| 不同报告指标差异大 | 标注集、截断k与平均方式 |
检查实现中的六种情况
在同一目录将下面测试保存为 test_rag_eval_demo.py。测试覆盖正文中的正常路径与边界条件。
测试代码:test_rag_eval_demo.py
"""检查指标分母、截断、重复与无引用情况。"""
import unittest
from rag_eval_demo import retrieval_metrics, answer_metrics
class RagEvalTests(unittest.TestCase):
def test_recall_at_two(self):
recall, rr, ndcg = retrieval_metrics(["d3", "d1", "d2"], {"d1", "d2"}, 2)
self.assertEqual(recall, 0.5)
self.assertEqual(rr, 0.5)
self.assertAlmostEqual(ndcg, 0.3868528072)
def test_ideal_ranking(self):
self.assertEqual(retrieval_metrics(["d1", "d2"], {"d1", "d2"}, 2),
(1, 1, 1))
def test_duplicates_and_no_hit(self):
self.assertEqual(retrieval_metrics(["d3", "d3"], {"d1"}, 2), (0, 0, 0))
def test_claim_and_link_denominators(self):
result = answer_metrics([("a", ["d1", "d2"]), ("b", [])], {"a": {"d1"}})
self.assertEqual(result[:4], (1, 2, 1, 2))
self.assertEqual(result[4:], (0.5, 0.5))
def test_no_citation_is_undefined(self):
self.assertIsNone(answer_metrics([("a", [])], {})[5])
self.assertIsNone(answer_metrics([], {})[4])
def test_invalid_retrieval_inputs(self):
with self.assertRaises(ValueError):
retrieval_metrics([], set(), 2)
with self.assertRaises(ValueError):
retrieval_metrics([], {"d1"}, 0)
if __name__ == "__main__":
unittest.main()cd "F:\学习\l5-rag-eval"
python -X utf8 -m unittest -v test_rag_eval_demo应显示6项通过。将主程序恢复到上面的固定输入后,可以再次对照完整输出。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“Recall、MRR、NDCG 与忠实性”,哪种做法更稳妥?
对应知识点:L5-rag-eval
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L5-rag-eval
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L5-rag-eval
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L5-rag-eval
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。