怎么知道 RAG 好不好
把 RAG 的评估拆成检索和回答两半。检索看命中率,回答让另一个模型当评委,对照参考答案判断对错、对照资料判断是否忠实。评委判了 60 次,我逐条核对,发现它自己也会错。
- 约 45 分钟
- 难度:进阶
- 实测:2026-09-14 deepseek-flash,deepseek-v4-pro(评委)
到目前为止,我们评估的都是"检索找没找对"。但用户最终看到的是回答。检索全对,回答也可能错:模型可能读漏了资料,可能把两段资料混在一起,也可能忍不住加了自己的"知识"。反过来,检索没找到最准的那一块,回答也可能是对的。
所以 RAG 的评估要分成两半:检索,和回答。这一课讲怎么做这两半,重点是回答的评估,以及它的一个大坑。
两半分开评
分开评的好处是出了问题知道去哪里修:
| 检索 | 回答 | 说明 | 该修哪里 |
|---|---|---|---|
| 对 | 对 | 正常 | |
| 错 | 错 | 没找到资料,模型也答不对 | 检索:切分、改写、检索方法 |
| 对 | 错 | 资料给对了,模型没用好 | 生成:提示词、模型 |
| 错 | 对 | 可能是模型凭记忆答对了 | 看看是不是违反了"只根据资料" |
只看最终回答的对错,你分不清是第二种还是第三种,就只能乱改一气。
检索的评估前面已经做好了:第 3 课的命中率和 MRR。这一课做回答的评估。
回答要评什么
对 RAG 的回答,最常看的是两件事:
- 正确性:回答对不对。需要一个参考答案来对照。
- 忠实度:回答里的每个说法,是不是都能在检索到的资料里找到依据。它不管答案对不对,只管有没有"越界"。
忠实度单独拿出来评,是因为它能发现一种隐蔽的问题:回答碰巧是对的,但依据是模型自己的记忆,不是资料。这次对了,下次就可能错,而且你没法追溯。
准备参考答案
给第 3 课那 20 道题每题写一个参考答案。每一个我都是照着检索评估里那一段原文写的,比如:
{"question": "怎么关闭 SSL 证书校验?", ..., "reference": "传 verify=False,比如 httpx.get(url, verify=False);用 Client 时在创建客户端时传入。"}
{"question": "httpx 和 requests 在处理重定向上有什么不一样?", ..., "reference": "requests 默认跟随重定向,httpx 默认不跟随;httpx 要显式传 follow_redirects=True(单个请求或 Client 上都可以)。"}
参考答案不需要写得很长,写清楚必须包含的要点就行。但后面会看到,参考答案怎么写,会直接影响评分。
让模型当评委
20 道题,每题两个回答(一个不查资料、一个用 RAG),人工逐条判断很累。常用的办法是让另一个模型来判断,叫模型评委(LLM-as-a-judge)。评委用了更强的 deepseek-v4-pro,并且打开思考:
def judge_correct(question, reference, answer):
return judge(f"""判断"回答"是否正确地回答了"问题"。以"参考答案"为准:回答包含参考答案的要点、且没有和它矛盾的内容,就算正确。
回答比参考答案多说了一些内容没关系,只要多说的部分没有错误。
输出 json:{{"correct": true 或 false, "reason": "一句话理由"}}
问题:{question}
参考答案:{reference}
回答:{answer}""")
def judge_faithful(context, answer):
return judge(f"""判断"回答"里的每一个事实性说法,是否都能在"资料"里找到依据。
回答说"资料里没有"之类的话不算事实性说法。只要有一处说法在资料里找不到依据,就判为不忠实。
输出 json:{{"faithful": true 或 false, "unsupported": "找不到依据的说法,没有就写空字符串"}}
资料:
{context}
回答:{answer}""")
几个设计要点:
- 评委和被评的不是同一个模型。用同一个模型给自己打分,容易对自己的错误视而不见。
- 要求输出理由。不只要 true 或 false,还要一句话说明为什么。后面你会看到,理由是发现评委出错的唯一线索。
- 判断标准写清楚。"多说了没关系,只要多说的部分没错"这一句,是为了避免评委因为回答比参考答案详细就判错。
judge 函数用第 02 模块第 4 课的 JSON 模式调用评委。完整代码在 code/04-rag/rag_eval.py,一次运行大约 80 次调用,花费约 0.1 美元。
评委的判决
不查资料:答对 15/20
RAG: 答对 19/20,忠实于资料 19/20
[不查资料答错] 响应是 404 或 500 时,怎么让它直接抛异常?
评委:回答中“3xx不会抛”与参考答案“状态码不是2xx时会抛出”矛盾
[不查资料答错] httpx 和 requests 在处理重定向上有什么不一样?
评委:回答错误地声称httpx默认跟随重定向,与参考答案中“httpx默认不跟随”相矛盾。
[不查资料答错] 怎么限制连接池里最多同时有多少个连接?
评委:回答遗漏了参考答案中默认 max_connections=100 这一要点,未完整包含参考答案的默认值信息。
[不查资料答错] 怎么显示下载进度?
评委:回答未使用参考答案中的 response.num_bytes_downloaded 属性,而是手动累计长度
[不查资料答错] 网页返回的中文是乱码,怎么指定解码用的字符集?
评委:回答未提及参考答案中的 default_encoding 参数指定字符集的方法
[RAG 答错] httpx 和 requests 在处理重定向上有什么不一样?
评委:回答中关于 requests 暴露的属性 response.next 的描述有误,requests 并没有该属性。
[RAG 不忠实] 怎么显示下载进度?
找不到依据:显示下载进度需要用流式响应(streaming),并检查 `response.num_bytes_downloaded` 属性
看起来结论很清楚:RAG 把正确率从 15/20 提到了 19/20。但先别急着下结论。每一条"答错",我都去看了原始回答,并对照 httpx 的文档和源码核实。
逐条核对评委
"3xx 不会抛",评委判对了。不查资料的回答说 raise_for_status() 遇到 3xx 不会抛异常。httpx 源码 _models.py 里的 raise_for_status 是这样写的:只要不是 2xx(is_success 为假),就抛出 HTTPStatusError,3xx 的错误类型叫 "Redirect response"。所以回答确实错了。
重定向,不查资料的回答错了,评委判对了。它又一次说 httpx 默认跟随重定向,还说"0.20+ 之后默认跟随",和 RepoBot v1 犯的错一模一样。
连接池,评委判得太严了。不查资料的回答正确地说出了用 httpx.Limits(max_connections=..., max_keepalive_connections=...),只是没提默认值是 100。用户问的是"怎么限制",没问默认值是多少。问题出在我的参考答案里顺手写了"默认最多 100 个连接",评委就把它当成了必须包含的要点。
下载进度,评委判得有道理。不查资料的回答用 len(chunk) 手动累加已下载的字节数。httpx 文档专门说过,开启了压缩时,解压后的内容长度和实际下载的字节数对不上,所以要用 response.num_bytes_downloaded。回答的做法在没有压缩时能用,但不是正确的做法。
编码,评委判错了。不查资料的回答说:在访问 response.text 之前,设置 response.encoding = "gbk"。我查了 httpx 源码,Response.encoding 有一个 setter,允许在读取 text 之前设置编码(读取之后再设置会抛出 ValueError)。这是完全正确的另一种做法。评委判错,理由是"未提及参考答案中的 default_encoding",它把参考答案当成了唯一正确的答案。
RAG 的重定向回答,评委判错了。评委说"requests 并没有 response.next 这个属性"。可 httpx 文档 compatibility.md 第 50 行原文就是:"The requests library exposes an attribute response.next, which can be used to obtain the next redirect request." RAG 的回答照着文档写,是对的。评委没看资料,凭自己的记忆判断,结果自己记错了。
RAG 的下载进度,"不忠实"判错了。我重新检索了这道题,取回的前两块都来自 advanced/clients.md,里面都有 num_bytes_downloaded,原文就写着用流式响应并检查这个属性。回答完全忠于资料,评委没看仔细。
核对之后的结果
| 评委给的 | 我核对后的 | |
|---|---|---|
| 不查资料 | 答对 15/20 | 答对 17/20 |
| RAG | 答对 19/20 | 答对 20/20 |
| RAG 忠实度 | 19/20 | 20/20 |
结论的方向没变:RAG 明显比不查资料好,不查资料的回答在两道题上犯了真正的错误。但具体的数字变了,而且评委在 60 次判断里判错了 3 次,还有 1 次判得过严。
这 4 次问题可以归成两类:
- 评委不用给它的材料,而用自己的知识判断。
response.next那条就是。评委也是一个大模型,它也会记错。 - 把参考答案当成唯一答案。编码和连接池那两条都是。有多种正确做法时,参考答案只写了一种,评委就会把其他正确做法判错。
怎么让评委更可靠
- 永远抽查。评委的判决不能直接当成结论。至少把所有"错误"都人工核对一遍,再随机抽一些"正确"的看看。
- 要求评委给理由。这次能发现问题,全靠理由。"requests 并没有该属性"这句理由一看就可疑,一查就知道评委错了。
- 参考答案写要点,并允许其他正确做法。在提示词里说明"参考答案之外的其他正确做法也算对"。参考答案里只写问题真正问到的内容。
- 忠实度评委要看资料。提示词里强调"只根据资料判断,不要用你自己的知识"。
- 用人工标注校准评委。人工标注几十条,看评委和人的判断有多一致,一致率太低就改评委的提示词。06 模块第 2 课会系统地讲这件事。
这一课的结论
- RAG 的评估要分成检索和回答两半,分开才知道问题出在哪。
- 回答评估最常看正确性和忠实度。
- 模型评委能大大节省人力,但它会错,而且错得很自信。评委的输出是需要核对的数据,不是最终结论。
练习
- 修改
eval_qa.jsonl里连接池和编码两道题的参考答案,去掉问题没有问到的内容,并在评委的提示词里加上"参考答案之外的其他正确做法也算对",重新运行rag_eval.py。评委的判决变了吗? - 在忠实度评委的提示词里加上"只根据资料判断,不要使用你自己的知识",重新运行,看下载进度那道题的判决有没有变化。
- 挑 5 道题,自己当评委,判断不查资料的回答是否正确。和模型评委比,你们有几道题判断不一致?
自测
1. 为什么 RAG 的评估要把检索和回答分开?
只看最终回答,分不清错误出在哪一步:是没找到正确的资料,还是资料给对了但模型没用好。分开评估,就知道该去改检索(切分、改写、检索方法),还是改生成(提示词、模型)。
2. 一个 RAG 回答是正确的,但忠实度评估显示它"不忠实"。这说明什么?
说明回答里有些内容在检索到的资料里找不到依据,很可能来自模型自己的记忆。这次碰巧是对的,但换个问题可能就错了,而且没法追溯来源。当然,也可能是评委判错了,需要人工核对。
3. 模型评委判一个回答"错误",理由是"没有提到参考答案里的某个方法"。你应该怎么做?
先看回答用的方法是否也是正确的。参考答案往往只写了一种做法,评委可能把其他正确的做法也判成错误。核实后如果回答是对的,就修改评委的提示词(允许其他正确做法)或者修改参考答案。
提问与讨论
这一课没看懂的地方,在这里问。看到别人的问题,也欢迎你来回答。
提问 +3 积分,回答别人 +6 积分。内容经审核后公开。
正在加载讨论…