RFC-0010: Agent 评测语料的分层 Lance 布局
| Field | Value |
|---|---|
| Status | Proposed |
| Schema / format name | corpus/v1(逻辑);物理为 corpus/*.lance 一组 dataset |
| Date | 2026-08-29 |
| Component | persisting-pchronicle |
| Implements | 尚未实现;本 RFC 定义目标布局 |
| Related | RFC-0001 Storyline · RFC-0004 ACTF · RFC-0009 OpenAI Messages · Storyline Lance |
摘要
本 RFC 定义一个四层 + 两条旁路的 Lance 布局,用于存放 Agent 评测语料(benchmark harness 产出的 attempt 级 JSON)。
核心判断来自对一份 1562MB / 18 文件真实语料的实测:99% 的体积是冗余和衍生物,真正的 信息量在 20MB 量级。因此布局的第一原则不是"如何优雅地建模轨迹",而是按访问频率和体积 把数据切成物理隔离的层,让日常查询永远不接触取证层的字节。
与既有 Storyline 三表(storyline-lance)的关系: 本布局是入库侧的语料层,负责无损收敛外部 harness 的异构输出;Storyline 仍是互操作枢纽。 两者共享 CAS 内容层的设计思路,但本布局的原子粒度更细(block 而非 step),并且显式建模 了 Storyline 没有的维度:多 attempt、analyzer 判定、取证产物、RL 偏好对。
动机
实测数据画像
语料路径约定为
<owner>/<experiment>/<benchmark>/<model-tag>/<run>_<run-id>/details/<task-id>.json,
一个文件对应一个 task 的一组 attempt。实测结论:
| 观察 | 实测值 |
|---|---|
| 文件级完全重复 | 1562MB → 892MB(18 文件中仅 15 个内容不同);两个 654MB 文件 md5 相同 |
| 取证转储占比 | attempts.1.artifacts.raw_researchharness_trace_events = 651.1MB(单文件 99.5%),同文件规范轨迹仅 6.7MB |
| 同文件内逐字节重复 | attempts.1.error(3.06MB 单字符串)完整包含 extra.harness_metrics.stdout(3.06MB) |
| 二次型前缀增长 | RL 族每行携带完整历史,第 N 步 messages 是第 N+1 步的精确前缀;单 session 放大 25.6x |
| 逐步重复 | ACTF 族 system_prompt / tools 每步重存;1000 步文件内 tool schema 重复 8000 份 |
| 端到端可压缩性 | 205.9MB 内容字符串 → 去重 92.1MB(2.2x)→ 压缩 14.95MB(合计 14x) |
schema_version 不可信
8 个 trajectory 为 dict 的文件全部声明 schema_version: "ACTF_v1.0",但
observation 元素有四种互不兼容的形状:
{content, is_error, tool_use_id, type}(Claude 风格){content}{role, text, tool_names, error}- 元素本身是 list(嵌套一层)
单文件出现 198 个不同键名,retry_count / retry_counts 只在部分文件出现,
compaction 事件只在部分文件出现。入库必须按形状探测而非按声明版本分派,且必须有
无损兜底通道。
三个来源族
| 族 | 形态 | 来源 |
|---|---|---|
actf_steps | trajectory = {schema_version, started_at, finished_at, steps[]};step 含 step_id / system_prompt / tools / user_content / assistant_content / observation / metric / started_at / finished_at | terminal_bench、swebench_pro、researchclawbench |
event_stream | trajectory = [{type, id, parentId, timestamp, ...}];类型 session / model_change / thinking_level_change / custom / message / compaction;message 角色 user / assistant / toolResult | skillsbench、pinchbench |
rl_rows | 顶层即 list;行含 session_id, step_id, messages[], chosen_response, rejected_response, reward, step_reward, is_trainable, is_terminal, is_truncated, blob_manifest, ... | cybergym |
event_stream 族的 parentId 意味着轨迹是树而非线性序列。
分层模型
四个存储层,按"访问频率 × 体积"划分。层之间只通过键引用,物理上是独立 Lance dataset, 可以独立压缩、独立维护、独立过期。
L0 溯源层 corpus_files ──< file_paths
│ (物理文件身份;一个内容可有多个观测路径)
▼
L1 分析层 attempts ──< analysis
│ (查询主脊:约 0.1% 字节,承载全部 triage 查询)
▼
L2 结构层 steps ──< blocks
│ (有序骨架 + 内容块索引;窄表,全列可字典编码)
▼
L3 内容层 content (CAS,按 digest 唯一,Blob v2 承载 payload)
▲ ▲
│ │
L4 冷层 artifacts │ (取证转储:约 95% 字节,默认不进查询路径)
│
旁路 A preferences (RL 偏好对:派生训练视图)
旁路 B tool_defs (工具定义的去重投影)
分层的实际含义:
| 层 | 字节占比 | 访问频率 | 是否可丢 |
|---|---|---|---|
| L0 溯源 | ~0% | 中(血缘追溯) | 否 |
| L1 分析 | ~0.1% | 极高(每次 triage) | 否 |
| L2 结构 | ~1% | 中(回放、导出) | 否,可从 L0 重建 |
| L3 内容 | ~4% | 中低(回放、导出) | 否,可从 L0 重建 |
| L4 冷层 | ~95% | 极低(取证) | 是,是 L2+L3 的衍生重复品 |
L0 溯源层
corpus_files
一行一个内容不同的物理文件。身份是内容而非路径,因此字节相同的文件天然合并。
| 列 | 类型 | 说明 |
|---|---|---|
file_digest | FixedSizeBinary(32) | BLAKE3-256,主键 |
n_bytes | Int64 | 原始字节数 |
source_family | Utf8(字典) | actf_steps / event_stream / rl_rows |
declared_schema_version | Utf8 nullable | 原样保留,不用于分派 |
shape_fingerprint | FixedSizeBinary(32) | 探测得到的形状指纹,用于分组同构文件 |
ingested_at | Timestamp(us, UTC) | |
raw_digest | FixedSizeBinary(32) | 指向 content,可选保留原始 JSON |
file_paths
一行一个观测到的路径。同一 file_digest 可有多行——实测中两个 654MB 文件字节相同但
路径不同(_2054 与 _clone_2081),这是唯一诚实的表达方式:内容是一份,路径是两个观测。
| 列 | 类型 | 说明 |
|---|---|---|
file_digest | FixedSizeBinary(32) | → corpus_files |
path | Utf8 | 相对语料根的完整路径 |
owner | Utf8(字典) | 路径第 1 段 |
experiment | Utf8(字典) | 路径第 2 段 |
benchmark | Utf8(字典) | 路径第 3 段 |
model_tag | Utf8(字典) | 路径第 4 段 |
run_name | Utf8(字典) | 路径第 5 段去掉尾部 _<id> |
run_id | Int64 nullable | 路径第 5 段尾部数字 |
is_clone | Boolean | run_name 含 _clone |
task_slug | Utf8 | 文件名去掉 _error_ 前缀与 .json |
filename_marks_error | Boolean | 文件名带 _error_ 前缀 |
observed_mtime | Timestamp(us, UTC) nullable |
路径维度是每一个查询的入口("benchmark=X 且 model=Y 的失败 attempt"),因此必须是 一等列而非从路径字符串现场解析。
L1 分析层
attempts
一行一个 attempt。这是查询主脊:所有 triage 查询必须只靠这张表完成,不读任何内容字节。
| 列 | 类型 | 说明 |
|---|---|---|
attempt_uid | FixedSizeBinary(32) | blake3(file_digest ‖ attempt_no),主键 |
file_digest | FixedSizeBinary(32) | → corpus_files |
attempt_no | Int32 | attempts 的键("1", "2"…) |
task_id | Utf8 | |
category | Utf8(字典) | |
k | Int32 | |
attempts_tried | Int32 | |
retry_count | Int32 nullable | 仅部分来源有 |
correct | Boolean | |
score | Float64 nullable | |
status | Utf8(字典) | |
solved_at | Timestamp(us, UTC) nullable | |
started_at / finished_at | Timestamp(us, UTC) nullable | |
owner … filename_marks_error | 同 file_paths | 反规范化的规范路径维度(取该 digest 路径集的字典序最小者) |
n_steps | Int32 | 以下均为入库期预计算的 rollup |
n_messages | Int32 | |
n_blocks | Int32 | |
n_tool_calls | Int32 | |
n_tool_errors | Int32 | |
input_tokens / output_tokens / cache_read_tokens / cache_write_tokens | Int64 nullable | |
cost_total | Float64 nullable | |
llm_infer_ms_total / tool_exec_ms_total / wall_ms | Float64 nullable | |
has_error | Boolean | |
error_digest | FixedSizeBinary(32) nullable | → content;巨型 error 字符串不进本表 |
final_answer_digest | FixedSizeBinary(32) nullable | → content |
ground_truth_json | lance.json nullable | 形状不一(有时 str,有时 {checklist_path}) |
meta_json | lance.json nullable | 已脱敏的 meta,见"凭证脱敏" |
unknown_json | lance.json nullable | 无损兜底 |
rollup 列是本设计成立的关键:它们把"筛出高成本 / 高延迟 / 工具报错多的 attempt"从一次 内容扫描降级为一次窄列扫描。
analysis
一行一个 (attempt, analyzer)。长格式,不是 21 个固定列也不是一个 JSON blob。
| 列 | 类型 | 说明 |
|---|---|---|
attempt_uid | FixedSizeBinary(32) | → attempts |
analyzer | Utf8(字典) | 实测 21 种,集合在增长 |
is_badcase | Boolean nullable | 实测 188 次出现 |
score | Float64 nullable | 实测 118 次出现 |
analyzer_error | Utf8 nullable | 实测 55 次出现 |
details_digest | FixedSizeBinary(32) nullable | → content;ExceptionAnalyzer.details 实测达 1.93MB |
details_inline_json | lance.json nullable | details 小于阈值时就地存放,可直接查询 |
选长格式的三个理由:analyzer 集合在增长(新增不动 schema);details/score/error
三个子键都是可选的(宽表会产生大量 null 列);最常见的查询是"任一 analyzer 命中",
长格式下是一次普通过滤而非 21 列 OR。
L2 结构层
steps
有序的回合骨架。metric / usage 完全摊平成真实列——它们是聚合查询的主要目标,
留在 JSON 里等于放弃列式优势。
| 列 | 类型 | 说明 |
|---|---|---|
attempt_uid | FixedSizeBinary(32) | → attempts |
step_ordinal | Int32 | 从 0 连续,本布局赋予的规范序 |
source_step_id | Utf8 nullable | 来源自带的 step_id / 事件 id |
parent_step_ordinal | Int32 nullable | event_stream 族 parentId 解析结果;线性时为 null |
session_id | Utf8 nullable | rl_rows 族与 session 事件 |
model / provider / api | Utf8(字典) nullable | |
stop_reason / finish_reason | Utf8(字典) nullable | |
prompt_tokens / completion_tokens | Int64 nullable | ACTF metric.{prompt,completion}_tokens_len |
cache_read_tokens / cache_write_tokens / total_tokens | Int64 nullable | event_stream 族 usage.* |
cost_input / cost_output / cost_cache_read / cost_cache_write / cost_total | Float64 nullable | usage.cost.* |
llm_infer_ms | Float64 nullable | ACTF metric.llm_infer_ms |
env_action_ms | Float64 nullable | ACTF metric.env_action_ms |
started_at / finished_at | Timestamp(us, UTC) nullable | |
system_prompt_digest | FixedSizeBinary(32) nullable | → content;逐步重复由 CAS 吸收 |
tools_digest | FixedSizeBinary(32) nullable | → content / tool_defs |
unknown_json | lance.json nullable |
blocks
本布局的核心表。 原子是内容块,不是 message 也不是 step。三个来源族在这个粒度上完全
同构:ACTF 的 assistant_content.{content, reasoning_content, tool_calls[]}、
event_stream 的 [thinking, text, toolCall] 块列表、rl_rows 的 messages[].content[]
都落成同一组行。
| 列 | 类型 | 说明 |
|---|---|---|
attempt_uid | FixedSizeBinary(32) | → attempts |
step_ordinal | Int32 | → steps |
msg_ordinal | Int32 | 步内消息序 |
block_ordinal | Int32 | 消息内块序 |
role | Utf8(字典) | system / user / assistant / tool / runtime |
block_type | Utf8(字典) | text / thinking / tool_call / tool_result |
tool_name | Utf8(字典) nullable | |
tool_call_id | Utf8 nullable | 关联 tool_call 与 tool_result |
is_error | Boolean nullable | |
content_digest | FixedSizeBinary(32) nullable | → content |
n_bytes | Int32 | 内容原始字节数 |
arguments_json | lance.json nullable | tool call 参数;小且是高频查询目标 |
thinking_signature | Utf8 nullable | |
unknown_json | lance.json nullable |
blocks 的所有过滤列都是低基数可字典编码的,payload 只是定长摘要,因此
WHERE tool_name = 'Bash' AND is_error 这类查询扫描代价接近于零。
物理写入按 (attempt_uid, step_ordinal, msg_ordinal, block_ordinal) 排序,
attempt_uid 上建 BTree 标量索引:筛一批 attempt 走索引,回放一个 attempt 是一次连续
范围读。
L3 内容层
content
CAS。按 digest 唯一,是全布局唯一存放大段字节的地方。
| 列 | 类型 | 说明 |
|---|---|---|
digest | FixedSizeBinary(32) | BLAKE3-256 of 未压缩原始字节,主键 |
n_bytes | Int64 | 未压缩长度 |
encoding | Utf8(字典) | raw / zstd |
content_type | Utf8(字典) | text/plain / application/json / … |
payload_inline | Binary nullable | 小于 inline_threshold 时就地存放 |
payload_blob | Lance Blob v2 | 大于阈值时走 blob,不投影则零字节读取 |
去重与卸载解耦,这是与现行 Storyline 内容层最重要的分歧。 摘要总是计算,去重在
content 表这一级无条件发生;inline 还是 blob 是 content 表内部的独立决定
(建议阈值 8KB)。现行设计用单一的 64KB DEFAULT_CONTENT_OFFLOAD_THRESHOLD 同时决定
这两件事,导致本语料的主要重复模式(小对象的成千上万次重复)完全逃逸。
需要说明清楚 CAS 相对列压缩的边界:同一 fragment 内的重复值,Lance 的字典编码和页 压缩本来就能吃掉,这部分不是 CAS 的功劳。CAS 不可替代的是两种情形——
- 跨 dataset / 跨 run 的重复:上千次 run 共享同一份 system prompt 与 tool schema, 页压缩看不到跨文件边界;实测两个 654MB 文件完全相同,压缩无能为力。
- 二次型前缀增长:
rl_rows族第 N 步与第 N+1 步的messages是不同的值,字典 编码失效;只有结构化引用能把 25.6x 放大压回 1x。
tool_defs(旁路 B)
tools_digest 指向的工具定义列表的去重投影,让"哪些 run 可用工具 X"变成关系查询。
实测单文件内 tool schema 重复 8000 份,去重后是个位数行。
| 列 | 类型 | 说明 |
|---|---|---|
tools_digest | FixedSizeBinary(32) | → content |
tool_name | Utf8(字典) | |
description_digest | FixedSizeBinary(32) nullable | → content |
parameters_json | lance.json nullable |
L4 冷层
artifacts
取证转储。独立 dataset、独立生命周期、默认不进查询路径。
| 列 | 类型 | 说明 |
|---|---|---|
attempt_uid | FixedSizeBinary(32) | → attempts |
artifact_name | Utf8(字典) | raw_researchharness_trace_events / harness_metrics.stdout / error / … |
digest | FixedSizeBinary(32) | → content |
n_bytes | Int64 | |
content_type | Utf8(字典) | |
derives_from | Utf8(字典) nullable | 标注它是 L2+L3 的衍生重复品时填 trajectory |
这一层承载实测 95% 的字节,却是 L2+L3 的衍生重复品——error 完整包含
harness_metrics.stdout,raw_researchharness_trace_events 是 trajectory.steps 的
逐次全量重放。因此它必须能被独立地过期、独立地丢弃,而不影响任何分析查询。
derives_from 让"可安全丢弃"成为可判定的属性。
preferences(旁路 A)
rl_rows 族的偏好对。它是派生的训练视图,不是轨迹;硬塞进轨迹模型正是 25.6x 放大的
成因,所以单独一张表。
| 列 | 类型 | 说明 |
|---|---|---|
attempt_uid | FixedSizeBinary(32) | → attempts |
session_id | Utf8 | |
step_ordinal | Int32 | → steps;状态就是该步的 block 前缀,不再复制 |
chosen_digest | FixedSizeBinary(32) nullable | → content |
rejected_digest | FixedSizeBinary(32) nullable | → content |
response_digest | FixedSizeBinary(32) nullable | → content |
reward / step_reward | Float64 nullable | |
is_trainable / is_terminal / is_truncated / is_session_completed | Boolean | |
dataset_type | Utf8(字典) | TRAIN / TEST / … |
dt | Date32 | |
env_id / env_name / job_id / agent_model | Utf8(字典) nullable | |
blob_manifest_json | lance.json nullable | |
unknown_json | lance.json nullable |
消除二次型冗余的机制在这里:preferences 不存 messages[],只存 step_ordinal;状态
按需从 blocks 取 step_ordinal <= N 的前缀重建。
关联关系与基数
| 父 | 子 | 外键 | 基数 | 语义 |
|---|---|---|---|---|
corpus_files | file_paths | file_digest | 1 : 1..N | 一份内容可有多个观测路径 |
corpus_files | attempts | file_digest | 1 : 1..k | 一文件含 attempts_tried 个 attempt |
attempts | analysis | attempt_uid | 1 : 0..21 | analyzer 集合可增长 |
attempts | steps | attempt_uid | 1 : 0..N | |
attempts | artifacts | attempt_uid | 1 : 0..M | |
attempts | preferences | attempt_uid | 1 : 0..N | 仅 rl_rows 族非空 |
steps | steps | parent_step_ordinal | 自引用 | event_stream 族的分支树 |
steps | blocks | (attempt_uid, step_ordinal) | 1 : 0..N | |
blocks | blocks | tool_call_id | 1 : 0..1 | tool_call ↔ tool_result 配对 |
blocks | content | content_digest | N : 1 | 多对一是去重发生的地方 |
steps | content | system_prompt_digest / tools_digest | N : 1 | |
analysis | content | details_digest | N : 1 | |
artifacts | content | digest | N : 1 | |
preferences | content | chosen / rejected / response_digest | N : 1 | |
content | tool_defs | tools_digest | 1 : N | 投影展开 |
所有指向 content 的边都是 N : 1,这是整个布局压缩比的来源。除此之外的边全部是
严格的树形父子关系,没有多对多。
关键 join 路径
triage attempts ⋈ analysis (不触碰 content)
成本分析 attempts ⋈ steps (不触碰 content)
工具行为 blocks ⋈ steps ⋈ attempts (不触碰 content payload)
回放 attempts ⋈ steps ⋈ blocks ⋈ content (范围读 + blob take)
训练导出 preferences ⋈ blocks ⋈ content
取证 attempts ⋈ artifacts ⋈ content (显式 opt-in)
身份与幂等
content.digest= BLAKE3-256(未压缩原始字节)corpus_files.file_digest= BLAKE3-256(文件原始字节)attempts.attempt_uid= BLAKE3-256(file_digest‖attempt_no的 LE u32)
身份取内容而非路径,带来三个性质:重复入库天然幂等;字节相同的文件自动合并为一个 attempt 并记录多个路径观测;血缘可从任何一层反查到原始文件。
代价是必须接受一个语义判断:字节完全相同(含全部时间戳)的两个文件是同一次逻辑运行的
拷贝,而非两次巧合相同的运行。实测的 _2054 / _clone_2081 属于前者。如果某个 harness
确实会产出字节相同的不同运行,则需要在 attempt_uid 中混入路径,这是本 RFC 的一个
未决点。
来源族到 blocks 的映射
actf_steps
| 来源字段 | 落点 |
|---|---|
steps[i].step_id | steps.source_step_id |
steps[i].system_prompt | steps.system_prompt_digest → content |
steps[i].tools | steps.tools_digest → content / tool_defs |
steps[i].user_content | blocks(role=user, block_type=text) |
steps[i].assistant_content.content | blocks(role=assistant, block_type=text) |
steps[i].assistant_content.reasoning_content | blocks(role=assistant, block_type=thinking) |
steps[i].assistant_content.tool_calls[j] | blocks(role=assistant, block_type=tool_call, tool_name, arguments_json) |
steps[i].observation | blocks 行,role 取 tool 或 runtime,block_type=tool_result;四种形状统一到 {content, is_error, tool_call_id},未能映射的键进 unknown_json |
steps[i].metric.* | steps 的摊平列 |
observation 的四种形状必须由形状探测分派,不得依赖 declared_schema_version。
event_stream
| 来源字段 | 落点 |
|---|---|
事件 id / parentId | steps.source_step_id / steps.parent_step_ordinal |
session 事件的 cwd / version | attempts.meta_json |
model_change / thinking_level_change | 生效区间内 steps.model / steps.provider;原事件保留于 unknown_json |
message.role=user 的 content[] | blocks(role=user) |
message.role=assistant 的 content[] 块 | blocks 行,block_type 取 thinking、text 或 tool_call |
message.role=toolResult | blocks(role=tool, block_type=tool_result, tool_call_id, tool_name, is_error) |
message.usage.* / usage.cost.* | steps 的摊平列 |
compaction 事件 | steps 一行,block_type 无内容块;标记于 unknown_json |
custom 事件 | steps.unknown_json |
一次 assistant 消息及其后继 toolResult 折叠为一个 step;parentId 分叉时
parent_step_ordinal 指向分叉点,不做线性化。
rl_rows
| 来源字段 | 落点 |
|---|---|
session_id / step_id | steps.session_id / steps.step_ordinal |
messages[] | 仅最后一条新增消息落 blocks;前缀通过 step_ordinal <= N 重建 |
chosen_response / rejected_response / response | preferences.*_digest |
reward / step_reward / is_* | preferences |
blob_manifest / meta_json | preferences 的 lance.json 列 |
入库时必须校验"第 N 步的 messages 是第 N+1 步的前缀"这一前提;若不成立则退化为全量落
blocks(CAS 仍会去重相同消息),并在 corpus_files.shape_fingerprint 上标注。
Lance 物理决策
lance.json用于小而需查询的半结构化列:blocks.arguments_json、attempts.meta_json/ground_truth_json、analysis.details_inline_json、preferences.blob_manifest_json,以及每张表的unknown_json。这些列在lance 9.0.1中以 JSONB(LargeBinary+lance.json扩展)存储,可用json_get_*/json_extract过滤,并可对热路径建 JSON 标量索引。 相比Utf8兜底列,它的类型不撒谎且可查询。- JSON 标量索引建在实际热路径上:
blocks.arguments_json的$.command、$.path。 索引按路径字面量匹配,查询必须用同一路径写法。 - Blob v2 仅用于
content.payload_blob,且content是唯一使用 blob 的表。 不投影即零字节读取。 - 排序与索引:各表按第一列为
attempt_uid的复合序物理排序;attempt_uid、content.digest、blocks.tool_name上建标量索引。 - 不使用 Variant:Lance 明确不实现 Parquet Variant;
parquet-variant仍标注 WIP 且属 Parquet 侧。JSONB 是当前唯一可用的原生二进制 JSON 路径。 - 兜底列必须存在于每张表。198 个键名、四种同版本异构形状、按来源出现的可选字段, 决定了任何封闭 schema 都会丢数据。
查询路径示例
-- triage:只碰 L1,不读一个内容字节
SELECT a.task_id, a.benchmark, a.model_tag, a.score, an.analyzer
FROM attempts a JOIN analysis an USING (attempt_uid)
WHERE a.correct = false AND an.is_badcase
AND a.benchmark = 'skillsbench';
-- 成本分析:只碰 L1 + L2 的窄列
SELECT model_tag, sum(output_tokens), avg(llm_infer_ms)
FROM attempts a JOIN steps s USING (attempt_uid)
GROUP BY model_tag;
-- 工具行为:碰 L2,payload 不物化
SELECT tool_name, count(*), sum(CAST(is_error AS INT))
FROM blocks
WHERE block_type = 'tool_call'
GROUP BY tool_name;
-- 命令级下钻:走 JSON 索引
SELECT attempt_uid, step_ordinal
FROM blocks
WHERE tool_name = 'Bash'
AND json_get_string(arguments_json, 'command') LIKE '%pyomo%';
前三个查询完全不接触 L3 / L4。这正是分层的目的。
生命周期与 GC
- L4 可过期:
artifacts中derives_from = 'trajectory'的行可按保留期删除, 仅需级联清理content中失去引用的 digest。删除后 L1/L2/L3 完整,全部分析查询不受影响。 contentGC 是引用计数:live set =blocks∪steps∪analysis∪artifacts∪preferences∪corpus_files中出现的全部 digest。因为content是所有层共享的, GC 必须在所有引用表的同一快照上计算,否则会误删。- 重建:L1/L2/L3 全部可从
corpus_files.raw_digest重放重建。若不保留原始 JSON, 则 L2/L3 成为事实源,此时 L4 的删除不可逆——这是一个需要显式配置的取舍。
凭证脱敏
实测在 attempts.*.meta.plan 下发现明文凭证:
environment.params.secret_key(10 处)与
execution.analysis_params.OnomyAnalyzer.api_key(2 处)。
因此入库管线 MUST 在写入前对 meta_json 执行键名模式脱敏
(secret / token / api_key / password / credential / authorization),
将值替换为 «redacted:<blake3-8>» 形式的占位符,保留可比较性而不保留明文。
corpus_files.raw_digest 指向的原始 JSON 若保留,则该 dataset 必须与 L1–L4 分离
授权。这是本 RFC 的一条硬约束,而不是建议。
预估收益
按实测推算:
| 项 | 原始 | 目标 |
|---|---|---|
| L4 取证层 | ~670MB | ~110MB(CAS + zstd),且离开查询路径 |
| L0–L3 | ~220MB | ~15–20MB(去重 2.2x × 压缩 ~6x) |
| 合计 | 1562MB | ~20MB 可查询 + ~110MB 冷 blob |
| triage 查询实际读取 | — | 几百 KB 量级 |
若判定 L4 可丢(它确实是 L2+L3 的衍生重复品),则总量约 20MB。
未决问题
- 字节相同文件的身份:
attempt_uid是否应混入路径?取决于是否存在会产出字节相同 的不同逻辑运行的 harness。当前假设不存在。 - step 折叠粒度:
event_stream族一次 assistant + 后继 toolResult 折叠为一 step 是 启发式;compaction事件与多 toolResult 的情形需要更明确的规则。 - 与 Storyline 的关系:本布局是否应作为 Storyline 三表的上游语料层,还是应由 Storyline 增补 attempt / analyzer / artifact 维度后统一?前者引入两套模型,后者会把 评测语料的关注点压进互操作枢纽。
observation四形状的收敛:统一到{content, is_error, tool_call_id}会丢失 形状 3 的tool_names(复数)语义,目前靠unknown_json兜底,是否需要一等列待定。- inline 阈值:8KB 为估计值,需按实际块大小分布(实测消息平均 665 字节)实测标定。