跳到主要内容

第 1 章 · 地基:测试用例与指标契约

这章讲什么: 评测有两块地基——装数据的 LLMTestCase规定指标长什么样的 BaseMetric。先把这两个搞清楚,后面所有指标(02、03 章)才有共同的词汇。本章不深入任何具体指标怎么算分。


1.1 一条评测样本:LLMTestCase

它要解决的小问题: 评测一条样本,到底需要喂哪些数据?答案取决于你用哪个指标——「答案相关性」只需要 input + actual_output;「上下文召回」还需要 retrieval_contextexpected_output。所以 LLMTestCase 是一个字段大多可选的容器,按指标所需填。

它是一个 Pydantic 模型,核心字段(test_case/llm_test_case.py:332 起的 LLMTestCase):

字段含义谁会用到
input给被测系统的输入(唯一必填)几乎所有指标
actual_output被测系统的真实输出几乎所有指标
expected_output理想答案(你给的参考)上下文召回、G-Eval 等
context理想/真值上下文幻觉检测等
retrieval_contextRAG 实际检索到的片段忠实度、上下文精度/召回
tools_calledagent 实际调用的工具列表工具正确性等 agent 指标
expected_tools期望调用的工具工具正确性
token_cost / completion_time成本与延迟性能类断言

一个直觉: LLMTestCase 就是一张「考卷 + 学生作答 + 参考答案 + 草稿纸」。不同的阅卷标准(指标)只看其中几栏。

怎么知道某个字段必填? 每个指标声明自己要哪些字段。例如答案相关性指标在类上写死了:

# metrics/answer_relevancy/answer_relevancy.py:26 —— 该指标的必填字段
_required_params: List[SingleTurnParams] = [
SingleTurnParams.INPUT,
SingleTurnParams.ACTUAL_OUTPUT,
]

SingleTurnParams 是个枚举(test_case/llm_test_case.py:174,旧名 LLMTestCaseParams 已废弃),把字段名做成可传递的常量。运行时 check_llm_test_case_params(...)metrics/utils.py:306)会检查用例里这些字段是否都非空,缺了就报错或跳过。这样「指标需要什么」是声明式的,引擎能在调 LLM 之前就拦下不合格的用例。


1.2 工具调用怎么表示:ToolCall

agent 指标要判断「工具调对没」,就需要一个结构化的工具调用表示。ToolCalltest_case/llm_test_case.py:236)记录 nameinput_parametersoutput 等,并且自定义了 __eq____hash__

为什么要自定义 hash? 因为要把工具调用放进集合做「期望集 vs 实际集」的比较,而 input_parameters 里常嵌套 dict/list 这些不可哈希的东西。它用一个递归的 _make_hashabletest_case/llm_test_case.py:207)把任意嵌套结构转成可哈希的元组/frozenset:

# 示意,非源码:把嵌套结构变可哈希的思路
def make_hashable(obj):
if isinstance(obj, dict): # dict → 排序后的键值对元组
return tuple(sorted((k, make_hashable(v)) for k, v in obj.items()))
if isinstance(obj, (list, tuple)): # list → 元素元组
return tuple(make_hashable(x) for x in obj)
return obj # 基本类型原样

重点看:比较两个工具调用是否「相同」时,只比 name + input_parameters + output(见 ToolCall.__eq__),description/reasoning 这些解释性字段不参与,因为它们不影响「这步调用对不对」。


1.3 多轮对话:ConversationalTestCase

聊天机器人的评测样本不是单条问答,而是一串轮次。ConversationalTestCasetest_case/conversational_test_case.py:173)核心就是 turns: List[Turn],每个 Turn:54)是 {role: "user"|"assistant", content: str},外加可选的 chatbot_role(用于「角色坚持度」这类指标)。

对应地有一套并行的 BaseConversationalMetric 基类和 MultiTurnParams 枚举——单轮、多轮两条平行世界,结构对称。


1.4 指标的统一契约:BaseMetric

它要解决的小问题: 引擎要能统一地跑「任意指标」,就必须有一份所有指标都遵守的契约——不管内部怎么算分,对外都长一个样。

BaseMetricmetrics/base_metric.py:44)规定了这份契约。它有一批结果字段和三个必须实现的方法

结果字段(算完往这里填) 必须实现的方法
┌─────────────────────────┐ ┌──────────────────────────────┐
│ threshold 阈值 │ │ measure(test_case) → float │ 同步打分
│ score 0~1 分数 │ │ a_measure(test_case)→ float │ 异步打分
│ success 通过与否 │ │ is_successful() → bool │ 分数 vs 阈值
│ reason 一句理由 │ └──────────────────────────────┘
│ error 出错信息 │
│ evaluation_cost 花了多少钱│
└─────────────────────────┘

三个方法都是 @abstractmethod,子类必须实现(metrics/base_metric.py:72:76:82)。约定俗成的分工:

  • measure() 同步入口。当 async_mode=True 时,它内部其实是起一个事件循环去 await self.a_measure(...)——也就是说真正的算分逻辑写在异步版里,同步版只是个壳。
  • a_measure() 异步实现,是干活的地方。
  • is_successful()BaseMetric 上只是个 @abstractmethod(基类不给实现)。各子类的惯例实现是 self.score >= self.threshold,并且包了 try/except:若 score 没算出来(None),不会崩,而是判为失败(见 AnswerRelevancyMetricGEval 各自的 is_successful)。

一个容易忽略的细节——指标方法会被自动「上追踪」。 BaseMetric 用了 __init_subclass__ 钩子(metrics/base_metric.py:66):每当有人继承它定义新指标,框架就调 observe_methods(cls):70)给它的方法套上 span。这就是为什么连指标内部的调用都能出现在追踪里——你不用手动加任何东西(这套机制见 04 章)。


1.5 指标怎么拿到 prompt:PromptMixin

BaseMetric 继承自 PromptMixinmetrics/base_metric.py:21)。它提供 _get_prompt(method, ...),把指标的 prompt 外置成模板文件,按「指标类名 + 方法名」去解析(resolve_template("metrics", self.__class__.__name__, method, ...))。

好处有二:

  • prompt 和代码分离,模板是纯文本 .txt(Jinja2),改 prompt 不动逻辑。
  • 同一份模板能渲染多模态变体(multimodal=True 时注入图片处理规则)。

例如 G-Eval 的模板就放在 metrics/g_eval/templates/ 下(generate_evaluation_steps.txtgenerate_evaluation_results.txt),文件名正对应 _get_prompt("generate_evaluation_steps") 的方法参数。02 章会读到这些模板的真实内容。


1.6 小结:这章给了你什么

  • LLMTestCase = 一条样本,字段大多可选,按指标所需填;_required_params 声明式地说明每个指标要哪些字段。
  • BaseMetric = 所有指标的契约:填 score/reason/success,实现 measure/a_measure/is_successful;同步壳 + 异步实现是通用模式。
  • prompt 外置成模板,按类名+方法名解析。

带着这套词汇,进入 02 章看「指标内部到底怎么把一段文本变成一个分数」。


代码地图

主题文件符号
单轮用例test_case/llm_test_case.pyLLMTestCaseSingleTurnParamsRetrievedContextData
工具调用test_case/llm_test_case.pyToolCall_make_hashable
多轮用例test_case/conversational_test_case.pyConversationalTestCaseTurnMultiTurnParams
指标契约metrics/base_metric.pyBaseMetricBaseConversationalMetricPromptMixin
必填字段校验metrics/utils.pycheck_llm_test_case_params
prompt 解析templates/resolver.pyresolve_template