CodexQA

Industry & PracticeTools & Frameworks

日志文件:Inspect 评测日志怎么读写

CodexQA 团队24 min read

每次 inspect eval 都会为每个任务写一份日志。从 v0.3.206 起,重复消息去重再加上 zstd,SWE-Bench 一类日志大约能缩小到原来的十分之一。

In this piece

日志文件

译注:本文译自 UK AI Security Institute 的 Inspect 文档 Log Files,https://inspect.aisi.org.uk/eval-logs.html 。文档页无单独发布日期(检索于 2026-11-06)。许可证见仓库 LICENSE(同系列文档记为 MIT License,Copyright (c) 2024 UK AI Security Institute)。

概述

每次使用 inspect eval,或调用 eval() 函数,所评测的每一个任务都会写出一份评测日志(evaluation log)。默认写到当前工作目录下的 ./logs 子目录(下文会说明怎样改这个位置)。每个任务的结果底部都有一条指向该日志的链接:

$
 inspect eval security_guide.py 
--model
 openai/gpt-6-luna

也可以用 Inspect 日志查看器交互式浏览日志。一次工作会话开始时运行下面这条命令即可(有新评测跑起来时,视图会自动更新):

$
 inspect view

这一节不讲怎么用 inspect view。它讲的是从命令行管理日志用法的细节,以及用来读取日志的 Python API。交互式浏览日志的细节见 日志查看器一节。

日志分析

本文主要讲怎样配置 Inspect 的日志行为(位置、格式、内容等)。除此之外,还有多种工具可以分析日志文件里的数据:

  1. 日志文件 API——用来访问日志里记下的全部数据的 API。
  1. 日志数据帧——从日志文件抽出数据帧(data frame)的 API。
  1. Inspect Scout——可以直接处理 Inspect 日志的记录分析工具。
  1. Inspect Viz——为配合 Inspect 日志而做的数据可视化框架。
  1. CJE——用因果推断,对照预言标签(oracle label,当作标准答案的标签)校准模型打分器的准确度。

日志位置

默认情况下,日志写到当前工作目录下的 ./logs 子目录。可以用评测选项或环境变量改写日志的位置:

$
 inspect eval popularity.py 
--model
 openai/gpt-6-luna 
--log-dir
 ./experiment-log

或者:

log 
=
 
eval
(popularity, model
=
"openai/gpt-6-luna"
, log_dir 
=
 
"./experiment-log"
)

注意,除了写日志,eval() 函数还会返回一个 EvalLog 对象,便于用程序访问这次评测的细节。下文会进一步说明怎样使用这个对象。

也可以设置环境变量 INSPECT_LOG_DIR,用来覆盖默认的 ./logs 位置。在你运行评测的那个位置放一个 .env 文件来定义它,往往会比较方便:

INSPECT_LOG_DIR
=
./experiment-log

INSPECT_LOG_LEVEL
=
warning

如果在 .env 文件里给 INSPECT_LOG_DIR 写的是相对路径,那么这个位置始终按相对于该 .env 文件来解析(而不是相对于你运行 inspect eval 时的当前工作目录)。

如果在 VS Code 里运行,那么在 .env 文件里改了 INSPECT_LOG_DIR 之后,应当重启正在使用 Inspect 的终端和笔记本。这是因为 VS Code 的 Python 扩展也会从 .env 文件读取变量,更新后的 INSPECT_LOG_DIR 要等重启之后 VS Code 才会重新读入。

把评测日志写到 Amazon S3 存储桶的细节,见下文 Amazon S3 一节。写到 Hugging Face 存储桶的细节,见下文 Hugging Face 存储桶 一节。写到 Azure 的细节,见下文 Azure Blob 存储 一节。

日志格式

Inspect 日志文件用 JSON 表示一次评测产出的数据层级。取决于你的配置,以及正在运行的 Inspect 版本,日志 JSON 会存在两种文件类型之一里:

类型说明
.eval为体积和速度优化的二进制文件格式。大小通常是 .json 文件的 1/8,并且按样本增量访问,因此无论文件多大,在 Inspect View 里加载都很快。
.json带原生 JSON 表示的文本文件格式。占用的磁盘空间大得多;如果大于 50MB,在 Inspect View 里加载可能很慢。

两种格式都得到下文所述的日志文件 API 和日志命令的完整支持,并且可以在同一个日志目录里自由混用。

格式选项

从 Inspect v0.3.46 起,.eval 是默认的日志文件格式。可以在 .env 文件里显式控制全局默认日志格式:

.env
INSPECT_LOG_FORMAT
=
eval

