GPT-6 的 Intelligent UI 到底怎么工作的:逆向拆开是 DIL 语言、服务端编译和沙箱运行时——回答从只能读的一段文字变成拖一下滑块就重算的界面

OpenAI 10 月 7 日把 GPT-6 和一个叫 Intelligent UI 的能力一起推给了全部付费档位:ChatGPT 的回答不再只有文字,滑块、表单、表格、图表、地图会直接长在对话里,而且拖一下滑块价格就跟着变,全程 0 次模型调用。第三方团队 OpenUI 在 ChatGPT 网页端逆向了整条链路,拆出来是一门叫 DIL 的模型侧语言、一台服务端编译器、一个跑在沙箱 iframe + Web Worker 里的运行时,以及一份约 70 个组件的目录——这套做法对前端做生成式 UI 的参考价值,比「模型会写界面了」这句话大得多。

一、先看它替换掉的是什么

在 Intelligent UI 之前,想在产品里让模型出界面,基本只有三条路:

  • 纯 Markdown 表格/列表:模型只能排文字,交互(拖动、筛选、输入)要用户手动改 prompt 重跑一次。
  • 生成一段 HTML 塞进 iframe:srcdoc 一把梭,每次回答整页重渲染,用户已经点过的选择全部归零,还得自己写 CSP。
  • 模型直接吐 React/JSX 交前端渲染:dangerouslySetInnerHTML 或 eval 的风险全在自己身上,模型写错一个标签整块崩掉。

Intelligent UI 的做法是把这三条路各砍一刀:界面不是文本、不是整页、也不是可信代码。它是一份被服务端编译过、只能操作白名单组件、执行环境被剥夺网络与定时器的「操作指令流」。

OpenAI 官方对架构的描述很克制——「一个原生的、可流式的组件库,加一台在模型生成的同时处理界面的编译器」。逆向结果把这句话具体化了。

二、模型写的是 DIL,不是 HTML

模型输出的语言 OpenAI 内部叫 DIL:Markdown 负责正文,JSX 风格标签负责组件,{@body …} 里写真正的 JavaScript。一段「团队席位报价」长这样:

## Team plan estimate
拖动滑块看这个团队每月要花多少钱。

{@body const [seats, setSeats] = DIL.useState(8)}
{@body const price = seats * 29}
<box border padding={3} gap={2}>
  <slider min={1} max={50} value={seats} onChange={setSeats}/>
  <title size="xl">${price}/mo</title>
</box>

用在哪个场景:这是模型给用户的一次性小工具(报价器、拼单计算器、A/B 对比面板),不是产品自己的页面,所以它必须自包含、必须能边生成边显示、必须在写到一半时也不崩。DIL 的语法限制全是冲着这三条去的——语句独占一行,未闭合的元素可以被自动闭合,于是服务端可以在「最后一个完整构造」处切一刀,先编译先渲染。

三、服务端编译器干了五件客户端本来要重复做的事

模型的输出从不直接进浏览器。服务端把它编译成一个 JavaScript 程序加一份 JSON 文本表(随消息存为 model_dil_v2),编译阶段顺手做了:

  1. 纯函数调用:标签全部变成 __dil.jsx(...),客户端运行时不需要再写一个 DIL 解析器。
  2. 错误隔离:每个表达式包进 __dilSafe(() => …, fallback),一个表达式抛错只丢这一个节点,不炸整棵渲染。
  3. 文本进常量表:静态文字被抽到 constants 里,流式输出时变化的是数据而不是程序——这是「界面能边流边更新」的关键。
  4. 稳定 state key:每个状态带 { key: "seats" },重新编译后用户拖到 9 的那个滑块不会弹回 8。
  5. 修复 + 校验:丢弃写了一半的语句、闭合未闭合标签、把不合规属性按组件目录删掉并记成诊断。捕获到的一次响应里,编译器删了两个属性:图标上的 fill(unknown_prop)和 <box gap="1">(invalid_literal,它的刻度是 4px 步进)。

四、执行在沙箱里,渲染在你的组件树上

