YouMind
登录

我们逆向工程了 ChatGPT Intelligent UI。以下是其实际工作原理

@rabi_guha
英语2026年10月08日
206K
2.0K
146
66
3.5K

TL;DR

本文详细介绍了 ChatGPT 新推出的 Intelligent UI 背后的技术架构,解释了自定义推理语言 (DIL) 的使用、服务器端编译为 JavaScript 和 JSON 的过程,以及通过沙箱 Worker 实现的安全客户端执行机制。

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。
Rabi Shanker Guha - inline image

ChatGPT Intelligent UI 架构图

推理格式

这就是模型实际输出的内容。在 ChatGPT 中,OpenAI 将这种语言称为 DIL:用 Markdown 写正文,用类似 JSX 的标签写组件,用 JavaScript 处理状态和逻辑。我们将通过一个简短的响应,带你走完它的每一个层级:

text
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)。上面的响应会被编译成这样(为了便于阅读进行了格式化):

javascript
1function __dilSafe(evaluate, failureValue) {
2 try { return evaluate(); } catch { return failureValue; }
3}
4
5DIL.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" }));
json
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 状态保存在带键的插槽中。接着,它会将生成的树与上一棵树进行对比,把差异编码为一系列操作指令。它本身不负责绘制任何东西。

下面这个示例展示了首次渲染时的操作指令,每行对应一个节点。列出各元素属性名的条目已省略:

text
1CREATE #1 title SET size = "lg" PLACE under root at 0
2CREATE #2 text "团队套餐价格估算" PLACE under #1 at 0
3CREATE #3 text PLACE under root at 1
4CREATE #4 text "拖动滑块查看" PLACE under #3 at 0
5CREATE #5 bold PLACE under #3 at 1
6CREATE #6 text "月费" PLACE under #5 at 0
7CREATE #7 text "。" PLACE under #3 at 2
8CREATE #8 box SET border = true, padding = 3, gap = 2 PLACE under root at 2
9CREATE #9 slider SET min = 1, max = 50, value = 8, onChange = fn#1 PLACE under #8 at 0
10CREATE #10 title SET size = "xl" PLACE under #8 at 1
11CREATE #11 text "$" PLACE under #10 at 0
12CREATE #12 text "232" PLACE under #10 at 1
13CREATE #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 文本及其编译后的形式。以下是从抓取的响应中提取的一次更新(已精简):

json
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

时间线大致如下:

Rabi Shanker Guha - inline image

GIF

客户端流式传输

页面会将每次新的更新传递给沙盒化的 Worker。Worker 对其求值,利用现有状态重新渲染,并向页面发送更新操作。得益于编译阶段添加的键,状态在多次重新编译中都能保留其值。如果新程序无法求值或渲染,Worker 会继续沿用上一个能正常工作的版本。

随后,页面会为每个变化添加动画:

  • 文本在 0.7 秒内淡入;
  • 新行和网格项在 0.42 秒内滑入;
  • 图表在 1.8 秒内绘制完成;
  • 容器高度平滑过渡,而不是生硬跳变。

逃生舱:AppBlock,iframe 中的应用

Rabi Shanker Guha - inline image

ChatGPT 生成的内联应用

有些请求需要原生组件无法胜任的功能,比如用 Web Audio 合成声音的鼓机。针对这类需求,模型可以编写一个 AppBlock:一个由 HTML、CSS 和 JavaScript 组成的自包含 Web 应用,直接嵌入在响应中。以下是其中一个的开头部分(已精简):

text
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 原生界面。生在这个时代真是太棒了!

Rabi Shanker Guha - inline image

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)

一键保存

使用 YouMind AI 深度阅读爆款文章

保存原文、追问细节、总结观点,并在一个 AI 工作空间里把爆款文章沉淀成可复用笔记。

了解 YouMind
写给创作者

把你的 Markdown 变成干净的 𝕏 文章

图片上传、表格、代码块,往 𝕏 上手动重排太痛苦。YouMind 把整篇 Markdown 一键转成干净、可直接发布的 𝕏 文章草稿。

试试 Markdown 转 𝕏

更多可拆解样本

近期爆款文章

探索更多爆款文章