Skip to main content

RAGAS的坑

上一篇把 5 个指标的算法拆透了,配对读法也讲了——RAGAS 不再是黑盒。但拿到分数之后还有一个问题:这些数字本身靠不靠谱?

跑两次同样的数据,faithfulness 一次 0.85 一次 0.80,差了 5 个点——是系统变了还是 RAGAS 自己在抖?judge 模型跟被评模型用同一个 provider,分数会不会偏高?有几条样本总是冒 NaN 怎么办?跑一次完整评测到底要花多少钱?

这篇讲五个实操坑和对策。知道这些坑,才能正确地使用 RAGAS 的分数——不被假信号误导,也不因为噪声而错过真问题。

坑一:Judge 同源偏置

1. 现象和原因

被评模型和 judge 模型用同一个 provider 的同族模型时(比如被评 gpt-5.4 + judge gpt-5.4-mini,或被评 Qwen-72B + judge Qwen-32B),RAGAS 分数会偏高。

原因不复杂:同族模型在预训练阶段用了相似的数据和对齐策略,对相同风格的输出有共同的偏好。judge 更倾向于认可同族模型的表达方式——同样一个回答,GPT judge 给 GPT 生成的回答打分,会比给 Qwen 生成的回答高几个百分点。

这不是 bug,是 LLM-as-judge 的固有特性,学术上叫 self-preference bias。

有多严重:在学术 benchmark 上,同源 vs 异源 judge 的分数差异通常在 3~8%。不是致命的偏差,但足以影响 A/B 测试的结论。如果你改了一版 prompt,分数提了 4%,但其中 5% 是同源偏置带来的,那实际上是退化了——你却以为改进了。

2. 本项目的状态

Ragent 默认用 Qwen 系列模型做问答生成,judge 用 gpt-5.4-mini。两者不是同族(Qwen vs GPT),同源偏置风险较低。但如果被评切到 GPT 模型,就会出现同族问题。

3. 对策

最佳做法:被评和 judge 用不同族模型。GPT 生成的回答用 Qwen 评,Qwen 生成的回答用 GPT 评。

退而求其次:固定一个 judge 模型(比如始终用 gpt-5.4-mini),不管被评模型怎么换。绝对分数可能偏,但不同版本之间的相对变化是公平的——因为偏置方向和幅度保持一致。

底线意识:知道这个偏差存在就行。不要过度解读 2~3% 的分数差异,尤其是在换被评模型的前后。

同源偏置影响的是绝对分数的可信度,不影响同一 judge 下不同版本之间的相对比较。所以固定 judge 是性价比最高的对策——绝对值可能虚高,但趋势是真的。

坑二:单跑方差

1. 现象和量级

同样的评估集、同样的 runs 文件、同样的 judge 模型,RAGAS 跑两次分数不一样。faithfulness 第一次 0.847,第二次 0.823,差了 2.4 个百分点。

在本项目 18 条可评样本上,单次跑的方差通常在 3~5% 以内。faithfulness 0.85 和 0.80 之间的差异很可能只是噪声,不代表系统真的变好或变差了。

2. 原因

RAGAS 内部每次调 judge 都是独立的 LLM 推理。即使设了 temperature=0,不同次调用返回的内容也可能有微妙差异——API 侧的 batching、量化精度、甚至请求排队顺序都会影响。

具体到指标层面:拆 claims 时可能多拆一个或少拆一个(“保修 1 年,自购买之日起算”拆成 1 个还是 2 个 claims,judge 两次的判断可能不同),判 supportable 时边界 case 可能这次判 yes 下次判 no。这些微小差异累加起来就是方差。

3. 本项目的应对:多轮取均值

python -m eval rag score --ragas-n 3

三轮独立评测,按样本取均值。每条样本的每个指标有 3 个独立分数,取均值后再汇总。单条样本的随机波动被平均掉了。

关键实现:三轮是并发跑的,不是串行——

# ragas_judge.py: compute() 里的并发多轮
if n_runs <= 1:
dfs = [_run(evaluable, ...).to_pandas()]
else:
with concurrent.futures.ThreadPoolExecutor(max_workers=n_runs) as ex:
futures = [ex.submit(_run, evaluable, ...) for _ in range(n_runs)]
dfs = [f.result().to_pandas() for f in futures]

