CodexQA

行业与实践工具与框架

Inspect 评测集:多任务一起跑,失败从上次停下的地方接着做

CodexQA 团队阅读约 10 分钟

eval-set 把多道任务放进同一个日志目录。失败默认立刻重试,最多 10 次,并复用已完成样本。max_tasks 默认取 10 和模型数里较大的那个。

本文目录

评测集

概述

文档里的多数例子一次只跑一个评测任务:把脚本名交给 inspect eval,或者直接调用 eval()。评测(evaluation)就是用固定的数据、求解和评分去测一个模型。这样适合开发单道评测。真正要比的时候,常常要把好几道评测放在一起,当成一个评测集(eval set,一次调度里共同追踪进度的一组任务)来跑。目的可以是试超参数、同时测多个模型,或跑完整套基准。

inspect eval-set 命令和 eval_set() 函数提供跑评测集的几项能力,包括:

  1. 按可配置的重试策略,自动重试失败的评测。重试(retry)就是失败后再跑,而不是整批作废。
  2. 复用失败任务里已经完成的样本,重试时不把做过的工作再做一遍。样本(sample)是评测数据里的一条题。
  3. 某道任务最终成功之后,清掉失败那几次留下的日志文件。
  4. 同一条命令可以多次再跑,从上次停下的地方接着做。

下面说明建立评测集可用的工具和做法。

运行评测集

用 inspect eval-set 命令或 eval_set() 函数跑一组评测。例如:

$ inspect eval-set mmlu.py mathematics.py \
   --model openai/gpt-4o,anthropic/claude-3-5-sonnet-20240620 \
   --log-dir logs-run-42

等价的 Python 写法:

from inspect_ai import eval_set

success, logs = eval_set(
   tasks=["mmlu.py", "mathematics.py"],
   model=["openai/gpt-4o", "anthropic/claude-3-5-sonnet-20240620"],
   log_dir="logs-run-42"      
)

两处都指定了单独的日志目录。日志目录(log directory)是评测集用来记住哪些工作已经完成的范围,所以每个评测集最好独占一个。默认是环境变量 INSPECT_LOG_DIR,没有就用 ./logs。父目录里只跑一个评测集时,这个默认值够用。

若要给每个模型单独的生成配置、模型参数或基址(base url,模型 API 的根地址),不要用一条 --model,改为每个模型一条 --model-spec。这样同一个模型也可以用不同选项在评测集里跑多次。见 多个模型。

评测集经常要跑很久。想脱离终端在后台跑,用 --detach 启动,再用 inspect ctl 查看。inspect ctl 是另一进程里观察和控制评测的命令。见 脱离终端启动。

eval_set() 返回一个二元组:布尔值表示是不是所有任务都成功完成;另一个是 EvalLog 头信息的列表。返回的日志里不含原始样本数据。

重新运行

评测集因为报错或被取消而没跑完时,可以再跑。把同一条命令再执行一次,还没完成的工作会被重新排上。如果评测集其实已经做完,会打印一句说明。

也可以给已有评测集追加任务、模型或轮次。轮次(epoch)是同一道评测再重复跑的次数。把原来的命令加上新增项再发一次即可。下面这个例子在前面的评测集上多加一个模型,并把轮次加到 3,也就是比原来多 2 轮:

$ inspect eval-set mmlu.py mathematics.py \
   --model openai/gpt-5,openai/gpt-4o,anthropic/claude-3-5-sonnet-20240620 \
   --epochs 3
   --log-dir logs-run-42

并发

默认情况下,eval_set() 会并行跑多个任务。max_tasks 是同时进行的任务数上限,默认取 10 和被测模型数里较大的那个。评测集调度器会尽量把正在跑的任务摊到各个模型上,避免挤在同一个模型服务商。

用 max_tasks 可以改掉这个默认:

eval_set(
   tasks=["mmlu.py", "mathematics.py", "ctf.py", "science.py"],
   model=["openai/gpt-4o", "anthropic/claude-3-5-sonnet-20240620"],
   max_tasks=8,
   log_dir="logs-run-42"      
)

动态任务

上面的例子里,任务来自文件系统。也可以在程序里动态造出一组任务,再交给 eval_set()。例如:

from inspect_ai import eval_set

@task
def create_task(dataset: str):
  return Task(dataset=csv_dataset(dataset))

mmlu = create_task("mmlu.csv")
maths = create_task("maths.csv")

eval_set(
   [mmlu, maths],
   model=["openai/gpt-4o", "anthropic/claude-3-5-sonnet-20240620"],
   log_dir="logs-run-42"      
)

注意任务是从带 @task 装饰器的函数造出来的。装饰器是写在函数定义上、用来登记额外行为的标记。这一步是硬要求:Inspect 要能记下 create_task() 的参数,用来区分这两道任务,重试时才能把任务和日志文件对上。

和 eval_set() 一起用的动态任务有两条基本要求:

  1. 必须像上面那样,用 @task 函数来创建。
  2. 参数要用普通 Python 类型,例如 str、int、list,不要用自定义对象。自定义对象很难每次都序列化成同一个结果。序列化就是把对象写成可以存下来、再读回来的形式。

