ChatGPT が Generative UI をついにリリースし、一夜にしてその名称を Intelligent UI に変更しました。私たちは、それがどのように実装されているのかを確認せずにはいられませんでした。
https://x.com/OpenAI/status/2107894997538525580
この記事では、 OpenAI がフラッグシップ機能である Intelligent UI をどのように実装したのか、Web とモバイルの両方でネイティブに動作するレイヤー、フォーマット、レンダリングの仕組みについて詳しく解説します。
構成要素
ChatGPT の実装では、モデル、バックエンドサーバー、クライアントの間で処理が分担されています。
- 推論フォーマット:モデルは DIL という言語でインターフェースを記述します。これは Markdown に JSX 風のタグと JavaScript を組み合わせたものです。
- サーバーサイドコンパイル:サーバーは部分的なレスポンスごとに、JavaScript プログラムとテキスト・データを含む JSON ドキュメントへ変換します。
- クライアントランタイム:サンドボックス化されたランタイムがプログラムを実行し、UI 操作を生成します。
- レンダリング:ChatGPT は、自前のネイティブコンポーネントに対してこれらの操作を適用します。
- デザインシステムとカタログ:モデルが利用できるコンポーネント、プロパティ、デザイントークンの一覧です。

ChatGPT Intelligent UI アーキテクチャ図
推論フォーマット
これはモデルが出力するものです。ChatGPT では、OpenAI が DIL と呼ぶ言語が使われています。文章には Markdown 、コンポーネントには JSX 風のタグ、状態やロジックには JavaScript を用います。ここでは、1 つの小さなレスポンスを例に、各レイヤーを追ってみましょう。
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 のカタログにあるコンポーネントです。2 行の {@body …} は JavaScript で、1 行目は seats という状態を宣言し、2 行目はそこから price を算出しています。スライダーは seats にバインドされているため、動かすと料金が更新されます。
モデルがインターフェースをトークン単位で生成していくため、専用のフォーマットが必要になります。
- 確実に書きやすいこと。 そのため、モデルがすでに熟知している記法を組み合わせて作られています。
- 途中で生成されていても使えること。 文はそれぞれ独立した行に配置され、開いたままの要素は自動的に閉じることができます。これにより、サーバーは最後の完全な構文で部分レスポンスを切り取り、そのままコンパイルできます。
サーバーサイドコンパイル
クライアントがモデルの出力をそのまま実行することはありません。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 ドキュメントを受け取ります。処理は、プログラムを実行するランタイムと、結果を描画するレンダラーに分かれています。
プログラムはモデルが生成したコードであるため、ChatGPT のページ上で直接実行されることはありません。ChatGPT は非表示の iframe (runner.html)を読み込みます。この iframe は allow-scripts と default-src 'none' というコンテンツセキュリティポリシーでサンドボックス化されており、Web Worker を起動します。
- ロックダウン。 プログラムを評価する前に、Worker はグローバルスコープからネットワークアクセス、タイマー、メッセージング、動的コード評価を削除し、残りのグローバル変数を凍結します。
- 評価。 次に new Function を使ってプログラムを評価します。ランタイムオブジェクト(DIL、__dil、GenUI)とカタログの複合コンポーネントは、パラメータとして渡されます。
- ウォッチドッグ。 タイムアウト内に応答しないプログラムは隔離され、Worker が再起動されます。
ランタイムは React 風の小さなリコンサイラーです。コンポーネントをレンダリングし、フックの状態をキー付きのスロットに保持します。その後、生成されたツリーを前回のものと比較し、差分を操作リストとしてエンコードします。描画自体は行いません。
以下の例は、初回レンダリング時の操作を示したもので、1 行が 1 ノードに対応しています。各要素のプロパティ名を列挙した項目は省略しています。
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} のようなデザイントークンを使ってスタイルを適用するため、このカタログが必要です。ピクセル幅や 16 進カラーコードといった生の CSS 値も一部のプロパティで受け付けますが、基本はデザイントークンが推奨されます。その結果、次のようになります。
- 生成されたインターフェースは、どのプラットフォームでも ChatGPT の他の部分と同じ見た目になります。
- コンパイラが出力を検証するためのスキーマを持ちます。コンポーネントに存在しないプロパティや型の誤ったリテラルは、コンパイル時に削除され、診断情報として記録されます。
私たちがキャプチャしたあるレスポンスでは、コンパイラが 2 つのプロパティを削除していました。icon の fill (unknown_prop 診断)と、box の gap="1" (invalid_literal 診断)です。
カタログは 3 つの部分で構成されています。
- ネイティブコンポーネント。 ChatGPT のクライアントコード内のコンポーネントレジストリに約 70 個のコンポーネントが定義されており、そのうち 39 個が私たちがキャプチャしたレスポンスに登場しました。
- 余白、角丸、色、サイズなどのデザイントークン。
- image や product コンポーネントのように、OpenAI が DIL で記述し、ビルド済みの状態でサンドボックスに送信する複合コンポーネント。私たちの調査では、モデルはこれらのコンポーネントを使用しましたが、独自に定義することはありませんでした。
ストリーミング
テキストのストリーミングは単純です。新しいトークンを画面に表示済みの内容へ追加していくだけです。しかし、インターフェースのストリーミングは次の 3 つの理由から難しくなります。
- 出力はまだ実行可能な状態ではないことが多い。 ほとんどの時点ではタグや式が開いたままの不完全なプログラムであり、そのままでは実行できません。
- 生成途中でもインターフェースが動作し続ける必要がある。 ユーザーがすでに触れたコンポーネントは、その状態を維持しなければなりません。
- 一部のコンテンツは別途届く。 画像などのデータはテキストからではなく、サーバーから提供されます。
サーバーサイドのストリーミング
トークンをただ追加していく通常のストリームでは、これを表現できません。そこで ChatGPT は、生テキスト、コンパイル済みプログラム、データを並べて保持する構造化メッセージに対して、パッチをストリーミングします。
レスポンスは Server-Sent Events ストリーム(POST /backend-api/f/conversation)を通じてブラウザに届きます。各イベントは、構築中のメッセージに対する JSON Patch 形式の更新です。1 つのイベントで、通常は生の 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]}
サーバーはインクリメンタルにコンパイルしません。数百ミリ秒ごと、おそらくモデル出力の新しいチャンクが届くたびに、それまでにモデルが生成したすべてを再コンパイルし、結果を送信します。コンパイルはタグが登場する前の最初のトークンから始まります。
コンパイラは次の処理を行う必要があります。
- 途中まで書かれたレスポンスのコンパイル
- テキストの更新
- UI の更新
タイムラインはおおよそ次のようになります。

