文章构建只评估检索环节的小型测试框架,用 Recall@k 检查证据遗漏,用倒数排名检查首条相关结果的位置。它还说明文档与分块标识的粒度对齐、代表性问题和标签复核要求,并明确这些指标无法衡量最终答案质量。
RAG 的回答出错,并不自动意味着语言模型出了问题。系统可能检索到了错误的文档、遗漏了相关段落,或者提供了相互冲突的证据。
本指南将构建一个只评估检索的小型评估框架,把“是否检索到了预期证据”这个问题与回答质量分开评估。这是一个演示示例,并非 Acadify 客户项目的实测结果。
先从两个检索指标入手:
Recall@k 有助于发现证据遗漏。Reciprocal rank 则有助于区分证据排在第一位,还是埋在结果列表的后面。这两个指标都无法衡量最终回答是否正确、安全,或者有证据支撑。
文档标识符的粒度应与标注一致。如果检索器返回的是 chunk,而标注对应的是文档,就要在评分前先将 chunk ID 映射为文档 ID。
每个可回答的问题都需要一组预期文档 ID。问题应能代表实际工作流程,并请人审核标注。
对于无法回答的问题,预期文档 ID 集合为空,并不能证明检索到的所有文档都不相关。在这里,空集表示经过批准的语料库中没有已知答案。我们会单独报告这类用例,并将其排除在召回率和排名指标的平均值计算之外。
下面的代码只使用 Python 标准库。准备好后,可以将用于测试的固定检索函数替换为你自己的检索 API。
from dataclasses import dataclass
from statistics import mean
from typing import Callable, Sequence
@dataclass(frozen=True)
class Case:
case_id: str
question: str
relevant_ids: frozenset[str]
def score(case: Case, retrieved: Sequence[str], k: int) -> dict:
if k < 1:
raise ValueError("k must be positive")
# Deduplicate while preserving rank. Score unique document results.
top = list(dict.fromkeys(retrieved))[:k]
relevant = case.relevant_ids
if not relevant:
return {"case_id": case.case_id, "answerable": False,
"retrieved_ids": top, "recall": None, "rr": None}
hits = relevant.intersection(top)
first_hit = next((rank for rank, doc_id in enumerate(top, 1)
if doc_id in relevant), None)
return {"case_id": case.case_id, "answerable": True,
"retrieved_ids": top,
"recall": len(hits) / len(relevant),
"rr": 1 / first_hit if first_hit else 0.0}
def evaluate(cases: Sequence[Case],
retrieve: Callable[[str], Sequence[str]], k: int) -> dict:
rows = [score(case, retrieve(case.question), k) for case in cases]
answerable = [row for row in rows if row["answerable"]]
return {
"k": k,
"answerable_cases": len(answerable),
"unanswerable_cases": len(rows) - len(answerable),
"mean_recall": mean(row["recall"] for row in answerable)
if answerable else None,
"mrr": mean(row["rr"] for row in answerable)
if answerable else None,
"rows": rows,
}
cases = [
Case("refund", "How do refunds work?", frozenset({"refund-policy"})),
Case("cancel", "How can I cancel?", frozenset({"cancel-guide"})),
Case("missing", "What is next year's price?", frozenset()),
]
fixture = {
"How do refunds work?": ["faq", "refund-policy", "refund-policy"],
"How can I cancel?": ["billing", "faq"],
"What is next year's price?": ["current-pricing"],
}
result = evaluate(cases, lambda question: fixture[question], k=2)
assert result["mean_recall"] == 0.5
assert result["mrr"] == 0.25
assert result["unanswerable_cases"] == 1
print(result)
这组固定测试数据刻意让退款文档排在第二位,并让取消操作的文档未被检索到。这里的平均值只描述这两个可回答的固定测试用例,不能用于估计生产环境的表现。
对每个失败用例,检查以下问题:
每次只修改一个组件。使用同一批经过审核的用例,对同一个语料库快照运行评估,这样才能确定差异来自哪里。保留一组留出测试集;反复针对评估集调优,可能会让分数提高,却没有改善实际工作流程。
完成检索后,使用明确的评判标准测试生成的回答:
自动化检查和基于模型的评判器可以帮助对结果进行初步筛查,但它们的判断仍需要通过人工审核来验证。通过检索测试,并不能证明回答正确,也不能证明授权与访问控制是安全的。
记录用例 ID、语料库及索引版本、检索设置、模型版本、prompt 版本,以及检索到的 ID。只在获得许可的位置存储段落和输出;分享运行记录前,应对机密信息或个人数据进行脱敏。
比较生产环境的表现时,要使用更有代表性的用例,在适当情况下重复运行具有随机性的测试,报告计算指标所用的样本总数与采样局限,并在跟踪质量的同时跟踪延迟和成本。
实际要记住的是:先定位失败发生在哪个阶段。系统从未检索到的证据,无法靠改写 prompt 找回来。
在你的 RAG 工作流程中,哪件事更难:创建可靠的标注、评估引用,还是复现检索失败?
关于作者:Acadify Solution 提供软件和 AI 工程服务。Acadify AI Lab 是我们开展 AI 评估的业务团队。本文由 AI Agent 生成,其中的数据集和代码均用于演示。更多信息可了解 Acadify Solution 或 Acadify AI Lab。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。