ChatGPT เปิดตัว Generative UI (ในที่สุดก็มาซะที) และเปลี่ยนชื่อฟิลด์นี้ข้ามคืนเป็น Intelligent UI เราเลยต้องมาดูกันว่าพวกเขาสร้างมันขึ้นมาอย่างไร
https://x.com/OpenAI/status/2107894997538525580
โพสต์นี้คือการเจาะลึกว่า OpenAI สร้าง Intelligent UI เรือธงของพวกเขาอย่างไร ทั้งเลเยอร์ รูปแบบ และการเรนเดอร์แบบเนทีฟทั้งบน Web และ Mobile
องค์ประกอบพื้นฐาน
การนำไปใช้งานของ ChatGPT แบ่งงานออกเป็น 3 ส่วน ได้แก่ โมเดล เซิร์ฟเวอร์แบ็กเอนด์ และไคลเอนต์:
- รูปแบบ Inference: โมเดลเขียนอินเทอร์เฟซด้วย DIL ซึ่งผสมผสาน Markdown เข้ากับแท็กที่คล้าย JSX และ JavaScript
- การคอมไพล์ฝั่งเซิร์ฟเวอร์: เซิร์ฟเวอร์แปลงแต่ละ partial response ให้กลายเป็นโปรแกรม JavaScript และเอกสาร JSON ที่ประกอบด้วยข้อความและข้อมูล
- Runtime ฝั่งไคลเอนต์: sandboxed runtime จะรันโปรแกรมและสร้าง UI operations ขึ้นมา
- การเรนเดอร์: ChatGPT นำ operations เหล่านั้นไปใช้กับ native components ของตัวเอง
- Design system และ catalog: คอมโพเนนต์ พร็อพเพอร์ตี้ และ design tokens ที่โมเดลสามารถเรียกใช้ได้

