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),编译阶段顺手做了:
- 纯函数调用:标签全部变成
__dil.jsx(...),客户端运行时不需要再写一个 DIL 解析器。 - 错误隔离:每个表达式包进
__dilSafe(() => …, fallback),一个表达式抛错只丢这一个节点,不炸整棵渲染。 - 文本进常量表:静态文字被抽到
constants里,流式输出时变化的是数据而不是程序——这是「界面能边流边更新」的关键。 - 稳定 state key:每个状态带
{ key: "seats" },重新编译后用户拖到 9 的那个滑块不会弹回 8。 - 修复 + 校验:丢弃写了一半的语句、闭合未闭合标签、把不合规属性按组件目录删掉并记成诊断。捕获到的一次响应里,编译器删了两个属性:图标上的
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」
- 把设计系统导出成目录:每个组件一条 schema(属性名、类型、token 取值),先有它,编译器才有东西可校验——没有目录,模型就只能瞎写 class。
- 把「生成」「执行」「渲染」三层拆开:模型输出中间表示 → 服务端(或 Worker)编译成纯数据 + 受限程序 → 渲染层只认白名单组件。哪怕你用不上 DIL,这套分层也顺手解决了 XSS 和流式半截状态两个老问题。
- 给状态加 key、给文本建常量表:这两条是流式界面不闪、不重置的最小实现,今天就能在你现有的消息流协议里加上。
先从一个「回答里嵌个小计算器」的场景做起:它足够小,能验证编译与沙箱链路;又足够真,能立刻看到 0 次模型调用的交互手感。
评论区
登录后可评论。