A2UI Atom 格式优化 Run 020 复盘:自动省略 null 属性为何被正确否决
这篇技术复盘基于 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 中,其核心循环包括:
- 分析历史并立假设:先读 history_summary.md 主索引,严禁重试已被回滚过的假设;
- 实现并验证:修改 prompt/编译器后必须先跑
uv run pytest agent_sdks/python/a2ui_agent/tests/; - 跑评测:用
optimize_format.py --format atom --model <model>在 5 条验证 prompt 子集上产出指标; - 对比基线:用
compare_results.py对比eval/iterative_format_optimizer/baselines/<format>/基线; - 决策与归档:按规则保留或回滚,再用
--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 | - |
三行指标传递了三层信息:
- Pytest Conformance 从 PASS 变为 FAIL:这是唯一的硬性失败点。按 scoring_model.md 的第一条正确性守护规则,单元测试符合性必须是 100% PASS,任何失败都触发强制回滚。
- Overall / Schema Pass Rate 记为 N/A:由于单测先行失败,评测子集没有产生可信的端到端指标,报告如实标注 N/A 而非给出数字——这体现了"不拿失败实验的指标去污染基线"的归档纪律。
- 报告结论(原文):"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 nullto 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 的量化守护体系一致。该文档把决策分为三层:
- 正确性守护(不可谈判):pytest 必须 100% 通过;
SchemaAcc(算法 schema 通过率)与QualityScore(模型评分语义质量)不得低于基线; - 效率上限(不可谈判的 REVERT 触发器):即使准确率不降,只要
Median Code Output Tok增加 > 5%、流式输出时间增加 > 10%、或推理 token 增加 > 15%,就必须回滚; - 综合得分 S_opt:
只有当 时变更才被保留(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% 上限)等实验都以类似方式被否决。可以推断,这套流水线的设计哲学是:宁可放弃一次看似合理的优化,也不允许绕过守护规则——回滚本身就是流水线的正常输出,而非异常。
可复现的验证路径
仓库保留了本次实验的完整证据链,任何读者都可以按以下路径复核(均为仓库内只读操作):
- 查看被否决的补丁:patch.diff 给出了
compiler.py第 703 行前后的精确 diff; - 定位契约测试:test_atom_format.py 的
test_compiler_primitives_and_relative_paths,运行uv run pytest agent_sdks/python/a2ui_agent/tests/test_atom_format.py即可验证当前编译器仍保持{"extra": None}的显式映射行为; - 理解格式语义:atom 提案的设计说明见 a2ui_atom.md,可对照 S 表达式与 A2UI 载荷的映射关系;
- 使用统一 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 得到的三点工程启示
- 单元测试是编译器规格的活契约。
:extra null→{"extra": None}这一行断言看似琐碎,却把"显式 null 必须保真"这一格式语义固化下来。对 LLM 输出编译器而言,这类保真契约直接影响下游渲染端能否区分"用户显式置空"与"属性不存在",任何以 token 节省为名的自动省略都不应越过它。 - 守护规则让"失败实验"成为一等公民。Run 020 没有产生任何保留的改动,但它同时产出了 diff、失败 trace、回滚理由与主表记录。后续 Agent 读到
Backtracked状态和 Anti-Repetition 约束后,不会再重提"自动省略默认属性"假设——失败记忆被制度化了。 - 正确性、效率、综合分是三道独立闸门。即使某个补丁让输出 token 下降,只要触碰第一道闸门(单测/schema/质量回归)就直接否决;效率上限(5%/10%/15%)则防止"准确不变但更贵"的伪优化。Run 020 恰好是"在第一道闸门被拦截"的教科书案例。
对希望复用这套方法的团队而言,Run 020 档案的价值不在它优化了什么,而在于它完整演示了一个假设如何被提出、被量化验证、被规格契约否决、被规则化归档——这正是 A2UI 推理格式能够在无人值守的 Agent 流水线中持续迭代而不劣化的机制基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.24 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python670
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#230
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python52874
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22545
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java36351