跳到主要内容

实验结果展示数据契约设计规格

字段
版本v0.2.0
状态Accepted
创建日期2026-06-12
最后更新2026-06-12
作者xiang.li
前置前端仿真洞察工作台设计规格

变更历史

版本日期变更说明
v0.2.02026-06-12行形态新增列筛选(in 多选)语义,新增消费者"筛选候选值发现"绑定列形态
v0.1.02026-06-12初版(Accepted)

@tbl-spec-rdc-01 文档变更历史


概述

本 spec 冻结实验结果的展示数据契约:后端以三种数据形态(行形态分页、列形态投影、单行全量)向前端供给实验结果,每个前端消费者按其真实数据需求绑定唯一形态。契约覆盖端点语义、字段寻址协议、列发现机制和规模边界,是结果管理页面所有数据交互的验证基准。

本 spec 同时修正前置 spec《前端仿真洞察工作台设计规格 v0.1.0》"既有汇总指标、Gantt、实验管理的 API 契约不变"的表述:实验结果读取契约以本 spec 为准,该句不再成立(详见 集成点 节)。

背景

实验结果行 = 标量指标列(吞吐、利用率、成本、并行度等,已提升为数据库表字段)+ 大型 JSON 字段(配置快照、完整结果、搜索统计)。展示侧存在六类消费者,但历史上只有两种供给方式——"全量行"与"分页行"——导致两类问题:

  • 列式需求被行式供给放大传输量:参数分析、Pareto 前沿、CSV 导出需要的是"全部行 × 少数字段"(数千行 × 几个标量 ≈ 数百 KB),实际拉取的是"全部行 × 全字段"(每行配置快照数 KB,合计数十 MB),浪费约两个数量级。
  • 契约无冻结文档导致实现漂移:2026-06 的分页迁移中,后端、API 层、视图层各自处于不同迁移进度且无 single source of truth 可对照,结果管理页面在中间状态下功能断裂(任务列表无法获取数据)。

用户实验规模常态为单实验数千行结果、实验数量多,列式消费者的传输浪费是常态成本而非边缘场景,需要契约层面解决。

名词定义

名词定义与易混淆概念的区分
结果行一次评估任务的持久化记录:标量指标 + 配置快照 + 完整结果 JSON不同于"任务"(运行时概念);结果行是任务完成后的不可变快照
行形态按页返回完整结果行的供给方式,服务表格类消费者行内含配置快照,与"单行全量"的区别是按页批量、不含完整结果 JSON 的全部明细
列形态按请求字段列表对全部结果行做投影、按列组织返回的供给方式不返回未请求的字段;行数全量但字段子集
单行全量按结果行主键返回该行全部字段(含完整结果 JSON)的供给方式端点每次返回 1 行;对比场景由消费者按 N 个选中主键调用 N 次
实验默认排序实验类别约定的默认排序键的降序(部署类实验为综合评分,通信评估类实验为创建时间)列模板取样与列形态行序的共同基准;结果集不变时行序稳定
列筛选以"字段路径 + 值集合"约束行形态结果集的服务端过滤,命中任一值的行保留(in 语义)作用于实验全部行而非当前页;与排序、分页可叠加,total 为筛选后计数
字段路径<域>:<路径> 寻址结果行内字段的字符串协议,域 ∈ {标量列, config, result},路径为点分隔层级同一协议同时用于排序键与投影字段,语义一致
列模板实验内按默认排序取首行序列化得到的结构样本,供前端做列发现不是 schema 定义;是结构样本,异构实验下可能不完整(见 局限 节)
消费者读取结果数据的前端功能单元(任务列表、参数分析、Pareto 前沿、CSV 导出、详情/对比、实验列表)

目标与非目标

目标

  1. 冻结三形态供给契约:每个消费者绑定唯一数据形态,形态间职责不重叠。
  2. 冻结字段路径协议:投影与排序共用同一寻址语法和校验规则。
  3. 冻结列发现机制:列模板的来源、稳定性保证与失效边界。
  4. 规模承诺:单实验 5000 行下,行形态与列形态请求的响应体 ≤ 2 MB、端到端响应 ≤ 1 s(单行全量不在此承诺内,单独预算见 非功能性需求 节)。

