CodexQA

Industry & PracticeTools & Frameworks

Inspect 自定义评分器:Score、未评分样本和评分模型

CodexQA 团队6 min read

英国 AI Security Institute 的 Inspect 文档:自定义评分器接收 TaskState 和 Target,返回 Score。说明 value 的 C/I/P/N、返回 None 与 Score.unscored() 的差别,以及用 grader 角色调用模型。附 close_enough、includes 和 model_graded_qa 的代码。

In this piece

自定义评分器

概述

自定义评分器是这样的函数:接收 TaskState 和 Target,产出一个 Score。评分器(scorer)负责给模型输出打分。TaskState 是当前样本的任务状态,里面有输入和模型输出。Target 是样本的标准答案。Score 是一次打分的结果。

async def score(state: TaskState, target: Target):
     # Compare state / model output with target
     # to yield a score
     return Score(value=...)

先说明核心对象 Score 和 Value,再给出几个自定义评分器的例子,把做法说具体。

示例

这个评分器从模型输出里取出最后一个数。当它落在 target 的相对容差之内时,把样本判为正确。它用 @scorer 登记指标,从 state 读取模型输出,与 target.text 比较,并返回带 answer 和 explanation 的 Score。指标(metric)是对许多样本的分数做汇总,例如正确率。

import re

from inspect_ai import Task, task
from inspect_ai.dataset import Sample
from inspect_ai.scorer import (
    CORRECT,
    INCORRECT,
    Score,
    Target,
    accuracy,
    scorer,
    stderr,
)
from inspect_ai.solver import TaskState, generate


@scorer(metrics=[accuracy(), stderr()])
def close_enough(rel_tol: float = 0.01):
    async def score(state: TaskState, target: Target) -> Score:
        numbers = re.findall(
            r"-?\d+(?:\.\d+)?", state.output.completion
        )
        if not numbers:
            return Score(
                value=INCORRECT, 
                explanation="No number found in output."
            )

        answer = numbers[-1]
        expected = float(target.text)
        correct = abs(float(answer) - expected) <= rel_tol * abs(expected)
        return Score(
            value=CORRECT if correct else INCORRECT,
            answer=answer,
            explanation=state.output.completion,
        )

    return score


@task
def arithmetic():
    return Task(
        dataset=[
            Sample(
                input="What is 18 * 7?",
                target="126"
            ),
        ],
        solver=generate(),
        scorer=close_enough(),
    )

下面各节说明这个例子用到的部件。

Score

Score 的组成部分包括:

