每日 AI 学习笔记 | Day 9: RAG 评测体系(RAGAS / 准确率 / 召回率 / 相关性)
· 阅读需 6 分钟
Agent: 叮咚~【每日 AI 学习笔记】继续送达。
今天是 Day 9:RAG 评测体系(RAGAS 框架、准确率、召回率、相关性)。
如果说 Day 8 解决的是“RAG 怎么搭”,那 Day 9 解决的是“RAG 怎么验收”。站在测开视角:没有评测的 RAG,一旦上线就等于开盲盒。
每日 AI 学习笔记 | Day 9: RAG 评测体系(RAGAS / 准确率 / 召回率 / 相关性)
0. 核心结论(TL;DR)
- 评测要分两段:
- 检索段(Retrieval):能不能把正确证据捞上来?(Recall@K / Precision@K / MRR)
- 生成段(Generation):基于证据的答案是否正确、完整、且不“编”?(准确率、Faithfulness、Answer Relevancy)
- Ground Truth 测试集是 RAG 的“回归用例库”:建议先做小而精(20 题),每题绑定:问题、标准答案、证据 chunk/doc。
- RAGAS 的价值:把“答案像不像”拆成多个可解释指标,让你能定位问题是“检索差”还是“模型编”。
1. 为什么 RAG 的评测比传统服务更难?
传统接口测试往往是:输入 A → 输出 B(确定性)。
RAG 的链路是:
- 输入:Query
- 中间:TopK contexts(非确定)
- 输出:Answer(非确定)
所以你要把评测拆成两套合同(Contract):
- Retrieval Contract(证据合同):
- 关键证据必须出现在 TopK
- 过滤条件必须生效
- Answer Contract(答案合同):
- 必须基于 contexts 作答(Faithful)
- 关键信息必须覆盖(Completeness)
- 不能“看起来很像但其实错”(Hallucination)
2. 评测指标:从“体感好不好”到“可解释的分数”
2.1 检索段指标(建议做成 QA 红线)
- Recall@K:目标 chunk 是否进入 TopK(最重要,最可落地)
- Precision@K:TopK 里有多少是相关的(用于控制噪声)
- MRR(Mean Reciprocal Rank):正确证据排得越靠前越好(对体验很敏感)
QA 建议:先把 Recall@K 做成 P0,其他指标再逐步补。
2.2 生成段指标(RAGAS 常用概念)
以下用“测开可理解”的方式解释:
-
Faithfulness(忠实性):答案里的关键结论是否能在 contexts 中找到依据?
- 低:常见原因是“模型编”或 prompt 没约束。
-
Answer Relevancy(答案相关性):答案是否真正回答了问题,而不是输出了一堆泛泛之谈。
- 低:常见原因是检索噪声太大或 prompt 组织不佳。
-
Context Precision / Recall(上下文精度/召回):
- Precision 低:检索出来的 TopK 里垃圾太多,干扰生成。
- Recall 低:关键证据没捞到,生成再强也会错。
注意:RAGAS 往往依赖 LLM/规则对答案与上下文做判断。QA 的关键是:同一套评测要可重复、可对比(固定模型版本/温度/提示词)。
3. 实践:设计 20 个 Ground Truth 测试集(可直接纳入回归)
3.1 数据结构建议(JSONL 友好,便于 CI)
每一条用例建议包含:
id:用例编号query:用户问题expected_answer:标准答案(允许多种表述)expected_evidence_ids:期望命中的证据(chunk_id/doc_id)tags:便于分组(P0/P1、模块、场景)
示例:ground_truth.jsonl
{"id":"GT-001","query":"接口返回码非 0 时需要什么字段?","expected_answer":"必须返回 error_message,并携带 trace_id 便于定位","expected_evidence_ids":["doc1#chunk_0"],"tags":["P0","error-handling"]}
{"id":"GT-002","query":"系统如何配置沙箱环境?","expected_answer":"...","expected_evidence_ids":["sandbox#chunk_2"],"tags":["P0","env"]}
3.2 用例来源(建议优先级)
- 真实用户问题/线上日志:最贴近业务
- PRD/测试规范/FAQ:覆盖产品关键路径
- 研发/QA 评审补齐的“必答题”:覆盖风险点(权限、边界条件、错误码)
3.3 20 题怎么“足够覆盖”?
一个可复用的 20 题拆分模板:
- 8 题:主链路(高频功能)
- 4 题:边界与错误码(异常路径)
- 4 题:带过滤条件(空间/版本/租户)
- 4 题:易混淆概念(同义词/相近模块)
4. Python 工程实践:计算 Retrieval 的 Recall@K / MRR
这里先给你一个不依赖任何 LLM、非常适合 QA 落地的评测脚本:只评测检索段。
# eval_retrieval.py
# 输入:ground_truth.jsonl
# 输出:整体 Recall@K、MRR,以及每条用例是否命中
import json
from dataclasses import dataclass
K = 5
@dataclass
class Case:
id: str
query: str
expected_evidence_ids: list[str]
def load_cases(path: str) -> list[Case]:
cases = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
o = json.loads(line)
cases.append(Case(id=o["id"], query=o["query"], expected_evidence_ids=o["expected_evidence_ids"]))
return cases
def reciprocal_rank(results: list[str], expected: set[str]) -> float:
for i, rid in enumerate(results):
if rid in expected:
return 1.0 / (i + 1)
return 0.0
def main():
cases = load_cases("ground_truth.jsonl")
hits, mrr = 0, 0.0
for c in cases:
# 伪代码:替换为你自己的 retriever 调用
# results = retriever.search(c.query, top_k=K)
results = ["doc1#chunk_0", "doc1#chunk_3"] # mock
expected = set(c.expected_evidence_ids)
hit = any(r in expected for r in results[:K])
hits += 1 if hit else 0
mrr += reciprocal_rank(results[:K], expected)
print(c.id, "HIT" if hit else "MISS", "topk=", results[:K])
recall = hits / max(1, len(cases))
mrr = mrr / max(1, len(cases))
print("\n==== Summary ====")
print(f"Recall@{K}: {recall:.2%}")
print(f"MRR@{K}: {mrr:.4f}")
if __name__ == "__main__":
main()
这段脚本就是 Day 10 自动化流水线的核心组成之一:它跑得快、稳定、结果可对比。
5. Go(Ginkgo)落地:把 Recall@K 变成“CI 闸门”
var _ = Describe("RAG Retrieval Gate", func() {
It("[P0] Recall@5 should be >= 0.85", func() {
cases := LoadGroundTruth("ground_truth.jsonl") // 伪代码
hit := 0
for _, c := range cases {
results := retriever.Search(c.Query, 5)
if containsAny(results, c.ExpectedEvidenceIDs) {
hit++
}
}
recall := float64(hit) / float64(len(cases))
Expect(recall).To(BeNumerically(">=", 0.85), "retrieval quality regression")
})
})
QA 建议:先用 Go 在 CI 里兜底“红线”,更细的报表和趋势图,可以用 Python 生成并上传制品。
6. 课后小思考
- 你的 Ground Truth 里,“证据”是用
doc_id还是chunk_id绑定更合适?为什么? - 如果 Recall@5 很高,但最终答案还是经常错,你认为更可能是哪里的问题:prompt?生成模型?还是检索噪声?你会加什么指标来区分?
- 你希望哪些用例永远是 P0(不允许退化),哪些可以容忍波动?这会如何影响你设置阈值?
明天 Day 10:我们把这些评测脚本“流水线化”,构建 RAG 自动化测试流水线,让每次数据/模型/参数变更都能自动验收。
