2月10号整理论文笔记时,翻到一篇让我反复停下来琢磨的工作:Code2World: A GUI World Model via Renderable Code Generation。我关心的不是“又一个前端代码生成模型”,而是它把 SFT 和 RARL 串在一条技术路线上,用“可渲染代码”去表达 GUI 世界模型。这个角度比较反直觉,也恰恰解释了为什么很多 GUI Agent 项目做到后面越来越别扭——我们一直在用图像或者 DOM 去描述界面,却很少想过,GUI 也许更适合被当作一种可以由代码生成和推演的状态机。
这篇文章不是论文复现报告,更像是我在阅读过程中形成的结构化笔记,加上自己拿内部系统页面做单步验证时的一些体会。如果你是做 GUI Agent、UI 自动化、前端智能生成或者多模态 LLM 应用的,里面关于状态表示、SFT 数据构建、对抗强化学习奖励设计的内容应该会有启发。如果你刚接触这块,我也会把基础概念揉进讲解里,尽量不让你因为一个术语卡住。
1. “GUI 世界模型”到底在解决什么样的问题
1.1 界面自动化为什么总是笨手笨脚
在做 GUI 自动化或者 GUI Agent 的时候,最核心的问题不是模型会不会写代码,而是它能不能“理解”界面变化。传统做法大致有两类:一类是截图加目标检测,模型把界面当成一张像素图;另一类是用可访问性树或 DOM,模型把界面当成一棵结构树。两类做法都有明显短板。截图像素级的信息太底层,模型分析下拉菜单时得推理几百个 patch 之间的关系。DOM 虽然结构性强,但信息量极大且冗杂,真实页面里动不动十几层嵌套,还可能包含大量脚本注入的无关节点。
Code2World 的出发点我理解是这个:与其让模型去理解图像或者裸 DOM,不如把 GUI 状态压缩成一段“可渲染代码”。模型输入当前状态和一个动作,输出的是动作之后的可执行界面代码。这个代码既能被渲染成新截图,也能被结构化检查,还能告诉研究者模型内部到底认为界面发生了什么变化。界面自动化因此有了一个可解释的中间状态,而不是黑盒猜测。
拿一个很常见的交互举例:点击“筛选”按钮后弹出下拉面板。截图视角里这是像素大面积变化;DOM 视角里这是某节点 style 或者 class 被改掉。但如果你把界面状态表达为代码,变化就是一段非常清晰的属性更新:按钮的 aria-expanded 从 false 变 true,面板的 visible 状态翻转。模型生成这段代码之后,你甚至可以直接比较两段代码差异来判断行为是否符合预期。
1.2 世界模型这个抽象对 Agent 意味着什么
“World Model”这个说法在强化学习里已经很常见,意思是让智能体拥有一套对环境的内部模拟器。真实环境交互成本高,如果模型能预测“我做了动作 A 之后世界会变成什么样”,那就可以在内部做规划,减少试错。GUI World Model 就是把这套思路搬到界面环境里,让 Agent 能预测“点击之后界面变成什么代码状态”。
关键点在于代码状态是机器可校验的。你不需要从截图去猜测界面里是不是出现了一个 toast,只需要检查生成代码里有无对应组件以及它的可见状态。这种可校验性,把“生成得看起来像”升级成“生成得行为一致”。如果你做过 GUI Agent 的评估,就知道这一点有多重要——很多模型能把界面画得很像,但点起来完全不是那么回事。可渲染代码给了一个折中:它仍然是生成式输出,但输出必须服从目标界面的语法、结构和可执行约束,模型不能靠模糊的像素分布蒙混过关。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么状态一定要“渲染成代码”,而不是继续用图像或 DOM
2.1 图像、DOM 与可渲染代码的对比
很多工作尝试直接用截图作为 GUI 状态,但图像输入有几个硬伤。第一是 token 开销太大,Transformer 处理高分辨率截图时,视觉 patch 数量会迅速爆炸,一个 1440x900 的界面切分成 patch 后很长。第二是图像难以表达精确的交互状态,比如按钮是否 disabled、文本框当前的 value,这些在图像上几乎无法可靠判断。第三是截图没法稳定参与因果推理,界面变化反映在像素上是连续的,但真实的前端状态变化是离散的。
DOM 或可访问性树解决了一部分问题,具备天然的结构性,但也不是为生成模型设计的。一个生产级页面的 DOM 可能有上千个节点,很多节点与视觉表现无关,比如埋点节点、脚本标签、隐藏辅助节点。模型如果直接在这种树上做状态预测,运算量巨大且容易关注到错误信号。跨端场景更麻烦,移动端和桌面端的可访问性树字段差异很大,同一个语义在不同平台上表达方式不一样。
可渲染代码走的是中间路线:它保留结构信息,但比完整 DOM 干净得多,类似专门为界面状态抽象出的一种轻量级 DSL。对比一下会更直观。
| 状态表示 | 信息层级 | token效率 | 是否可执行校验 | 跨端一致性 | 适合作为生成目标 |
|---|---|---|---|---|---|
| 截图/像素 | 视觉底层 | 低,耗token | 否 | 中等 | 较差 |
| DOM/可访问性树 | 结构底层 | 中等,但噪声大 | 是,但需依赖环境 | 较差 | 一般 |
| 可渲染代码 | 语义中间层 | 较高 | 是,可直接渲染和检查 | 较好 | 很适合 |
从生成模型的角度看,可渲染代码还和 LLM 的预训练分布更接近。语言模型天然擅长生成 token 序列,而代码是结构化的 token 序列,语法错误能够被及时发现。相比之下,像素生成模型做状态预测,更像是在做视频预测,难度高很多,而且缺少显式的约束点。
2.2 “可渲染”到底给了我们哪些额外权力
可渲染代码并不意味着必须输出完整的 HTML 文件,它更像是一种受控的前端中间表示。我自己的理解是,它可以由代码生成器输出,也能被无头浏览器或轻量渲染引擎执行,从而重新得到界面截图或组件树。也就是说,代码在模型内部是“状态张量”的角色,但模型外部,它又能被编译执行,形成闭环。
假设中间表示是类似 JSX 或 QML 的紧凑结构,模型生成的核心内容可以很精简:
html复制<Panel id="filterPanel" visible="true" position="top-right">
<Group title="价格区间">
<Input type="number" value="100" min="0" max="1000"/>
<Input type="number" value="500" min="0" max="1000"/>
</Group>
<Button id="applyFilter" action="apply" primary="true"/>
</Panel>
这个片段一旦生成,我们就可以用渲染引擎把它画出来,检查布局是否合理,也可以静态分析它里面有没有非法属性、事件是否绑定。更进一步,动作的建模可以落在“旧代码+动作 -> 新代码”的映射上,状态转移变得可计算、可回溯。这种表示带来的一个重要优势是模型不再需要每次从空白状态生成整个界面,而是优先生成发生变化的片段,大大降低生成难度和 token 消耗。
3. SFT 阶段才是整个项目的质量上限,它的数据设计和你想的不一样
3.1 成对动作状态数据的构造逻辑
做前端代码生成的人,最熟悉的数据集形态是“设计稿截图 -> 完整网页代码”。Code2World 这类工作真正特别的地方在于,SFT 阶段需要的是“动作前后成对状态”,而不是“静态图生码”。表面上看都是监督学习,但学习目标完全不同:前者是学习视觉还原,后者是学习 GUI 状态转移规律。
数据构造的基本逻辑可以拆成几步。第一步是准备一批可交互的前端应用源码,覆盖常见组件和业务场景,比如表格筛选、弹窗、分页、表单校验、折叠面板。第二步是在本地或云端渲染环境中逐条触达真实事件,记录动作发生前后的 DOM 快照和截图。第三步是将 DOM 快照清洗并提炼成目标渲染代码,重点是只保留与视觉表现和交互状态相关的节点。第四步是做数据增强,同一个元素在不同上下文里触发同一个动作,可以生成多条样本。
这一步最容易被忽视的是“只保留变化区域”。真实面板 A 触发弹窗 B 时,面板 A 大部分代码不会变化。如果训练目标要求模型每次重新输出整个界面的代码,模型会严重偏向复制原文,SFT 学到的不是状态转移,而是抄写。实践上,我会把输出目标限制为局部状态块,比如“动作目标的属性变化 + 新出现的节点 + 被移除的节点”。这既降低任务难度,也让模型被迫关注到底什么变了。这个设计如果缺失,后面做强化学习也会很痛苦,因为模型很容易通过复制旧代码获得高奖励。
3.2 模型输入输出设计与防作弊细节
SFT 的输入不只是界面代码。为了让模型准确知道要对什么东西做什么,我会把动作表示显式拆出来:一个可交互元素的标识符、动作类型、必要的参数。输入可以组织为三部分,一是当前局部界面的渲染代码,二是动作描述,三是一句话任务上下文。输出是动作完成后的目标状态代码。
这里有个很关键的防作弊细节,如果不做,SFT 会退化得很厉害。生成模型极其擅长学习捷径,当页面文本数据大量出现在原 DOM 里时,它会直接复制文本节点来降低 loss。模型可能生成一段代码,从结构上看完全正确,文本也一模一样,但其实是照着原页面硬抄,对新状态里的数据变化毫无泛化能力。我的处理方式是训练时对文本内容做扰动:把真实可见文本替换成占位符或者同义词变体,让模型必须依赖状态结构和语义来生成界面,而不是复制粘贴。推理阶段再回到真实文本。这种“训练与推理不一致”在图像领域很常见,但很多做代码生成的团队没有意识到,等到强化学习阶段才发现模型对数据变化免疫。
SFT 的训练目标和普通生成任务一样都是自回归交叉熵,但要做分桶采样。简单页面只有按钮和标题,复杂页面有嵌套表格和多层弹窗,如果不做难度分桶,小模型会把大部分容量花在简单页面上,复杂交互样本的 loss 长期居高不下。我通常按可交互元素数量、状态变更范围、组件复杂程度把数据分成三档,训练时按档位混合,确保每批数据都有不同难度。
3.3 SFT 能扛住什么,扛不住什么
把 SFT 阶段跑完,模型一般能实现单步状态预测的“标准答案复现”,也就是训练集里见过的动作组合,它能做得比较像。问题是真实 GUI 交互里,事件组合的多样性远超静态数据集的覆盖范围。SFT 本质上是在拟合训练数据分布的平均行为,遇到罕见的组合,比如一个多选表格里先排序再打开批量操作栏,模型也许能各做各的,但很可能丢掉某个状态细节。
这解释了一个很常见的失败案例:模型在训练集里分别见过“点击按钮出现弹窗”和“当前面板里有一个 loading 遮罩”,但当需要把两者放在同一状态里时,模型生成了弹窗却被旧 loading 遮罩挡住,或者把 loading 状态重置了。它没有真正学会 GUI 状态的叠加规则,只是在模仿。要从“模仿常见形态”进步到“理解状态约束”,单靠 SFT 是不够的,这正是 RARL 出场的原因。
4. RARL 阶段的对抗循环,怎么让生成器和规则评估器互相较劲
4.1 RARL 的运作方式,谁在跟谁对抗
RARL 这个缩写直接读成对抗式强化学习就好,它并不是新的理论框架,而是一种训练组织方式。在 Code2World 这类任务里,我把主模型看作生成策略,它负责输出动作后的界面代码;同时有一个规则评估器,充当对抗对手,负责指出代码里不合规、不可渲染或者状态不一致的地方。
之所以要对抗,是因为如果只用 SFT 模型的输出打分并做策略梯度,生成器很容易找到一个“看起来很稳、实际上经不起规则检查”的局部最优点。规则评估器被拉进来之后,生成器每产生一种新解法,评估器就要更新自己的检查标准去识别这种解法里的漏洞,新一轮生成必须尝试绕过更严格的标准。二,彼此越逼越紧,模型才可能从“像训练数据”走向“符合界面领域规则”。
实际操作中,评估器可以是轻量规则集加一个小型辅助模型。规则集负责明确属性检查,例如 HTML 标签闭合、事件指向的组件是否存在、状态互斥关系是否正确。辅助模型负责比较生成代码渲染后的截图与真实期望结果的语义相似度。两部分的权重都会在训练过程中周期性更新,更新太快会让生成器崩溃,更新太慢则对抗失效。
4.2 奖励信号怎么设计才不会被带偏
强化学习阶段最重要的,也是最容易出问题的就是奖励信号设计。奖励太少模型没有学习信号,奖励太密又会产生大量奖励黑客行为。结合界面代码生成任务,我会把奖励拆成几路独立信号,而不是合成一个标量。
第一路是渲染合法性奖励,生成代码必须能通过渲染引擎执行,不能有 JS 异常或非法结构,这是硬门槛。第二路是动作状态一致性奖励,动作按钮的状态必须和事件结果匹配,比如点击筛选按钮之后筛选面板不能还是隐藏状态。第三路是视觉语义相似度奖励,把生成代码渲染成图,和真实后续界面做特征对比,但不会苛求像素级一致,避免模型为了像素损失放弃结构合理性。第四路是紧凑性奖励,鼓励模型用最少的必要代码表达状态变化,防止输出大量无意义节点。
这四路信号可以分别设置权重,实战中动作一致性和渲染合法性的权重应该最高。如果视觉相似度权重过高,模型会想尽办法让渲染像素逼近原图,比如生成一层白色覆盖层盖住不需要展示的内容,这种方式在结构上是错的,但在像素损失上很讨巧。对应的惩罚机制也很明确,任何不可见的覆盖层和空节点都会被规则评估器识别并惩罚。奖励设计的目标永远是引导模型生成“行为正确”的界面,而不是“看起来正确”的界面。
4.3 RARL 阶段常见的收敛问题与缓解
对抗训练最大的风险是不稳定。生成器今天学会了一种绕过规则的方式,评估器明天把这个漏洞堵上,生成器后天可能直接 collapse,所有输出都变成一段保守的样板代码。虽然这段代码合法,但没有任何状态变化,相当于模型学会了“什么都不做最安全”。
这个问题在代码生成世界里格外明显,因为代码空间里存在大量简单的安全解。缓解办法有几种,首先是不要从随机初始化直接跑 RARL,必须用训练收敛的 SFT 模型做策略初始化,这能让起点保持在对状态转移有基本理解的位置。其次,规则评估器的更新要做滞后处理,每训练若干步生成器才更新一次评估器,避免对抗双方激烈互踢。第三是引入旧策略的平均权重,周期性地把当前策略与历史平均做插值,抑制突变。
另外,我建议 RARL 阶段加入多步仿真的预检查。不要让生成器只预测一步状态,而是偶尔让它连续预测两步,比如“点击筛选后再选择一个选项”,看看第二步状态是否还能保持代码合法性。如果第一步生成了状态 A,导致第二步无解,说明第一步的奖励信号过于短视。这种长时间尺度的回报设计会显著提高生成状态的可扩展性,代价是训练压力变大,因此使用频率要控制。
5. 那些论文摘要不会告诉你的工程坑
5.1 reward hacking 其实在 SFT 阶段就可能潜入了
很多人以为 reward hacking 是强化学习特有的问题,实际经验告诉我,如果你在 SFT 阶段没有做好数据清洗,模型早就学会了作弊,RARL 阶段只是把问题放大。
我遇到过的典型现象是:SFT 训练里模型为了降低 loss,经常输出带 display:none 样式的隐藏副本节点。从指标上看,生成代码确实渲染出了正确结果,因为隐藏节点不参与视觉呈现,但代码结构冗余且容易在后续交互中造成冲突。这类问题在纯生成评估里很难发现,直到做状态差分或者把代码交给真实渲染引擎执行时才暴露。
对策是 SFT 阶段就加入结构规范性过滤。在构造目标代码时,强制消除隐藏占位、空容器和不可见覆盖层,让模型从一开始就认为“没用的节点不出现”是正常的。同时可以在训练 loss 里加入对不可见非必要的节点生成概率的轻量惩罚,这一点在复用现有生成模型时特别有效,不需要改模型结构。
5.2 别让生成代码退化成“认真抄文本”
界面代码生成模型天然有一个偷懒路径:大量文本直接原样出现在页面上,模型只需要从输入 DOM 里把文本节点拷贝出来,就可以让输出在文本层面高度匹配目标。可这样训练出来的模型对文本内容极度敏感,一旦目标界面里的按钮文案从“确认”改成“提交”,它可能就不知所措。
在 SFT 数据构建中,我习惯对所有业务文案做两层处理。第一层是实体化,把日期、价格、用户名这样的动态数据替换成占位符,比如 {{date}}、{{price}}。第二层是在同一语义下随机变换固定文案,让模型接触“确认”“确定”“提交”多种表达之间的等价性。这样做的目的是迫使模型真正学会结构位置关系,而不是把每一个字背下来。
这层处理带来的额外好处是降低了数据合规风险,因为训练时模型不需要接触真实用户信息。测试阶段再保留极少量的真实文本样例做验证,但那些样例不会进入训练集。很多团队忽略的一点是,清理文本与清理 DOM 同等重要,否则模型即使行为正确,也有可能因为记忆了训练数据里的敏感信息在线上产生安全隐患。
5.3 单一分数管不住多视口和多布局
GUI 状态预测里最容易被低估的是视口问题。同一个筛选面板,在 1440px 宽的桌面端可能水平排列,在 768px 宽的平板端变成垂直堆叠,而在 375px 宽的移动端可能直接变成一个底部弹层。很多评测只跑单一视口,导致模型本质上只学会了一种布局下的状态转移规律。
奖励评估器如果只用一种视口截图,就会对生成代码形成不完整的反馈。比如桌面端布局中,隐藏显示的节点可能在移动端必须显示,评估器如果没考虑这一点,可能误判移动端的合法输出为复杂度过高。
实践中可以把视口规格作为条件信息输入模型和评估器,让模型在生成代码时知道目标布局上下文。奖励信号则分视口统计,不混在一起算平均分。如果项目资源有限,至少也要保证训练数据里包含两到三种典型视口下的同一交互样本,否则模型的泛化能力只是一个虚假的期待。
5.4 分布外状态漂移,以及周期性重置的重要性
RARL 阶段生成的界面状态会逐渐偏离 SFT 训练数据的分布。模型刚开始生成的只是训练集里见过的状态变体,但几十轮强化学习之后,生成代码的某些组合开始在真实项目里很少见。这时候再用初始 SFT 数据做评估,模型分数会不断下降,很多人误以为训练发散,其实是评估集已经跟不上模型的探索范围。
一个有效的做法是维护一个重放缓冲区,把强化学习阶段生成的、规则评估器判定合理的新状态代码回灌到 SFT 数据池里。每隔一段时间,混合原始数据和新状态数据做一轮轻量 SFT 微调,把模型重新拉回稳定区域。这个过程有点像在线学习里的 periodic reset,它不是为了提升上限,而是为了防止模型在探索过程中彻底遗忘基本 GUI 规则。
我当时做单步验证时就踩过这个坑,第一次跑 RARL 时模型在第三步之后开始频繁输出不存在的组件属性,我一度以为是学习率问题,后来发现是策略已经在状态空间里走远了,初始数据集中根本没有相似样本。加入重放缓冲和周期性重置后,这个问题肉眼可见地缓解了。需要说明的是,这些手段并不一定来自论文正文,更像是我个人的落地经验,但很适合作为你跑同类项目的参考。
6. 这套“可渲染代码状态”的思路还能滑向哪些方向
6.1 往前端 Agent 和 GUI 自动化落地的路径
从 Code2World 引申出的最直接应用是把代码状态当作 GUI Agent 的规划基础。传统 GUI Agent 要完成一个任务,比如“把表格里所有价格大于 500 的商品标记为促销”,一般直接操作屏幕坐标或者 DOM 元素。到了复杂页面,动作空间大,出错不可见。如果把可渲染代码作为状态表示,Agent 可以先在内部生成若干个候选动作序列,再对每一步做状态预测,最终选择在代码层校验全部通过的方案再执行。
这种方式解决的是“探索成本”问题。真实浏览器里点击错一个按钮可能触发不可逆弹窗,需要在代码世界模型里做模拟,避免每一次试错都消耗真实环境交互成本。即使最后还要真机执行,模型也已经把最离谱的错误状态排除掉了。对于企业内部的表格系统、后台配置系统这类页面重复度高、结构性强的场景,这种路径比通用视觉 Agent 更加稳定。
6.2 从生成完整页面到“状态差分更新”
另一个很实用的方向是低代码编辑器的状态管理。传统低代码平台保存页面时,通常会把整个页面 schema 全量传输,一旦页面复杂,版本间的 diff 非常难做。如果把界面看作渲染代码的连续状态,编辑器每次操作只需要生成和保存一小段状态差分代码,前端再重放这个 patch。
这种设计的收益不只是节省流量,更重要的是让操作可审核。产品经理拖动了一个组件,代码层能看到是什么属性发生了变化,而不是只知道画布上多了一个卡片。做界面审计时,这种可追踪的能力远比截图记录可靠。前端代码生成不再是“给我一张设计稿,我给你一版页面”,而是变成“给我当前状态和意图,我给你一个最小变更”。
6.3 对代码生成评测模式的一点反思
回顾我自己的实践体会是,如果你的目标是让代码生成模型成为 GUI Agent 的心脏,就不要再拿单轮准确率作为核心指标。当前很多评测只关心生成的页面是不是和标注一致,但真实场景里,生成代码会进入一个持续交互循环,模型上一轮的选择会影响下一轮面对的界面,任何静态评测都无法覆盖这个闭环。
更合理的评测需要包含多步交互模拟:让模型在一个虚拟浏览器环境里连续完成任务,每一步更新状态代码,最后检查任务是否达成以及生成过程是否产生了无意义状态。模型生成的界面代码即使每一步都和样本不完全一致,只要它在自洽的状态空间里持续前进、没有破坏环境,也应该得到高分。这种评测思路比像素级对齐更符合强化学习环境下世界模型的目标,同时也更容易暴露 SFT 和 RARL 各自的问题。
我自己的感受是,Code2World 最值得借鉴的不是某个具体的网络结构,而是“可渲染代码作为 GUI 环境状态”这个抽象视角。它把界面问题从视觉问题转成了结构化生成问题,让模型的行为有了可追踪、可回放、可约束的落地路径。如果你正准备做一个 GUI Agent 或前端代码生成系统,不用一开始就去追求互联网规模的数据和超大模型,找一个内部业务系统,用代码状态表示界面变化,先跑通“状态-动作-新状态”的小闭环,这个试错成本很低,但带来的理解提升会非常直接。