也可以用 --log-format 选项按一次评测来指定:

inspect
 eval ctf.py 
--log-format
=
eval

无论选哪种格式,eval() 返回的 EvalLog 都一样,为日志文件提供的各个 API(read_eval_log()、write_eval_log() 等)的行为也一样。

底层文件格式并不固定,因此用 Python 日志文件 API 来读写日志文件就尤其重要(不要直接去读写 JSON)。

如果确实需要碰到底层 JSON(例如用另一种语言读日志),见下文日志命令一节,那里说明怎样取得任意日志文件的纯文本 JSON 表示。

存储优化

从 0.3.206 版起,Inspect 带有能大幅影响日志文件大小的存储优化。第一项是在模型事件之间去掉重复消息;第二项是改用 zstd 压缩。

这两项合在一起,日志文件大小会有很大改进。对典型的智能体基准(例如 SWE-Bench、Cybench),我们看到过 10:1 的改进。对更长时程的任务,改进还要大得多,因为这项优化针对的是 O(N^2) 的存储增长。

要试用这些改动,先确认正在运行的是 0.3.206 或更新的版本:

pip
 show inspect_ai

可以用 inspect log convert 命令把已有日志转换成新格式。例如:

inspect
 log convert logs_old 
\

 
--to
 eval 
\

 
--output-dir
 logs_new 
\

 
--stream
 10

注意,--stream 选项会限制转换过程中一次放在内存里的样本总数,对较大的日志文件这一点可能很要紧。

如果用 Inspect Scout 做记录分析,要确保用上支持读取这种压缩日志格式的新版本(v0.4.22 或更新)。

图像日志

默认情况下,内联图像和其他媒体字节会留在日志文件里。日志记录不会去抓取尚未解析的运行时媒体引用。使用 .eval 日志时,媒体日志不会造成性能问题;但如果用的是 .json 日志,大量图像可能变得难以处理(也就是 .json 日志文件因此涨到 100MB 或更大)。

可以用 --no-log-images 标志关掉它。例如,这里启用 .json 日志格式,并关掉图像日志:

inspect
 eval images.py 
--log-format
=
json 
--no-log-images

这个选项只控制是否保留。固定任务输入里受信任的媒体,仍会先物化出来再提交给模型,然后从已完成的日志里去掉。

也可以用环境变量 INSPECT_EVAL_LOG_IMAGES,在 .env 配置文件里设置全局默认值。

拒绝日志

如果想主动发现模型拒绝(refusal,模型拒绝回答)正在发生,可以指定 --log-refusals 标志(或传给 eval() 的 log_refusals 选项),把拒绝记成警告。例如:

inspect
 eval ctf.py 
--log-refusals

注意,无论哪种情况,任务显示画面的右下角都会给出这次评测或评测集期间的拒绝计数。

要在发生拒绝时让样本失败(而不是只记一条警告),使用生成选项 --fail-on-refusal;见 因拒绝而失败。

模型 API 日志

默认情况下,Inspect 会为每个模型的前几次调用(以及全部出错的调用)记下原始的模型 API 请求和响应。这样有足够的数据去核对发出和收到的载荷是否符合预期,又不必承担把每一次调用都记下来的存储成本。

要记下全部模型 API 调用,使用 --log-model-api 标志:

inspect
 eval ctf.py 
--log-model-api

要完全关掉模型 API 日志(只留错误),使用 --no-log-model-api。

无论这些选项怎么设,提供方的请求 id 和响应 id 都会记下,因此可以把某一次具体请求指给提供方:

  • ModelEvent.request_ids:为生成这次响应而发给提供方的每一次请求的请求 id 和 HTTP 状态,包括重试。OpenAI 以及与 OpenAI 兼容的提供方、Anthropic、Groq、Mistral 和 Bedrock 都会记录,批处理模式除外。
  • ModelOutput.response_id:提供方给这次响应的 id(例如 resp_... 或 msg_...)。

日志文件 API

EvalLog

eval() 返回的 EvalLog 对象,为日志文件的内容提供程序接口:

类 inspect_ai.log.EvalLog

