ChatGPT 終於推出了 Generative UI,而且一夜之間就把這個領域改名為 Intelligent UI。我們當然得好好研究一下他們是怎麼實現的。
https://x.com/OpenAI/status/2107894997538525580
這篇文章將深入剖析 OpenAI 如何打造他們的旗艦級 Intelligent UI,包括它在 Web 與行動端原生運作的層級、格式與渲染方式。
基本組成
ChatGPT 的實作把任務拆分給模型、後端伺服器與客戶端:
- 推論格式:模型使用 DIL 來撰寫介面,這是一種結合 Markdown、類似 JSX 標籤與 JavaScript 的語言。
- 伺服器端編譯:伺服器會把每一段部分回應轉換成一個 JavaScript 程式,以及一份包含文字與資料的 JSON 文件。
- 客戶端執行環境:沙盒化的執行環境負責執行程式並產生 UI 操作指令。
- 渲染:ChatGPT 會將這些操作指令套用到自己的原生元件上。
- 設計系統與目錄:模型可以使用的元件、屬性與 design tokens。

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,而它們的文字內容則被移進 constants 表。
編譯過程替所有客戶端省下了重複的工作:
- 純函式呼叫。 標記語言會轉成對 __dil.jsx 的呼叫,讓 JavaScript 執行環境不必靠 DIL 解析器就能跑這段程式。
- 錯誤隔離。 運算式會被包在 __dilSafe 裡,一旦某個運算式拋出錯誤,只會移除單一元素,而不會讓整次渲染中斷。
- 文字獨立成表。 靜態文字會移進 constants 表,因此在串流輸出時,不斷增加的文字改變的是資料而非程式本身。
- 穩定的狀態鍵值。 每個狀態都會拿到一組鍵值({ key: "seats" }),確保它的值能在每次重新編譯後保留下來。
- 修復與驗證。 不完整的陳述與標籤會被丟棄,未關閉的元素會自動補齊,而未通過目錄驗證的屬性則會被移除並記錄為診斷資訊。
JSON 文件會存放文字常數,以及伺服器為這次回應解析出的任何資料,例如圖片搜尋結果(參見「資料」一節)。
客戶端執行環境
客戶端會收到編譯後的程式與 JSON 文件。它的工作分成兩塊:負責執行程式的執行環境(runtime),以及負責繪製結果的渲染器(renderer)。
由於程式是模型寫的,它不會直接在 ChatGPT 頁面中執行。ChatGPT 會載入一個隱藏的 iframe(runner.html),透過 allow-scripts 與 default-src 'none' 的內容安全政策進行沙盒化,並在裡面啟動一個 Web Worker。
- 鎖定(Lockdown)。 在執行程式前,Worker 會從全域範圍移除網路存取、計時器、訊息傳遞與動態程式碼執行功能,並將剩下的全域變數凍結。
- 求值(Evaluation)。 接著用 new Function 執行程式。執行環境物件(DIL、__dil、GenUI)與目錄中的複合元件會以參數形式傳入。
- 看門狗(Watchdog)。 如果程式未在逾時時間內回應,就會被隔離,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} 這樣的 design tokens 來設定樣式。某些屬性接受原始 CSS 值(例如像素寬度與十六進位色碼),但官方仍建議優先使用 design tokens。這樣做的好處是:
- 生成的介面在各平台上都能與 ChatGPT 的其他部分保持一致。
- 編譯器有一份 schema 可用來檢查輸出。 如果某個屬性不存在於元件上,或傳入了錯誤型別的值,編譯時就會被移除並記錄為診斷資訊。
在我們擷取的一段回應中,編譯器移除了兩個屬性:icon 上的 fill(產生 unknown_prop 診斷)與 box 上的 gap="1"(產生 invalid_literal 診斷)。
目錄分為三個部分:
- 原生元件。 ChatGPT 客戶端程式碼的元件註冊表中定義了約 70 個元件;在我們擷取的回應中出現了其中 39 個。
- Design tokens,用於間距、圓角、顏色與尺寸。
- 複合元件,由 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 寫成、直接嵌入回應中的自足網頁應用程式。以下是其中一段的開頭(已縮短):
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 網頁應用程式產生的流量,以及 chatgpt.com 公開提供的 JavaScript。研究於 2026 年 10 月使用 GPT-6 與 GPT-6 Thinking 進行。
分析工作由 Codex 與 Claude 協助完成。文章由 Codex 撰寫,視覺化由 Claude 製作
(更深入的版本請見 https://www.openui.com/blog/how-chatgpt-intelligent-ui-works)