非目标

  1. 不改变结果行的存储结构(不做配置快照去重、不拆表)。
  2. 不将分析计算(Pareto 支配判定、参数聚合)下推后端,前端保留计算职责。
  3. 不覆盖写入路径(结果入库、实验创建)与任务运行时状态(进度、取消)。
  4. 不定义事件级 trace 数据出口(归《前端仿真洞察工作台设计规格》Wave 2)。

用例说明

用例 1:打开实验详情(行形态)。用户从实验列表点入一个 5000 行的实验。前端先取实验元信息(名称、计数),再取第 1 页结果行(默认每页 100 行)与列模板。表格按列模板确定列集,用户点击"吞吐"列头,前端以字段路径作为排序键重新请求第 1 页,后端在全部 5000 行上排序后返回前 100 行。

用例 2:参数分析(列形态)。用户切到参数分析 Tab,从列模板派生的轴候选中选 X 轴 = 配置中的 batch 维度、Y 轴 = 吞吐。前端以两个字段路径请求投影,后端返回 5000 行 × 2 字段的列式数组(数十 KB),前端绘制散点。切换轴时仅重新请求新字段。

用例 3:跨页对比(单行全量)。用户在第 3 页勾选 2 行、第 7 页勾选 1 行,点击对比。前端按 3 个结果行主键逐一请求单行全量,获得完整结果 JSON(含 Gantt、内存明细)进入对比视图。

用例 4:CSV 导出(列形态)。用户配置了 18 个可见列后点导出。前端把 18 个可见列翻译成字段路径列表请求投影,后端返回全量行 × 18 字段,前端拼装 CSV 下载。导出内容与表格可见列一致。

用例 5:列筛选(行形态 + 列形态协同)。用户点开"算法"列头的筛选器,前端以该列的字段路径请求列形态投影(全量行 × 1 字段),去重后展示候选值勾选框(如 dmodk / ring / halving-doubling)。用户勾选两个值,前端把筛选条件附加到行形态请求,后端在全部行上过滤后分页返回,total 变为筛选后计数,页码回到第 1 页。再叠加点击"延迟"列头排序,排序在筛选结果集内进行。清除筛选后恢复全量。

详细设计

概念模型

供给侧三形态与消费侧六消费者的绑定关系是本契约的核心:

消费者绑定形态行范围字段范围
实验列表元信息(实验级,非结果行)实验 meta + 结果计数
任务列表表格行形态单页(≤500)标量 + 配置快照(不含完整结果明细)
参数分析列形态全量请求的轴字段 + 指标字段
Pareto 前沿列形态全量成本 + 性能两类标量
CSV 导出列形态全量用户可见列对应字段
筛选候选值发现列形态全量被筛选的单字段(复用列形态端点投影单字段,前端去重,无独立去重端点)
任务详情 / 对比单行全量1~N(用户选中)全字段含完整结果 JSON

@tbl-spec-rdc-02 消费者与数据形态绑定

绑定是排他的:消费者不得越形态取数(例如参数分析不得退回拉全量行)。新增消费者时必须先在本表登记绑定形态。

设计原理

为什么是三形态而不是一个通用查询接口:六个消费者的(行范围 × 字段范围)组合只聚成三类——"页 × 全字段"、"全量 × 字段子集"、"单行 × 全字段"。三个专用形态各自语义封闭、可独立验收;通用查询接口(任意行 × 任意字段)表达力超出需求,校验面与误用面更大。

为什么列形态做在服务端投影而不是前端缓存全量:实验内各行配置快照仅在扫描维度上不同、其余内容相同,全量行传输的字节大部分是重复配置;服务端在数据库层按 JSON 路径抽取字段,传输量与"行数 × 请求字段数"成正比,与配置体积无关。规模常态为数千行时,这是数量级差异。

