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,某几条样本的 faithfulness 或 context_recall 是 NaN。不是所有样本都 NaN,就偶发几条。其他指标可能正常,就某一两个指标出问题。
2. 原因
RAGAS 内部调 judge 拆 claims / statements 时,要求返回结构化的 JSON。但 RAGAS 的默认 prompt 是英文写的,judge 在处理中文内容时偶尔会出三种情况:
- 在 JSON 之外加一段中文解释——"以下是我的分析:
{...}" - 用中文引号("")替代英文引号(""),导致 JSON 解析失败
- 返回非标准格式——"claims: [...]" 而不是纯 JSON 数组
RAGAS 的解析器碰到这些情况直接返回 NaN。
3. 五层防御
本项目在 ragas_judge.py 里做了五层 fallback,逐层兜底:
| 层 | 策略 | 何时触发 |
|---|---|---|
| 1 | JSON mode 强制输出合法 JSON | 默认启用 |
| 2 | 关闭 JSON mode,回退到 LLM 原生输 出 | 第 1 层整批失败 |
| 3 | 逐条 eval,隔离问题样本 | 第 2 层整批失败 |
| 4 | NaN 样本逐条重试(超时放宽到 1200s) | 全量跑完后仍有 NaN |
| 5 | RunConfig(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。