ChatGPT vừa ra mắt Generative UI (cuối cùng cũng có) và đổi tên lĩnh vực này thành Intelligent UI chỉ sau một đêm. Chúng tôi buộc phải tìm hiểu xem họ đã triển khai nó như thế nào.
https://x.com/OpenAI/status/2107894997538525580
Bài viết này là một nghiên cứu chuyên sâu về cách OpenAI triển khai Intelligent UI — tính năng chủ lực của họ, bao gồm các lớp, định dạng và cơ chế render native trên cả Web lẫn Mobile.
Các khối xây dựng
Cách triển khai của ChatGPT phân chia công việc giữa model, backend server và client:
- Định dạng inference: model viết giao diện bằng DIL, kết hợp Markdown với các thẻ kiểu JSX và JavaScript.
- Biên dịch phía server: server chuyển đổi từng phần phản hồi thành một chương trình JavaScript và một tài liệu JSON chứa văn bản cùng dữ liệu.
- Runtime phía client: một runtime được sandbox hóa sẽ thực thi chương trình và tạo ra các thao tác UI.
- Rendering: ChatGPT áp dụng các thao tác này vào chính những component native của mình.
- Design system và catalog: tập hợp các component, thuộc tính và design token mà model được phép sử dụng.

Sơ đồ kiến trúc Intelligent UI của ChatGPT
Định dạng inference
Đây là những gì model viết ra. Trong ChatGPT, đó là một ngôn ngữ mà OpenAI gọi là DIL: dùng Markdown cho văn bản, các thẻ kiểu JSX cho component, và JavaScript cho state cùng logic. Chúng ta sẽ theo dõi một phản hồi nhỏ đi qua từng lớp:
1## Ước tính gói Team2Kéo thanh trượt để xem **giá hàng tháng** cho nhóm của bạn.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}/tháng</title>8</box>
Tiêu đề và đoạn văn là Markdown thông thường. Các thẻ là những component lấy từ catalog của ChatGPT. Hai dòng {@body …} là JavaScript: dòng đầu khai báo một state tên là seats, dòng thứ hai suy ra price từ state đó. Thanh trượt được liên kết với seats, nên khi kéo nó, giá tiền sẽ tự động cập nhật.
Cần có một định dạng riêng vì model tạo ra giao diện theo từng token một.
- Nó phải dễ viết một cách đáng tin cậy, nên được xây dựng từ những cú pháp mà model vốn đã nắm rất rõ.
- Nó phải dùng được ngay cả khi mới viết một nửa. Các câu lệnh nằm trên những dòng riêng biệt, và bất kỳ phần tử đang mở nào cũng có thể được đóng tự động. Nhờ vậy, server có thể cắt một phản hồi chưa hoàn chỉnh ở cấu trúc trọn vẹn cuối cùng mà vẫn biên dịch được.
Biên dịch phía server
Client không bao giờ thực thi trực tiếp output của model. Server của OpenAI biên dịch nó thành một chương trình JavaScript và một tài liệu JSON, rồi lưu trữ kèm theo tin nhắn (dưới dạng model_dil_v2). Phản hồi trên được biên dịch thành như sau (đã định dạng lại cho dễ đọc):
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": "Ước tính gói Team",4 "1": "Kéo thanh trượt để xem ",5 "2": "giá hàng tháng",6 "3": " cho nhóm của bạn.",7 "4": "$",8 "5": "/tháng"9 },10 "appData": { "opGenui": { "componentResults": {}, "modelDataBindings": {} } }11}
Markdown được biên dịch vào cùng một cây với các component. Tiêu đề trở thành title, đoạn văn thành text có chứa bold bên trong, còn các từ ngữ thì được đưa vào bảng constants.
Quá trình biên dịch xử lý luôn những việc mà lẽ ra mọi client đều phải lặp lại:
- Lời gọi hàm thuần túy. Markup được chuyển thành các lời gọi tới __dil.jsx, giúp runtime JavaScript có thể chạy chương trình mà không cần parser cho DIL.
- Cô lập lỗi. Các biểu thức được bọc trong __dilSafe, nên nếu một biểu thức ném lỗi, chỉ một phần tử bị loại bỏ thay vì làm sập toàn bộ quá trình render.
- Văn bản nằm ở bảng riêng. Văn bản tĩnh được chuyển vào bảng constants, nhờ đó khi phản hồi stream về, phần văn bản dài ra sẽ chỉ làm thay đổi dữ liệu chứ không ảnh hưởng đến chương trình.
- Khóa state ổn định. Mỗi state nhận một khóa ({ key: "seats" }), giúp giá trị của nó được giữ nguyên qua mọi lần biên dịch lại.
- Sửa lỗi và kiểm tra. Các câu lệnh và thẻ chưa hoàn chỉnh bị loại bỏ, phần tử chưa đóng sẽ được đóng tự động, còn những thuộc tính không vượt qua bước kiểm tra với catalog sẽ bị xóa và ghi nhận dưới dạng thông tin chẩn đoán.
Tài liệu JSON lưu trữ các hằng số văn bản và mọi dữ liệu mà server phân giải cho phản hồi, chẳng hạn như kết quả tìm kiếm hình ảnh (xem phần Dữ liệu).
Runtime phía client
Client nhận về chương trình đã biên dịch cùng tài liệu JSON. Công việc của nó được chia đôi: runtime chịu trách nhiệm thực thi chương trình, còn renderer đảm nhiệm việc vẽ ra kết quả.
Vì chương trình do model viết, nó không chạy trực tiếp trên trang ChatGPT. ChatGPT tải một iframe ẩn (runner.html), được sandbox bằng allow-scripts cùng chính sách bảo mật nội dung default-src 'none', và iframe này khởi chạy một Web Worker.
- Khóa chặt môi trường. Trước khi chạy chương trình, worker loại bỏ quyền truy cập mạng, timer, messaging và khả năng thực thi code động khỏi phạm vi toàn cục, đồng thời đóng băng các global còn lại.
- Thực thi. Sau đó, nó chạy chương trình bằng new Function. Các đối tượng runtime (DIL, __dil, GenUI) và những composite component trong catalog được truyền vào dưới dạng tham số.
- Giám sát. Nếu một chương trình không phản hồi trong khoảng thời gian quy định, nó sẽ bị cách ly và worker được khởi động lại.
Runtime là một reconciler nhỏ theo phong cách React. Nó render component và lưu hook state trong các slot có khóa. Tiếp đó, nó so sánh cây kết quả với cây trước đó rồi mã hóa sự khác biệt thành một danh sách các thao tác. Bản thân nó không vẽ gì cả.
Ví dụ minh họa dưới đây cho thấy các thao tác trong lần render đầu tiên, mỗi dòng tương ứng một node. Những mục liệt kê tên thuộc tính của từng phần tử đã được lược bỏ:
1CREATE #1 title SET size = "lg" PLACE under root at 02CREATE #2 text "Ước tính gói Team" PLACE under #1 at 03CREATE #3 text PLACE under root at 14CREATE #4 text "Kéo thanh trượt để xem " PLACE under #3 at 05CREATE #5 bold PLACE under #3 at 16CREATE #6 text "giá hàng tháng" PLACE under #5 at 07CREATE #7 text " cho nhóm của bạn." 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 "/tháng" PLACE under #10 at 2
Hàm không bao giờ rời khỏi worker; handler của thanh trượt chỉ được gửi đi dưới dạng một định danh (fn#1). Trên đường truyền, các thao tác được mã hóa thành chuỗi nhị phân gồm các số nguyên, còn chuỗi ký tự thì lưu ở một bảng riêng.
Rendering
Trang ChatGPT áp dụng các thao tác này vào cây component của chính nó. Mỗi lệnh CREATE sẽ khởi tạo một component native từ design system của ChatGPT, và trang web sẽ tạo hiệu ứng động cho các thay đổi ngay khi chúng xuất hiện. Trang chỉ chấp nhận thao tác dành cho những loại component đã biết, nên output của model không thể chèn markup hay style tùy ý. Ngoại lệ duy nhất là các giá trị CSS thô mà một số thuộc tính cho phép (xem phần Design system và catalog) cùng các ứng dụng AppBlock chạy trong iframe (xem phần Lối thoát).
Tương tác diễn ra theo chiều ngược lại. Khi người dùng kéo thanh trượt đến 9, trang sẽ gửi định danh và tham số của handler tới worker. Worker gọi setSeats(9), render lại và trả về các thao tác cập nhật. Hoàn toàn không cần gọi đến model.
Design system và catalog
Catalog quy định những gì model được phép yêu cầu. Điều này là cần thiết vì model không tự xây dựng giao diện từ các quy tắc layout và styling thô. Nó chọn từ những component mà ChatGPT đã biết cách vẽ, rồi tạo kiểu cho chúng bằng các design token như padding={3}. Một số thuộc tính chấp nhận giá trị CSS thô, ví dụ độ rộng pixel hay mã màu hex, nhưng design token vẫn được ưu tiên hơn. Kết quả là:
- Giao diện được tạo ra trông đồng nhất với phần còn lại của ChatGPT trên mọi nền tảng.
- Trình biên dịch có schema để đối chiếu output. Thuộc tính không tồn tại trên component hoặc giá trị sai kiểu dữ liệu sẽ bị loại bỏ trong lúc biên dịch và ghi nhận thành thông tin chẩn đoán.
Trong một phản hồi mà chúng tôi ghi lại được, trình biên dịch đã loại hai thuộc tính: fill trên một icon (chẩn đoán unknown_prop) và gap="1" trên một box (chẩn đoán invalid_literal).
Catalog gồm ba phần:
- Component native. Khoảng 70 component được định nghĩa trong registry nằm ở mã nguồn client của ChatGPT; 39 trong số đó xuất hiện ở các phản hồi chúng tôi thu thập được.
- Design token dành cho khoảng cách, bo góc, màu sắc và kích thước.
- Composite component do OpenAI viết bằng DIL và gửi sẵn vào sandbox, chẳng hạn như component hình ảnh và sản phẩm. Trong dữ liệu thu thập được, model sử dụng các component này chứ không bao giờ tự định nghĩa component mới.
Streaming
Stream văn bản thì đơn giản: mỗi token mới chỉ cần nối thêm vào những gì đang hiển thị. Nhưng stream một giao diện thì khó hơn nhiều, vì ba lý do:
- Output thường chưa thể chạy được. Ở hầu hết thời điểm, nó là một chương trình chưa hoàn chỉnh, còn thẻ hoặc biểu thức đang mở, và không thể thực thi nguyên trạng.
- Giao diện phải hoạt động liên tục trong lúc đang dần hình thành. Các component mà người dùng đã tương tác phải giữ nguyên state.
- Một số nội dung được gửi đến riêng biệt. Dữ liệu như hình ảnh đến từ server chứ không nằm trong luồng văn bản.
Streaming phía server
Một luồng token nối tiếp đơn thuần không thể đáp ứng điều này. Thay vào đó, ChatGPT stream các bản vá (patch) vào một tin nhắn có cấu trúc, nơi chứa song song văn bản thô, chương trình đã biên dịch và dữ liệu.
Phản hồi được đẩy tới trình duyệt qua luồng server-sent event (POST /backend-api/f/conversation). Mỗi event là một bản cập nhật kiểu JSON-Patch cho tin nhắn đang được xây dựng. Thông thường, một event sẽ cập nhật đồng thời cả văn bản DIL thô lẫn dạng đã biên dịch của nó. Đây là một bản cập nhật trích từ phản hồi chúng tôi ghi lại, đã rút gọn:
1{"o": "patch", "v": [2 {"p": "/message/content/parts/0", "o": "append", "v": " Món cừu nướng Chủ Nhật cùng bạn bè — đồ ăn hào phóng, …"},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": "Đây là kế hoạch cho món cừu nướng Chủ Nhật chuẩn vị cùng bạn bè — …"}},5 {"p": "/message/metadata/model_dil_v2/constants", "o": "append", "v": {"1": "Vì bạn đang"}},6 {"p": "/message/metadata/model_dil_v2/fallbackMarkdown", "o": "append", "v": " Món cừu nướng Chủ Nhật cùng bạn bè — …"}7]}
Server không biên dịch theo kiểu tăng dần. Cứ vài trăm mili-giây, nhiều khả năng là mỗi khi có một chunk output mới từ model, nó lại biên dịch lại toàn bộ những gì model đã viết tính đến thời điểm đó rồi gửi kết quả đi. Quá trình biên dịch bắt đầu ngay từ token đầu tiên, trước khi bất kỳ thẻ nào xuất hiện.
Trình biên dịch phải lo liệu những việc sau:
- Biên dịch một phản hồi mới viết một nửa
- Cập nhật văn bản
- Cập nhật UI
Dòng thời gian diễn ra đại khái như sau:

GIF
Streaming phía client
Trang web chuyển từng bản cập nhật mới cho worker trong sandbox. Worker thực thi nó, render lại dựa trên state hiện có, rồi gửi các thao tác cập nhật về cho trang. State giữ nguyên giá trị qua các lần biên dịch lại nhờ những khóa được thêm vào trong quá trình biên dịch. Nếu chương trình mới không thể thực thi hoặc render, worker sẽ giữ lại phiên bản hoạt động gần nhất.
Sau đó, trang web tạo hiệu ứng động cho từng thay đổi:
- văn bản mờ dần hiện lên trong 0.7 s;
- hàng mới và các phần tử grid trượt vào trong 0.42 s;
- biểu đồ được vẽ trong 1.8 s;
- chiều cao vùng chứa chuyển tiếp mượt mà thay vì nhảy đột ngột.
Lối thoát: AppBlock, một ứng dụng trong iframe

Ứng dụng inline do ChatGPT tạo ra
Một số yêu cầu đòi hỏi những thứ mà component native không được thiết kế để xử lý, chẳng hạn như máy trống tổng hợp âm thanh bằng Web Audio. Với những trường hợp này, model có thể viết một AppBlock: một ứng dụng web độc lập bằng HTML, CSS và JavaScript, nhúng thẳng vào phản hồi. Dưới đây là phần mở đầu của một AppBlock, đã rút gọn:
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 được render theo cách khác biệt so với các component Intelligent UI.
Kết hợp tất cả lại
Bạn nhập một prompt. Model bắt đầu viết giao diện, server biến nó thành thứ mà ChatGPT có thể chạy, còn trang web thì xây dựng dần từng mảnh khi phản hồi stream về. Khi giao diện đã hiện ra, việc kéo thanh trượt hay đánh dấu checkbox sẽ cập nhật cục bộ mà không cần hỏi lại model.
Một ngôn ngữ dành riêng cho model, bước biên dịch trên server, các trình render native cùng một design system có cơ sở vững chắc đã kết hợp mọi thứ lại với nhau. Mỗi bộ phận đảm nhận một vai trò then chốt, và cùng nhau chúng mang đến thế hệ giao diện AI native tiếp theo cho hàng tỷ người dùng trên toàn cầu. Thật là một thời đại tuyệt vời!

Ảnh chụp màn hình Intelligent UI của ChatGPT
Phương pháp nghiên cứu
Mọi quan sát đều đến từ chính các tài khoản ChatGPT của chúng tôi, từ lưu lượng mạng mà ứng dụng web ChatGPT tạo ra, và từ mã JavaScript mà chatgpt.com cung cấp công khai. Nghiên cứu được thực hiện vào tháng 10 năm 2026 với GPT-6 và GPT-6 Thinking.
Phân tích được thực hiện với sự hỗ trợ của Codex & Claude. Bài viết soạn thảo bằng Codex, Hình ảnh minh họa do Claude tạo.
(Bản chi tiết hơn có tại https://www.openui.com/blog/how-chatgpt-intelligent-ui-works