为什么排序在服务端:行形态只持有当前页,客户端排序只能排页内数据,语义错误。排序键与投影字段共用字段路径协议,避免两套寻址语法漂移。

为什么列发现用结构样本而不是 schema 注册:结果行的配置结构由评估配置决定,随评估功能演进而变化;维护显式 schema 注册表会产生第二个需要同步的事实源。取实验内固定首行作结构样本,零维护成本,代价是异构实验下样本可能不完整(见 局限 节)。

字段路径协议

字段路径是唯一的字段寻址语法,排序与投影共用:

  • 语法<域>:<路径> 或裸标量列名。域 config 寻址配置快照,域 result 寻址结果指标;路径为点分隔的层级键。
  • 校验:分两段——域前缀必须命中域白名单(config / result,或裸标量列名命中排序白名单);剥离域前缀后的路径段必须匹配 ^[A-Za-z0-9_.]+$。任一段不通过则整个请求拒绝(HTTP 400),禁止静默忽略。该约束同时是 SQL 注入的防线:路径经数据库 JSON 抽取函数参数化执行,禁止字符串拼接。完整文法以 附录 C 为权威定义。
  • 缺失语义:语法合法但某行不存在该路径时,该行该字段取 null(异构行是合法状态);用于排序时 null 排在序列末端。
  • 白名单:裸标量列名必须在服务端排序白名单内,白名单外拒绝(400)。白名单的语义是"真实存在的表列集合"(裸列名直接进入排序子句,必须封闭),应从数据模型自动派生,不维护手写清单。
  • 零维护演进:新增 config 参数或 result 指标不需要任何白名单变更——config:/result: 路径仅做字符集校验 + 参数化抽取,新路径即写即用,旧行按缺失语义返回 null

三形态端点语义

行形态(分页):输入实验 ID、页码、页大小(上限 500)、排序键(字段路径)、排序方向、列筛选集合(可选)。输出当前页结果行数组、总行数(筛选后计数)、列模板。结果行含标量指标与配置快照;排序与筛选均作用于实验内全部行,处理顺序为先筛选、后排序、再分页。

列筛选语义

  • 每条筛选 = 字段路径 + 值集合,行的该字段值命中集合中任一值则保留(in 语义);多条筛选之间为与(AND)关系。
  • 值集合元素必须为标量(数值 / 字符串 / 布尔);非标量值拒绝(400)。
  • 字段寻址与校验完全复用字段路径协议——可排序的字段即可筛选,不引入第二套寻址。
  • 缺失处置:行内不存在该字段路径时该字段为 null,不命中任何 in 值,故该行被过滤。这与排序中 null 保留并排末端是两种不同的结果处置——二者同源于同一缺失检测(字段路径不存在即 null),但处置方向相反:筛选删、排序留。
  • 候选值由消费者经列形态投影该字段后去重得到(见 概念模型 绑定表),契约不提供独立的去重端点。
  • 一期仅 in 操作符;其它 op 值拒绝(400)。数值区间筛选见 后续工作。

列形态(投影):输入实验 ID、字段路径列表(1~64 个)。输出按请求顺序的字段名数组 + 等长的列式值数组(行数 = 实验全量行数),行序固定为实验默认排序(见 名词定义)。不分页:列形态的目的就是全量小字段,分页反而破坏分析语义。本端点取代《实验数据库页面性能优化 plan》中"全量行 metrics 端点"的契约。

单行全量:输入结果行主键。输出该行全部字段,含完整结果 JSON。不存在时 404。

实验元信息:输入实验 ID。输出实验 meta 与结果计数,不含任何结果行

约束与规则

  • 形态排他(硬约束):消费者越形态取数视为契约违例,review 阶段拒绝。
  • 页大小上限 500(硬约束):防止行形态退化为全量端点;默认页大小 100,500 是硬上限而非默认值。
  • 投影字段数上限 64(硬约束):防止列形态退化为全量行端点。
  • 筛选条数上限 16、单条值集合上限 64(硬约束):防止筛选参数膨胀为任意查询语言;超限拒绝(400)。该 64 与投影字段数上限 64 是相互独立的约束,数值巧合,无耦合。
  • 列模板稳定性(硬约束):同一实验内列模板不随页码、排序参数变化,保证表格列集不因翻页时隐时现。
  • 错误显式化(硬约束):非法排序键、非法字段路径、越界页大小一律 400 带原因,禁止降级到默认行为(与项目配置加载禁默认值规则同源)。