แผนผังสถาปัตยกรรม ChatGPT Intelligent UI
รูปแบบ Inference
นี่คือสิ่งที่โมเดลเขียนออกมา ใน ChatGPT มันคือภาษาที่ OpenAI เรียกว่า DIL: ใช้ Markdown สำหรับข้อความ แท็กคล้าย JSX สำหรับคอมโพเนนต์ และ JavaScript สำหรับ state กับ logic เราจะลองติดตาม response เล็กๆ อันหนึ่งผ่านทุกเลเยอร์:
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}/mo</title>8</box>
หัวข้อและย่อหน้าเป็น Markdown ธรรมดา ส่วนแท็กต่างๆ คือคอมโพเนนต์จาก catalog ของ ChatGPT บรรทัด {@body …} สองบรรทัดนั้นคือ JavaScript: บรรทัดแรกประกาศ state ชื่อ seats และบรรทัดที่สองคำนวณ price จากค่านั้น slider ถูกผูกไว้กับ seats ดังนั้นเมื่อเลื่อนแถบ ราคาจะอัปเดตตามทันที
จำเป็นต้องมีรูปแบบเฉพาะ เพราะโมเดลเขียนอินเทอร์เฟซออกมาทีละ token
- ต้องเขียนได้ง่ายและแม่นยำ จึงสร้างขึ้นจากรูปแบบสัญลักษณ์ที่โมเดลคุ้นเคยอยู่แล้ว
- ต้องใช้งานได้แม้จะเขียนยังไม่เสร็จ คำสั่งต่างๆ จะอยู่คนละบรรทัด และอิลิเมนต์ที่เปิดค้างไว้จะถูกปิดโดยอัตโนมัติ ทำให้เซิร์ฟเวอร์ตัด partial response ที่โครงสร้างสมบูรณ์สุดท้ายแล้วนำไปคอมไพล์ต่อได้
การคอมไพล์ฝั่งเซิร์ฟเวอร์
ไคลเอนต์จะไม่รันเอาต์พุตของโมเดลตามที่เขียนมาโดยตรง เซิร์ฟเวอร์ของ OpenAI จะคอมไพล์มันให้เป็นโปรแกรม JavaScript และเอกสาร JSON แล้วเก็บไว้พร้อมกับข้อความนั้น (ในชื่อ model_dil_v2) response จะถูกคอมไพล์ออกมาเป็นแบบนี้ (จัดรูปแบบให้อ่านง่ายขึ้น):
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": "/mo"9 },10 "appData": { "opGenui": { "componentResults": {}, "modelDataBindings": {} } }11}
Markdown ถูกคอมไพล์เข้าไปใน tree เดียวกันกับคอมโพเนนต์ หัวข้อกลายเป็น title ย่อหน้ากลายเป็น text ที่มี bold อยู่ข้างใน และคำต่างๆ จะถูกย้ายเข้าไปอยู่ในตาราง constants
การคอมไพล์ช่วยจัดการงานที่มิฉะนั้นไคลเอนต์ทุกตัวจะต้องทำซ้ำเอง:
- การเรียกฟังก์ชันแบบตรงไปตรงมา Markup จะถูกแปลงเป็นการเรียก __dil.jsx ทำให้ JavaScript runtime ประมวลผลโปรแกรมได้โดยไม่ต้องมี parser สำหรับ DIL
- การแยกส่วนข้อผิดพลาด นิพจน์ต่างๆ จะถูกครอบด้วย __dilSafe ดังนั้นถ้านิพจน์ไหนเกิด error ก็จะแค่ลบอิลิเมนต์นั้นทิ้ง แทนที่จะทำให้การเรนเดอร์ทั้งหมดหยุดชะงัก
- ข้อความแยกไว้อีกตาราง ข้อความคงที่จะถูกย้ายไปที่ตาราง constants เมื่อ response แบบ stream ไหลเข้ามา ข้อความที่ยาวขึ้นจะเปลี่ยนแค่ข้อมูล ไม่ใช่เปลี่ยนตัวโปรแกรม
- State keys ที่เสถียร state แต่ละตัวจะได้รับ key ({ key: "seats" }) เพื่อให้ค่าของมันยังคงอยู่ทุกรอบการคอมไพล์ใหม่
- การซ่อมแซมและตรวจสอบ คำสั่งและแท็กที่ไม่สมบูรณ์จะถูกตัดทิ้ง อิลิเมนต์ที่ไม่ได้ปิดจะถูกปิดให้ และพร็อพเพอร์ตี้ที่ตรวจสอบกับ catalog ไม่ผ่านจะถูกลบออกพร้อมบันทึกเป็น diagnostics
เอกสาร JSON จะเก็บค่าคงที่ของข้อความและข้อมูลที่เซิร์ฟเวอร์ประมวลผลมาให้สำหรับ response นั้น เช่น ผลการค้นหาภาพ (ดูหัวข้อ Data)
Runtime ฝั่งไคลเอนต์
ไคลเอนต์จะได้รับโปรแกรมที่คอมไพล์แล้วและเอกสาร JSON งานของมันแบ่งออกเป็นสองส่วน คือ runtime ที่ทำหน้าที่รันโปรแกรม และ renderer ที่วาดผลลัพธ์ออกมา
เนื่องจากโปรแกรมนี้เป็นโค้ดที่โมเดลเขียนขึ้น จึงไม่ได้รันในหน้าเพจของ ChatGPT โดยตรง ChatGPT จะโหลด iframe ซ่อนไว้ (runner.html) ที่ถูก sandbox ด้วย allow-scripts และมี content security policy เป็น default-src 'none' ซึ่งจะเริ่มต้น Web Worker ขึ้นมา
- Lockdown ก่อนประเมินผลโปรแกรม worker จะตัดการเข้าถึงเครือข่าย ตัวจับเวลา การส่งข้อความ และการประเมินโค้ดแบบไดนามิกออกจาก global scope ของมัน แล้วทำการ freeze globals ที่เหลือไว้
- การประเมินผล จากนั้นจะประเมินผลโปรแกรมด้วย new Function โดยส่งออบเจกต์ของ runtime (DIL, __dil, GenUI) และ composite components จาก catalog เข้าไปเป็นพารามิเตอร์
- Watchdog หากโปรแกรมไม่ตอบสนองภายในเวลาที่กำหนด จะถูกกักกัน (quarantine) และ worker จะถูกรีสตาร์ทใหม่
runtime นี้เป็น reconciler ขนาดเล็กสไตล์ React มันจะเรนเดอร์คอมโพเนนต์และเก็บ hook state ไว้ใน keyed slots จากนั้นเปรียบเทียบ tree ที่ได้กับ tree ก่อนหน้า แล้วเข้ารหัสความแตกต่างให้อยู่ในรูปของรายการ operations โดยไม่ได้วาดอะไรเลย
ตัวอย่างต่อไปนี้แสดงให้เห็น operations จากการเรนเดอร์ครั้งแรก โดยหนึ่งบรรทัดแทนหนึ่งโหนด รายการที่แสดงชื่อพร็อพเพอร์ตี้ของแต่ละอิลิเมนต์ถูกตัดออกเพื่อความกระชับ:
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 "/mo" PLACE under #10 at 2
ฟังก์ชันจะไม่หลุดออกจาก worker เด็ดขาด handler ของ slider จะถูกส่งไปในรูปแบบของ identifier เท่านั้น (fn#1) ในการรับส่งข้อมูลจริง operations จะถูกเข้ารหัสเป็นลำดับเลขฐานสอง (binary sequence) ของจำนวนเต็ม โดยแยกสตริงไปเก็บไว้ในอีกตารางหนึ่ง
การเรนเดอร์
หน้าเพจของ ChatGPT จะนำ operations ไปใช้กับ component tree ของตัวเอง แต่ละคำสั่ง CREATE จะสร้าง native component จาก design system ของ ChatGPT ขึ้นมา และหน้าเพจจะใส่แอนิเมชันให้กับการเปลี่ยนแปลงทันทีที่ได้รับมา หน้าเพจจะยอมรับ operations เฉพาะประเภทคอมโพเนนต์ที่รู้จักเท่านั้น เอาต์พุตของโมเดลจึงไม่สามารถแทรก markup หรือ style อะไรก็ได้เข้ามา ข้อยกเว้นคือค่า CSS ดิบที่บางพร็อพเพอร์ตี้ยอมรับ (ดูหัวข้อ Design system และ catalog) และแอป AppBlock ที่รันใน iframe (ดูหัวข้อ Escape hatch)
ส่วนการโต้ตอบจะทำงานในทิศทางตรงกันข้าม เมื่อผู้ใช้ลาก slider ไปที่ 9 หน้าเพจจะส่ง identifier และอาร์กิวเมนต์ของ handler ไปให้ worker จากนั้น worker จะเรียก setSeats(9) แล้วเรนเดอร์ใหม่และส่ง update operations กลับมา โดยไม่ต้องเรียกใช้โมเดลเลย
Design system และ catalog
catalog กำหนดว่าโมเดลสามารถร้องขออะไรได้บ้าง สิ่งนี้จำเป็นเพราะโมเดลไม่ได้สร้างอินเทอร์เฟซจากกฎการจัดวางและสไตล์แบบดิบๆ แต่เลือกจากคอมโพเนนต์ที่ ChatGPT รู้วิธีวาดอยู่แล้ว และตกแต่งด้วย design tokens เช่น padding={3} ค่า CSS ดิบอย่างความกว้างเป็นพิกเซลหรือรหัสสีแบบ hex สามารถใช้กับบางพร็อพเพอร์ตี้ได้ แต่ระบบจะแนะนำให้ใช้ design tokens มากกว่า ผลลัพธ์ที่ได้คือ:
- อินเทอร์เฟซที่สร้างขึ้นมามีหน้าตาเหมือนส่วนอื่นๆ ของ ChatGPT บนทุกแพลตฟอร์ม
- คอมไพเลอร์มี schema ไว้ตรวจสอบเอาต์พุต พร็อพเพอร์ตี้ที่ไม่มีอยู่ในคอมโพเนนต์ หรือ literal ที่ผิดประเภท จะถูกลบออกระหว่างการคอมไพล์และบันทึกเป็น diagnostic
ใน response หนึ่งที่เราบันทึกไว้ คอมไพเลอร์ได้ลบพร็อพเพอร์ตี้ออกไปสองตัว คือ fill บน icon (diagnostic แบบ unknown_prop) และ gap="1" บน box (diagnostic แบบ invalid_literal)
catalog ประกอบด้วย 3 ส่วน:
- Native components มีคอมโพเนนต์ราว 70 ตัวที่ถูกนิยามไว้ใน component registry ในโค้ดไคลเอนต์ของ ChatGPT โดย 39 ตัวในนั้นปรากฏใน response ที่เราบันทึกมาได้
- Design tokens สำหรับระยะห่าง รัศมี สี และขนาด
- Composite components ที่ OpenAI เขียนขึ้นด้วย DIL และส่งเข้าไปใน sandbox แบบสำเร็จรูปแล้ว เช่น คอมโพเนนต์ image และ product ในการบันทึกของเรา โมเดลใช้คอมโพเนนต์เหล่านี้แต่ไม่เคยสร้างคอมโพเนนต์ของตัวเองเลย
Streaming
การสตรีมข้อความนั้นง่ายมาก แค่เพิ่ม token ใหม่ต่อท้ายสิ่งที่อยู่บนหน้าจออยู่แล้ว แต่การสตรีมอินเทอร์เฟซนั้นยากกว่า ด้วยเหตุผล 3 ข้อ:
- เอาต์พุตมักยังรันไม่ได้ ในหลายจังหวะมันยังเป็นโปรแกรมที่ไม่สมบูรณ์ มีแท็กหรือนิพจน์เปิดค้างอยู่ จึงไม่สามารถนำไปรันตามที่เขียนมาได้
- อินเทอร์เฟซต้องใช้งานได้ตลอดขณะที่กำลังขยายตัว คอมโพเนนต์ที่ผู้ใช้แตะไปแล้วต้องรักษา state ของมันไว้
- เนื้อหาบางส่วนมาถึงแยกต่างหาก ข้อมูลอย่างรูปภาพมาจากเซิร์ฟเวอร์ ไม่ได้มาจากข้อความ
Streaming ฝั่งเซิร์ฟเวอร์
การสตรีม token ที่ต่อกันไปเรื่อยๆ แบบธรรมดานั้นไม่สามารถรองรับเรื่องนี้ได้ ChatGPT จึงใช้วิธีสตรีม patch ไปยัง structured message ที่เก็บ raw text โปรแกรมที่คอมไพล์แล้ว และข้อมูลของมันไว้เคียงข้างกัน
response จะเดินทางมาถึงเบราว์เซอร์ผ่าน server-sent event stream (POST /backend-api/f/conversation) แต่ละ event คือการอัปเดตแบบ JSON-Patch-style ให้กับ message ที่กำลังถูกสร้างขึ้น โดยปกติ event เดียวจะอัปเดตทั้ง raw DIL text และรูปแบบที่คอมไพล์แล้วไปพร้อมกัน นี่คือตัวอย่างการอัปเดตหนึ่งครั้งจาก response ที่เราบันทึกมา (ตัดให้สั้นลง):
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]}
เซิร์ฟเวอร์ไม่ได้คอมไพล์แบบค่อยเป็นค่อยไป แต่ทุกๆ ไม่กี่ร้อยมิลลิวินาที ซึ่งน่าจะเป็นทุกครั้งที่ได้รับ chunk ใหม่จากเอาต์พุตของโมเดล มันจะคอมไพล์ทุกอย่างที่โมเดลเขียนมาจนถึงจุดนั้นใหม่ทั้งหมดแล้วส่งผลลัพธ์ออกไป การคอมไพล์เริ่มตั้งแต่ token แรก ก่อนที่แท็กใดๆ จะปรากฏขึ้นด้วยซ้ำ
คอมไพเลอร์ต้องดูแลเรื่องต่อไปนี้:
- การคอมไพล์ response ที่เขียนยังไม่เสร็จ
- การอัปเดตข้อความ
- การอัปเดต UI
ไทม์ไลน์จะออกมาประมาณนี้:

