跳到主要内容

Ragas 指标引擎 — 一条样本如何被打成分

本章讲:一个 LLM 裁判指标内部到底做了什么。先用 faithfulness(忠实度)走通整条路,再看 answer_relevancy 的另一种巧思,最后讲指标基类的骨架和新旧两套体系的差别。

1. 先建直觉:LLM 裁判指标的通用套路

直接问 LLM「这个答案 0~10 分打几分?」很不稳——分数飘、不可复现。Ragas 的核心套路是把一个模糊的整体判断,拆成一串非黑即白的原子判断,再用算术把它们聚合成分数:

整体问题(模糊) 原子判断(LLM 好答) 聚合(确定性算术)
「答案忠于资料吗?」 ──→ 把答案拆成 N 句陈述 ──→ 忠实句数 / 总句数
每句问:能从资料推出吗?(0/1) = 0~1 的分数

好处:LLM 只做「这句话能不能从这段资料推出来」这种它擅长的二元判断,而把「怎么变成一个分」交给确定性的除法。这让结果更稳、更可解释(你能看到哪几句被判为不忠)。

2. 主线:faithfulness 怎么算(拆句 → NLI 判定 → 比例)

忠实度衡量「答案有没有照着检索到的资料说,没瞎编」。它分两步 LLM 调用:

输入: user_input(问题) + response(答案) + retrieved_contexts(资料)

① 拆陈述 StatementGeneratorPrompt
答案「爱因斯坦是德国出生的物理学家,以相对论闻名」
──→ ["爱因斯坦是德国出生的理论物理学家",
"爱因斯坦以发展相对论闻名"]

② 逐句判定 NLIStatementPrompt(给定资料,每句 verdict=0/1)
[{statement:..., reason:..., verdict:1},
{statement:..., reason:..., verdict:0}]

③ 聚合 忠实句数 / 总句数 = 1/2 = 0.5

第①步:把答案拆成原子陈述。 prompt 明确要求「拆成可独立理解的陈述、不准用代词」(src/ragas/metrics/_faithfulness.py:37,StatementGeneratorPrompt.instruction)。去代词很关键——「他」「它」脱离上下文没法单独判定。

第②步:NLI 判定每句忠不忠。 NLI(Natural Language Inference,自然语言推理)= 判断「前提能否推出假设」。这里前提是检索到的资料,假设是每条陈述。prompt 要求「能从资料直接推出给 1,否则给 0」(src/ragas/metrics/_faithfulness.py:74,NLIStatementPrompt.instruction),且每条都要带 reason,逼 LLM 先讲理由再下判断。

第③步:聚合成分数。 纯算术,无 LLM:

# src/ragas/metrics/_faithfulness.py:182-194 的 _compute_score(简化)
def _compute_score(self, answers):
faithful = sum(1 if a.verdict else 0 for a in answers.statements)
n = len(answers.statements)
return faithful / n if n else np.nan # 没拆出陈述 → NaN(而非报错)

串起来的真实实现Faithfulness._ascore(src/ragas/metrics/_faithfulness.py:202-214):先 _create_statements 拆句,空就返回 np.nan;再 _create_verdicts 判定;最后 _compute_score。整条链是 async,因为它要发 2 次 LLM 请求。

巧妙处:可换成本地小模型。 子类 FaithfulnesswithHHEM 把第②步的 LLM 判定换成 Vectara 的幻觉检测小模型(vectara/hallucination_evaluation_model),分批跑避免 OOM(src/ragas/metrics/_faithfulness.py:217-273)。第①步拆句仍用 LLM,只把最贵的逐句判定下放给本地模型——这是省钱/提速的常见手法。

3. 第二种巧思:answer_relevancy 用「反向生成问题」

忠实度靠拆句,答案相关性则用了完全不同的把戏:如果答案切题,那么从答案反推出来的问题,应该和原问题很像。