字段类型说明
valueValue赋给样本的分值(例如 "C" 或 "I",或一个原始数值)。
answerstr从模型输出中抽出、用来比较的文本(可选)。
explanationstr分数的说明,例如完整模型输出或评分模型的输出(可选)。
reasonScoreReason str`
metadatadict[str,Any]要写入日志文件的、关于这次分数的额外元数据(可选)。

例如,下面这些都是合法的 Score 对象:

Score(value="C")
Score(value="I")
Score(value=0.6)
Score(
    value="C" if extracted == target.text else "I",
    answer=extracted,
    explanation=state.output.completion
)

Score.value 可以是你的指标知道如何解释的任何 Value。内置的正确性评分器使用常量 CORRECT("C")、INCORRECT("I")、PARTIAL("P")和 NOANSWER("N")。指标如 accuracy() 默认使用的 value_to_float() 转换器,把这些值分别映射为 1.0、0.0、0.5 和 0.0。它也转换数值、数字字符串,以及常见的布尔字符串,例如 "yes" / "no" 和 "true" / "false"。

你可以返回其他字符串,但汇总指标需要一个能理解它们的转换器。例如:

from inspect_ai.scorer import accuracy, value_to_float

accuracy(
    to_float=value_to_float(correct="pass", incorrect="fail")
)

如果你是从一次补全内部抽取答案(例如用正则找文本,或看补全的开头或结尾),应尽量在 Score 里始终返回 answer。查看评测日志时,这会让打分细节容易理解得多。

未评分样本

当样本不在该评分器的范围内时,返回 None。例如,混合数据集里的代码测试评分器,可以对作文题返回 None。这样不会产生分数条目,也不会增加 unscored_samples。

当本来预期要给出判断、但没能得到时,使用 Score.unscored(),例如评分器的裁决无法解析:

return Score.unscored(
    reason="grader_failed",
    answer=extracted,
    explanation="Grader did not return a parseable verdict.",
)

这会创建一条值为 NaN 的分数,并保留所给的 reason、answer 和 explanation。指标会排除这个值。覆盖计数在 epoch 归约之后计算:默认的均值归约器忽略未评分的 epoch,其他归约器可以不同地处理它们。见 Interpreting coverage。

可选的 reason 接受标准的 ScoreReason 标签或自定义字符串。用 reason 描述结局,用 explanation 放上理解它所需要的细节。

对错误答案,或没有遵守任务要求格式的情况,返回一条已评分的裁决。对执行失败或非法的评分器输入,抛出异常,以便 Inspect 应用本次运行的错误处理设置。这些结局如何影响结果,见 Scoring Policy。

分值

Value 是主要标量类型,以及这些类型的 list 或 dict 的联合:

Value = Union[
    str | int | float | bool,
    Sequence[str | int | float | bool],
    Mapping[str, str | int | float | bool],
]

绝大多数评分器会使用 str(例如用 "C" 和 "I" 表示正确/不正确)或 float(其他类型是为了更复杂的情形)。要记住:评分器里使用的任何 Value 类型,都必须被该评分器声明的指标所支持(下文还会谈到)。

接下来看两个内置评分器的源码,作为实现自己的评分器的起点。如果正在写自定义评分器,也应阅读 Scoring Workflow,其中有优化开发过程的提示。

评分器里的模型

实现评分器时,常常要用到模型。最好的做法是用模型角色指定评分模型,让它们在运行时再选定。默认情况下,模型评分器查找名为 "grader" 的模型角色:

grader_model = get_model(role="grader")

也可以用 get_model() 取得当前被评测的模型,或其他模型接口。例如:

# use the model being evaluated for grading
grader_model = get_model()

# use another model for grading
grader_model = get_model("google/gemini-2.5-pro")

用 get_model() 的 config 参数覆盖默认生成选项:

grader_model = get_model(
    "google/gemini-2.5-pro",
    config = GenerateConfig(
        temperature = 0.0
    )
)

示例:Includes

下面是内置 includes() 评分器的源码:

@scorer(metrics=[accuracy(), stderr()])   # <1>
def includes(ignore_case: bool = True):

    async def score(state: TaskState, target: Target):   # <2>

        # check for correct
        answer = state.output.completion
        target = target.text   # <3>
        if ignore_case:
            correct = answer.lower().rfind(target.lower()) != -1
        else:
            correct = answer.rfind(target) != -1

        # return score
        return Score(
            value = CORRECT if correct else INCORRECT,    # <4>
            answer=answer  # <5>
        )

    return score
  1. 函数使用 @scorer 装饰器,并登记两个供该评分器使用的指标。
  2. score 函数声明为 async。这样它就能参加 Inspect 为昂贵的模型生成调用所做的优化调度(这个评分器不调用模型,但其他评分器会)。
  3. 使用 Target 上的 text 属性。这是一个方便属性,用来从 Target 取出简单文本值(因为目标在技术上可以是字符串列表)。
  4. 分值使用特殊常量 CORRECT 和 INCORRECT。accuracy()、stderr() 和 bootstrap_stderr() 知道如何把这些常量转换成浮点值(分别是 1.0 和 0.0)。
  5. 把完整的模型补全作为分数的 answer(answer 可选,但强烈建议提供,评测开发时经常要回看它)。

示例:模型评分

下面是 model_graded_qa() 评分器代码的一个有所简化的版本:

@scorer(metrics=[accuracy(), stderr()])
def model_graded_qa(
    template: str = DEFAULT_MODEL_GRADED_QA_TEMPLATE,
    instructions: str = DEFAULT_MODEL_GRADED_QA_INSTRUCTIONS,
    grade_pattern: str = DEFAULT_GRADE_PATTERN,
    model: str | Model | None = None,
) -> Scorer:

    # resolve grading template and instructions,
    # (as they could be file paths or URLs)
    template = resource(template)
    instructions = resource(instructions)

    # resolve model
    grader_model = get_model(model)

    async def score(state: TaskState, target: Target) -> Score:
        # format the model grading template
        score_prompt = template.format(
            question=state.input_text,
            answer=state.output.completion,
            criterion=target.text,
            instructions=instructions,
        )

        # query the model for the score
        result = await grader_model.generate(score_prompt)

        # extract the grade
        match = re.search(grade_pattern, result.completion)
        if match:
            return Score(
                value=match.group(1),
                answer=match.group(0),
                explanation=result.completion,
            )
        else:
            return Score.unscored(
                reason="grader_failed",
                explanation="Grade not found in model output: "
                + f"{result.completion}",
            )

    return score

如果分数无法解析,这个评分器会记一条未评分结果,并把评分模型的输出当作 explanation。如何选择分数结局,见 未评分样本。

注意对 model_grader.generate() 的调用使用了 await。这一点很关键,才能让评分器正确参加生成工作的调度。

还要注意,我们用 TaskState 的 input_text 属性取得原始用户输入的字符串版本,再代进评分模板。使用 input_text 有两个好处:(1)它保证覆盖数据集里的原始输入(而不是 messages 里被改写过的提示);(2)它把输入归一成字符串(输入本来也可能是消息列表)。

内置评分器的全部定制选项,见 Model Grading。

UK AI Security Institute,Custom Scorers,文档页无单独发布日期(检索于 2026-10-04),https://inspect.aisi.org.uk/custom-scorers.html ,MIT License(Copyright (c) 2024 UK AI Security Institute)

Found it useful? Pass it on

WeChat

Scan with WeChat to open it on your phone and forward it.

Subscribe via RSS

Submit a correction