集成点

  • 上游:结果行由评估管线写入,本 spec 不约束写入格式,但消费"标量列已提升为表字段"这一存储事实——新增需排序/投影的高频标量时,应提升为表字段而非留在 JSON 内(性能边界见 非功能性需求 节)。
  • 下游:前端结果管理页面的全部数据请求按本契约实现;前端 API 层是契约的唯一翻译点,视图组件不直接感知端点形状。
  • 对前置 spec 的修正:《前端仿真洞察工作台设计规格 v0.1.0》"既有汇总指标、Gantt、实验管理的 API 契约不变"中"实验管理"部分由本 spec 取代;该 spec 其余内容(trace 协议、WebSocket 信封、Wave 路线)不受影响。修正以该 spec 附录 B 实现说明 + 本 spec 本节双向链接固定。

引用

备选方案

维度方案 A:三形态分层(选定)方案 B:全量行 + 前端缓存方案 C:分析计算下推后端方案 D:存储层配置去重
5000 行传输量列式消费者数百 KB数十 MB(配置重复传输)数 KB(仅聚合结果)数 MB(仍是行式)
前端分析灵活性全保留(任意轴组合)全保留丧失(轴组合 = 端点爆炸)全保留
改动面新增投影端点,存储不动无改动(现状)每种分析一个聚合端点写入路径 + 全部读取方重构
失效风险字段路径校验内存与传输随行数线性恶化分析需求变更即改后端迁移期数据一致性风险

选择理由:方案 A 在不动存储、不丢前端灵活性的前提下把列式消费者的传输量压掉两个数量级,且字段路径协议与已落地的服务端排序同机制,无新概念。方案 B 是被本 spec 取代的现状;方案 C 用灵活性换传输量,在投影已足够小的情况下得不偿失;方案 D 收益(存储体积)与本 spec 要解决的问题(传输与契约)不对口。

与业界的差异:本项目结果行体积比主流实验跟踪系统的典型 run 重一个数量级,业界普遍的"整行全量下发"前提不成立,故采用 TensorBoard HParams 一系的服务端逐列投影路线(完整对比与理由见 附录 A)。

非功能性需求

维度本 spec 的考虑
性能单实验 5000 行:行形态单页 ≤ 1 s、≤ 2 MB;列形态 8 字段投影 ≤ 1 s、≤ 500 KB;单行全量 ≤ 500 ms。JSON 路径投影/排序在数据库层逐行解析 JSON,无索引加速;超出 2 万行时该方案需重估(见 局限 节)
兼容性消费者全在同 repo,契约变更一次性同步迁移,不留旧格式分支(遵循项目不向后兼容规则)。本 spec 取代旧"详情端点内嵌全量结果行"契约
可靠性列形态全量请求失败不得静默降级为空图表,必须显式错误态;导出失败禁止回退到"仅当前页"的静默截断
安全性字段路径正则白名单 + 参数化 JSON 抽取,杜绝 SQL 注入;服务运行于内网,鉴权 N/A
可观测性N/A(内部工具,沿用服务端通用请求日志)

局限与后续工作

局限

风险 / 局限影响缓解措施
列模板取单行样本,异构实验(行间配置结构不同)下列发现不完整个别列不出现在候选中实验内配置结构常态一致;发现异构需求后将样本扩展为多行键并集(后续工作 2)
JSON 路径投影/排序无索引,行数上限受数据库逐行解析速度约束2 万行以上可能超性能预算常态数千行在预算内;超界时把高频字段提升为表字段(集成点已约定),或引入物化列
列形态不分页,单字段超长字符串值(如长错误信息)可能放大响应体响应体超预算投影面向标量/短字符串;约束消费者不得投影完整结果 JSON 域的明细子树
三形态各自端点,消费者绑定靠 review 约束而非类型系统强制新代码可能误用形态前端 API 层收口为唯一翻译点;消费者绑定表作为 review 检查清单