GIF
クライアントサイドのストリーミング
ページは新しい更新をサンドボックス化された Worker に渡します。Worker はそれを評価し、既存の状態で再レンダリングを行い、更新操作をページに送り返します。コンパイル時に追加されたキーのおかげで、状態は再コンパイルをまたいで値を保持します。新しいプログラムの評価やレンダリングに失敗した場合、Worker は最後に正常に動作したものを維持します。
その後、ページは各変更をアニメーションさせます。
- テキストは 0.7 秒かけてフェードイン
- 新しい行やグリッドアイテムは 0.42 秒かけてスライドイン
- グラフは 1.8 秒かけて描画
- コンテナの高さはジャンプせずにスムーズに遷移
エスケープハッチ:iframe 内のアプリ AppBlock

ChatGPT が生成したインラインアプリ
Web Audio で音を合成するドラムマシンのように、ネイティブコンポーネントでは対応できないリクエストもあります。そのような場合、モデルは AppBlock を生成できます。これは HTML 、CSS 、JavaScript で構成された自己完結型の Web アプリで、レスポンス内に埋め込まれます。以下はその冒頭部分(短縮版)です。
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 で実行できる形に変換し、レスポンスのストリーミングに合わせてページが少しずつ組み立てていきます。一度表示されれば、スライダーを動かしたりチェックボックスをオンにしたりする操作は、モデルに再度問い合わせることなくローカルで UI を更新します。
モデル専用の言語、サーバーでのコンパイルステップ、ネイティブレンダラー、そして堅牢なデザインシステムが、これらすべてを結びつけています。それぞれの要素が重要な役割を果たし、一体となることで、世界中の何十億人ものユーザーに次世代の AI ネイティブインターフェースを提供しています。なんて素晴らしい時代でしょう!

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 をご覧ください)