GIF
Streaming ฝั่งไคลเอนต์
หน้าเพจจะส่งการอัปเดตใหม่แต่ละครั้งไปยัง sandboxed worker จากนั้น worker จะประเมินผล เรนเดอร์ใหม่โดยใช้ state เดิมที่มีอยู่ แล้วส่ง update operations กลับไปยังหน้าเพจ state จะรักษาค่าเดิมไว้ได้ตลอดการคอมไพล์ใหม่เพราะ key ที่เพิ่มเข้ามาระหว่างการคอมไพล์ หากโปรแกรมใหม่ประเมินผลหรือเรนเดอร์ไม่ผ่าน worker จะใช้โปรแกรมล่าสุดที่ยังทำงานได้อยู่ต่อไป
จากนั้นหน้าเพจจะใส่แอนิเมชันให้การเปลี่ยนแปลงแต่ละจุด:
- ข้อความจะค่อยๆ เฟดเข้ามาในเวลา 0.7 วินาที
- แถวและ grid items ใหม่จะสไลด์เข้ามาในเวลา 0.42 วินาที
- กราฟจะค่อยๆ วาดตัวในเวลา 1.8 วินาที
- ความสูงของ container จะเปลี่ยนแบบ smooth transition แทนที่จะกระโดดทีเดียว
ทางออกสำรอง: AppBlock แอปใน iframe

