内置不够用?手搓 RAGAS 自定义指标(含 SQL/MultiHop)(10/13)

总目录

第 1 章 你的 RAG 为什么总在「一本正经地胡说」

第 2 章 一张图看懂 RAGAS:核心概念与 7 大指标

第 3 章 数据构建------样本、数据集与模型接入

第 4 章 Faithfulness、Context Recall...RAGAS 核心指标怎么算

第 5 章 生成侧指标与指标选型:别再乱用 Faithfulness

第 6 章 把指标真正跑起来:执行、质控与回归闸门

第 7 章 没标注数据?用 RAGAS 自动合成测试集

第 8 章 让评估贯穿研发:实验追踪与 @experiment/Dataset 工作流

第 9 章 从笔记本到 CI:把 RAGAS 做成生产级流水线

第 10 章 内置不够用?手搓 RAGAS 自定义指标(含 SQL/MultiHop)

第 11 章 RAGAS 不是唯一:6 大评估工具横向对比(功能篇)

第 12 章 怎么选?不同团队画像的选型决策矩阵

第 13 章 团队落地 RAGAS 的五阶段路线图(可直接抄作业)

在上一篇文章《从笔记本到 CI:把 RAGAS 做成生产级流水线》中,我们把评估做成了团队流水线的一环。但当内置指标覆盖不了你的业务语义时怎么办?接下来手搓 RAGAS 自定义指标(含 SQL / MultiHop 实战)。

读者对象 :已学完前 8 章,掌握 faithfulness、answer_relevancy、context_precision/recall 等内置指标,并能在 EvaluationDataset 上跑评估的工程师。

本章目标:当内置指标无法覆盖你的业务语义时,学会用 RAGAS 的扩展点做定制化评估------从"让 LLM 当裁判"到"自己写一个指标类"。