并发三轮,时间不会翻 3 倍,大约多 50%~80%(受 API 并发限流影响)。

为什么不用 OpenAI API 的 n 参数:OpenAI Chat API 支持 n=3 让一次请求返回 3 个候选回答。但这 3 个候选共享同一次请求的上下文,不算真正独立的采样。--ragas-n 3 是发 3 次完全独立的请求,每次从头推理,方差压制效果更好。

上线门槛建议:

比较方式门槛说明
单次跑,改造前 vs 改造后差 ≥ 5% 才认定有效低于 5% 可能是噪声
--ragas-n 3 取均值后比较差 ≥ 3% 才认定有效均值更稳,门槛可以低一些
看单条样本变化不建议单条方差太大,没有统计意义

坑三:中文场景下的 NaN

1. 现象

跑完 RAGAS,某几条样本的 faithfulnesscontext_recall 是 NaN。不是所有样本都 NaN,就偶发几条。其他指标可能正常,就某一两个指标出问题。

2. 原因

RAGAS 内部调 judge 拆 claims / statements 时,要求返回结构化的 JSON。但 RAGAS 的默认 prompt 是英文写的,judge 在处理中文内容时偶尔会出三种情况:

  • 在 JSON 之外加一段中文解释——"以下是我的分析:{...}"
  • 用中文引号("")替代英文引号(""),导致 JSON 解析失败
  • 返回非标准格式——"claims: [...]" 而不是纯 JSON 数组

RAGAS 的解析器碰到这些情况直接返回 NaN。

3. 五层防御

本项目在 ragas_judge.py 里做了五层 fallback,逐层兜底:

策略何时触发
1JSON mode 强制输出合法 JSON默认启用
2关闭 JSON mode,回退到 LLM 原生输出第 1 层整批失败
3逐条 eval,隔离问题样本第 2 层整批失败
4NaN 样本逐条重试(超时放宽到 1200s)全量跑完后仍有 NaN
5RunConfig(max_retries=2) 每次 LLM 调用内部重试贯穿始终

前三层是 _run() 函数内部的 fallback:

# ragas_judge.py: _run() 的三层 fallback

# 第一层:batch + JSON mode
try:
return _do_eval(records, use_json_mode=True)
except Exception as _e:
batch_exc = _e

# 第二层:batch 无 JSON mode
try:
return _do_eval(records, use_json_mode=False)
except Exception as _e2:
...

# 第三层:逐条 eval,隔离问题样本
for i, r in enumerate(records):
try:
dfs.append(_do_eval([r], use_json_mode=False).to_pandas())
except Exception:
dfs.append(pd.DataFrame({k: [float("nan")] for k in RAGAS_METRIC_KEYS}))

第一层用 response_format: json_object 强制 judge 输出合法 JSON,能解决大部分中文引号和格式问题。整批失败才退到第二层。第三层逐条跑的好处是:一条样本有问题不会拖垮整批——问题样本标 NaN,其他样本照常拿分。

第四层在 compute() 里:全量跑完后,对仍然是 NaN 的样本单独重试一次,超时从默认 900s 放宽到 1200s——

# ragas_judge.py: NaN 样本逐条重试
if failed:
for qid, k in sorted(failed):
record = record_map[qid]
try:
retry_df = _run([record], ..., timeout=1200).to_pandas()
fv = float(retry_df.iloc[0].get(k))
if fv == fv: # not NaN
per_metric[k][qid] = fv
except Exception:
pass

经过五层防御,实测 NaN 率控制在 5% 以下。偶尔剩的 NaN 样本在最终汇总时跳过(不参与均值计算),不会拉偏整体分数。

4. 要不要自定义中文 prompt

能不动就别动。RAGAS 的内部 prompt 跟算法深度耦合——改了 prompt,拆 claim 的粒度可能变了,同样一段 response 拆出 3 个 claims 还是 5 个 claims 会直接影响分数。改完之后跟之前版本的分数就不可比了。

上面的 fallback + retry 策略已经把 NaN 率控制在可接受范围。只有当 NaN 率 > 10% 且集中在同一个指标(比如 faithfulness 每次都有大量 NaN)时,才值得考虑覆盖默认 prompt——代价是后续 RAGAS 版本升级时需要手动同步你的自定义 prompt。

坑四:成本失控

1. 成本构成

解锁付费内容,👉 戳