从空目录到第一份评测报告:用 Inspect AI 把评测系统真的搭出来(实操陪读篇)

评测路由系列的动手篇:30 分钟、七步、每步给完整代码和我本机跑出的真实输出——建 4 条金集 mini 版(字段对齐数据规格书)、用免 API key 的 mockllm 跑通管道、一行参数换真模型/接公司网关、写自定义规则评分器、双模型对比拿到 1.00 vs 0.00、从日志读出请求级标签、CI 门禁脚本。文末给「你刚搭的文件 ↔ 施工图模块」对照表和七问自检清单。全部代码在 inspect-ai 0.3.254 上亲手验证。

系列前三篇都是图纸:决策地图回答选什么,施工图回答怎么搭,数据规格书回答建什么表。但图纸不产手感——看完还是不知道周一早上打开空目录,第一个文件写什么。这篇就是抡锤子:七步,每步给完整代码 + 我本机跑出来的真实输出 + 「你应该看到什么」。跟着敲,30 分钟后你会有一个能跑的评测系统,而且你会发现它和施工图里的 M6/M7/M8 是同一个东西的最小版本。

先声明证据档位:本文所有代码和输出都在我本机亲手跑通(inspect-ai 0.3.254,Python 3.14 venv,2026-08-09),属于本站证据分级的第一档。文中没有任何真实模型的分数——教程用 Inspect 内置的 mock 模型跑管道,你换上自己的 API key 才会得到属于你的数字。

第 0 步:先装心智模型,再装软件

Inspect AI(英国 AISI 出品)把一次评测抽象成一个三元组,整个框架都是围绕它长的:

flowchart LR
    D["Dataset<br>一组带答案的题"] --> S["Solver<br>让模型作答"]
    S --> O["Completion<br>模型输出"]
    O --> SC["Scorer<br>判分"]
    SC --> R["Score<br>C 或 I"]
    R --> L["Log<br>.eval 日志与报告"]

Task = Dataset × Solver × Scorer。对照施工图:Dataset 就是 M6(评测数据集)的最小版本,Scorer 就是 M7(评分器体系),跑 Task 并落日志的引擎就是 M8(评测执行引擎)。你今天搭的不是玩具,是那十个模块里评测层的种子。

第 1 步:环境(2 分钟)

mkdir eval-lab && cd eval-lab
python3 -m venv .venv
.venv/bin/pip install inspect-ai
.venv/bin/inspect --version   # 我装到的是 0.3.254

第 2 步:金集 mini 版——4 条题就够起步

dataset/faq.jsonl,每行一条样本。字段名刻意对齐数据规格书的样本 schemaid 是样本主键,task_class 是任务类目,source_trace_id 是血缘字段——教程里它是假的,生产里它指回真实 trace:

{"id": "smp-001", "question": "vLLM 的 PagedAttention 机制主要为了减少哪种硬件资源的浪费?直接答资源名。", "answer": "显存", "task_class": "faq_zh", "source_trace_id": "tr-demo-001"}
{"id": "smp-002", "question": "HTTP 状态码 429 通常表示客户端触发了什么机制?", "answer": "限流", "task_class": "faq_zh", "source_trace_id": "tr-demo-002"}
{"id": "smp-003", "question": "LLM 推理指标 TTFT 的英文全称是什么?", "answer": "time to first token", "task_class": "faq_zh", "source_trace_id": "tr-demo-003"}
{"id": "smp-004", "question": "KV cache 的本质是用什么换什么:显存换算力,还是算力换显存?", "answer": "显存换算力", "task_class": "faq_zh", "source_trace_id": "tr-demo-004"}

出题原则只有一条:答案里有一个必然出现的关键短语——这样第一版评分器可以用最硬的字符串包含判分,不用请 LLM 裁判(Oracle 梯度:能用便宜的验证就绝不用贵的)。

第 3 步:第一跑——零分,但管道通了

faq_task.py(完整文件,可直接复制):

from inspect_ai import Task, task
from inspect_ai.dataset import FieldSpec, json_dataset
from inspect_ai.scorer import includes
from inspect_ai.solver import generate, system_message


