首页
/ A2UI Atom 格式优化 Run 020 复盘:自动省略 null 属性为何被正确否决

A2UI Atom 格式优化 Run 020 复盘:自动省略 null 属性为何被正确否决

2026-09-13 11:50:46作者:温玫谨Lighthearted

这篇技术复盘基于 A2UI 仓库中一次真实的迭代优化实验记录(Run 020),完整还原"在 Atom 编译器中自动省略默认 null 属性"这一假设从提出、打补丁、触发 pytest 契约失败到按守护规则回滚(Backtrack)的全过程。读完本文,你将理解 A2UI 推理格式(inference format)的迭代优化流水线如何运转、单元测试如何充当编译器规格契约的守门人,以及"正确性守护规则"如何在效率优化的诱惑面前强制回滚破坏性变更。

背景:A2UI 的迭代式推理格式优化流水线

A2UI 项目定义了一种基于 JSON 的声明式 UI 协议,Agent 侧 SDK 负责把模型输出编译成合法的上行/下行载荷。除了原始的 direct_json 方式外,仓库在 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/atom/ 下实验了更紧凑的 atom 推理格式:模型以 S 表达式(S-expression)描述 UI 树,例如:

(Card (Text :text $title :count 42 :ratio 3.14 :visible true :disabled false :extra null))

AtomCompiler 负责把这种语法编译为 createSurface.components 结构的 A2UI 载荷。由于"格式越紧凑,模型输出 token 越少",团队围绕该格式建立了一条自动化迭代优化流水线:Agent 提出最小化假设 → 修改 prompt 或编译器代码 → 跑 pytest 与评测子集 → 按量化决策规则决定 KEEP 或 REVERT → 将运行归档进 history 目录

该流水线的操作规程记录在 agent_instructions.md 中,其核心循环包括:

  1. 分析历史并立假设:先读 history_summary.md 主索引,严禁重试已被回滚过的假设;
  2. 实现并验证:修改 prompt/编译器后必须先跑 uv run pytest agent_sdks/python/a2ui_agent/tests/
  3. 跑评测:用 optimize_format.py --format atom --model <model> 在 5 条验证 prompt 子集上产出指标;
  4. 对比基线:用 compare_results.py 对比 eval/iterative_format_optimizer/baselines/<format>/ 基线;
  5. 决策与归档:按规则保留或回滚,再用 --archive 把 report、results 与 patch.diff 归档到 eval/iterative_format_optimizer/history/<format>/ 下。

Run 020 就是这条流水线产出的其中一次实验记录,其完整档案保存在 run_020 目录 中,包含三个文件:report.md(本报告主体)、patch.diff(被否决的代码补丁)和 run_meta.json(运行元数据)。

Run 020 的假设:在 _compile_component 中自动省略默认属性

本次实验的假设记录在 run_meta.json 中:

hypothesis: Streamline default attribute Omissions in AtomCompiler._compile_component (auto-omitting default boolean/null values).

即:在 AtomCompiler._compile_component 组件编译阶段,把"非 schema 必填、且值为 null(或字符串 "null")"的可选属性从编译产物中自动删除,期望借此精简生成的组件字典。评估使用的模型为 google/gemini-3.5-flash

对应的补丁(patch.diff)改动 compiler.py 第 703 行附近的严格 schema 校验逻辑,在原有"必填属性检查"之前插入自动省略循环:

-        # Strict schema validation: required properties and enum constraints
+        # Auto-omit null optional properties not required by schema
         if hasattr(self.schema_helper, "get_component_required"):
             req_props = self.schema_helper.get_component_required(comp_type)
+            for k, v in list(comp_dict.items()):
+                if k not in ("id", "component") and k not in req_props:
+                    if v is None or v == "null":
+                        del comp_dict[k]
             for req in req_props:
                 if req not in ("id", "component") and req not in comp_dict:
                     if req == "children" and "template" in comp_dict:

从源码结构看,这段插入位置紧跟组件字典清理逻辑之后、必填属性补齐之前,意图是:凡不属于 id/component 保留键、又不属于 schema required 列表、且值显式为 None"null" 的属性,一律从 comp_dict 中剔除。逻辑本身只有一小段,风险却集中在一处——它改变了编译器的既有输出契约,而该契约已被单元测试固化。

被打破的契约::extra null 必须显式映射为 {"extra": None}

实验在 pytest 阶段即被拦截。报告(report.md)记录的失败信息是:

FAILED agent_sdks/python/a2ui_agent/tests/test_atom_format.py::TestAtomFormat::test_compiler_primitives_and_relative_paths
KeyError: 'extra'
self.assertEqual(txt["extra"], None)

这条断言出自 test_atom_format.py 中的 test_compiler_primitives_and_relative_paths 测试。该测试编译一段包含布尔、数值与 null 字面量的 S 表达式:

def test_compiler_primitives_and_relative_paths(self):
    """Test compilation of boolean, null, number literals and relative path bindings."""
    text = (
        "(Card (Text :text $title :count 42 :ratio 3.14 :visible true :disabled"
        " false :extra null))"
    )
    compiled = self.compiler.compile(text)
    comps = compiled["createSurface"]["components"]
    txt = next(c for c in comps if c["component"] == "Text")
    self.assertEqual(txt["text"], {"path": "/title"})
    self.assertEqual(txt["count"], 42)
    self.assertEqual(txt["ratio"], 3.14)
    self.assertEqual(txt["visible"], True)
    self.assertEqual(txt["disabled"], False)
    self.assertEqual(txt["extra"], None)

测试的关键在于最后一行:S 表达式中显式写出的 :extra null,编译器必须把它保留为组件字典里的 {"extra": None} 键值对,而不是丢弃该键。也就是说,"显式给出的 null 属性"与"从未出现的可选属性"在 atom 格式契约里是两种不同状态,编译器有义务忠实传递前者。补丁中的自动省略循环把 :extra null 一并删除后,字典中不再有 extra 键,txt["extra"] 随即抛出 KeyError: 'extra'——这正是报告中记录的报错。

换句话说,单元测试在这里承担了编译器规格(specification)的角色:它锁定了 atom 格式"字面量保真"的行为契约。Run 020 的失败并非测试过时或误报,而是补丁行为与既定契约直接冲突。

报告的指标表:基线全绿,当前实验判为 N/A

report.md 的 Summary Table 完整继承了该次实验的量化对比,值得逐行解读:

Metric Baseline Current Diff
Pytest Conformance PASS FAIL -
Overall Pass Rate 100.0% N/A -
Algorithmic Schema Pass Rate 100.0% N/A -

三行指标传递了三层信息:

  1. Pytest Conformance 从 PASS 变为 FAIL:这是唯一的硬性失败点。按 scoring_model.md 的第一条正确性守护规则,单元测试符合性必须是 100% PASS,任何失败都触发强制回滚。
  2. Overall / Schema Pass Rate 记为 N/A:由于单测先行失败,评测子集没有产生可信的端到端指标,报告如实标注 N/A 而非给出数字——这体现了"不拿失败实验的指标去污染基线"的归档纪律。
  3. 报告结论(原文):"Auto-omitting optional null properties broke existing compiler specification contract. Reverted change immediately."(自动省略可选 null 属性破坏了既有编译器规格契约,已立即回滚。)

为何必须立即回滚:决策规则中的"Parser/Compiler 回归"分类

Run 020 的回滚判定并不是临场发挥,而是流水线预置规则的直接应用。agent_instructions.md 在"Step 2:实现并验证代码"一节中,把单测失败细分为三类:

  • Prompt-Only Regression:只改了 prompt/模板却导致单测失败 → 必须立即回滚 prompt 修改;
  • Parser/Compiler Regression:改动破坏了既有合法编译行为 → 必须修复编译器代码或回滚
  • Format Capability Evolution:有意为支持新语法而改动,导致旧测试失败 → 必须同步更新 agent_sdks/python/a2ui_agent/tests/{format}/ 下的单元测试以覆盖新能力。

Run 020 明确落在第二类:run_meta.json 的 notes 字段写得很清楚——

"Pytest failed: test_compiler_primitives_and_relative_paths expects :extra null to explicitly map to {"extra": None} in compiled component dict. Auto-omitting null properties broke existing compiler specification contract. Reverted per Rule 1 / Parser Regression rule."

这与 scoring_model.md 的量化守护体系一致。该文档把决策分为三层:

  1. 正确性守护(不可谈判):pytest 必须 100% 通过;SchemaAcc(算法 schema 通过率)与 QualityScore(模型评分语义质量)不得低于基线;
  2. 效率上限(不可谈判的 REVERT 触发器):即使准确率不降,只要 Median Code Output Tok 增加 > 5%、流式输出时间增加 > 10%、或推理 token 增加 > 15%,就必须回滚;
  3. 综合得分 S_opt

Sopt=0.50SchemaAcc+0.30QualityScore0.15CodeTokBaseCodeTok0.05ReasonTokBaseReasonTok0.03InputTokBaseInputTokS_{\text{opt}} = 0.50 \cdot \text{SchemaAcc} + 0.30 \cdot \text{QualityScore} - 0.15 \cdot \frac{\text{CodeTok}}{\text{BaseCodeTok}} - 0.05 \cdot \frac{\text{ReasonTok}}{\text{BaseReasonTok}} - 0.03 \cdot \frac{\text{InputTok}}{\text{BaseInputTok}}

