我们逆向工程了 ChatGPT 智能 UI,揭秘其真实工作原理

模型用 DIL 写界面,服务器编译为 JS 程序与 JSON 文档,沙箱 Worker 执行并产出操作列表,ChatGPT 原生组件渲染——四层协作实现流式生成

RABI SHANKER GUHA(@RABI_GUHA)随笔 · 已翻译 · 约 17 分钟
阅览室 · AI 最佳实践#ChatGPT#编译#组件#模型#流式#UI#界面#DIL原文

ChatGPT 推出了生成式 UI(终于),并在一夜之间把这个领域更名为智能 UI。我们不得不看看他们是怎么实现的。

查看原推文

本文深入研究了 OpenAI 如何实现其旗舰级智能 UI——各层结构、格式,以及在 Web 和移动端上的原生渲染。

构建模块

ChatGPT 的实现将工作划分给模型、后端服务器和客户端三部分:

  • 推理格式:模型用 DIL 编写界面,DIL 将 Markdown 与类 JSX 标签和 JavaScript 结合在一起。
  • 服务端编译:服务器将每个部分响应转换为一个 JavaScript 程序,以及一份包含文本和数据的 JSON 文档。
  • 客户端运行时:一个沙箱化运行时执行该程序并产生 UI 操作。
  • 渲染:ChatGPT 将这些操作应用到自己的原生组件上。
  • 设计系统与目录:模型可用的组件、属性和设计令牌。
ChatGPT 智能 UI 架构图
ChatGPT 智能 UI 架构图

推理格式

这就是模型所写的内容。在 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 中的应用

ChatGPT 生成的内联应用
ChatGPT 生成的内联应用

有些请求需要原生组件并非为之设计的功能,比如用 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 智能 UI 的屏幕截图
ChatGPT 智能 UI 的屏幕截图

方法论

所有观察均来自我们自己的 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)

已读完 · 本文由熊猫易读翻译重排