本章约定(先读)

  • 【事实】 标注的是已验证的 API 行为(基于 ragas==0.4.3 实测/官方文档)。
  • 【观点】 标注的是作者给出的最佳实践建议,可按团队情况调整。
  • 版本表述谨慎 :截至 ragas==0.4.3,官方迁移指南对 evaluate() 的"移除版本"表述并不一致(部分文档称 removed in v1.0 ,迁移指南仅称 a future release )。【事实】 evaluate() 在 v0.4.3 会触发 DeprecationWarning;【观点】 其确切移除版本请以你本地实测为准,不要凭文档猜测写死。
  • 【事实】 ragas.metrics 中的小写单例(如 faithfulness、answer_relevancy)官方明确在 v1.0 移除 ;【观点】 新代码一律改用 ragas.metrics.collections 中的类式 API(Faithfulness(...) 等)。
  • 官方仓库 :主仓库已迁至 vibrantlabsai/ragas(https://github.com/vibrantlabsai/ragas);旧镜像 explodinggradients/ragas(https://github.com/explodinggradients/ragas)内容一致,可作为旧路径引用。
  • 【事实】 模型注入现行写法:from ragas.llms import llm_factory; llm = llm_factory("gpt-4o-mini", client=OpenAI())。

9.1 内置指标不够用时怎么办:扩展点总览

内置指标覆盖的是"通用质量维度":忠实度、相关性、上下文精度/召回等。【观点】 一旦你的业务出现下面这些信号,就该考虑超越内置指标:

  • 需要判断回答是否合规 / 礼貌 / 安全(业务专属语义,无法用忠实度表达);
  • 需要按**评分量规(rubric)**做 1--5 分制的人工式打分;
  • 答案是翻译、摘要、抽取式事实,更适合 BLEU / ROUGE / Exact Match 这类确定性指标;
  • 你的系统是 Text-to-SQL / 代码生成,要检查 SQL 是否可执行、结果是否正确;
  • 测试集需要 多跳(multi-hop) 问题来压测检索与推理;
  • 以上都不够,要从零写一个指标。

RAGAS 为这些场景提供了清晰的扩展点,下表按"是否需要 LLM"和"是否内置"归类:

扩展点 是否需要 LLM 是否内置 导入位置(v0.4.3) 典型用途
AspectCritic 是 是 ragas.metrics 自定义二元维度裁判(礼貌/合规/有害)
RubricsScore 是 是 ragas.metrics 按 rubric 做离散分制打分
BleuScore / RougeScore / ExactMatch 否 是 ragas.metrics.collections 翻译/摘要/抽取式答案的确定性评估
SQL / 领域指标 否或视情况 否(需自写) 自定义类 检查 SQL 可执行性/正确性
TestsetGenerator + MultiHop 合成器 是 是 ragas.testset 合成多跳测试集
自定义 Metric 子类 视情况 否(需自写) ragas.metrics.base 任何上面都覆盖不了的需求

【重要事实】关于 API 分代:RAGAS 现在同时有两套指标 API 并存------

  • Legacy(V1) :ragas.metrics 下的小写单例与 AspectCritic、RubricsScore 等类,调用方式多为 scorer.single_turn_ascore(sample),返回 float。
  • Modern(V2 / collections) :ragas.metrics.collections 下的类式指标(如 Faithfulness、BleuScore),调用方式多为 result = await scorer.ascore(user_input=..., response=..., ...),返回 MetricResult(含 .value 与 .reason)。

【事实】AspectCritic 与 RubricsScore 截至 v0.4.3 仍住在 ragas.metrics,尚未迁移到 ragas.metrics.collections (迁移矩阵中 AspectCritic 标注为 removed / use @discrete_metric() ,即 collections 里暂无对应类)。因此本章 9.2、9.3 必须用 from ragas.metrics import ...,不要误写成从 collections 导入。其余内置指标(含非 LLM 指标)则推荐 collections 类式 API。

参考:RAGAS 迁移指南 https://docs.ragas.io/en/stable/howtos/migrations/migrate_from_v03_to_v04/ ;指标 API 总览(DeepWiki,旧镜像)https://deepwiki.com/explodinggradients/ragas/12.2-metrics-api

小结(9.1) :先用对扩展点分类;新代码优先 collections 类式 API,但 AspectCritic/RubricsScore 仍在 ragas.metrics。

动手检查点(9.1) :确认你本地 ragas.__version__ == "0.4.3";在 Python 里分别执行 from ragas.metrics import AspectCritic 与 from ragas.metrics.collections import Faithfulness,验证两者都能导入成功(前者 legacy、后者 modern)。


9.2 AspectCritic:按自定义维度让 LLM 当裁判批评

【事实】 AspectCritic 用于按你用自然语言定义的"维度(aspect)"对回答做二元裁判 (通过=1 / 不通过=0)。它本质是多次 LLM 调用 + 多数投票:对每个样本,critic 会用 strictness(默认 3,建议 2--4)次独立判断,再取多数票得到 0/1。

【重要·来自审稿 REVISE】导入路径务必写成:

python 复制代码
from ragas.metrics import (
    AspectCritic,
)  # ✅ 正确(v0.4.3)

# from ragas.metrics.
#   collections import AspectCritic  # ❌
# 错误:collections 未导出该类,尚未迁移

也就是说,ragas.metrics.collections 模块在 v0.4.3 没有 AspectCritic,不要从 collections 导入。

9.2.1 它适合什么

  • 合规:回答是否泄露了隐私/内部信息;
  • 礼貌:回答语气是否礼貌、专业;
  • 安全:是否含有害/误导内容;
  • 任何"能用一句话说清标准"的维度。

9.2.2 完整可运行示例(依赖:ragas==0.4.3、openai)

python 复制代码
# 依赖与版本:
#   pip install "ragas==0.4.3" openai
# 运行前设置环境变量:export OPENAI_API_
#   KEY=sk-...

from openai import OpenAI
from ragas.llms import llm_factory
from ragas.metrics import (
    AspectCritic,
)  # ← legacy 位置,非 collections
from ragas.dataset_schema import (
    SingleTurnSample,
)

# 模型注入(v0.4.3 现行写法)
evaluator_llm = llm_factory(
    "gpt-4o-mini", client=OpenAI()
)

# 例 1:礼貌度(自定义维度)
politeness = AspectCritic(
    name="politeness",
    definition=(
        "回答是否礼貌、"
        "专业,"
        "且没有傲慢或冒犯性措辞?"
        "符合返回 1,"
        "否则返回 0。"
    ),
    llm=evaluator_llm,
    # 多数投票次数,建议 2--4
    strictness=3,
)

# 例 2:合规(是否泄露个人隐私)
no_pii = AspectCritic(
    name="no_pii_leak",
    definition=(
        "回答是否包含任何"
        "员工的手机号、"
        "身份证号、"
        "家庭住址等个人隐私信息?"
        "包含返回 0,"
        "不包含返回 1。"
    ),
    llm=evaluator_llm,
)

sample = SingleTurnSample(
    user_input=(
        "你们公司负责这笔"
        "退款的同事联系方"
        "式能给我吗?"
    ),
    response=(
        "抱歉,"
        "出于隐私保护我无法提供同事"
        "的个人联系方式,"
        "但您可以拨打客服"
        "热线 400-xxx "
        "处理退款。"
    ),
    reference="",
)

# 【事实】legacy 指标返回 float(0.
#   0 或 1.0),不是 MetricResult
score = (
    await politeness.single_turn_ascore(
        sample
    )
)
print("politeness:", score)  # 例如 1.0

score_pii = (
    await no_pii.single_turn_ascore(
        sample
    )
)
print(
    "no_pii_leak:", score_pii
)  # 例如 1.0(未泄露)

注意对比 :如果你用 collections 里的现代指标(如 Faithfulness),调用会返回 MetricResult,要取 .value;而 AspectCritic 作为 legacy 类,返回的是裸 float。【观点】 这是"它尚未迁移"的直接体现,写评估流水线时要区分处理。

小结(9.2) :AspectCritic 用自然语言定义维度、LLM 多数投票给 0/1;必须从 ragas.metrics 导入 ;返回 float。

动手检查点(9.2) :给你的客服场景定义一个 AspectCritic(name="tone_friendly", ...),对 5 条真实回答打分,观察 strictness 取 2 与取 5 时分数是否更稳定(投票次数越多通常越稳,但成本更高)。


9.3 Rubrics:按评分量规(rubric)打分

当"通过/不通过"太粗、你想要 1--N 分制 的细粒度评估时,用 RubricsScore。【事实】 你提供一个 rubrics 字典,键为 score{k}_description(k 从 1 到 N),值为该分数对应的详细描述;LLM 依据描述把回答归到某一档,返回离散整数。

【事实】 与 AspectCritic 一样,RubricsScore 在 v0.4.3 也住在 ragas.metrics(legacy),调用 single_turn_ascore 返回整数。

9.3.1 完整可运行示例(依赖同上)

python 复制代码
from openai import OpenAI
from ragas.llms import llm_factory
from ragas.metrics import RubricsScore                     # ← 同样在 ragas.metrics
from ragas.dataset_schema import SingleTurnSample

evaluator_llm = llm_factory("gpt-4o-mini", client=OpenAI())

# 定义一个"回答完整性"量规(1--5 分)
rubrics = {
    "score1_description": "回答完全错误,且未触及参考内容的任何要点。",
    "score2_description": "回答部分正确,但存在重大错误或明显遗漏,影响对参考内容的理解。",
    "score3_description": "回答大体准确,但缺少清晰度、完整性或若干次要细节。",
    "score4_description": "回答准确清晰,仅有轻微遗漏或细微不准确。",
    "score5_description": "回答完全准确、清晰,且无遗漏地覆盖了参考内容。",
}

completeness = RubricsScore(
    name="answer_completeness",
    llm=evaluator_llm,
    rubrics=rubrics,
)

sample = SingleTurnSample(
    user_input="地球绕太阳转吗?",
    response="地球是平的,不绕太阳转。",
    reference="科学共识确认地球是球形行星,绕太阳公转,证据来自天文观测、卫星影像与引力测量。",
)

score = await completeness.single_turn_ascore(sample)
print("completeness score:", score)     # 例如 1(完全错误)

【观点】 rubric 的描述要彼此互斥且覆盖连续梯度(1 分最差、N 分最好),否则 LLM 容易在边界档位犹豫、分数抖动。写 rubric 时建议拉一位领域专家过一遍措辞。

进阶 :若每条样本需要各自的 rubric(而非全班同一把尺子),可用 InstanceRubrics(按样本给 rubric),见 https://docs.ragas.io/en/v0.4.3/concepts/metrics/available_metrics/general_purpose/ 。

小结(9.3) :RubricsScore 用 score{k}_description 字典定义分制,返回离散整数;适合细粒度、可解释打分;仍在 ragas.metrics。

动手检查点(9.3):为"答案专业性"写一套 1--5 分量规,对 10 条回答打分,统计分布;再让另一位同事独立用同一 rubric 手工打分,计算你们与 LLM 评分的一致率(简单做法:皮尔逊/斯皮尔曼相关)。


9.4 非 LLM 指标:BLEU / ROUGE / Exact Match

有些任务不该 用 LLM 当裁判。【观点】 当答案有"标准文本"或"标准结构"时,确定性指标更便宜、更可复现、且无评委偏见:

  • 翻译 / 摘要 :BleuScore、RougeScore 比较生成文本与参考文本的 n-gram 重叠;
  • 抽取式事实 / 代码 / SQL 片段 :ExactMatch 判断是否逐字一致(通常先归一化大小写、去空白)。

【事实】 这些指标在 ragas.metrics.collections 中,且不需要 LLM ;调用返回 MetricResult,通过 .value 取分、.reason 取解释(此处 .reason 通常为空或简短说明)。

9.4.1 完整可运行示例(依赖:ragas==0.4.3)

python 复制代码
from ragas.metrics.collections import (
    BleuScore,
    RougeScore,
    ExactMatch,
)

# 非 LLM 指标无需注入 llm
bleu = BleuScore()
rouge = RougeScore()
exact = ExactMatch()

# 例:摘要评估
summary_resp = (
    "巴黎是法国的首都,也是最大城市。"
)
summary_ref = (
    "巴黎为法国首都及最大城市。"
)

r_bleu = await bleu.ascore(
    response=summary_resp,
    reference=summary_ref,
)
r_rouge = await rouge.ascore(
    response=summary_resp,
    reference=summary_ref,
)
print(
    "BLEU:", r_bleu.value
)  # float ∈ [0, 1]
print(
    "ROUGE:", r_rouge.value
)  # float ∈ [0, 1]

# 例:抽取式事实(Exact Match,
#   先归一化更易通过)
fact_resp = (
    "The capital of France is Paris."
)
fact_ref = (
    "the capital of france is paris."
)
r_em = await exact.ascore(
    response=fact_resp,
    reference=fact_ref,
)
print(
    "ExactMatch:",
    r_em.value,
    # 1.0(取决于内部归一化;
    #   未归一化时为 0)
)

9.4.2 怎么解读(避免误用)

  • 【观点】 BLEU/ROUGE 高分 ≠ 语义正确。它们只衡量 n-gram 重叠,对同义改写极不友好;在开放生成任务上单独看会误导。
  • 【事实】 它们对语言/形态敏感:中文不经过分词预处理时,字符级 n-gram 与英文词级不可直接横比。
  • 【观点】 用法建议------翻译/摘要用 BLEU/ROUGE 做回归监控 (和上周比,别和"正确率"画等号);抽取式答案、标识符、SQL 片段用 ExactMatch;开放问答仍以 LLM 裁判类指标为主。

小结(9.4) :非 LLM 指标便宜可复现,住在 ragas.metrics.collections、返回 MetricResult;适合有标准文本/结构的任务,但别把它当成"语义正确"的代理。

动手检查点(9.4) :对同一批摘要,分别计算 BLEU 与 answer_relevancy(LLM 类),画散点图------你会观察到两者相关性通常不高,这正是"确定性指标不可替代 LLM 裁判"的直观证据。


9.5 SQL 类指标(自定义实战:可执行 / 正确)

Text-to-SQL、NL2SQL 或"数据问答"系统有一个特有需求:生成的 SQL 到底能不能跑、跑出来的结果对不对 。【事实】 RAGAS 在 v0.4.3 没有 内置 SQL 指标,必须用 9.7 的"自定义 Metric"机制自己写------这恰恰是最好的练手场景。

下面给出两个确定性(非 LLM) SQL 指标,以及一个可选的 LLM 语义等价裁判。

9.5.1 指标一:SQL 可执行性(是否能在目标库跑通)

python 复制代码
# 依赖与版本:pip install "ragas==0.4.
#   3" openai
# 本指标为非 LLM 指标,
# 只需 ragas + 一个 sqlite(或你的业务库
#   )连接

from dataclasses import dataclass, field
import typing as t
import sqlite3
from ragas.metrics.base import (
    MetricType,
    SingleTurnMetric,
)
from ragas.dataset_schema import (
    SingleTurnSample,
)
from ragas.callbacks import Callbacks


@dataclass
class SQLExecutableMetric(
    SingleTurnMetric
):
    (
        """检查生成的 SQL """
        """能否在目标数据库"""
        """上成功执行。"""
        """可执行=1.0,否则=0."""
        """0。"""
    )

    name: str = "sql_executable"
    _required_columns: t.Dict[
        MetricType, t.Set[str]
    ] = field(
        default_factory=lambda: {
            MetricType.SINGLE_TURN: {
                "response"
            }
        }
    )
    dsn: str = (
        # sqlite 连接串;
        #   生产可换成你的库 DSN
        ":memory:"
    )

    async def _single_turn_ascore(
        self,
        sample: SingleTurnSample,
        callbacks: Callbacks = None,
    ) -> float:
        sql = (
            sample.response or ""
        ).strip()
        if not sql:
            return 0.0
        try:
            conn = sqlite3.connect(
                self.dsn
            )
            conn.execute(
                sql
                # 仅编译/执行,不关心返
                #   回行数
            )
            conn.close()
            return 1.0
        except Exception:
            return 0.0

9.5.2 指标二:SQL 结果一致性(生成 SQL 与参考 SQL 结果是否相同)

仅"能跑"不够------还要"跑出对的答案"。最稳妥的确定性做法是:在同一份数据库上分别执行生成 SQL 与参考 SQL,比较结果集。

python 复制代码
@dataclass
class SQLResultMatchMetric(
    SingleTurnMetric
):
    (
        """比较生成 SQL """
        """与参考 SQL """
        """的执行结果集是否一致("""
        """集合语义)。"""
        """一致=1.0。"""
    )

    name: str = "sql_result_match"
    _required_columns: t.Dict[
        MetricType, t.Set[str]
    ] = field(
        default_factory=lambda: {
            MetricType.SINGLE_TURN: {
                "response",
                "reference",
            }
        }
    )
    dsn: str = ":memory:"

    async def _single_turn_ascore(
        self,
        sample: SingleTurnSample,
        callbacks: Callbacks = None,
    ) -> float:
        gen_sql = (
            sample.response or ""
        ).strip()
        ref_sql = (
            sample.reference or ""
        ).strip()
        try:
            conn = sqlite3.connect(
                self.dsn
            )
            gen_rows = conn.execute(
                gen_sql
            ).fetchall()
            ref_rows = conn.execute(
                ref_sql
            ).fetchall()
            conn.close()
            # 用集合语义比较(忽略行的顺
            #   序)
            return (
                1.0
                if set(
                    map(tuple, gen_rows)
                )
                == set(
                    map(tuple, ref_rows)
                )
                else 0.0
            )
        except Exception:
            return 0.0

9.5.3 指标三(可选):LLM 语义等价裁判

当参考 SQL 不存在、或两条 SQL 写法不同但语义等价时,可用 LLM 判断"是否回答同一问题"。这里 AspectCritic 的思路可直接复用:

python 复制代码
from ragas.metrics import AspectCritic

sql_equiv = AspectCritic(
    name="sql_semantic_equivalent",
    definition=(
        "给定用户问题与两条 SQL(response 与 reference),"
        "若两条 SQL 在语义上等价(查询同一意图、返回等价结果集),返回 1;"
        "否则返回 0。仅因写法/别名不同但语义一致也应判为 1。"
    ),
    llm=evaluator_llm,   # 复用 9.2 的 llm_factory 实例
)

9.5.4 跑起来

python 复制代码
sample = SingleTurnSample(
    user_input=(
        "查一下订单表里的" "订单总数"
    ),
    response=(
        "SELECT COUNT(*) "
        "FROM orders;"
    ),  # 生成的 SQL
    reference=(
        "SELECT COUNT(1) "
        "FROM orders;"
    ),  # 参考 SQL(语义等价)
)

exe = SQLExecutableMetric()
match = SQLResultMatchMetric()
print(
    "executable:",
    await exe._single_turn_ascore(
        sample
    ),
)  # 1.0
print(
    "result_match:",
    await match._single_turn_ascore(
        sample
    ),
)  # 1.0(结果集相同)

【观点】 真实项目中推荐"可执行性 + 结果一致性"作为门禁(gate) (CI 里 0 分直接挂红灯),把 LLM 语义裁判作为"参考 SQL 缺失时"的兜底。别忘了 SQL 注入与权限:【事实】 上面的示例用 :memory: 内存库演示,生产环境务必指向只读副本/影子库,绝不能拿线上主库做评估执行。

小结(9.5):SQL 指标 RAGAS 没内置,需自写;最稳的是"可执行 + 结果集一致"双指标(确定性),语义等价再上 LLM。

动手检查点(9.5) :造 20 条 (生成SQL, 参考SQL) 样本,其中混入 5 条语法错误、5 条语法对但结果错;分别跑 SQLExecutableMetric 与 SQLResultMatchMetric,确认两类错误都能被区分捕获。


9.6 MultiHop 查询合成与高级测试集

普通测试集多是"单跳"问题(一个文档就能答)。【观点】 要压测检索与多步推理,必须刻意合成**多跳(multi-hop)**问题------需要跨多个文档/节点拼接信息才能回答。

【事实】 RAGAS 的 ragas.testset 用**知识图谱(KnowledgeGraph)**驱动查询合成,并提供两类多跳合成器:

  • MultiHopAbstractQuerySynthesizer:跨多个主题相似节点的抽象问题(需推理综合);
  • MultiHopSpecificQuerySynthesizer:两个实体重叠 节点间的具体问题(需拼接多段上下文)。

参考(DeepWiki,旧镜像):https://deepwiki.com/explodinggradients/ragas/7.3-multi-hop-synthesizers

9.6.1 完整可运行示例(依赖:ragas==0.4.3、openai、langchain-core)

python 复制代码
# 依赖与版本:
#   pip install "ragas==0.4.3" openai langchain-core
# 运行前设置环境变量:export OPENAI_API_KEY=sk-...

from openai import OpenAI
from ragas.llms import llm_factory
from ragas.embeddings.base import embedding_factory        # 现代工厂;无参时按环境变量自动选择
from ragas.testset import TestsetGenerator
from ragas.testset.synthesizers import (
    SingleHopSpecificQuerySynthesizer,
    MultiHopAbstractQuerySynthesizer,
    MultiHopSpecificQuerySynthesizer,
)
from langchain_core.documents import Document

# 1) 模型注入(v0.4.3 现行写法)
llm = llm_factory("gpt-4o-mini", client=OpenAI())
embeddings = embedding_factory()               # 自动读取 OPENAI_API_KEY,使用默认 openai 嵌入

# 2) 准备语料(这里用纯文本构造,无需外部 loader)
docs = [
    Document(page_content="埃菲尔铁塔位于法国巴黎,由古斯塔夫·埃菲尔的公司设计,1889 年建成。"),
    Document(page_content="巴黎是法国首都,也是该国人口最多的城市,位于塞纳河畔。"),
    Document(page_content="古斯塔夫·埃菲尔还是自由女神像内部结构的设计者,该雕像位于美国纽约。"),
    Document(page_content="塞纳河横穿巴黎,沿岸有包括埃菲尔铁塔在内的多处地标。"),
]

# 3) 构造生成器
generator = TestsetGenerator(llm=llm, embedding_model=embeddings)

# 4) 刻意偏向多跳:提高两类 MultiHop 合成器的权重
query_distribution = [
    (SingleHopSpecificQuerySynthesizer(llm=llm), 0.2),   # 单跳 20%
    (MultiHopAbstractQuerySynthesizer(llm=llm), 0.4),    # 多跳抽象 40%
    (MultiHopSpecificQuerySynthesizer(llm=llm), 0.4),   # 多跳具体 40%
]

# 5) 生成测试集(内部会自动建知识图谱并应用 transform)
testset = generator.generate_with_langchain_docs(
    documents=docs,
    testset_size=12,
    query_distribution=query_distribution,
)

# 6) 查看并筛选多跳样本
df = testset.to_pandas()
print(df["synthesizer_name"].value_counts())            # 应能看到 multi_hop_* 占多数
multihop = df[df["synthesizer_name"].str.contains("multi_hop")]
print(multihop[["user_input", "reference", "synthesizer_name"]].head())

关键字段 :生成的 testset 每行含 user_input(合成问题)、reference_contexts(理想检索上下文)、reference(理想答案)、synthesizer_name(合成器类型,用来确认多跳占比)。

【观点】 多跳样本天然更难,适合作为"进阶测试集"单独跟踪;【事实】 合成问题往往是对源文档语句的转述,记得补充少量人工编写的边界样本(反事实、跨语言、长尾实体)以避免评测集偏见。

小结(9.6) :用 TestsetGenerator + MultiHop* 合成器、通过 query_distribution 权重刻意制造多跳问题;结果用 synthesizer_name 校验分布。

动手检查点(9.6) :把 query_distribution 改成全单跳(权重 1.0 给 SingleHopSpecificQuerySynthesizer)与全多跳两种极端,各生成 10 条,分别喂给你的 RAG,对比 context_recall------多跳集上的召回通常明显更低,这正说明它需要被单独优化。


9.7 自定义 Metric 编写实战(本节为本次修订补全,原稿空缺)

前面 SQL 指标已经"半只脚"踏进自定义指标。本节从零 走一遍:实现一个简单、确定性的自定义指标,并说明怎么升级成 LLM 版。我们会写一个 KeywordCoverageMetric:检查回答是否覆盖了所有"必填关键词",覆盖比例作为分数(0--1)。

为什么选它:确定性、无 LLM、逻辑一目了然,最适合作为"第一个自定义指标";之后你只需把打分内核换成 LLM 调用,就能得到业务裁判指标。

9.7.1 编写指标类

python 复制代码
# 依赖与版本:pip install "ragas==0.4.3" openai

from dataclasses import dataclass, field
import typing as t
from ragas.metrics.base import MetricType, SingleTurnMetric
from ragas.dataset_schema import SingleTurnSample, EvaluationDataset
from ragas.callbacks import Callbacks


@dataclass
class KeywordCoverageMetric(SingleTurnMetric):
    """【实战】检查回答是否覆盖全部必填关键词;覆盖比例 = 分数。"""
    name: str = "keyword_coverage"

    # 自定义参数:必填关键词列表(由使用方传入)
    required_keywords: t.List[str] = field(default_factory=list)

    # 【事实】必须声明该指标需要哪些字段;缺失时 RAGAS 会报错提示
    _required_columns: t.Dict[MetricType, t.Set[str]] = field(
        default_factory=lambda: {MetricType.SINGLE_TURN: {"response"}}
    )

    async def _single_turn_ascore(self, sample: SingleTurnSample, callbacks: Callbacks = None) -> float:
        if not self.required_keywords:
            return 1.0
        text = (sample.response or "").lower()
        hit = sum(1 for kw in self.required_keywords if kw.lower() in text)
        return hit / len(self.required_keywords)

要点(【事实】 来自官方"Write your own Metrics":https://docs.ragas.io/en/v0.3.8/howtos/customizations/metrics/_write_your_own_metric/ ):

  1. 继承 SingleTurnMetric(单轮)或 MultiTurnMetric(多轮);需要 LLM 就再混入 MetricWithLLM。
  2. 必须定义 _required_columns,告诉框架样本里缺了哪些字段。
  3. 实现 _single_turn_ascore(sample) -> float 作为打分内核。

9.7.2 在样本上跑

python 复制代码
samples = [
    SingleTurnSample(
        user_input="埃菲尔铁塔在哪?",
        response=(
            "埃菲尔铁塔位于法"
            "国巴黎。"
        ),  # 覆盖 巴黎/铁塔
    ),
    SingleTurnSample(
        user_input="埃菲尔铁塔在哪?",
        response=(
            "这是一座著名的铁" "塔。"
        ),  # 只覆盖 铁塔
    ),
]

metric = KeywordCoverageMetric(
    required_keywords=["巴黎", "铁塔"]
)
for s in samples:
    print(
        await metric.single_turn_ascore(
            s
        )
    )
    # 第 1 条 ≈ 1.0,第 2 条 = 0.5

# 也可以包装成 EvaluationDataset 批量使
#   用
dataset = EvaluationDataset(
    samples=samples
)
# 【观点】生产评估推荐走 collections 类
#   式 / experiment 流水线,
#   避免触发 evaluate()
#    的 DeprecationWarning

9.7.3 升级成 LLM 版(自定义业务裁判)

把打分内核换成 LLM 调用即可------这正是 AspectCritic 的内部思路。骨架:

python 复制代码
from ragas.metrics.base import (
    MetricWithLLM,
    SingleTurnMetric,
)


@dataclass
class ComplianceJudgeMetric(
    MetricWithLLM, SingleTurnMetric
):
    name: str = "compliance_judge"
    _required_columns: t.Dict[
        MetricType, t.Set[str]
    ] = field(
        default_factory=lambda: {
            MetricType.SINGLE_TURN: {
                "response"
            }
        }
    )

    async def _single_turn_ascore(
        self,
        sample: SingleTurnSample,
        callbacks: Callbacks = None,
    ) -> float:
        # 用 self.llm(由 MetricWithLLM
        #   提供,经 llm_factory 注入)
        #   调用评审提示
        # 返回 0.0 / 1.
        #   0 或其它归一化分数
        ...

【观点】 写 LLM 指标时务必控制方差:同一输入多次评分可能浮动 ±5%,建议聚合 ≥30 条样本再下结论,并对关键维度加 strictness 式多数投票。

9.7.4 小结

  • 自定义指标 = 继承 SingleTurnMetric/MultiTurnMetric(+MetricWithLLM)+ 声明 _required_columns + 实现 _single_turn_ascore。
  • 确定性指标(如关键词覆盖、SQL 可执行性)不依赖 LLM,便宜可复现,优先用于门禁。
  • LLM 指标灵活但需关注成本与方差。

9.7.5 动手实验

  1. 把 KeywordCoverageMetric 改成大小写/标点归一化后再匹配,验证对"Paris."与"paris"也能命中;
  2. 给 KeywordCoverageMetric 增加 partial_credit=False 开关:任一词缺失即判 0 分(硬门禁);
  3. 进阶:参考 9.5,把打分内核换成"在 sqlite 上执行回答里出现的 SQL 并校验非空",做一个 AnswerContainsRunnableSQLMetric。

本章小结

  • 扩展点地图 :AspectCritic(自定义二元裁判)、RubricsScore(rubric 分制)、BleuScore/RougeScore/ExactMatch(确定性)、SQL/领域指标(自写)、TestsetGenerator 多跳合成、自定义 Metric 类。
  • 【事实】导入分代 :AspectCritic、RubricsScore 在 v0.4.3 仍位于 ragas.metrics,不要 从 ragas.metrics.collections 导入(后者未导出 AspectCritic);其余内置指标推荐 collections 类式 API,返回 MetricResult(.value/.reason)。
  • 【事实】 ragas.metrics 小写单例官方明确 v1.0 移除;evaluate() 在 v0.4.3 触发 DeprecationWarning,确切移除版本以实测为准。
  • 模型注入 :from ragas.llms import llm_factory; llm = llm_factory("gpt-4o-mini", client=OpenAI())。
  • 自定义指标是兜底扩展点:确定性优先做门禁,LLM 版做灵活业务裁判。

全章动手检查点(Checklist)

  • 9.1:确认 ragas==0.4.3,分别导入 AspectCritic 与 collections 的 Faithfulness 成功。
  • 9.2:为你的场景定义 1 个 AspectCritic,观察 strictness 影响。
  • 9.3:写一套 1--5 分量规并做人机一致率统计。
  • 9.4:对摘要同时算 BLEU 与 answer_relevancy,验证二者弱相关。
  • 9.5:造 20 条 SQL 样本,用"可执行 + 结果一致"双指标区分两类错误。
  • 9.6:生成"全单跳 vs 全多跳"两套测试集,对比 context_recall。
  • 9.7:实现并运行 KeywordCoverageMetric,完成 3 个动手实验。

参考链接

前一章:从笔记本到 CI:把 RAGAS 做成生产级流水线

相关推荐
老唐7771 小时前
(附代码)使用OpenCV和关键点检测计算瓶子角度
人工智能·深度学习·opencv·目标检测·计算机视觉·ai·transformer
william_yangshun1 小时前
【AI实战】rakazo中文版使用教程:3869星开源AI agent,Docker一条命令跑起来
人工智能·docker·开源
Ai-_Man1 小时前
DeepSeek生成的内容能否下载吗?
开发语言·javascript·人工智能·小程序·ecmascript
sorry3551 小时前
从零实现 nanoGPT(五):保存模型,写一个独立的文本生成程序
人工智能
做个文艺程序员1 小时前
MQ第10篇:RAG与消息队列实战:Java构建AI客服系统的检索增强生成完整架构
人工智能·架构·rocketmq·向量数据库·rag·ai客服·spring ai
Evelvnk1 小时前
计算机学习资料
开发语言·人工智能·计算机·php
用户8314550980311 小时前
如一 Agent 架构解读(十九):错误的修养——工具报错是 Agent 唯一的自纠教材
人工智能·llm
招财牛猫1 小时前
当 AI 公司用 AI 抓内鬼:从 OpenAI 会议室窃听案看内部威胁检测的技术实现
人工智能
中伟视界1 小时前
边缘AI目标检测在矿区场景的工程化部署:量化、裁剪与推理优化
人工智能·目标检测·目标跟踪