ChatGPT 终于上线了 Generative UI(生成式 UI),并在一夜之间将其更名为 Intelligent UI(智能 UI)。我们忍不住去扒了扒他们到底是怎么实现的。
https://x.com/OpenAI/status/2107894997538525580
本文将深入剖析 OpenAI 是如何实现其旗舰级 Intelligent UI 的,包括它在 Web 端和移动端原生渲染时所涉及的各个层级、数据格式与机制。
构建模块
ChatGPT 的实现方式将工作拆分给了模型、后端服务器和客户端:
- 推理格式:模型使用 DIL 语言来编写界面,DIL 将 Markdown 与类似 JSX 的标签以及 JavaScript 结合在了一起。
- 服务端编译:服务器将每一段部分响应转换为一个 JavaScript 程序和一个包含文本与数据的 JSON 文档。
- 客户端运行时:一个沙盒化的运行时负责执行程序并生成 UI 操作指令。
- 渲染:ChatGPT 将这些操作应用到自身的原生组件上。
- 设计系统与目录:即模型可以调用的组件、属性和设计 token。

ChatGPT Intelligent UI 架构图
推理格式
这就是模型实际输出的内容。在 ChatGPT 中,OpenAI 将这种语言称为 DIL:用 Markdown 写正文,用类似 JSX 的标签写组件,用 JavaScript 处理状态和逻辑。我们将通过一个简短的响应,带你走完它的每一个层级:
1## 团队套餐价格估算2拖动滑块查看您团队的**月费**。3{@body const [seats,setSeats] = DIL.useState(8)}4{@body const price = seats*29}5<box border padding={3} gap={2}>6 <slider min={1} max={50} value={seats} onChange={setSeats}/>7 <title size="xl">${price}/月</title>8</box>
标题和段落是普通的 Markdown。标签则是来自 ChatGPT 目录的组件。那两行 {@body …} 是 JavaScript:第一行声明了一个状态 seats,第二行据此计算出 price。滑块绑定了 seats,因此拖动它就会更新价格。
之所以需要一种专属格式,是因为模型是一个 token 一个 token 地输出界面的。
- 它必须易于稳定地编写,因此它建立在模型已经非常熟悉的语法之上。
- 它在只写了一半时也必须可用。语句各自独占一行,任何未闭合的元素都可以被自动闭合。这样一来,服务器就能在最后一条完整结构处截断部分响应,并依然成功编译它。
服务端编译
客户端绝不会直接执行模型原样输出的代码。OpenAI 的服务器会将其编译为一个 JavaScript 程序和一个 JSON 文档,并与消息一起存储(标记为 model_dil_v2)。上面的响应会被编译成这样(为了便于阅读进行了格式化):
1function __dilSafe(evaluate, failureValue) {2 try { return evaluate(); } catch { return failureValue; }3}45DIL.render(__dil.jsx(() => {6 const __dilConstants = DIL.useConstants();7 const __dilModelDataBindings = DIL.useAppData((appData) => appData.opGenui?.modelDataBindings ?? {});8 const [seats, setSeats] = DIL.useState(8, { key: "seats" });9 const price = __dilSafe(() => seats * 29, undefined);10 return __dil.jsx(__dil.Fragment, null,11 __dil.jsx("title", { size: "lg" }, __dilConstants["0"]),12 __dil.jsx("text", null, __dilConstants["1"], __dil.jsx("bold", null, __dilConstants["2"]), __dilConstants["3"]),13 __dil.jsx("box", { border: true, padding: 3, gap: 2 },14 __dilSafe(() => __dil.jsx("slider", { min: 1, max: 50, value: seats, onChange: setSeats }), null),15 __dil.jsx("title", { size: "xl" }, __dilConstants["4"], __dilSafe(() => price, null), __dilConstants["5"])));16}, { key: "body:2" }));
1{2 "constants": {3 "0": "团队套餐价格估算",4 "1": "拖动滑块查看",5 "2": "月费",6 "3": "。",7 "4": "$",8 "5": "/月"9 },10 "appData": { "opGenui": { "componentResults": {}, "modelDataBindings": {} } }11}
Markdown 会与组件一起被编译进同一棵树中。标题变成了 title,段落变成了内部嵌套 bold 的 text,而它们的文字则被移入了常量表。
编译过程替每个客户端省去了原本都要重复做的工作:
- 纯函数调用。标记语言被转换为对 __dil.jsx 的调用,这样 JavaScript 运行时无需 DIL 解析器就能执行程序。
- 错误隔离。表达式被包裹在 __dilSafe 中,因此抛出异常的表达式只会移除单个元素,而不会导致整个渲染中断。
- 文本独立成表。静态文本被移入常量表,这样在响应流式传输时,不断增长的文本改变的是数据而非程序。
- 稳定的状态键。每段状态都会分配一个键({ key: "seats" }),确保其值在每次重新编译后都能保留。
- 修复与校验。不完整的语句和标签会被丢弃,未闭合的元素会被补全,无法通过目录校验的属性会被移除并记录为诊断信息。
JSON 文档保存着文本常量,以及服务器为该响应解析出的所有数据,比如图片搜索结果(参见“数据”部分)。
客户端运行时
客户端会接收到编译后的程序和 JSON 文档。它的工作被拆分为两部分:执行程序“运行时”,以及绘制结果的“渲染器”。
由于程序是由模型编写的代码,它不能直接在 ChatGPT 页面中运行。ChatGPT 会加载一个隐藏的 iframe(runner.html),通过 allow-scripts 和 default-src 'none' 的内容安全策略进行沙盒化隔离,并在其中启动一个 Web Worker。
- 锁定(Lockdown)。在执行程序之前,Worker 会从全局作用域中移除网络访问、定时器、消息通信和动态代码执行能力,并冻结剩余的全局变量。
- 执行。随后,它使用 new Function 来执行程序。运行时对象(DIL、__dil、GenUI)和目录中的复合组件会作为参数传入。
- 看门狗。如果程序在规定时间内没有响应,就会被隔离,同时 Worker 会重启。
这个运行时是一个类似 React 风格的小型协调器(reconciler)。它负责渲染组件,并将 hook 状态保存在带键的插槽中。接着,它会将生成的树与上一棵树进行对比,把差异编码为一系列操作指令。它本身不负责绘制任何东西。
下面这个示例展示了首次渲染时的操作指令,每行对应一个节点。列出各元素属性名的条目已省略:
1CREATE #1 title SET size = "lg" PLACE under root at 02CREATE #2 text "团队套餐价格估算" PLACE under #1 at 03CREATE #3 text PLACE under root at 14CREATE #4 text "拖动滑块查看" PLACE under #3 at 05CREATE #5 bold PLACE under #3 at 16CREATE #6 text "月费" PLACE under #5 at 07CREATE #7 text "。" PLACE under #3 at 28CREATE #8 box SET border = true, padding = 3, gap = 2 PLACE under root at 29CREATE #9 slider SET min = 1, max = 50, value = 8, onChange = fn#1 PLACE under #8 at 010CREATE #10 title SET size = "xl" PLACE under #8 at 111CREATE #11 text "$" PLACE under #10 at 012CREATE #12 text "232" PLACE under #10 at 113CREATE #13 text "/月" PLACE under #10 at 2
函数永远不会离开 Worker;滑块的处理函数仅以标识符(fn#1)的形式传递。在网络传输中,这些操作指令被编码为整数的二进制序列,字符串则存放在单独的表中。
渲染
ChatGPT 页面会将这些操作应用到自己的组件树上。每个 CREATE 都会实例化一个来自 ChatGPT 设计系统的原生组件,页面会在收到变更时为其添加动画效果。页面只接受已知组件类型的操作,因此模型的输出无法引入任意的标记或样式。例外情况是某些属性接受的原始 CSS 值(参见“设计系统与目录”),以及在 iframe 中运行的 AppBlock 应用(参见“逃生舱”)。
交互的流程则完全相反。当用户将滑块拖到 9 时,页面会把处理函数的标识符和参数发送给 Worker。Worker 调用 setSeats(9),重新渲染,并返回更新操作。整个过程不需要调用模型。
设计系统与目录
目录定义了模型可以请求哪些东西。之所以需要它,是因为模型并不是靠原始的布局和样式规则来拼凑界面的。它会从 ChatGPT 已经知道如何绘制的组件中进行选择,并使用诸如 padding={3} 这样的设计 token 来设置样式。某些属性允许使用原始 CSS 值(如像素宽度和十六进制颜色),但首选依然是设计 token。因此:
- 生成的界面在各个平台上都与 ChatGPT 的其他部分保持视觉一致。
- 编译器有一套 schema 可以用来校验输出。如果某个属性在组件上不存在,或者字面量类型错误,就会在编译时被移除并记录为诊断信息。
在我们抓取到的某次响应中,编译器移除了两个属性:icon 上的 fill(unknown_prop 诊断)和 box 上的 gap="1"(invalid_literal 诊断)。
目录由三部分组成:
- 原生组件。大约有 70 个组件定义在 ChatGPT 客户端代码的组件注册表中;在我们抓取的响应中出现了其中的 39 个。
- 设计 token,用于控制间距、圆角、颜色和尺寸。
- 复合组件,由 OpenAI 用 DIL 编写并预先构建好发送到沙盒中,例如图片和商品组件。在我们的抓取结果中,模型使用了这些组件,但从未自己定义过新组件。
流式传输
流式输出文本很简单:每个新 token 直接追加到屏幕上已有的内容后面即可。但流式输出界面要难得多,原因有三:
- 输出通常还不能运行。在绝大多数时刻,它都是一个不完整的程序,某个标签或表达式还没闭合,无法按原样执行。
- 界面在不断增长的同时必须保持可用。用户已经交互过的组件必须保留其状态。
- 部分内容是单独到达的。像图片这样的数据来自服务器,而不是文本流。
服务端流式传输
单纯追加 token 的普通流无法表达这些需求。因此,ChatGPT 改为对一个结构化消息流式发送补丁(patch),该消息将原始文本、编译后的程序及其数据并列存放。
响应通过 server-sent event 流(POST /backend-api/f/conversation)到达浏览器。每个事件都是对正在构建的消息进行的 JSON-Patch 风格的更新。单个事件通常会同时更新原始 DIL 文本及其编译后的形式。以下是从抓取的响应中提取的一次更新(已精简):
1{"o": "patch", "v": [2 {"p": "/message/content/parts/0", "o": "append", "v": " 周日和朋友一起吃烤羊腿——丰盛的美食,……"},3 {"p": "/message/metadata/model_dil_v2/code", "o": "replace", "v": "DIL.render(__dil.jsx(()=>{…"},4 {"p": "/message/metadata/model_dil_v2/constants", "o": "append", "v": {"0": "这是一个周日和朋友一起享用正宗烤羊腿的计划——……"}},5 {"p": "/message/metadata/model_dil_v2/constants", "o": "append", "v": {"1": "既然你"}},6 {"p": "/message/metadata/model_dil_v2/fallbackMarkdown", "o": "append", "v": " 周日和朋友一起吃烤羊腿——……"}7]}
服务器并不会增量编译。每隔几百毫秒(很可能是每当有新的模型输出块产生时),它就会把模型到目前为止写下的所有内容重新编译一遍,并发送结果。编译从第一个 token 就开始了,此时甚至还没有出现任何标签。
编译器需要处理以下问题:
- 编译写了一半的响应
- 更新文本
- 更新 UI
时间线大致如下:

GIF
客户端流式传输
页面会将每次新的更新传递给沙盒化的 Worker。Worker 对其求值,利用现有状态重新渲染,并向页面发送更新操作。得益于编译阶段添加的键,状态在多次重新编译中都能保留其值。如果新程序无法求值或渲染,Worker 会继续沿用上一个能正常工作的版本。
随后,页面会为每个变化添加动画:
- 文本在 0.7 秒内淡入;
- 新行和网格项在 0.42 秒内滑入;
- 图表在 1.8 秒内绘制完成;
- 容器高度平滑过渡,而不是生硬跳变。
逃生舱:AppBlock,iframe 中的应用

ChatGPT 生成的内联应用
有些请求需要原生组件无法胜任的功能,比如用 Web Audio 合成声音的鼓机。针对这类需求,模型可以编写一个 AppBlock:一个由 HTML、CSS 和 JavaScript 组成的自包含 Web 应用,直接嵌入在响应中。以下是其中一个的开头部分(已精简):
1<AppBlock title="Drum Lab" icon="app-chatgpt" variant="inline" app_block_id="drum-lab-01">2<div id="dl" class="w-full min-w-0 space-y-4 text-base">3 <style>4 #dl{color:var(--viz-text)}#dl button{touch-action:manipulation}#dl .panel{background:var(--viz-panel);border:1px solid var(--viz-border);border-radius:15px}…5 </style>6 …7 <button id="dl-play" class="btn" style="background:var(--viz-text);color:var(--viz-card);min-width:100px">▶ Play</button>8 …9</div>10<script>11(function(){12const root=document.getElementById('dl');if(root.dataset.init)return;root.dataset.init="yes";13…14function audioInit(){if(!audio){const C=window.AudioContext||window.webkitAudioContext; if(!C)return false;audio=new C();…15…16})();17</script>18</AppBlock>
AppBlock 的渲染方式与 Intelligent UI 组件不同。
总结
你输入一段提示词。模型开始编写界面,服务器将其转化为 ChatGPT 可执行的内容,页面则在响应流式传输的过程中一块块地将它搭建起来。一旦界面就绪,拖动滑块或勾选复选框都会在本地直接更新界面,无需再次请求模型。
一门模型专属语言、一个服务端编译步骤、原生渲染器以及一套扎实的设计系统,共同促成了这一切。每个部分都发挥着关键作用,它们携手为全球数十亿用户带来了下一代 AI 原生界面。生在这个时代真是太棒了!

ChatGPT Intelligent UI 截图
研究方法
本文的所有观察结果均来自我们自己的 ChatGPT 账号、ChatGPT Web 应用产生的网络流量,以及 chatgpt.com 公开提供的 JavaScript 代码。测试于 2026 年 10 月使用 GPT-6 和 GPT-6 Thinking 完成。
分析工作在 Codex 和 Claude 的协助下完成。由 Codex 撰写,可视化图表由 Claude 制作。
(更深入的版本请见 https://www.openui.com/blog/how-chatgpt-intelligent-ui-works)