也可以把 solver 传进 @task 函数,只要这个求解器本身是由带 @solver 的函数创建的。求解器(solver)是评测里负责出题、调用模型的那一段。

重试选项

有一批选项控制评测集怎么重试:

选项说明
--retry-attempts最多重试多少次(默认 10)
--retry-immediate / --no-retry-immediate任务一失败就马上重试,不必等所有任务都结束(这是默认)。加上 --no-retry-immediate 则回到旧的整批重试。启用整批重试时,--retry-wait 和 --retry-connections 会被忽略。
--retry-wait在设置了 --no-retry-immediate 时,两次尝试之间的等待秒数,并且按指数加长(默认 30,于是等待依次是 30、60、120、240 秒,以此类推)。默认的 --retry-immediate 模式下忽略此项。
--retry-connections在设置了 --no-retry-immediate 时,每次重试按这个倍率降低最大连接数(默认 1.0,也就是不降低)。默认的 --retry-immediate 模式下忽略此项。
--no-retry-cleanup重试之后不要清理失败的日志文件。

例如,把基础等待时间设为 120 秒:

inspect eval-set mmlu.py mathematics.py \
   --log-dir logs-run-42
   --retry-wait 120

用 eval_set() 函数则是:

eval_set(
   ["mmlu.py", "mathematics.py"],
   log_dir="logs-run-42",
   retry_wait=120
)

发布

可以用打包选项,给一个评测集做出独立的日志查看器。日志查看器是在浏览器里翻评测日志的页面。

选项说明
--bundle-dir把独立日志查看器的文件写到这个目录。
--bundle-overwrite覆盖已有的打包目录(默认不覆盖)。

打包目录可以部署到任意静态网站服务器,例如 GitHub Pages、S3 存储桶 或 Netlify,用来单独打开这个评测集的日志查看器。更多细节见 日志查看器的发布。

Inspect Flow

评测集负责运行、重试和追踪一组任务。工作变大之后(很多任务、很多模型、参数还不一样,然后还要审阅并提升结果),临时脚本可能不够。提升(promote)在这里是把通过检查的日志标成可以对外或进入下一步的结果。Inspect Flow 是建在评测集之上的配套包,用来按规模运行和管理评测。它提供:

  • 声明式、类型安全的规格,一次写清任务、模型和参数。声明式是把要跑什么写在规格里,而不是一步步写脚本。
  • 矩阵式参数扫描,按任务、模型和超参数系统地铺开。
  • 可复用的默认值,并且自动继承,公共设置只配一次。
  • Flow Store,给评测日志做索引,跨运行、跨目录复用。
  • 可组合的评测后步骤,用来打标签、校验和提升日志。

详见 Inspect Flow 文档。

日志上下文

前面说过,每个评测集最好独占一个日志目录。原因有两个:

  1. 日志目录持久记下哪些任务已经完成,于是评测集可以按需要跑很多次,直到全部做完。例如跑到一半遇到服务商的速率限制(rate limit,短时间请求太多被拒绝)。几小时后再启动,这个专用目录让你能接着做。
  2. 这样可以把这一套里的评测日志当成一个整体来列举和分析,而不是和其他运行的结果混在一起。

评测集里的任务全部完成后,对同一个日志目录再跑 inspect eval-set 或 eval_set() 就是空操作,没有更多工作可做。这时可以用 list_eval_logs() 把日志收起来分析:

results = list_eval_logs("logs-run-42")

如果调用的是 eval_set(),它返回 bool 和 list[EvalLog] 的元组。这个布尔值表示是不是所有任务都完成了:

success, logs = eval_set(...)
if success:
    # analyse logs
else:
    # will need to run eval_set again

默认的 eval_set() 已经会做不少重试(默认最多 10 次)。所以 success=False 表示连这些重试都用完了,任务仍然没完成。原因可能是服务中断,也可能是评测代码里的缺陷抛出了运行时错误。

样本保留

重试一份日志时,Inspect 会尽量复用原任务里已经完成的样本。比起从头再来,这能省下不少时间和费用。

#### 标识与打乱

能不能复用已完成样本,关键限制是:新任务里的样本必须和旧的对得上。为此,每条样本都要有稳定且唯一的标识。有两种做法:

  1. 样本可以自带显式的 id 字段,里面就是这个唯一标识;或者
  2. 依赖 Inspect 给样本分配的自增 id。但如果数据集被打乱,这样做会不对。打乱(shuffle)是把样本顺序随机重排。Inspect 若发现调用了 dataset.shuffle(),会记一条警告并且不复用样本。如果用别的办法打乱,这道自动保护不会生效。

如果评测必须打乱数据集,又希望重试时保留样本,就应该在数据集里放上显式的 id 字段。

#### 最大样本数

另一项要考虑的是 max_samples,一个任务里同时跑的样本数上限。并发样本越多,吞吐越高,但已完成的样本也越不常写入日志。任务被打断时,能恢复的样本就会更少。

