Skip to content

智能体编辑基准测试

Tabula 的整个前提是:一份保持局部性的样式配置档案,能让一个 AI 智能体的编辑比一套依赖传统级联的 CSS 更可靠。 在本项目之前,这个前提在任何地方都没有一项直接的支撑性基准测试(.orchestrator/RESEARCH.md §4,未决问题 1)—— benchmark/ 目录就是为验证它而构建的工具。

本仓库不包含任何结果。 运行下文描述的这套测试装置,并不等同于运行那项研究本身;这里的任何内容都还没有被用来 产出关于任何模型的一个数字。

它衡量什么

按照 SPEC J16 的优先级顺序,共有四个问题:

问题方式
Q1扁平化配置档案是否让普通的智能体编辑比重度依赖级联的 CSS 更可靠?八项编辑任务——悬停状态、间距、圆角、一个新变体、把一段样式在兄弟元素间移动——在两侧分支中定义得完全一致。
Q2className 透传模式是否会泄漏?四项只能从调用现场解出的任务,其中一项是消费方的覆盖值理应故意失败,而智能体必须诊断出原因。
Q3排版 basestrict 的对比(SPEC J5)目前只针对 strict 分支的两项排版任务;base 配置档案对应的部分已在模式中指定,并被记为 todo,取决于一份目前尚不存在的 base 配置档案测试夹具(fixture)。
Q4封闭词汇表 vs. 已注册的例外两项任务的目标取值都没有现成的设计令牌,因此扁平分支必须铸造一个新的(tabula except add)并重建,而级联分支则可以直接写一个字面量。

两个分支——arms/flat(Tabula 的 strict 配置档案,六个组件,一份已提交的 .tabula/)与 arms/cascade(同样六个组件,用传统的全局 CSS 实现)——在剥离 class 属性之后会渲染出字节级完全相同的 DOM (arms/anchors.json,由一次自检来验证);如果这项检查失败,这套测试装置得出的任何比较就都失去了意义, 因为标记上的差异本身就足以解释观察到的任何编辑成功率差异。

什么算作成功

从不是一张截图,也从不是人类的主观判断。 一项任务的成功标准指名一个元素、一个 CSS longhand 属性、 一个可选的伪类条件——以及,关键的一点——一个**指定的(specified)**值,而不是一个计算出来/渲染出来的值:

json
{ "ref": { "bench": "button.solid" }, "property": "background-color",
  "expectedToken": "color.danger", "condition": "hover" }

这个指定值随后由每种范式自己的机制来计算,绝不使用一份共享的重新实现:扁平分支会用真实的 @tabula-css/cli 流水线,从其设计令牌重建注册表,并通过 @tabula-css/mergeresolve() 来作答;级联分支的样式表则用 PostCSS 编译,再由一个专门构建的级联模拟器(已记录、经过子集校验——见 benchmark/DECISIONS.md §3) 按特异性、源码顺序、!important 与继承来计算胜出的声明。

对扁平分支而言,还有另外三道门禁也是"成功"的一部分,因为它们本就是这份配置档案自身契约的一部分: 严格版 ESLint 配置必须保持干净、已提交的 .tabula/ 必须与一次全新构建相符,并且没有任何渲染出的元素 可以携带词汇表之外的一个 class。一次产出了正确像素、却打破了配置档案门禁的编辑,并不算成功——CI 会拒绝它。

每项任务还携带 mustNotChange 不变式(这样一项任务就不能靠一次波及过广、足以破坏别的东西的改动来"通过"), 以及一份 filesInScope 列表——在两个分支中,编辑到这份列表之外都会直接判定任务失败。

运行它

bash
npm run build              # the suite runs against packages' built dist/, not source
npm run test:benchmark      # build + the harness's own self-test suite

node benchmark/dist/run.js list
node benchmark/dist/run.js show q1-01-primary-hover-destructive --arm flat
node benchmark/dist/run.js score q1-01-primary-hover-destructive --arm flat --patch my.json
node benchmark/dist/run.js report results/

