CodexQA

Industry & PracticeTools & Frameworks

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

CodexQA 团队10 min read

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

In this piece

评测集

概述

文档里的多数例子一次只跑一个评测任务:把脚本名交给 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)。

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