字段类型说明
versionint文件格式版本(目前是 2)。
statusstr评测状态("started"、"success" 或 "error")。
evalEvalSpec顶层评测细节,包括任务、模型、创建时间等。
planEvalPlan这次评测使用的求解器列表和模型生成配置。
resultsEvalResults由评分器指标算出的汇总结果。
statsEvalStats模型用量统计(输入和输出 token)。
errorEvalError错误信息(当 status == "error 时),包括回溯。译注:原文这里的引号没有闭合。
tagslist[str]当前标签(评测当时的标签,与评测之后的任何编辑合并)。
metadatadict[str, Any]当前元数据(评测当时的元数据,与评测之后的任何编辑合并)。
log_updateslist[LogUpdate]评测之后对标签和元数据的编辑(带来源跟踪)。
sampleslist[EvalSample]所评测的每一个样本,包括它的输入、输出、目标和分数。
reductionslist[EvalSampleReduction]多轮次评测中样本取值的归约。

在分析一份日志的结果之前,应当始终先检查状态,确认它们代表一次成功的运行:

log 
=
 
eval
(popularity, model
=
"openai/gpt-6-luna"
)

if
 log.status 
==
 
"success"
:

 ...

下一节会进一步说明怎样处理失败评测的日志(例如重试这次评测)。

位置

eval() 和 read_eval_log() 返回的 EvalLog 对象有一个 location 属性,指出它被写到或读自的存储位置。

如果没有传入显式的 location,write_eval_log() 函数就会使用这个 location。这样就可以按下面的方式修改 eval() 返回的日志文件内容:

log 
=
 
eval
(my_task())[
0
]

# edit EvalLog as required

write_eval_log(log)

或者,对从文件系统读入的 EvalLog:

log 
=
 read_eval_log(log_file_path)

# edit EvalLog as required

write_eval_log(log)

如果处理的是评测集(Eval Set)的结果,返回的日志是日志头,而不是带全部样本的完整日志。如果要编辑 eval_set 返回的日志,应当先完整读入,再编辑,然后写回。例如:

success, logs 
=
 eval_set(tasks)

 

for
 log 
in
 logs:

 log 
=
 read_eval_log(log.location)

 
# edit EvalLog as required

 write_eval_log(log)

注意,EvalLog.location 是 URI,而不是传统文件路径(例如它可以是 file:// URI、s3:// URI,或 fsspec 支持的任何其他 URI)。

函数

可以用 inspect_ai.log 模块里的下列辅助函数来枚举、读取和写入 EvalLog 对象:

函数说明
list_eval_logs列出给定位置上的全部评测日志。
read_eval_log从日志文件路径或 IO[bytes] 读入一份 EvalLog(传入 header_only 则不读样本)。
read_eval_log_sample从日志文件读入单个 EvalSample。
read_eval_log_samples增量读取全部样本(返回一个生成器,一次产出一个样本)。
read_eval_log_sample_summaries读取全部样本的摘要(包括每个样本的评分)。
write_eval_log把一份 EvalLog 写到日志文件路径(传入 if_match_etag 可做 S3 条件写入)。

一种常见流程是:先为跑一组评测定义 INSPECT_LOG_DIR,全部做完之后再调用 list_eval_logs() 分析结果:

# setup log dir context

os.environ[
"INSPECT_LOG_DIR"
] 
=
 
"./experiment-logs"

# do a bunch of evals

eval
(popularity, model
=
"openai/gpt-6-luna"
)

eval
(security_guide, model
=
"openai/gpt-6-luna"
)

# analyze the results in the logs

logs 
=
 list_eval_logs()

注意,list_eval_logs() 会递归列出日志文件。传入 recursive=False 则只列出根一级的日志文件。

日志头

评测日志文件可能相当大(数个 GB),因此常常只读日志头就有用。日志头包含元数据和汇总分数。用 header_only 选项只读日志文件的头:

log_header 
=
 read_eval_log(log_file, header_only
=
True
)

日志头是一份标准的 EvalLog 对象,只是没有 samples 字段。reductions 字段对 eval 日志文件会包含,对 json 日志文件则不包含。

摘要

只读取样本的摘要级信息(输入、目标、错误状态和评分)也可能有用。为此,使用 read_eval_log_sample_summaries() 函数:

summaries 
=
 read_eval_log_sample_summaries(log_file)

summaries 是 EvalSampleSummary 对象的列表,每个样本一个。为了让摘要保持小,有些样本数据被“抽薄”了:input 里的图像被去掉,metadata 只保留标量值(字符串截到 1k),分数只包含它们的 value。

只读样本摘要,耗时会比逐个读完全部样本低几个数量级。因此如果只需要摘要级数据,应当始终优先用这个函数,而不是去读整个日志文件。

#### 过滤

也可以把 read_eval_log_sample_summaries() 当作过滤手段,用来决定哪些样本要完整读入。例如,假设只想读那些包含错误的样本:

errors: 
list
[EvalSample] 
=
 []

for
 sample 
in
 read_eval_log_sample_summaries(log_file):

 
if
 sample.error 
is
 
not
 
None

 errors.append(

 read_eval_log_sample(log_file, sample.
id
, sample.epoch)

 )

流式读取

如果日志文件大到没法舒服地放进内存,建议用下面的选项和工作流去流式读取,而不是一次全部载入内存:

  1. 使用 .eval 日志文件格式,它支持压缩,也支持对样本的增量访问(细节见上文日志格式一节)。如果已有 .json 文件,可以用下文所述的日志命令轻松批量转换成 .eval。
  1. 如果只需要访问日志文件的“头”(其中有一般的评测元数据,以及评测结果),使用 read_eval_log() 的 header_only 选项:

log = read_eval_log(log_file, header_only = True)

  1. 如果要读单个样本,要么用 read_eval_log_sample() 有选择地读,要么用 read_eval_log_samples() 迭代地读(这样可以保证一次只有一个样本被读进内存):

read a single sample

sample = read_eval_log_sample(log_file, id = 42)

read all samples using a generator

for sample in read_eval_log_samples(log_file): ...

注意,如果传入的日志不是 status=="success",read_eval_log_samples() 会报错(因为它无法读完一份不完整日志里的全部样本)。如果无论如何都要读这些样本,传入 all_samples_required=False 选项:

译注:未完成的日志读不到全部样本,所以默认直接报错;只有明确允许时才继续读。

# will not raise an error if the log file has an "error" or "cancelled" status

for
 sample 
in
 read_eval_log_samples(log_file, all_samples_required
=
False
):

 ...

附件

样本日志里常常有大块内容,在日志文件的多个地方重复出现(输入、消息历史、事件等)。为了让日志文件的体积可控,图像和其他大块内容会去重,并作为附件存放。

读取日志文件时,可能想解析这些附件,以便拿到底层内容。对一个 EvalSample,可以用 resolve_sample_attachments() 函数来做:

from
 inspect_ai.log 
import
 resolve_sample_attachments

sample 
=
 resolve_sample_attachments(sample)

注意,read_eval_log() 和 read_eval_log_sample() 函数也接受 resolve_attachments 选项,如果想在读取当时就解析,可以传入。

注意,多数时候你并不想解析附件。对一个 EvalSample,需要解析附件的两种情况是:

  1. 你想访问 input 和 messages 字段里 base64 编码的图像;或者
  1. 你在直接读 events 记录,并且想访问底层内容(注意,events 里去重的不只是图像,因此只要在读它,多半就会想解析附件)。

评测重试

当评测任务因错误失败,或以其他方式被打断(例如 Ctrl+C),评测日志仍然会写出来。很多情况下错误是暂时的(例如网络连通或速率限制),随后可以重试。

对这些情况,Inspect 提供了 eval-retry 命令和 eval_retry() 函数,用来恢复被错误打断的任务(包括保留原任务里已经完成的样本)。例如,如果有一个失败任务,日志文件是 logs/2024-05-29T12-38-43_math_Gprr29Mv.json,可以在 shell 里这样重试:

$
 inspect eval-retry logs/2024-05-29T12-38-43_math_43_math_Gprr29Mv.json

或在 Python 里:

eval_retry(
"logs/2024-05-29T12-38-43_math_43_math_Gprr29Mv.json"
)

一次重试会运行日志所点名的代码:它的任务文件、评分器和求解器;如果是 hf/ 任务,则是它点名的 Hugging Face 仓库,其 eval.yaml 选定数据集以及求解器和评分器参数。它也会使用日志里记录的模型端点、沙箱和其他设置。只重试你信任的日志。

注意,重试只对由 @task 装饰的函数创建的任务有效(如果 Task 是在 @task 函数之外动态创建的,Inspect 不知道怎样为重试把它重建出来)。

还要注意,eval_retry() 不会覆盖先前的日志文件,而是新建一份(并保留原文件里的 task_id)。

下面是一个例子:用更低的 max_connections 重试一次失败的评测(理由是并发连接太多,可能造成了速率限制错误):

log 
=
 
eval
(my_task)[
0
]

if
 log.status 
!=
 
"success"
:

 eval_retry(log, max_connections 
=
 
3
)

样本保留

重试一份日志文件时,Inspect 会尝试复用原任务里已经完成的样本。和从头再来相比,这可以省下大量时间和费用。

#### 标识与打乱

能否复用已完成样本,一个重要约束是要把它们和新任务里的样本正确对上。为此,Inspect 要求每个样本有稳定的唯一标识。可以用下面两种方式之一做到:

  1. 样本可以有一个显式的 id 字段,里面放唯一标识;或者
  1. 可以依赖 Inspect 为样本分配的自增 id,但如果数据集被打乱,这样做不能正确工作。如果检测到调用了 dataset.shuffle() 方法,Inspect 会记一条警告,并且不复用样本;但如果用别的办法打乱,这项自动保护不会生效。

如果打乱数据集对你的评测很重要,又想为重试的任务保留样本,那么应当在数据集里放一个显式的 id 字段。

#### 最大样本数

另一个要考虑的是 max_samples,即一个任务内同时运行的最大样本数。并发样本数更大,吞吐更高,但已完成样本写进日志文件的频率也会更低,于是任务被打断时,总共能恢复的样本也就更少。

默认情况下,max_samples 跟着模型当前的连接上限走:在自适应连接(默认)下它跟随控制器,在静态并发下它被设成 max_connections(注意,把它设得低于 max_connections 很少有道理)。较小的上限通常会让样本被频繁写进日志。另一方面,设一个非常大的上限(例如数据集有 100 个样本,却把 max_connections 设成 100)可能让一次中断之后几乎没有可恢复的样本。

如果任务涉及工具调用和/或沙箱,那么多半会想把 max_samples 设得大于 max_connections,因为样本有时在调用模型(占用并发连接),有时在沙箱里执行代码(占用并发子进程调用)。任务运行时,可以实时看到连接和子进程的利用率,并据此调整 max_samples。

上面讨论的是怎样交互式地管理单次评测运行的重试。如果是批量跑很多评测任务,并重试其中失败的那些,见评测集(Eval Sets)的文档。

编辑日志

评测跑完之后,可能需要修改结果——例如纠正评分错误,或根据人工复查调整样本分数。Inspect 提供了修改日志的函数,同时维持数据完整性和审计轨迹。

分数编辑

用 edit_score() 函数修改单个样本的分数。例如,下面这个例子会修改第一个样本的分数,把它先前的取值留在分数历史里,同时记录这次改动的作者和原因:

from
 inspect_ai.log 
import
 read_eval_log, write_eval_log, edit_score, ProvenanceData

from
 inspect_ai.scorer 
import
 ScoreEdit

# Read the log file

log 
=
 read_eval_log(
"my_eval.json"
)

# Create a score edit with provenance tracking

edit 
=
 ScoreEdit(

 value
=
0.95
, 
# New score value

 explanation
=
"Corrected model grader bug"
, 
# Optional new explanation

 provenance
=
ProvenanceData(

 author
=
"anthony"
,

 reason
=
"there was a bug in the model grader"
,

 )

)

# Edit the score (automatically recomputes metrics)

edit_score(

 log
=
log,

 sample_id
=
log.samples[
0
].
id
, 
# Can be string or int

 score_name
=
"accuracy"
,

 edit
=
edit

)

# Write back to the log file

write_eval_log(log)

注意,使用 edit_score 改的是载入内存的日志,不会改已经写好的日志文件。一定要用 write_eval_log 把改动存回评测文件(或一份副本)。

把 metadata 传给 ScoreEdit 会替换 Score.metadata,而不是合并进去。评分器记下的键(例如一个 reason,或模型打分器的 grading 记录)不再属于当前元数据,除非这次编辑把它们重复写上——编辑前的那份字典仍可在分数历史里找到。如果想保留它们,要显式合并:

edit 
=
 ScoreEdit(

 value
=
0.95
,

 metadata
=
{
**
(score.metadata 
or
 {}), 
"edited_by"
: 
"reviewer"
},

)

分数历史

每个分数都维持一份完整的编辑历史。原始分数和此后的全部编辑都会保留:

# Access the edit history

score 
=
 log.samples[
0
].scores[
"accuracy"
]

print
(
f"Original value: 
{
score
.
history[
0
]
.
value
}
"
)

print
(
f"Current value: 
{
score
.
value
}
"
)

print
(
f"Was edited: 
{
len
(score.history) 
>
 
1
}
"
)

print
(
f"Number of edits: 
{
len
(score.history)
}
"
)

# Iterate through all edits

for
 i, edit 
in
 
enumerate
(score.history):

 provenance 
=
 edit.provenance

 author 
=
 provenance.author 
if
 provenance 
else
 
"original"

 
print
(
f"Edit 
{
i
}
: value=
{
edit
.
value
}
, author=
{
author
}
"
)

重算指标

edit_score() 函数默认会自动重算汇总指标。如果要做多次编辑,可以关掉它,稍后手动重算:

from
 inspect_ai.log 
import
 recompute_metrics

# Make edits without recomputing metrics each time

edit_score(log, sample_id_1, 
"accuracy"
, edit1, recompute_metrics
=
False
)

edit_score(log, sample_id_2, 
"accuracy"
, edit2, recompute_metrics
=
False
)

# Recompute metrics once after all edits

recompute_metrics(log)

# Write back to the log file

write_eval_log(log)

指标和轮次归约器从注册表重建。如果这次评测用了定义在其任务文件里的自定义指标,要在编辑或重算之前先导入那个文件(在跑这次评测的那个进程里,这一步已经做过了):

import
 my_task 
# registers the custom metrics defined in my_task.py

edit_score(log, sample_id, 
"accuracy"
, edit)

否则 edit_score() 和 recompute_metrics() 会抛出 LookupError,其中点名那个指标,并且日志不会被改动。

他人的日志

读取、查看、编辑、重算和恢复一份日志,都不会导入该日志所点名的任何文件。写日志的人控制着里面的每一个字段,包括它记录的任务文件,以及它的指标选项。这些操作不导入任务文件,并且会拒绝那些会创建模型、或创建指标与归约器以外任何对象的指标选项(抛出点名该选项的 ValueError)。

如果信任一份日志,把 trust_log=True 传给 edit_score()、recompute_metrics() 或 recover_eval_log()(对 inspect log recover 则是 --trust-log)。这会为尚未注册的指标导入日志所记录的任务文件,并按记录重建指标选项。它会运行任务文件里的代码,因此只对你信任的日志使用。

重试一份日志(inspect eval-retry)和重新给它打分(inspect score)则不同:它们会运行日志所点名的代码,并使用日志所点名的设置。只在你信任的日志上运行它们(见评测重试和评分工作流)。

分数编辑事件

编辑一个分数时,会自动往该样本的事件日志里加一条 ScoreEditEvent。这样就有了一份全部分数修改的完整审计轨迹,可以在日志查看器里看到。

标签与元数据编辑

也可以在评测之后编辑与一份日志关联的标签和元数据。这对质量复查、分类和过滤这类工作流有用——例如,评测当时把日志标成 "needs_qa",复查之后再改成 "qa_passed"。

用 edit_eval_log() 添加或去掉标签,以及设置或去掉元数据键:

from
 inspect_ai.log 
import
 (

 read_eval_log, write_eval_log, edit_eval_log,

 TagsEdit, MetadataEdit, ProvenanceData,

)

# Read the log file

log 
=
 read_eval_log(
"my_eval.eval"
)

# Edit tags and metadata

log 
=
 edit_eval_log(log, [

 TagsEdit(tags_add
=
[
"qa_passed"
], tags_remove
=
[
"needs_qa"
]),

 MetadataEdit(

 metadata_set
=
{
"reviewer"
: 
"alice"
},

 metadata_remove
=
[
"draft_notes"
],

 ),

], ProvenanceData(author
=
"alice"
, reason
=
"QA complete"
))

# Write back to the log file

write_eval_log(log)

编辑之后,可以直接在日志上访问当前的标签和元数据:

log.tags 
# ["qa_passed", ...]

log.metadata 
# {"reviewer": "alice", ...}

评测当时的原始取值始终保留在 log.eval.tags 和 log.eval.metadata 里。评测之后的全部编辑记在 log.log_updates 里,作为只追加的编辑历史,并带来源信息(作者和原因),从而提供完整的审计轨迹。

空操作编辑会被自动滤掉——添加一个已经存在的标签,或去掉一个并不存在的标签,不会产生一条编辑记录。

Amazon S3

把评测日志存在 S3 上,比用本地文件系统更持久,也更安全。虽然 inspect eval 命令有一个接受 S3 URL 的 --log-dir 参数,但把 inspect 指到一个 S3 存储桶的最方便办法,是把环境变量 INSPECT_LOG_DIR 加进 .env 文件(也可以和 S3 凭据放在一起)。例如:

INSPECT_LOG_DIR=s3://my-s3-inspect-log-bucket
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_DEFAULT_REGION=eu-west-2

译注:上面的访问密钥是文档里的示例占位,不是可用凭据。

如果把日志存在 S3 上,有一件事要记住:它们不再能用本地文本编辑器轻松查看。多半会想配置一个 FUSE 文件系统,以便在本地轻松浏览 S3 日志。

S3 上的大型 .eval 日志使用分段上传(multipart upload)。在默认的 asyncio 后端下,后续写入可以在服务端复制已有日志数据,只上传新数据和 ZIP 目录。如果 Inspect 在一次写入中途被杀掉,未完成的上传会留在存储桶里,并按存储计费,直到被删除。建议在日志存储桶上设一条生命周期规则,一天之后中止未完成的分段上传。

Hugging Face 存储桶

可以用 hf://buckets/<owner>/<bucket>/<path> 这种 URI 方案,把评测日志存在 Hugging Face 存储桶里。存储桶适合做持久、共享的日志存储,并通过 huggingface_hub 的文件系统集成来访问。

先安装 Hugging Face Hub 包,然后创建一个存储桶,并向 Hugging Face 认证:

pip
 install 
"huggingface_hub>=1.6.0"

hf
 auth login

hf
 buckets create my-org/inspect-logs 
--private

然后把日志目录设成一个存储桶路径:

INSPECT_LOG_DIR=hf://buckets/my-org/inspect-logs/runs
HF_TOKEN=hf_...

之后就可以直接从存储桶运行评测并查看日志:

inspect
 eval popularity.py 
--model
 openai/gpt-6-luna

inspect
 view 
--log-dir
 hf://buckets/my-org/inspect-logs/runs

Inspect 不会自动创建 Hugging Face 存储桶;要先建好存储桶,并用一个能写入它的账号或令牌认证。存储桶内容是可改的,因此如果想避免覆盖先前的日志,共享运行应使用唯一前缀。

注意,hf://buckets/... 是 Hugging Face 存储桶的 URI 方案。inspect view bundle 使用的 hf/<org>/<name> 输出路径,是把一个静态查看器发布到 Hugging Face Space,而不是存储桶。

Azure Blob 存储

可以用任何与 Azure 兼容的 fsspec 方案(az://、abfs:// 或 abfss://),把评测日志存在 Azure Blob 存储里。Inspect 依赖 fsspec + adlfs,因此除了安装 Azure 依赖,不需要改代码。

pip install "adlfs>=2025.8.0"

建议(托管身份 / 工作负载身份)

如果跑在 Azure 上(App Service、Container Apps、VM、ASK),并且已经分配了托管身份、授予了 Storage Blob Data Contributor(只读则用 Reader),就不要设置任何密钥环境变量。没有显式密钥时,adlfs 会回退到 DefaultAzureCredential,并安全地使用托管身份。

只设置日志目录(对简短的 az:// URI,也可以再设账号名):

AZURE_STORAGE_ACCOUNT_NAME=myaccount # optional for abfs*/fully-qualified URIs
INSPECT_LOG_DIR=az://mycontainer/inspect-logs

要显式设置 AZURE_STORAGE_ANON=false。留空不设时,默认的 None 会被解释成匿名访问(true),从而跳过你的托管身份或 SAS 凭据,并造成授权失败。

或者使用完全限定的 Data Lake(分层命名空间)URI:

INSPECT_LOG_DIR=abfss://mycontainer@myaccount.dfs.core.windows.net/inspect-logs

回退凭据选项(没有托管身份时)

优先级顺序:SAS 令牌 > 账号密钥 > 连接字符串。

AZURE_STORAGE_ACCOUNT_NAME=myaccount
# SAS token (scoped, time-bound; omit leading '?')
AZURE_STORAGE_SAS_TOKEN=sv=2024-...&ss=bfqt&srt=...
# Account key (broad permissions; avoid in production)
# AZURE_STORAGE_ACCOUNT_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
# Connection string (legacy, broad)
# AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=...;AccountKey=...;
INSPECT_LOG_DIR=az://mycontainer/inspect-logs

这里同样要设置 AZURE_STORAGE_ANON=false——留空会回到匿名模式,adlfs 会忽略上面的凭据。

运行评测与查看器

inspect eval popularity.py --model openai/gpt-6-luna
inspect view # streams directly from Azure

对于 Web 部署(App Service / Container Apps),只要复制同一套环境变量设置即可;托管身份仍然是最安全的方式。

日志文件名

默认情况下,日志文件按下面的约定命名:

{timestamp}_{task}_{id}

其中 timestamp 是日志创建的时间;task 是这份日志所对应任务的名字;id 是唯一的任务 id。

日志文件名里的 {timestamp} 部分是必需的,用来保证日志文件在文件系统里按顺序出现。其余部分可以用环境变量 INSPECT_EVAL_LOG_FILE_PATTERN 定制,它可以包含 task、model 和 id 字段的任意组合。例如,要在日志文件名里带上 model:

export
 
INSPECT_EVAL_LOG_FILE_PATTERN
=
{task}_{model}_{id}

inspect
 eval ctf.py 

和其他面向日志文件的环境变量一样,在你运行评测的那个位置用 .env 文件来定义它,往往会比较方便。

日志命令

上面给出了若干让你从代码里处理评测日志的 Python 函数。不过,你可能在用另一种语言(例如 TypeScript)写编排或可视化工具,调用 Python API 并不特别方便。Inspect CLI 有几条命令,就是为了让其他语言更容易处理 Inspect 日志:

命令说明
inspect log list列出日志目录里的全部日志。
inspect log dump把日志文件内容打印成 JSON。
inspect log convert在日志文件格式之间转换。
inspect log export-config从日志文件导出一份运行配置 YAML。
inspect log schema打印日志文件的 JSON 模式。

列出日志

可以用 inspect log list 命令枚举给定日志目录里的全部日志。如果设置了 INSPECT_LOG_DIR,这条命令会使用它(也可以直接指定 --log-dir)。多半还会想用 --json 标志,以便得到关于日志文件更细、更结构化的信息。例如:

$
 inspect log list 
--json
 
# uses INSPECT_LOG_DIR

$
 inspect log list 
--json
 
--log-dir
 ./security_04-07-2024

也可以用 --status 选项,只列出状态为 success 或 error 的日志:

$
 inspect log list 
--json
 
--status
 success

$
 inspect log list 
--json
 
--status
 error

没有用来筛选可重试日志的 CLI 过滤器:--status 只接受单个值,而 retryable 不是它的取值之一(started、success、cancelled 或 error)。列出未完成的那些状态,已经是这条命令能做到的最近一步,因此把两份列表和你成功的运行对照着看:

$
 inspect log list 
--json
 
--status
 error

$
 inspect log list 
--json
 
--status
 cancelled

读取日志

inspect log list 命令会返回一组指向日志文件的 URI,它们会用上各种协议(例如 file://、s3://、gcs:// 等)。你可能会想直接去读这些 URI,但始终应当用 inspect log dump 命令来读,原因有两个:

  1. 如上文日志格式所述,日志文件可能以二进制或文本存放。无论底层格式是什么,inspect log dump 命令都会把任何日志文件打印成纯文本 JSON。
  1. 日志文件可能位于远程存储系统(例如 Amazon S3)上,用户已经在自己的 Inspect 环境里为它们配置了读/写凭据,而你会想确保用上这些凭据。

例如,这里读取一份本地日志文件,以及 Amazon S3 上的一份日志文件:

$
 inspect log dump file:///home/user/log/logfile.json

$
 inspect log dump s3://my-evals-bucket/logfile.json

转换日志

可以用 inspect log convert 命令在两种底层日志格式之间转换。convert 命令接受一个源路径(一份日志文件,或一个放着日志文件的目录),以及两个必需参数,用来指定转换(--to 和 --output-dir)。例如:

$
 inspect log convert source.json 
--to
 eval 
--output-dir
 log-output

或者对整个目录:

$
 inspect log convert logs 
--to
 eval 
--output-dir
 logs-eval

已经是目标格式的日志,只是被复制到输出目录。默认不会覆盖目标目录里的日志文件,但可以加上 --overwrite 标志强制覆盖。

注意,输出目录始终是必需的,这是为了强制执行一种做法:不要做出并排放着、除了格式之外完全相同的日志文件这种转换。

导出运行配置

inspect log export-config 命令读取一份日志文件,并写出一个 YAML(或 JSON)文件,其中记下那次运行所用的完整配置——任务、模型、模型角色、生成参数、求解器和评测设置。输出可以直接交给 inspect eval --run-config,用来复现这次运行:

$
 inspect log export-config logs/my_run.eval 
>
 run.yaml

$
 inspect eval 
--run-config
 run.yaml

这样就闭合了来回路径:eval → log → export-config → eval。默认输出到标准输出;用 --output 写到文件,用 --format json 则输出 JSON 而不是 YAML。

--run-config 接受的完整模式见运行配置文件。

日志模式

日志文件以 JSON 存放。调用 inspect log schema 即可得到日志文件格式的 JSON 模式:

$
 inspect log schema

因为评测日志包含大量数值数据和计算,有些 number 取值可能是 NaN 或 Inf。这些数值受到 Python 的 JSON 解析器的原生支持,但浏览器和 Node.js 内置的 JSON 解析器并不支持。

要在 JavaScript 里从评测日志正确读出 Nan 和 Inf,建议使用 JSON5 解析器。对其他语言,Nan 和 Inf 可能已有原生支持(如果没有,见这些其他语言的 JSON5 实现)。

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