@task
def faq_zh():
    return Task(
        dataset=json_dataset(
            "dataset/faq.jsonl",
            FieldSpec(
                input="question",           # 你的字段名 → Inspect 的标准字段
                target="answer",
                id="id",
                metadata=["task_class", "source_trace_id"],
            ),
        ),
        solver=[
            system_message("你是简洁的技术问答助手,直接给出答案,不要展开。"),
            generate(),
        ],
        scorer=includes(),                  # 判分:target 出现在输出里即算对
    )

跑它——注意模型名是 mockllm/model,这是 Inspect 内置的假模型,不要任何 API key,永远回一句固定话。它的用途就是把管道和模型解耦,先验证管道:

.venv/bin/inspect eval faq_task.py --model mockllm/model

我跑出来的真实输出(截取关键部分):

╭──────────────────────────────────────────────────╮
│faq_zh (4 samples): mockllm/model                 │
╰──────────────────────────────────────────────────╯
total time:                0:00:03
mockllm/model              297 tokens [I: 165, O: 132]

includes
accuracy  0.000
stderr    0.000

Log: logs/2026-08-09T14-25-29-00-00_faq-zh_DXMQEYeEmfuLv8TkQqN3pu.eval

accuracy 0.000 就是本步的成功标志。 假模型答非所问,零分天经地义;但报告出来了、日志落盘了、四条样本每条都有判分记录——管道通了。把”管道通”和”分数好”当成两个独立的里程碑,是评测工程的第一课:以后任何一次改动(换数据集、换评分器、升级 Inspect 版本),都先用 mockllm 跑一遍管道自检,不花一分钱 API 费。

第 4 步:换真模型 = 换一个命令行参数

管道通了,换真模型只动 --model,代码一行不改:

export ANTHROPIC_API_KEY=...
.venv/bin/inspect eval faq_task.py --model anthropic/claude-sonnet-4-0

# 或 OpenAI、Google、本地 vLLM、Ollama……格式都是 provider/model-name
.venv/bin/inspect eval faq_task.py --model openai/gpt-4o-mini

更重要的是接公司网关——如果你读过施工图篇,评测引擎应该和业务走同一个 LLM 入口(M1),这样评测消耗也进成本账本。任何 OpenAI 兼容端点(LiteLLM、自建网关、vLLM serve)都用 openai-api/<服务名>/<模型名> 三段式接入:

export GATEWAY_API_KEY=sk-your-gateway-key
.venv/bin/inspect eval faq_task.py \
  --model openai-api/gateway/qwen3-32b \
  --model-base-url http://your-llm-gateway:4000/v1

服务名(这里是 gateway)会被 Inspect 用来找 GATEWAY_API_KEY 环境变量——这个约定我从 0.3.254 的源码里核实过。到这一步,“集成”的前半已经完成:评测引擎不再直连模型厂商,而是消费你的统一网关。

第 5 步:自定义评分器——你的第一个自研资产

内置评分器覆盖常见判分(核实自官方文档):includes / match / pattern / answer / exact / f1 / choice / math,以及两个 LLM 裁判 model_graded_qa / model_graded_fact。但业务判分逻辑早晚要自己写——开源 vs 自研分界表里,rubric 和业务评分器就在自研列。

写一个真实业务里高频的场景:结构化输出校验(模型必须输出合法 JSON 且字段齐全)。先加两条样本 dataset/extract.jsonl——注意 answer列表,列出必须出现的字段名:

{"id": "smp-005", "question": "从这句话抽取信息,只输出一个 JSON 对象,必须包含字段 name 和 city:小明住在杭州。", "answer": ["name", "city"], "task_class": "struct_out", "source_trace_id": "tr-demo-005"}
{"id": "smp-006", "question": "从这句话抽取信息,只输出一个 JSON 对象,必须包含字段 model 和 tokens:qwen3-32b 这次消耗了 512 个 token。", "answer": ["model", "tokens"], "task_class": "struct_out", "source_trace_id": "tr-demo-006"}

再建 extract_task.py。自定义评分器就是一个装饰器加一个异步函数,签名固定为 score(state, target)

import json

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