score 在补丁通过时退出码为 0,失败时为 1(一种正常的、提供信息的结果),当测试装置本身无法运行时 (一个格式错误的补丁、一个与任务本身无关的构建错误)为 2——与 Tabula 工具链其余部分同样的三态退出码约定。

接入一个智能体

智能体的实际执行有意被排除在本仓库的范围之外。 这里没有任何东西会调用一个模型或读取一个 API key, 自检测试也完全离线运行。这套协议是补丁进、评分出,因此任何能把一段提示词变成一份补丁的测试装置都可以驱动它:

  1. show <task> --arm <arm> --json 会吐出这份数据包:任务说明、每一个作用域内文件的内容,以及该分支自身面向 智能体的上下文——对扁平分支来说,就是生成出的 llms.txt / vocabulary.txt / tokens.resolved.json; 对级联分支来说,就是那份普通的样式表,因为在那种范式里,样式表本身就是文档。两个分支都不会被给予 任何对方的惯用法自然不会提供的额外上下文。
  2. 把这份数据包交给一个智能体,收集一份补丁:要么是 {"format":"files","files":{"<path>":"<full content>"}},要么是一份统一 diff。
  3. score <task> --arm <arm> --patch <file> --out results/ 会写出一份 TaskResult JSON 文件。
  4. report results/ 会把目录里的每一份结果文件聚合成一张定宽的文本表格:
Tabula agent-editing benchmark — aggregate

scored 24   passed 18   failed 6

question                            flat     cascade
─────────────────────────────────  ───────  ───────
Q1 flat vs cascade edit success      7/8      5/8
Q2 className passthrough             3/4      2/4
Q3 typography strictness             2/2       — 
Q4 vocabulary closure                2/2      1/2
─────────────────────────────────  ───────  ───────
all                                 14/16    8/14

failures by kind
    3  cascade-subset-violation
    2  wrong-value
    1  patch-apply

(这张表里的数字只是格式演示——见上文"不包含任何结果"的说明。)

一份针对 Claude Code 无头模式的完整实战流程记录在 benchmark/README.md § Plugging in an agent 中。 对任何一次有意义的比较而言,有两点需要保持固定:给两个分支相同的智能体、预算和尝试次数, 并且永远不要给一个分支添加对方惯用法自然不会提供的额外上下文——show 命令的上下文清单正是严格按这条原则选定的。

安全性

评分过程会执行被打过补丁的那个分支的代码——脱离渲染器就没有办法观察一个渲染出的 DOM,而静态的 class 提取在这里也行不通(variants() 的配置与 cn() 调用会让实际的 class 字符串成为 props 的函数,而不是字面的源码文本)。 补丁是智能体编写的代码;请在容器中运行不受信任的补丁。 每一次评分都在 benchmark/.work/(已被 gitignore)下 针对该分支的一份全新副本运行——arms/ 中已提交的测试夹具永远不会被写入——并且补丁路径在任何写入之前都会被校验 (绝对路径、.. 片段与工作区逃逸全都会被拒绝)。

已知局限

  • 媒体查询被有意关闭。 没有任何任务会涉及一个断点。
  • 单一轴状态。 所有标准都在默认主题下评估;多轴标准在当前的任务模式里还无法表达。
  • 两个分支在某些地方使用不同的属性名padding-inline-start 对比 padding-left)——判定标准是按各分支自身的 惯用法书写的;问出的问题相同,只是拼写不同。
  • Q2 诊断测试夹具的缺陷(一次失败的 className 覆盖)是通过重命名一个绑定、而不是重新排列一次 cn() 调用 来体现的,这是特意让它对 linter 保持不可见——这项任务衡量的是纯粹的诊断能力,而不是这份配置档案自身的工具 本来是否会替你捕捉到这个 bug。

关于每一项被迫做出的设计选择、它击败的替代方案,以及原因——包括级联分支被构造为必须保持在其内部的那个确切的 级联模拟器子集——完整记录见 benchmark/DECISIONS.md

Released under the MIT License.