后续工作

  1. 数值区间筛选(中优先级,前置:in 多选筛选落地后按需求触发):为数值列扩展 range 操作符,沿用同一筛选参数结构。
  2. 列模板多行并集(低优先级,前置:出现真实异构实验)。
  3. 2 万行以上规模档(中优先级,前置:实验规模增长到接近边界):物化列或列式缓存。
  4. 投影结果服务端缓存(低优先级,前置:同一实验的重复投影请求成为可观测热点)。

验收标准

场景指标目标值测试方法
行形态分页5000 行实验任意页响应时间 / 响应体≤ 1 s / ≤ 2 MB集成测试构造 5000 行实验,请求多个页码计时测体积
行形态排序标量键与 config 路径键排序正确性与全量排序金标准一致单测:构造已知乱序数据,校验各页拼接后的全序
列形态投影5000 行 × 8 字段响应体≤ 500 KB集成测试测响应体积
列形态缺失语义部分行缺失字段缺失处为 null,行数不缩减单测:构造异构行
字段路径校验非法路径(含 $';../全部 400,无 SQL 执行单测注入用例集
单行全量任意页勾选行可对比跨页对比数据完整(含结果 JSON)集成测试:跨页取 3 行断言字段全集
列模板稳定性同实验不同页码/排序参数列模板逐字节一致集成测试对比多次请求
形态排他前端代码无越形态取数列式消费者零调用行形态/全量行接口代码 review 检查清单 + grep 断言
列筛选正确性in 多选 + 多条 AND + 与排序分页叠加与全量过滤金标准一致,total 为筛选后计数单测:构造已知数据集,校验筛选+排序+翻页组合
列筛选校验非法字段路径 / 超限条数或值集合全部 400单测边界用例(17 条筛选、65 个值、非法 path)
筛选缺失语义部分行缺失被筛字段缺失行被过滤,不计入 total单测:构造异构行
实验元信息详情元信息端点响应不含结果行字段单测断言响应 schema

附录

附录 A:业界调研

四个实验跟踪系统在"大量 run × 宽配置字段"结果展示上的设计对比(调研日期 2026-06-12):

维度MLflowW&BOptuna DashboardTensorBoard HParams
分页服务端 token 分页(max_results + page_token)GraphQL cursor 分页(edges/pageInfo)无分页;after-offset 增量拉取 + 服务端 TTL 缓存服务端 offset 分页(start_index + slice_size + total_size)
排序服务端,order_by: "metrics.x DESC"服务端,order: "-summary_metrics.x"前端 DataGrid 客户端排序服务端,ColParams 多键排序
字段投影无,整行全量粗粒度两档:config/summary 整块 all-or-nothing;仅时序 history 支持 keys + 采样无,整 trial 全量逐列 include_in_result,后端裁列,filter/sort/project/paginate 单请求闭环
列发现无端点,前端从已加载 runs 推断 key 并集无端点,前端从 JSON key 推断并集后端预计算 union/intersection search space 随响应下发独立 schema 端点(hparam_infos/metric_infos),数据请求按 schema 构造

@tbl-spec-rdc-03 业界实验跟踪系统结果展示设计对比

来源:MLflow REST APIMLflow Search RunsW&B Public APIW&B Run docsOptuna Dashboard _app.pyOptuna Dashboard _serializer.pyTB HParams api.protoTB HParams http_api.md

与本 spec 的对照

  • 业界主流(MLflow / W&B / Optuna)放弃行级字段投影、整行全量下发——前提是单 run 标量集合仅数 KB,分页/采样已足够控制总量;投影只在真正昂贵的时序数据上提供(W&B history)。本项目结果行携带配置快照与完整结果 JSON,行体积重一个数量级,该前提不成立,故走 TensorBoard HParams 的服务端逐列投影路线,但用平铺 query 参数(fields 列表)替代其 proto + ColParams 组合,降低协议复杂度。
  • 列发现的三档梯度——前端从数据推断(MLflow/W&B)、后端预计算并集(Optuna)、独立 schema 端点(TB HParams)——本 spec 的列模板取第二档的简化变体(单行样本随行形态响应下发),以零维护成本换"异构实验列不全"的已声明局限。
  • 排序全部主流系统在服务端做(Optuna 客户端排序为其已知缺陷来源),与本 spec 一致。

项目内引用:实验数据库页面性能优化 plan(已归档,本 spec 的契约前身)、前端仿真洞察工作台设计规格(产品定位与波次路线)。

附录 B:实现说明

单行全量端点与对比视图迁移(2026-06-12)

  • 端点GET /api/evaluation/experiments/{experiment_id}/results/{result_id}services/api/experiments.py)。按实验 category 分发模型,行形态同款 dict 之上合并 builder 未覆盖的表列(eval 行补 full_result,comm 行补全部输入参数列),实现"全字段"语义;result_rank 置 null(排名是列表上下文概念)。实验或结果行不存在、result_id 不属于该实验均 404。
  • 对比视图绑定迁移:对比身份从 taskId 改为结果行主键(ComparisonEntry.resultId)。修复两个问题:跨页/未加载行无法对比;搜索类任务一个 task 多结果行时旧路径经 /tasks/{id}/results 按 task_id .first() 取任意行。ComparisonView 按 entry 逐一调用单行全量端点(并发拉取、失败条目以横幅告知不静默丢弃),不再读取标签页缓存。
  • Pareto 联动:列形态投影字段白名单增加 id(真实表列),PARETO_FIELDS 增投影 id,brush 框选后携带 result_id 进对比。
  • 测试tests/services/test_experiments_projection.py::TestSingleResultRow 覆盖 eval 行 full_result 嵌套字段、comm 行全列、404 三式(缺行/缺实验/跨实验越权)、投影 id 字段。