原问题 q: 「爱因斯坦在哪出生?」
答案 a: 「爱因斯坦生于德国。」

① 让 LLM 看着答案 a,反向生成 n 个问题(strictness=3)
["爱因斯坦的出生地是哪?", "爱因斯坦出生在哪个国家?", ...]
② 把原问题 q 和这 n 个生成问题都转成嵌入向量
③ 算 q 与每个生成问题的余弦相似度,取均值 = 分数

余弦相似度的计算是纯 numpy(src/ragas/metrics/_answer_relevance.py:96-112,calculate_similarity):把原问题和生成问题嵌入后做点积除以范数。

两个巧妙的细节:

  • noncommittal 闸门。 prompt 同时让 LLM 判断答案是否「含糊其辞」(如「我不知道」),给 noncommittal=0/1(src/ragas/metrics/_answer_relevance.py:38)。聚合时 score = cosine_sim.mean() * int(not all_noncommittal)(src/ragas/metrics/_answer_relevance.py:127)——只要所有生成问题都判为含糊,直接清零。这堵住了「答案虽含糊但嵌入碰巧相似」的漏洞。
  • 多次采样降方差。 strictness=3 生成 3 个问题求均值(generate_multiple(..., n=self.strictness),src/ragas/metrics/_answer_relevance.py:142-144),单次 LLM 生成噪声大,多采样取平均更稳。

这个指标同时需要 LLM 和 embeddings,所以它继承 MetricWithLLM, MetricWithEmbeddings, SingleTurnMetric 三个 mixin(src/ragas/metrics/_answer_relevance.py:63-64)。

4. 指标基类的骨架(旧体系)

所有经典指标都从 Metric(dataclass + ABC)派生,关键设计有三:

(a) 必需列声明 + 校验。 每个指标用 _required_columns 声明它需要样本里有哪些字段。faithfulnessuser_inputresponseretrieved_contexts 三样(src/ragas/metrics/_faithfulness.py:136-144)。required_columns 的 setter 会校验列名必须在 VALID_COLUMNS 白名单里,否则 ValueError(src/ragas/metrics/base.py:107-118)。evaluate() 在跑之前据此做 validate_required_columns,缺列直接报错而不是跑到一半失败。

(b) 列名可带后缀标记。 列名能写成 "reference:optional""reference:ignored",get_required_columns 据此过滤(src/ragas/metrics/base.py:120-139)。这让一个指标能声明「这列有最好,没有也能跑」。

(c) 同步/异步双入口 + 列裁剪 + 回调。 single_turn_score(同步)和 single_turn_ascore(异步)都先调 _only_required_columns_single_turn 把样本裁剪到只剩需要的列(src/ragas/metrics/base.py:399-410),再开一个回调 group 包住实际打分,最后落到子类实现的 _single_turn_ascore。异步版还套了 asyncio.wait_for(..., timeout=timeout) 做超时(src/ragas/metrics/base.py:481-484)。

教学示例:写一个最小的自定义指标(示意,非源码):

# 示意,非源码:展示旧体系指标的最小骨架
from dataclasses import dataclass, field
from ragas.metrics.base import MetricWithLLM, SingleTurnMetric, MetricType

@dataclass
class MyMetric(MetricWithLLM, SingleTurnMetric):
name: str = "my_metric"
# 声明需要哪些列(会被校验)
_required_columns = field(default_factory=lambda: {
MetricType.SINGLE_TURN: {"user_input", "response"}
})

async def _single_turn_ascore(self, sample, callbacks):
row = sample.to_dict()
# ... 调 self.llm 做你的判断,返回 0~1 的 float
return 1.0

多次采样的投票器。 当一个判断采样多次(n>1)时,Ensember.from_discrete 对每个位置做多数投票把多份结果合一(src/ragas/metrics/base.py:646-676)——简单的 [0,0,1] → 0

5. 新体系:SimpleLLMMetric 与 collections