只有当 Sopt(Current)>Sopt(Baseline)S_{opt}(\text{Current}) > S_{opt}(\text{Baseline}) 时变更才被保留(KEEP),否则执行 --revert。Run 020 在第一层就出局,第二、三层指标根本没有进入比较——这也解释了报告表中 Current 列为何是 N/A。

值得注意的是 run_meta.json 中 metrics 段的 total_samples: 6 与各项 0 值:评测框架仍然登记了样本规模,但所有指标因回滚而归零记账,避免把失败运行的数字误读为性能数据。

回滚后的状态:主历史表中的 Backtracked 标记

每次实验无论保留与否都会归档,并同步进 history_summary.md 主索引。该表的 atom 格式行中,Run 020 的条目为:

| atom | 020 | google/gemini-3.5-flash | Unbounded | Streamline default attribute Omissions in AtomCompiler._compile_component (auto-omitting default boolean/null values). | PASS | 100.0% | 100.0% | 0.00s | 0 | 0 | Backtracked | Pytest failed: test_compiler_primitives_and_relative_paths expects :extra null to explicitly map to {"extra": None} in compiled component dict. Auto-omitting null properties broke existing compiler specification contract. Reverted per Rule 1 / Parser Regression rule. |

主表记录的 Overall Acc 与 Algo Acc 均回到 100.0% 基线水平,Status 标为 Backtracked,Notes 与 report.md 的失败原因完全一致。从主表纵向看,这种"尝试省略/精简 → 触发守护规则 → 回滚"并非孤例:Run 017(关键字别名解析误伤标准键值对)、Run 023(自动加引号导致输出 token 增加)、Run 028(简短签名指令导致代码 token 膨胀 40% 超过 5% 上限)等实验都以类似方式被否决。可以推断,这套流水线的设计哲学是:宁可放弃一次看似合理的优化,也不允许绕过守护规则——回滚本身就是流水线的正常输出,而非异常。

可复现的验证路径

仓库保留了本次实验的完整证据链,任何读者都可以按以下路径复核(均为仓库内只读操作):

  1. 查看被否决的补丁patch.diff 给出了 compiler.py 第 703 行前后的精确 diff;
  2. 定位契约测试test_atom_format.pytest_compiler_primitives_and_relative_paths,运行 uv run pytest agent_sdks/python/a2ui_agent/tests/test_atom_format.py 即可验证当前编译器仍保持 {"extra": None} 的显式映射行为;
  3. 理解格式语义:atom 提案的设计说明见 a2ui_atom.md,可对照 S 表达式与 A2UI 载荷的映射关系;
  4. 使用统一 CLI 快速试验:流水线提供了 optimize_format.py 统一入口,例如
    # 测试一条 S 表达式的编译
    uv run python eval/iterative_format_optimizer/skills/inference-format-optimizer/scripts/optimize_format.py --format atom --compile "(Card (Text \"Hi\"))"
    # 对比当前运行与基线
    uv run python eval/iterative_format_optimizer/skills/inference-format-optimizer/scripts/compare_results.py --baseline eval/iterative_format_optimizer/baselines/atom/unbounded_run_meta.json <current_run_dir>
    
    该脚本同时支持 --revert(回滚)与 --archive(原子归档),规程上禁止手写 mkdir/cp/git diff 手工归档,以保证历史目录结构一致。

从 Run 020 得到的三点工程启示

  1. 单元测试是编译器规格的活契约:extra null{"extra": None} 这一行断言看似琐碎,却把"显式 null 必须保真"这一格式语义固化下来。对 LLM 输出编译器而言,这类保真契约直接影响下游渲染端能否区分"用户显式置空"与"属性不存在",任何以 token 节省为名的自动省略都不应越过它。
  2. 守护规则让"失败实验"成为一等公民。Run 020 没有产生任何保留的改动,但它同时产出了 diff、失败 trace、回滚理由与主表记录。后续 Agent 读到 Backtracked 状态和 Anti-Repetition 约束后,不会再重提"自动省略默认属性"假设——失败记忆被制度化了。
  3. 正确性、效率、综合分是三道独立闸门。即使某个补丁让输出 token 下降,只要触碰第一道闸门(单测/schema/质量回归)就直接否决;效率上限(5%/10%/15%)则防止"准确不变但更贵"的伪优化。Run 020 恰好是"在第一道闸门被拦截"的教科书案例。

对希望复用这套方法的团队而言,Run 020 档案的价值不在它优化了什么,而在于它完整演示了一个假设如何被提出、被量化验证、被规格契约否决、被规则化归档——这正是 A2UI 推理格式能够在无人值守的 Agent 流水线中持续迭代而不劣化的机制基础。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.21 K
2.81 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
945
1.86 K
docsdocs
暂无描述
Markdown
906
5.84 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
537
607
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
864
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
4.28 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.39 K
1.48 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
550
401
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.19 K
347