程序是模型写的,所以它不跑在 ChatGPT 页面里:

  • 页面加载一个隐藏的 runner.html,属性是 sandbox="allow-scripts",CSP 是 default-src 'none',里面起一个 Web Worker。
  • Worker 先自废武功:移除网络、定时器、消息通信、动态代码执行,再冻结剩下的全局对象,然后才用 new Function 求值程序(DIL / __dil / GenUI 作为参数传入)。
  • 超时未响应的程序被隔离,Worker 重启(看门狗)。
  • 运行时是个 React 风格的小 reconciler:渲染 → 保留 keyed hook state → 与上一棵树 diff → 把差异编码成操作列表(CREATE #9 slider / SET min=1 / PLACE under #8 at 0),函数不离开 Worker,事件处理器只以 fn#1 这样的标识传出,线上是二进制整数序列 + 独立字符串表。
  • 页面收到操作后只实例化自己设计系统里的原生组件,未知组件类型的操作一律不接受——所以模型输出无法注入任意标签或样式。

于是交互回路变成:用户把滑块拖到 9 → 页面发 fn#1(9) → Worker 里 setSeats(9) → 重 render → 差异操作流回页面 → $232/mo 变 $261/mo。模型调用 0 次,延迟只受一条本地消息往返限制。

五、组件目录才是真正的「设计系统接口」

模型不是从布局和样式规则自由发挥,它只能从目录里点菜:约 70 个组件注册在客户端代码里(捕获到的响应中出现 39 个),加上间距/圆角/颜色/字号四类 design token,以及 OpenAI 用 DIL 写好、预置进沙箱的复合组件。padding={3} 落地是 12px、gap={2} 是 8px,都走 4px 刻度——生成的界面在每个平台都长得像 ChatGPT 自己,同时编译器拿到了一份可以校验的 schema。

官方给的数字也值得记一下:Intelligent UI 面向每周 12 亿 ChatGPT 用户;GPT-6 Instant 在需要联网搜索的问题上平均比 GPT-5.6 Instant 早 44% 开始出答案,因为它被训练成边想边答,而不是想完再答——这和「界面边生成边出现」是同一个思路的两个出口。

六、社区当场吵起来的四件事

HN 这条发布 740 分、447 条评论,前端相关的争论集中在:

  • 「为什么不直接流式 HTML?」——追问者的直觉是 HTML 解析器本来就是流式的。回应是:元素要闭合才能渲染,而且半截程序不可执行;DIL 的可切断语法正是为此设计。
  • 「每次回答长得都不一样怎么办?」——有开发者拿两次问同一个食谱的截图对比,界面结构不同。生成式 UI 的一致性不是免费的,目录 + token 只保证「像同一个产品」,不保证「同一个问题给同一套布局」。
  • 「本地交互会不会偷偷调模型?」——逆向给了确定答案:0 次,交互全在 Worker 里闭环;真正会外逃的只有显式的 AppBlock(iframe 逃逸口)。
  • 「这是护城河还是补作业?」——有人认为组件目录 + 布局判断是模型训练出来的能力,不能靠 harness 复刻;也有人认为这就是 Anthropic artifacts 的内联版。对前端来说结论一样:能把界面质量和模型能力解耦的,是那份组件目录和校验器。

七、可落地的下一步:三步搭一个自己的「可流式生成 UI」

  1. 把设计系统导出成目录:每个组件一条 schema(属性名、类型、token 取值),先有它,编译器才有东西可校验——没有目录,模型就只能瞎写 class。
  2. 把「生成」「执行」「渲染」三层拆开:模型输出中间表示 → 服务端(或 Worker)编译成纯数据 + 受限程序 → 渲染层只认白名单组件。哪怕你用不上 DIL,这套分层也顺手解决了 XSS 和流式半截状态两个老问题。
  3. 给状态加 key、给文本建常量表:这两条是流式界面不闪、不重置的最小实现,今天就能在你现有的消息流协议里加上。

先从一个「回答里嵌个小计算器」的场景做起:它足够小,能验证编译与沙箱链路;又足够真,能立刻看到 0 次模型调用的交互手感。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 153 阅读