@scorer(metrics=[accuracy(), stderr()])
def json_fields():
    """规则评分器:输出必须是合法 JSON 且包含 target 列出的全部字段。"""

    async def score(state: TaskState, target: Target) -> Score:
        text = state.output.completion.strip()
        try:
            obj = json.loads(text)
        except json.JSONDecodeError:
            return Score(value=INCORRECT, answer=text[:60],
                         explanation="输出不是合法 JSON")
        missing = [k for k in target if k not in obj]
        if missing:
            return Score(value=INCORRECT, answer=text[:60],
                         explanation=f"缺字段: {missing}")
        return Score(value=CORRECT, answer=text[:60],
                     explanation="JSON 合法且字段齐全")

    return score


@task
def struct_out():
    return Task(
        dataset=json_dataset(
            "dataset/extract.jsonl",
            FieldSpec(input="question", target="answer", id="id",
                      metadata=["task_class", "source_trace_id"]),
        ),
        solver=[generate()],
        scorer=json_fields(),
    )

我用一个”一条答对、一条答错”的假模型验证过它的两条路径,真实输出:

smp-005 C | JSON 合法且字段齐全
smp-006 I | 输出不是合法 JSON
accuracy = 0.5

CI 就是 Inspect 的 CORRECT/INCORRECT(源码里就是字符 "C""I")。注意这个评分器落在 Oracle 梯度的「规则验证」档:确定性、免费、毫秒级——它能覆盖的判分,永远不要交给 LLM 裁判。

第 6 步:双模型对比——三臂实验的雏形

评测系统的核心用法不是”给一个模型打分”,而是同一份题、同一把尺子,比较多个模型——施工图 M9 三臂对比的雏形。建 compare.py,用 mockllm 的 custom_outputs 回调造出”强/弱”两个假模型(换真模型时只改 get_model 那行):

from inspect_ai import eval as inspect_eval
from inspect_ai.model import ModelOutput, get_model
from faq_task import faq_zh

STRONG = {"PagedAttention": "显存", "429": "限流机制",
          "TTFT": "Time To First Token", "KV cache": "显存换算力"}
WEAK   = {"PagedAttention": "内存", "429": "服务器内部错误",
          "TTFT": "首个 token 的延迟", "KV cache": "算力换显存"}

def make_mock(answers):
    def respond(input, tools, tool_choice, config) -> ModelOutput:
        question = input[-1].text
        for key, ans in answers.items():
            if key in question:
                return ModelOutput.from_content("mockllm/model", ans)
        return ModelOutput.from_content("mockllm/model", "不知道")
    return get_model("mockllm/model", custom_outputs=respond)

for name, answers in [("strong", STRONG), ("weak", WEAK)]:
    [log] = inspect_eval(faq_zh(), model=make_mock(answers), display="plain")
    acc = log.results.scores[0].metrics["accuracy"].value
    print(f"{name}: accuracy={acc:.2f}  log={log.location}")

真实输出:

strong: accuracy=1.00  log=.../logs/2026-08-09T14-26-43-00-00_faq-zh_6X6Q....eval
weak: accuracy=0.00  log=.../logs/2026-08-09T14-26-43-00-00_faq-zh_3cF7....eval

一个实现细节值得记:custom_outputs 传的是回调(按问题内容作答)而不是答案列表——因为 Inspect 并发跑样本,列表按消费顺序出队,并发下会答错题;回调按题给答案,天然免疫乱序。这个坑我先替你踩了。

第 7 步:从日志里读出「请求级标签」+ 三个集成钩子

每次运行都落一个 .eval 日志。图形界面用 inspect view 打开(浏览器里逐样本看完整 transcript),程序化读取用 read_eval_log

from inspect_ai.log import read_eval_log

log = read_eval_log("logs/2026-08-09T14-26-43-00-00_faq-zh_6X6Q....eval")
for s in log.samples:
    print(s.id, {k: (v.value, v.answer) for k, v in s.scores.items()})

真实输出:

smp-001 {'includes': ('C', '显存')}
smp-002 {'includes': ('C', '限流机制')}
smp-003 {'includes': ('C', 'time to first token')}
smp-004 {'includes': ('C', '显存换算力')}

认出来了吗?(sample_id, model, score)——这就是数据规格书里的「请求级结果标签」,积累多了就是路由器的训练数据。你在第 6 步跑双模型对比时,已经在生产它了。

三个集成钩子,按投入从低到高:

① CI 门禁(对应门禁篇):一个 20 行的 gate.py——跑评测、读 accuracy、低于阈值就非零退出:

import sys
from inspect_ai import eval as inspect_eval
from faq_task import faq_zh

THRESHOLD = 0.75
[log] = inspect_eval(faq_zh(), model="openai-api/gateway/qwen3-32b")
acc = log.results.scores[0].metrics["accuracy"].value
print(f"accuracy={acc:.3f} threshold={THRESHOLD}")
sys.exit(0 if acc >= THRESHOLD else 1)

挂进 GitHub Actions(prompt 或路由策略改动必须过评测才许合并):

- run: pip install inspect-ai
- run: python gate.py
  env:
    GATEWAY_API_KEY: ${{ secrets.GATEWAY_API_KEY }}

② nightly 回归:crontab 每晚对全模型池跑一遍金集,日志按日期归档——施工图 4.3 时序图的最小实现就是一行 cron 加一个 for 循环。

③ 新模型准入:新模型上架网关前,先跑一遍全部 task,达标才进模型注册表——把第 6 步的 compare.py 里加一个候选模型就是准入脚本。

对照表:你刚搭的东西在系列图纸里叫什么

你刚写的文件施工图模块数据规格书对应物
dataset/*.jsonlM6 评测数据集样本 schema(id/task_class/source_trace_id/oracle)
includes() / json_fields()M7 评分器体系oracle_type 的 rule 档
inspect eval + logs/*.evalM8 执行引擎评测运行记录(四元组的雏形)
compare.pyM9 基线对比请求级结果标签的生产现场
--model-base-url 指向网关M1 统一网关评测流量并入成本账本
gate.py + CI yaml回归门禁Eval 是新的 PRD

所以”从 0 到 1 做一个评测系统”这个问题的答案是:你已经做完了。剩下的从 1 到 10 不是新概念,是规模化:4 条样本长到 200 条(按漏斗每周吸新)、评分器从 2 个长到四档齐全(judge 要配校准集)、手动跑变 nightly、单模型变全模型池。每一步在系列前三篇里都有图纸。

自检清单(合上文章,答得出算过关)

  1. 为什么第一跑得 0 分反而是成功?(管道与分数是两个独立里程碑)
  2. mockllm/model 的存在解决了什么工程问题?(管道自检与模型解耦,零 API 成本)
  3. 样本里的 source_trace_id 教程里没用到,生产里它连接什么?(评测样本 ↔ 线上 trace 的血缘)
  4. json_fields 为什么不该换成 LLM 裁判?(Oracle 梯度:规则可判的绝不上 judge)
  5. 双模型对比跑完,日志里的 (sample, model, score) 在上一篇的术语里叫什么?(请求级结果标签)
  6. 把评测从直连厂商改成走公司网关,要改哪两个东西?(--model 三段式名字 + --model-base-url,外加对应的 API key 环境变量)
  7. custom_outputs 为什么用回调比用列表稳?(并发消费会乱序,回调按题作答)

诚实的提醒

  • 本文全部代码与输出在 inspect-ai 0.3.254 上亲手跑通;openai-api/<service>/<model> 的 API key 查找约定核实自该版本源码。Inspect 迭代很快,未来版本 API 可能变化,以官方文档为准。
  • 文中没有任何真实模型的分数——1.00 和 0.00 是我构造的假模型跑出来的,用途是验证管道和判分逻辑,不代表任何模型的真实能力。
  • 4 条样本的 accuracy 没有统计意义:就算 4/4 全对,Wilson 95% 区间也宽到 [0.51, 1.00](此数字已脚本验算)——它只能告诉你”管道没坏”,不能告诉你”模型很强”。金集长到几百条之前,别拿 accuracy 做任何决策。
  • 「先 mockllm 自检再换真模型」「custom_outputs 用回调」是我的实践建议,非官方规范。

下一步的最小动作(还是那半天实验,但现在你有工具了):把你自己最近 30 条真实 LLM 使用记录按第 2 步的格式写成 JSONL,挑两个价差大的模型跑第 6 步的对比,看一眼每成功任务的成本差——那份报告就是你的评测系统从教程变成资产的时刻。

参考来源

工程实践 / 官方文档

本站相关旧文