新体系的基类是 SimpleBaseMetric,打分入口是 score(**kwargs) / ascore(**kwargs),返回的是带分数 + 理由 + trace 的 MetricResult 对象,而不是裸 float(src/ragas/metrics/base.py:720-752)。

SimpleLLMMetric 在此之上加了 prompt 驱动:ascoreprompt.format(**kwargs) 拼 prompt,llm.agenerate(prompt, response_model=self._response_model) 拿结构化输出,包成 MetricResult 并把输入输出存进 traces(src/ragas/metrics/base.py:892-911)。

它还能存盘/加载:save() 把 prompt、config、response_model schema 序列化成 JSON(src/ragas/metrics/base.py:935-1021),load() 反序列化(src/ragas/metrics/base.py:1145-1212)。注意自定义 response_model 无法被序列化,加载时要手动再传——这是 save() 会发警告的原因。

collections/ 下是这套新体系对每个经典指标的重写(src/ragas/metrics/collections/__init__.py 列出了 FaithfulnessAnswerRelevancyContextPrecision 等)。它们的基类 BaseMetric 在初始化时强制校验 LLM 必须是现代 InstructorLLM,否则报错并提示用 llm_factory(src/ragas/metrics/collections/base.py:118-125)——这是新旧体系最硬的边界。

用装饰器快速造指标(新体系)。 create_metric_decorator 让你把普通函数变成指标:它从函数签名动态建一个 Pydantic 校验模型校验入参,把返回值包成 MetricResult,还按 allowed_values(["pass","fail"] / (0.0,1.0) / int)选离散/数值/排名校验器(src/ragas/metrics/decorator.py:107-205)。

# 示意,非源码:用装饰器三行造一个离散指标
from ragas.metrics import discrete_metric

@discrete_metric(name="sentiment", allowed_values=["positive", "negative"])
def sentiment(user_input: str, response: str) -> str:
return "positive" if "good" in response else "negative"

sentiment.score(user_input="how are you?", response="I'm good!").value # "positive"

6. 边界与坑

  • 失败默默变 NaN。 默认 raise_exceptions=False,某条样本某指标算崩了会被 Executor 吞掉填 np.nan(src/ragas/executor.py:71-84),不会中断整批。聚合时用 safe_nanmean 跳过 NaN。调试时务必开 raise_exceptions=True,否则你只看到一片 NaN 不知道为什么。
  • 拆不出陈述 → NaN。 faithfulness 若答案拆不出任何陈述,直接返回 np.nan(src/ragas/metrics/_faithfulness.py:210-211),不是 0。
  • 新旧 LLM 不通用。 旧指标兼容 LangChain LLM(但已发弃用警告),新 collections 指标只收 InstructorLLM。混用会在初始化就报错。
  • 作用域: 本章以 faithfulness、answer_relevancy 两个旗舰指标讲透通用套路;其余 ~25 个指标(BLEU/ROUGE、工具调用准确率、主题贴合度等)各有细节,但「拆原子判断→聚合」或「传统文本指标」的骨架是一致的。

7. 代码地图

主题文件关键符号
指标基类(旧)src/ragas/metrics/base.pyMetric, MetricWithLLM, MetricWithEmbeddings, SingleTurnMetric, Ensember
指标基类(新)src/ragas/metrics/base.pySimpleBaseMetric, SimpleLLMMetric, create_auto_response_model
新体系组件校验src/ragas/metrics/collections/base.pyBaseMetric, _validate_llm, _validate_embeddings
装饰器造指标src/ragas/metrics/decorator.pycreate_metric_decorator, CustomMetric
忠实度src/ragas/metrics/_faithfulness.pyFaithfulness, StatementGeneratorPrompt, NLIStatementPrompt, _compute_score, FaithfulnesswithHHEM
答案相关性src/ragas/metrics/_answer_relevance.pyResponseRelevancy, calculate_similarity, _calculate_score