默认情况下,max_samples 跟着模型当前的连接上限走:自适应连接(默认)时跟控制器;静态并发时设成 max_connections。max_connections 是同时打开的模型连接数上限。把它设得比 max_connections 还低,通常没有意义。上限较小,样本一般会较频繁地写入日志。上限很大时则相反,例如数据集有 100 条样本、又把 max_connections 设成 100,一旦中断,能恢复的样本可能所剩无几。

任务如果会调用工具或使用沙箱,通常会把 max_samples 设得比 max_connections 大。沙箱是隔离执行模型所写代码的环境。样本有时在调用模型,占用并发连接;有时在沙箱里执行代码,占用并发子进程。跑任务时可以实时看到连接和子进程的占用,再据此调整 max_samples。

任务枚举

跑评测集时,任务可以像上面的例子那样逐个指定,也可以从文件系统枚举出来。任务可以有很多组织方式,下面是较常见的几种。

一个文件里的多个任务

最简单的组织是一个源文件里定义多个任务。看这个源文件 ctf.py,里面有两个任务:

@task
def jeopardy():
  return Task(
    ...
  )

@task
def attack_defense():
  return Task(
    ...
  )

用下面的命令把两个任务都跑起来。从这里开始的例子都假定已经设置了环境变量 INSPECT_EVAL_MODEL,所以不必再显式传 --model:

$ inspect eval-set ctf.py --log-dir logs-run-42

等价写法:

eval_set("ctf.py", log_dir="logs-run-42")

开发和调试时也可以单独跑其中一个任务:

$ inspect eval ctf.py@jeopardy

一个目录里的多个任务

再看一个目录里的多个任务。假设目录是这样,jeopardy.py 和 attack_defense.py 各自定义了一个或多个 @task 函数:

security/
  import.py
  analyze.py
  jeopardy.py
  attack_defense.py

这一套里的全部任务列出如下:

$ inspect list tasks security
jeopardy.py@crypto
jeopardy.py@decompile
jeopardy.py@packet
jeopardy.py@heap_trouble
attack_defense.py@saar
attack_defense.py@bank
attack_defense.py@voting
attack_defense.py@dns

可以这样跑这个评测集:

$ inspect eval-set security --log-dir logs-security-02-09-24

注意目录里有的文件并不包含评测,例如 import.py 和 analyze.py。inspect eval-set 不会读取或执行这些文件,它只执行含有 @task 定义的文件。

若要跑不止一个目录,把多个目录名一起传进去即可。例如:

$ inspect eval-set security persuasion --log-dir logs-suite-42

等价写法:

eval_set(["security", "persuasion"], log_dir="logs-suite-42")

列出与筛选

递归列出

传给 eval-set 的目录,或展开后的目录名通配,会递归扫描任务。通配(glob)是用模式一次匹配多个路径。因此目录可以很深,任务脚本和非任务脚本混在一起,eval-set 命令或函数仍会自动发现全部任务。

递归扫描目录时要记住几条规则:

  1. 以 . 或 _ 开头的源文件和目录不会被扫描。
  2. 名为 env、venv 和 tests 的目录不会被扫描。

属性与筛选

评测套件有时只靠目录结构来划分,但还会有横切的条件,用来决定实际跑哪些。横切是说这个条件和目录位置无关,每道任务都可以带上。例如想把一部分任务标成 “light” 套件,跑起来更便宜、也更快。做法是在任务装饰器上加属性。例如:

@task(light=True)
def jeopardy():
  return Task(
    ...
  )

有了这个属性,就可以列出 security 里所有 light 任务,再交给 eval():

light_suite = list_tasks(
  "security", 
  filter = lambda task: task.attribs.get("light") is True
)
logs = eval_set(light_suite, log_dir="logs-light-42")

inspect list tasks 也可以用纯文本或 JSON 列出任务。要筛选时,加上一条或多条 -F:

$ inspect list tasks security
$ inspect list tasks security --json
$ inspect list tasks security --json -F light=true

可以用 xargs 把 inspect list tasks 的结果送进 inspect eval-set。xargs 是把前一条命令的输出当成后一条命令的参数:

$ inspect list tasks security | xargs \
   inspect eval-set --log-dir logs-security-42

用属性筛选时有一件事必须记住:inspect list tasks 和底层的 list_tasks() 扫描任务时不执行代码,只做解析。解析是读源码结构,不运行函数。因此筛选表达式里的任务属性必须是常量,不能是函数调用的结果。例如:

# this is valid for filtering expressions
@task(light=True)
def jeopardy():
  ...

# this is NOT valid for filtering expressions
@task(light=light_enabled("ctf"))
def jeopardy():
  ...

UK AI Security Institute,Eval Sets,文档页无单独发布日期(检索于 2026-10-29),https://inspect.aisi.org.uk/eval-sets.html ,MIT License(Copyright (c) 2024 UK AI Security Institute)。

觉得有用,转给同事

微信扫码

用微信扫一扫,在手机上打开后即可转发。

用 RSS 订阅

提交勘误