附录 C:完整接口签名 / 数据结构

行形态

GET /api/evaluation/experiments/{experiment_id}/results
?page=<int, ≥1>
&page_size=<int, 1..500>
&sort_by=<字段路径 | 标量列名, 可选>
&sort_order=<asc | desc>
&filters=<JSON 数组, 可选, 1..16 条>
// 每条: { "field": <字段路径 | 标量列名>, "op": "in", "values": [<标量值>, ... ≤64 个] }
// 多条之间 AND;field 校验同 sort_by;超限 / 非法 field / 未知 op → 400
→ 200 {
results: ResultRow[], // 标量指标 + config_snapshot,不含完整结果 JSON 明细
total: int,
page: int,
page_size: int,
column_template: ResultRow | null
}
→ 400 非法 sort_by / sort_order / page_size
→ 404 实验不存在

列形态

GET /api/evaluation/experiments/{experiment_id}/metrics
?fields=<字段路径列表, 逗号分隔, 1..64 个>
→ 200 {
fields: string[], // 按请求顺序回显
rows: (number|string|null)[][], // 行数 = 实验全量行数, 列序与 fields 对齐
total: int
}
→ 400 非法字段路径 / 字段数越界
→ 404 实验不存在

单行全量

GET /api/evaluation/experiments/{experiment_id}/results/{result_id}
→ 200 ResultRow & { full_result: object } // 全字段
→ 404 实验或结果不存在

实验元信息

GET /api/evaluation/experiments/{experiment_id}
→ 200 { id, name, description, category, total_tasks, created_at, updated_at }

字段路径文法

field_path  = scalar_name | domain ":" json_path
domain = "config" | "result"
json_path = segment ("." segment)*
segment = [A-Za-z0-9_]+
约束: domain 须命中域白名单; 剥离 domain 后的 json_path 匹配 ^[A-Za-z0-9_.]+$;
scalar_name 须在服务端排序白名单; 任一段不通过整个请求 400