
我们逆向工程了 ChatGPT 智能 UI,揭秘其真实工作原理
模型用 DIL 写界面,服务器编译为 JS 程序与 JSON 文档,沙箱 Worker 执行并产出操作列表,ChatGPT 原生组件渲染——四层协作实现流式生成
ChatGPT 推出了生成式 UI(终于),并在一夜之间把这个领域更名为智能 UI。我们不得不看看他们是怎么实现的。
本文深入研究了 OpenAI 如何实现其旗舰级智能 UI——各层结构、格式,以及在 Web 和移动端上的原生渲染。
构建模块
ChatGPT 的实现将工作划分给模型、后端服务器和客户端三部分:
- 推理格式:模型用 DIL 编写界面,DIL 将 Markdown 与类 JSX 标签和 JavaScript 结合在一起。
- 服务端编译:服务器将每个部分响应转换为一个 JavaScript 程序,以及一份包含文本和数据的 JSON 文档。
- 客户端运行时:一个沙箱化运行时执行该程序并产生 UI 操作。
- 渲染:ChatGPT 将这些操作应用到自己的原生组件上。
- 设计系统与目录:模型可用的组件、属性和设计令牌。

推理格式
这就是模型所写的内容。在 ChatGPT 中,它是一种被 OpenAI 称为 DIL 的语言:用 Markdown 写散文,用类 JSX 标签写组件,用 JavaScript 写状态和逻辑。我们将跟随一个简短的响应,逐层剖析:
## Team plan estimate
Drag the slider to see the **monthly price** for your team.
{@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>标题和段落是普通的 Markdown。标签是来自 ChatGPT 组件目录的组件。两行 {@body …} 是 JavaScript:第一行声明了一段状态 seats,第二行从中推导出 price。滑块绑定到 seats,因此移动它就会更新价格。
之所以需要一种专门的格式,是因为模型是逐 token 写出界面的。
- 它必须易于可靠地书写, 因此它由模型已经非常熟悉的记法构成。
- 它必须在写到一半时仍然可用。 语句各自独占一行,任何未闭合的元素都可以自动闭合。这样一来,服务器就可以在最后一个完整结构处截断部分响应,并且仍然能够编译它。
服务端编译
客户端从不按原样执行模型的输出。OpenAI 的服务器会将其编译为一个 JavaScript 程序和一个 JSON 文档,并与消息一起存储(作为 model_dil_v2)。该响应会编译成如下内容(为便于阅读已格式化):
function __dilSafe(evaluate, failureValue) {
try { return evaluate(); } catch { return failureValue; }
}
DIL.render(__dil.jsx(() => {
const __dilConstants = DIL.useConstants();
const __dilModelDataBindings = DIL.useAppData((appData) => appData.opGenui?.modelDataBindings ?? {});
const [seats, setSeats] = DIL.useState(8, { key: "seats" });
const price = __dilSafe(() => seats * 29, undefined);
return __dil.jsx(__dil.Fragment, null,
__dil.jsx("title", { size: "lg" }, __dilConstants["0"]),
__dil.jsx("text", null, __dilConstants["1"], __dil.jsx("bold", null, __dilConstants["2"]), __dilConstants["3"]),
__dil.jsx("box", { border: true, padding: 3, gap: 2 },
__dilSafe(() => __dil.jsx("slider", { min: 1, max: 50, value: seats, onChange: setSeats }), null),
__dil.jsx("title", { size: "xl" }, __dilConstants["4"], __dilSafe(() => price, null), __dilConstants["5"])));
}, { key: "body:2" }));{
"constants": {
"0": "Team plan estimate",
"1": "Drag the slider to see the ",
"2": "monthly price",
"3": " for your team.",
"4": "$",
"5": "/mo"
},
"appData": { "opGenui": { "componentResults": {}, "modelDataBindings": {} } }
}Markdown 会被编译成与组件相同的树。标题变成一个 title,段落变成一个内部带有加粗文本的 text,而它们的文字则移入常量表。
编译完成了每个客户端否则都不得不重复进行的工作:
- 普通函数调用。 标记会变成对 __dil.jsx 的调用,因此 JavaScript 运行时无需 DIL 解析器即可求值该程序。
- 错误隔离。 表达式被包裹在 __dilSafe 中,因此抛出异常的表达式只会移除一个元素,而不会中止整个渲染过程。
- 独立表中的文本。 静态文本移入常量表,因此在响应流式传输时,不断增长的文本改变的是数据而非程序。
- 稳定的状态键。 每个状态片段都会获得一个键({ key: "seats" }),因此其值在每次重新编译后都能保留。
- 修复与验证。 不完整的语句和标签会被丢弃,未闭合的元素会被闭合,未通过目录验证的属性会被移除并记录为诊断信息。
JSON 文档保存文本常量以及服务器为响应解析出的任何数据,例如图像搜索结果(参见数据)。
客户端运行时
客户端接收编译后的程序和 JSON 文档。其工作分为两部分:运行时负责执行程序,渲染器负责绘制结果。
程序是由模型编写的代码,因此它不在 ChatGPT 页面中运行。ChatGPT 会加载一个隐藏的 iframe(runner.html),该 iframe 以 allow-scripts 进行沙箱隔离,并采用 default-src 'none' 的内容安全策略,它会启动一个 Web Worker。
- 锁定。 在评估程序之前,worker 会从其全局作用域中移除网络访问、定时器、消息传递和动态代码评估,并冻结剩余的全局对象。
- 评估。 然后它使用 new Function 评估程序。运行时对象(DIL、__dil、GenUI)以及目录中的复合组件作为参数传入。
- 看门狗。 在超时时间内未响应的程序会被隔离,worker 会被重启。
运行时是一个小型协调器,风格类似 React。它渲染组件并将 hook 状态保存在带键的槽位中。然后它将生成的树与前一个树进行比较,并将差异编码为操作列表。它不绘制任何内容。
下面的示例展示了首次渲染时的操作,每个节点一行。省略了列出每个元素属性名的条目:
CREATE #1 title SET size = "lg" PLACE under root at 0
CREATE #2 text "Team plan estimate" PLACE under #1 at 0
CREATE #3 text PLACE under root at 1
CREATE #4 text "Drag the slider to see the " PLACE under #3 at 0
CREATE #5 bold PLACE under #3 at 1
CREATE #6 text "monthly price" PLACE under #5 at 0
CREATE #7 text " for your team." PLACE under #3 at 2
CREATE #8 box SET border = true, padding = 3, gap = 2 PLACE under root at 2
CREATE #9 slider SET min = 1, max = 50, value = 8, onChange = fn#1 PLACE under #8 at 0
CREATE #10 title SET size = "xl" PLACE under #8 at 1
CREATE #11 text "$" PLACE under #10 at 0
CREATE #12 text "232" PLACE under #10 at 1
CREATE #13 text "/mo" PLACE under #10 at 2函数永远不会离开 worker;滑块的处理器仅以标识符(fn#1)的形式发送。在传输时,这些操作被编码为整数的二进制序列,字符串则保存在单独的表中。
渲染
ChatGPT 页面将这些操作应用到自身的组件树上。每个 CREATE 都会从 ChatGPT 的设计系统中实例化一个原生组件,页面则在操作到达时以动画呈现变化。页面只接受已知组件类型的操作,因此模型输出无法引入任意的标记或样式。例外情况是某些属性所接受的原始 CSS 值(参见「设计系统与目录」)以及 AppBlock 应用,后者运行在 iframe 中(参见「逃生舱」)。
交互则朝相反方向运行。当用户将滑块拖到 9 时,页面会把处理器的标识符和参数发送给 worker。worker 调用 setSeats(9),重新渲染,并返回更新操作。整个过程不涉及模型调用。
设计系统与目录
目录定义了模型可以请求的内容。之所以需要它,是因为模型并非依据原始布局与样式规则来构建界面,而是从 ChatGPT 已经知道如何绘制的组件中进行选择,并用诸如 padding={3} 这样的设计令牌为其设置样式。某些属性也接受原始 CSS 值,例如像素宽度和十六进制颜色,但优先使用设计令牌。因此:
- 生成的界面在每个平台上都与其他 ChatGPT 界面风格一致。
- 编译器有可供校验输出的 schema。 组件上不存在的属性,或类型错误的字面量,会在编译期间被移除,并记录为一条诊断信息。
在我们捕获的一次响应中,编译器移除了两个属性:图标上的 fill(一条 unknown_prop 诊断)以及盒子上的 gap="1"(一条 invalid_literal 诊断)。
目录包含三个部分:
- 原生组件。 ChatGPT 客户端代码的组件注册表中定义了约 70 个组件;在我们捕获的响应中出现了其中 39 个。
- 设计令牌,用于间距、圆角、颜色和尺寸。
- 复合组件由 OpenAI 用 DIL 编写,并预先构建后发送到沙箱,例如图像和产品组件。在我们的抓取记录中,模型使用了这些组件,但从未定义过自己的组件。
流式传输
流式传输文本很简单:每个新 token 追加到屏幕上已有的内容之后。流式传输界面则更难,原因有三:
- 输出通常尚不可运行。 在大多数时刻,它都是一个不完整的程序,仍有标签或表达式处于打开状态,无法按原样执行。
- 界面必须在增长过程中保持可用。 用户已经操作过的组件必须保持其状态。
- 部分内容是单独到达的。 图像等数据来自服务器,而非来自文本。
服务端流式传输
单纯追加 token 的流无法表达这一点。ChatGPT 改为对流式传输一个结构化消息的补丁,该消息将原始文本、编译后的程序及其数据并排保存。
响应通过服务器发送事件流(POST /backend-api/f/conversation)到达浏览器。每个事件都是对正在构建的消息的一次 JSON-Patch 风格更新。单个事件通常会同时更新原始 DIL 文本及其编译形式。以下是某次捕获响应中的一次更新,已作缩短:
{"o": "patch", "v": [
{"p": "/message/content/parts/0", "o": "append", "v": " Sunday lamb roast with friends — generous food, …"},
{"p": "/message/metadata/model_dil_v2/code", "o": "replace", "v": "DIL.render(__dil.jsx(()=>{…"},
{"p": "/message/metadata/model_dil_v2/constants", "o": "append", "v": {"0": "Here's a plan for a proper Sunday lamb roast with friends — …"}},
{"p": "/message/metadata/model_dil_v2/constants", "o": "append", "v": {"1": "Since you're"}},
{"p": "/message/metadata/model_dil_v2/fallbackMarkdown", "o": "append", "v": " Sunday lamb roast with friends — …"}
]}服务器不会增量编译。每隔几百毫秒,很可能在模型每输出一个新片段时,它都会重新编译模型到目前为止写下的全部内容,并发送结果。编译从第一个 token 就开始,那时任何标签都还没出现。
编译器必须处理:
- 编译写到一半的响应
- 更新文本
- 更新 UI
时间线大致如下:
客户端流式传输
页面把每次新更新传给沙箱化的 worker。worker 对其进行求值,用现有状态重新渲染,并把更新操作发送给页面。由于编译期间添加的键,状态在多次重新编译之间会保留其值。如果新程序求值或渲染失败,worker 会保留上一个能正常工作的程序。
随后页面会为每处变化添加动画:
- 文本在 0.7 秒内淡入;
- 新行和网格项在 0.42 秒内滑入;
- 图表在 1.8 秒内绘制;
- 容器高度以过渡方式变化,而不是跳跃式变化。
逃生舱:AppBlock,iframe 中的应用

有些请求需要原生组件并非为之设计的功能,比如用 Web Audio 合成声音的鼓机。对于这类需求,模型可以编写一个 AppBlock:一个自包含的 Web 应用,由 HTML、CSS 和 JavaScript 构成,嵌入在响应中。下面是一个 AppBlock 的开头,已作删减:
<AppBlock title="Drum Lab" icon="app-chatgpt" variant="inline" app_block_id="drum-lab-01">
<div id="dl" class="w-full min-w-0 space-y-4 text-base">
<style>
#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}…
</style>
…
<button id="dl-play" class="btn" style="background:var(--viz-text);color:var(--viz-card);min-width:100px">▶ Play</button>
…
</div>
<script>
(function(){
const root=document.getElementById('dl');if(root.dataset.init)return;root.dataset.init="yes";
…
function audioInit(){if(!audio){const C=window.AudioContext||window.webkitAudioContext; if(!C)return false;audio=new C();…
…
})();
</script>
</AppBlock>AppBlock 的渲染方式与智能 UI 组件不同。
整合起来
你输入一条提示。模型开始编写界面,服务器将其转换为 ChatGPT 可运行的内容,页面随着响应流式传输而逐块构建。一旦完成,拖动滑块或勾选复选框就会在本地更新界面,无需再次询问模型。
一种模型专用语言、一个服务器编译步骤、原生渲染器和一个有据可依的设计系统,将这一切整合在一起。每个部分都服务于一个关键环节,它们共同为全球数十亿用户带来下一代 AI 原生界面。生逢其时!

方法论
所有观察均来自我们自己的 ChatGPT 账户、ChatGPT 网页应用产生的流量,以及 chatgpt.com 公开提供的 JavaScript。这些观察于 2026 年 10 月使用 GPT-6 和 GPT-6 Thinking 完成。
分析借助 Codex 与 Claude 完成。使用 Codex 撰写,可视化由 Claude 制作
(更深入的版本见 https://www.openui.com/blog/how-chatgpt-intelligent-ui-works)