แอปอินไลน์ที่สร้างโดย ChatGPT
บาง request ต้องการสิ่งที่ native components ไม่ได้ออกแบบมาเพื่อรองรับ เช่น กลองไฟฟ้าที่สังเคราะห์เสียงด้วย Web Audio ในกรณีเหล่านี้ โมเดลสามารถเขียน AppBlock ได้ ซึ่งเป็นเว็บแอปแบบครบจบในตัวด้วย HTML, CSS และ JavaScript ที่ฝังอยู่ใน response นี่คือส่วนเริ่มต้นของ AppBlock หนึ่ง (ตัดให้สั้นลง):
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>
AppBlocks จะถูกเรนเดอร์ต่างจากคอมโพเนนต์ของ Intelligent UI
สรุปภาพรวมทั้งหมด
คุณพิมพ์ prompt ลงไป โมเดลเริ่มเขียนอินเทอร์เฟซ เซิร์ฟเวอร์แปลงมันเป็นสิ่งที่ ChatGPT รันได้ และหน้าเพจก็ค่อยๆ ประกอบร่างมันขึ้นมาทีละชิ้นขณะ response สตรีมเข้ามา พอทุกอย่างพร้อม การเลื่อน slider หรือติ๊ก checkbox จะอัปเดตอินเทอร์เฟซในเครื่องทันที โดยไม่ต้องไปถามโมเดลใหม่
ภาษาเฉพาะของโมเดล ขั้นตอนการคอมไพล์บนเซิร์ฟเวอร์ ตัวเรนเดอร์แบบเนทีฟ และ design system ที่มีรากฐานชัดเจน ล้วนผสานเข้าด้วยกัน ทุกส่วนทำหน้าที่สำคัญของตนเอง และเมื่อรวมกัน พวกมันก็มอบอินเทอร์เฟซยุคใหม่ของ AI แบบเนทีฟให้กับผู้ใช้นับพันล้านคนทั่วโลก ช่างเป็นช่วงเวลาที่น่าตื่นเต้นจริงๆ!

ภาพหน้าจอของ ChatGPT Intelligent UI
ระเบียบวิธีวิจัย
ข้อสังเกตทั้งหมดมาจากบัญชี ChatGPT ของเราเอง จาก traffic ที่เว็บแอป ChatGPT สร้างขึ้น และจาก JavaScript ที่ chatgpt.com เผยแพร่สู่สาธารณะ ข้อมูลเหล่านี้รวบรวมในเดือนตุลาคม 2026 โดยใช้ GPT-6 และ GPT-6 Thinking
วิเคราะห์ข้อมูลด้วยความช่วยเหลือจาก Codex & Claude เขียนบทความด้วย Codex ภาพประกอบโดย Claude
(อ่านฉบับเจาะลึกเพิ่มเติมได้ที่ https://www.openui.com/blog/how-chatgpt-intelligent-ui-works





