ChatGPT Intelligent UI の技術解剖:モデルがUIを記述し、コンパイラとクライアントがそれを動かす

DIL による記述、サーバーサイドのコンパイル、クライアント実行の一連の流れに沿って、ストリーミング表示、状態更新、データ注入、そして AppBlock を分解する。

著者:Rabi Guha / OpenUI10-09 · 約12分

ChatGPT Intelligent UI は、回答をボタンやグラフ、スライダーを備えたインターフェースへと変換できます。原文にある解説用の月額見積もりでは、シート数を調整するだけで価格の変化を確認できますが、この回答を生成するステップと、ドラッグした後に更新されるステップはまったく同一ではありません。

このシステムは処理をモデル、コンパイラ、クライアントに分担させています:モデルは DIL を使ってテキスト、コンポーネント、計算ロジックを記述し、コンパイラはその記述をプログラムとデータへと変換し、クライアントはロジックを実行してネイティブコンポーネントで結果を表示します。ユーザーがその後スライダーを動かした際、すでに記述済みの計算はクライアント側でローカルに完了するため、モデルに再生成させる必要はありません。

AI インターフェースエンジニアリングチームの Thesys は、公開されたウェブトラフィックを分析し、サーバー送信イベント(SSE)のパッチをキャプチャし、フロントエンドに配信されるコアランタイムスクリプトとコンポーネントレジストリを調査することで、ウェブ版におけるモデル出力からUI表示に至る実行パイプラインを明らかにしました。(以下の実装の詳細は、Thesys によるウェブ版のリバースエンジニアリングによる観察に基づいています)

ChatGPT Intelligent UI のコアアーキテクチャのデータフロー図
Thesys がリバースエンジニアリングによって再現した ChatGPT Intelligent UI の4層データ流通路:モデルが DIL を出力し、サーバーが未完成の構文の補修と定数の抽出を担当し、クライアントの Worker サンドボックスが差分を計算し、ホストページがネイティブ描画を行う。(出典:Thesys アーキテクチャ解説図)

モデル、コンパイラ、クライアントはそれぞれ何を担っているのか?

ネイティブコンポーネントの実行経路において、クライアントの役割はさらに2つに分かれます。ランタイムがロジックを実行して更新操作を生成し、レンダラーがその操作に従ってコンポーネントを描画します。同一の月額見積もりを例に、モデル、コンパイラ、クライアントの役割分担を順を追ってつなぎ合わせてみましょう。

モデル:DIL を用いてコンテンツ、コンポーネント、ロジックを記述する

大規模モデルが回答を生成する際、使用されるのは単なるプレーンテキストでも従来の JSON 構文木でもなく、DIL と呼ばれる専用言語です。これは以下の3つの要素が融合したものです:

  • 標準的な Markdown:段落のテキストや見出しのレイアウトを記述します。
  • JSX 風のコンポーネントタグ:レイアウトコンテナや機能コンポーネント(<box>、<slider>、<title> など)を宣言します。
  • インライン JavaScript({@body ...} で表記):内部のリアクティブな状態(State)や算出ロジックを宣言します。

典型的な解説用見積もりのサンプルを見てみましょう(注:サンプルの $29/席/月は計算ロジックをデモするための解説用データであり、実際のプラン価格ではありません):

## Team plan estimate
Drag the slider to see the **monthly price** for your team.
{@body const [seats,setSeats] = DIL.useState(8)}
{@body const price = seats*29}
<box border padding={3} gap={2}>
  <slider min={1} max={50} value={seats} onChange={setSeats}/>
  <title size="xl">${price}/mo</title>
</box>

このコードスニペットでは、モデルが DIL.useState(8) によって初期シート数を 8 と定義し、標準的な JavaScript 式で合計金額を計算し、状態をスライダーコンポーネントにバインドしています。元ポストではメイン経路を「モデルは生のコードを一切書かない」と要約していますが、これは正確ではありません——モデルは実際に JavaScript のロジックを直接記述しており、それが専用のマークアップ言語の中に配置されているに過ぎません。

コンパイラ:記述を実行可能なプログラムとデータに変換する

クライアントのブラウザは、モデルが生成した未加工の DIL 文字列をそのまま直接実行するわけではありません。OpenAI のバックエンドサーバーがモデルの出力ストリームを処理し、単一の JavaScript 実行関数と JSON 定数設定テーブル(セッションメタデータ内では model_dil_v2 と表記)へと動的にコンパイルします。回答に含まれる標準的な Markdown(見出し、段落、太字)も同一のコンポーネントツリーへとコンパイルされ(title や bold 付きの text などに変換)、テキスト部分は constants 定数テーブルへと切り離されて移されます。

サーバーサイドコンパイラは主に以下の5つの重要な加工を行います:

  • 関数の標準化:XML スタイルのすべてのタグを __dil.jsx(...) 関数呼び出しへとトランスパイルするため、クライアント側に DIL 構文のパーサーを用意する必要がなくなります。
  • 式の耐障害性向上:コンパイラは式を __dilSafe でラップし、エラー発生時にはフォールバック値を返すことで局所的なエラーを隔離します。これにより、1箇所の例外が回答全体に波及するのを抑えられますが、計算式自体が正しいかどうかまでは検証しません。
  • テキストの定数テーブルへの抽出:静的なテキストは constants というハッシュ辞書に移されます(例:{"0": "Team plan estimate", "4": "$"})。これにより、その後のタイピングストリームにおいてテキストが増加しても、JSON 辞書を変更するだけで済み、JavaScript コードそのものを変更する必要がなくなります。
  • 状態への安定キーの付与:各ステートフックに安定したキー(例:{ key: "seats" })を注入し、UI が後から再コンパイルされた場合でも、ユーザーが動かしたスライダーの位置が初期値にリセットされないようにします。
  • ホワイトリスト検証と自動修復:デザインシステムの辞書(Catalog)に基づいて不正なプロパティを厳格にフィルタリングします(例えば、モデルが未定義の fill プロパティや形式の誤った数値を記述した場合、コンパイル時に直接除外して診断フラグを付与します)。

クライアント:ロジックを隔離して実行し、結果をネイティブUIとして表示する

サンドボックス内でロジックを実行し、状態を保持する

大規模モデルが生成したコードは、本質的に信頼できない外部入力です。ホストページ上で直接実行すれば、XSS 攻撃やデータ漏洩の深刻なリスクをもたらします。

ChatGPT はブラウザ側で階層的なサンドボックス分離アプローチを採用しています:

  • ページは非表示のサンドボックス iframe(runner.html)をあらかじめ読み込み、厳格な CSP(コンテンツセキュリティポリシー:default-src 'none')を付与してリソースアクセスを遮断します。
  • この iframe は直ちに専用の Web Worker を起動します。
  • 利用可能な機能の制限:Worker はプログラムを実行する前に、モデルコードがアクセス可能なネットワーク、タイマー、メッセージング、動的コード実行の各機能を排除し、残りのグローバルオブジェクトを凍結します。ランタイム自体は制御された評価エントリを介してコンパイル済みプログラムを実行します。
  • 内蔵されたミニ調和エンジン(Mini Reconciler):Worker 内部では React に似たリコンサイラ(調和エンジン)が動作しています。コンポーネントロジックを実行し、新しいコンポーネントツリーと前回のツリーを比較して必要な更新操作を算出し、低レベルのバイナリ UI 操作命令(例:CREATE #9 slider)へとシリアライズします。
  • 関数はサンドボックスの外に出ない:コンポーネントにバインドされたイベントハンドラ関数(onChange など)はメインページには一切送信されず、メインページが受け取るのは識別子(fn#1 など)のみです。
  • ウォッチドッグ機構(Watchdog):あるロジックが無限ループに陥ったり実行がタイムアウトしたりした場合、ウォッチドッグが Worker を強制終了して再起動します。

ホストページでコンポーネントを描画し、ユーザー操作を受け付ける

メインページは Worker から出力された UI 命令を受け取ると、ChatGPT 自身のネイティブコンポーネントライブラリ(約 70 個のネイティブコンポーネントを含み、キャプチャされた応答内には 39 個が出現)へとマッピングします。ページには標準のアニメーションが組み込まれており、テキストは 0.7 秒でフェードインし、新しい行は 0.42 秒でスライドインし、グラフの展開には 1.8 秒かかります。

さらに重要なのが逆方向のインタラクション経路です。ユーザーが指やマウスでシート数を 8 から 9 にドラッグすると、メインページは fn#1(9) だけを Worker に送信します。Worker 内部で setSeats(9) が実行され、リコンサイラが価格を $261/mo と再計算し、メインページへ UI 更新操作を送り返します。この価格計算はクライアント側で完了し、モデルを再度呼び出す必要はありません。ただし、これはページ全体がステータス通知などの他のネットワークリクエストを一切発行しないという意味ではありません。

生成された UI が ChatGPT ネイティブのビジュアルと統一感を保てる核心は、各層で共有されているコンポーネント辞書とデザインシステム(Catalog)にあります。ここには主に3つの内容が含まれています:

  • ネイティブコンポーネント:ボタン、スライダー、テキストなど、クライアントが描画方法をすでに知っている要素群。
  • デザイントークン(Token):余白、角丸、カラーなどのルールを統一します。一部の属性ではピクセル幅や16進数カラーコードも受け付けますが、トークンの指定が優先されます。
  • ビルド済みの複合コンポーネント:OpenAI があらかじめ DIL で記述し、サンドボックスに送り込んでいる画像コンポーネントやプロダクトコンポーネントなど。

分析チームがキャプチャしたサンプルでは、モデルはビルド済みコンポーネントを呼び出すのみで、自ら新しい複合コンポーネントを定義することはありませんでした。

Catalog は同時に3重の制約としても機能します。モデルが事前定義されたタグのみを選択するよう制限し、コンパイラが属性や型を検証するための標準スキーマ(Schema)となり、ホストページがどのように具体的に描画すべきかを指示します。

低レベルの UI 命令のみをやり取りするこのプロトコル自体は、優れたクロスプラットフォームの汎用性を備えています。サンドボックスの実行環境には React Native エンジン Hermes に必要なグローバルオブジェクトが保持されているものの、モバイル版で全く同一のランタイムが動作しているかどうかは、現時点では分析チームがコードの手がかりから推測した段階に過ぎず、直接のパケット検証はまだ行われていません。

スライダーを動かした後の変化はクライアント側で発生

初回生成時:① → ② → ③ → ④。ドラッグ操作時:④ → ③ → ④;①②の初期プログラムは変化しません。

デモ解説:ローカル計算
今回の調整でのモデル呼び出し: 0 回
シート数を調整 (seats): 8 人
① モデル出力 (DIL) 初期記述は不変

初回生成:初期値 8 シート、価格 = シート数 × 29。スライダーを動かしてもこの記述は書き換わりません。

DIL 記述を表示
## チーム月額見積もり
Drag the slider...
{@body const [seats,setSeats] 
       = DIL.useState(8)}
{@body const price = seats*29}
<box border padding={3} gap={2}>
  <slider min={1} max={50} 
          value={seats} 
          onChange={setSeats}/>
  <title size="xl">
    ${price}/mo
  </title>
</box>
② サーバーサイドコンパイル 初期プログラムは不変

初期プログラム:安定したステートキーを注入、初期値は 8 のまま。スライダー操作時に再コンパイルは不要です。

コンパイル結果のスニペットを表示
// コンパイル生成物 JS + JSON:
DIL.render(__dil.jsx(() => {
  const [seats, setSeats] = 
    DIL.useState(8, {key: "seats"});
  const price = __dilSafe(
    () => seats * 29, undefined
  );
  return ...
}));

// 定数テーブル (JSON):
"constants": {
  "0": "チーム月額見積もり",
  "4": "$", "5": "/mo"
}
③ クライアント Worker サンドボックス インタラクション時に状態を更新
// サンドボックス内部状態 (Hook State):
state.seats = 8;
state.price = 232;

// ユーザーイベントを検知:
recv: fn#1(8)
// ネイティブ UI 操作をディスパッチ:
UPDATE #12 text "232"
(金額更新の概略を示すものであり、完全な操作ログではありません)
④ ChatGPT ホスト側のネイティブ描画 インタラクション時に画面を更新
チーム月額見積もり
人数を調整して月額合計を確認します。
$232/mo
変化がどこで起きているかに注目:モデルとコンパイラにおける初期人数は常に 8 のままであり、クライアントの状態と表示価格だけがスライダー操作に伴って変化します。イベントがページからランタイムへと渡され、価格が人数 × 29 で再計算された後、UI 更新操作が返されます。(これは原文の仕組みを解説するための本ページのデモであり、ChatGPT の Worker を実際に実行しているわけではありません。29 ドル/シート/月は計算式を説明するためのものであり、実際のプラン価格ではありません。操作ログは簡略化した命令を示しています。)

ストリーミングコンパイル:UI が完成していないのに、なぜ表示して操作できるのか?

通常のプレーンテキストをストリーミング出力するのは非常に単純で、新しいトークンが届くたびに末尾へ結合していくだけで済みます。しかし、ネストしたノードやロジッククロージャを含むインタラクティブな UI をストリーミング出力するには、さらに2つの課題を解決しなければなりません:

  1. 構文がまだ不完全であること:大規模モデルは1トークンずつ順次出力します。モデルが <box border><slider min= と出力した直後で後ろの属性がまだ出力されていない場合、通常のパーサーはこの未完成な構文を直接実行できません。
  2. タイプライター状態の一貫性:モデルが回答の後半を生成している最中に、ユーザーが前半のスライダーをすでに動かしている可能性があります。このときバックエンドから絶え間なく新しいデータが送られてきても、ユーザーが現在操作しているコントロールを勝手にリセットしてはなりません。
ストリーミング応答タイムラインの可視化分解
Thesys による 6,341 文字の応答に関する完全なストリーミングキャプチャのスイムレーン図:基本設定、思考フェーズ、テキストストリーム、UI コンパイル再送、非同期画像の着信という時系列の対応関係が明確に示されている。(出典:Thesys リバースエンジニアリング解析時系列図)

6,341 文字を含み、本文ストリームが 18.7 秒続いたある応答のキャプチャにおいて、リバースエンジニアリングチームは 84 回の更新イベント(SSE チャンネルを介して配信される JSON-Patch)を記録しました。そのうち 76 回に新しいテキストが含まれており、以下の3つに分類されます。このほか画像結果の送信が 5 回、メッセージ作成が 1 回、完了マーカーが 2 回ありました:

1回の応答で行われた84回の更新の内訳とは?

Thesys がリバースエンジニアリングでキャプチャした、6,341 文字・本文ストリーミング時間 18.7 秒のセッションサンプルに基づきます。各セグメントをクリックして背後の処理ロジックを確認できます:

構造変更パケット (Structure Changed):52 回 コード全文を再送
モデルの現在の出力状態 (Model Output)
<box border padding={3}>
  <slider min={1} max={50} value={seats} />
  // サブコンポーネントが新たに追加された...
サーバーからの配信パッチ (SSE JSON-Patch)
{"p": "/message/metadata/model_dil_v2/code",
 "o": "replace",
 "v": "DIL.render(__dil.jsx(() => { ...再コンパイルされた完全なJSプログラム全体... }))"}
エンジニアリング上のトレードオフと要因: モデルがコンポーネントを追加したりツリー構造を変更したりするたびに、コンパイル後の JavaScript の閉じ括弧が変わるため、今回のキャプチャではサーバーがプログラム全体を再コンパイルして丸ごと再送(replace)していました。キャプチャした 275 KB のパッチ通信量のうち、重複して送信されたコードがパッチデータ量の約 83% を占めていました。このサンプルにおける完全な再コンパイル更新の中央値間隔は約 290 ミリ秒でした。

分布バーは更新回数に基づいて描画されています。83% は重複コードがパッチのバイト数に占める割合を指し、算出基準が異なります。上記は単一のキャプチャ結果であり、すべてのセッションの平均的な挙動を示すものではありません。以下のコードは簡略化したデモです。

  1. 構造変更パケット(52 回):これらの更新において、サーバーはそこまでに出力されたすべての DIL に対して全体再コンパイルを行いました。元の観察によれば、コンパイルは最初のトークンから開始され、コンポーネントタグがない場合でも Markdown を処理します。サーバーが定期的に再コンパイルを行う契機が、各出力チャンクによって厳密にトリガーされているかどうかは、依然としてチームの推測にとどまります。構造が変化すると、コンパイル済みプログラムの閉じ括弧も変化するため、テキストストリームのように末尾に追加するだけでは済まず、replace を使ってブロック全体を再送せざるを得ません。今回のキャプチャでは、重複して送信されたコードがパッチデータ量の約 83% を占めていました。サーバーが記述全体を再コンパイルしても、クライアントは変更が必要な UI 部分のみを更新できます。クライアントの Worker は評価後に新旧のコンポーネントツリーを比較し、ホストページへ更新操作を送信します。
  2. テキスト追加のみパケット(14 回):モデルが通常の段落内でテキストを入力しているだけで、コンポーネント構造に触れていない場合、コンパイルされる JavaScript コードには変化が生じないため、ネットワーク配信は JSON 定数テーブルに新しい文字を追加するだけで済みます。同一のスクリプトを重複して送信しなかったからといって、サーバーがコンパイル処理をスキップしたとは限りません。クライアントは既存の構造を維持したままテキストノードを更新します。
  3. 未クローズタグの保護(10 回):モデルが書きかけのタグの内部にとどまっていることを検知すると、サーバーのトリミング機構が中途半端なタグを一時的に切り捨てるか、または内容を持つ開始タグに対して閉じタグを自動補完します。クライアントはすでに利用可能な部分を表示し続けます。新しいプログラムの評価やレンダリングに失敗した場合、Worker は直前の正常に動作したバージョンを保持します。

データの流し込み:画像や価格はコンパイル後の UI にどう入り込むのか?

大規模モデルが UI を生成する際、特定の商品、地図上の地点、実物の写真を表示する必要が頻繁に生じます。モデル自身に画像の URL や最新の公式価格を記憶から「書き出させる」と、リンク切れや価格の転記ミス、細部のハルシネーション(捏造)を引き起こす可能性が高まります。

Intelligent UI では、巧妙なデータバイパス制御が用いられています:

画像:モデルが検索要件を記述し、サーバーが結果を補完する

画像を表示する際、モデルは具体的なリンクを書かず、検索の意図のみを記述します:

<AsyncImage 
  query="slow roasted rosemary garlic lamb shoulder browned golden roast in baking tray" 
  aspectRatio="5:4" 
  maxWidth="152px"
/>

サーバーのコンパイラがこのタグのクローズを検知すると、バックグラウンドで直ちに実際の画像検索を実行し、数秒後に appData データパッチを通じて検証済みの正規画像 URL を注入します。分析チームは同様のサーバー解決コンポーネントとして、画像グループ、商品や地点タグ(Entity)、出典引用(Cite)なども確認しています。

ツールデータ:既存フィールドを参照し、モデルによる再記述を1回減らす

モデルが事前に外部検索ツールを呼び出していた場合(例えばヘッドホンの仕様を取得した場合)、UI 内では {turn448791product0.displayed_price} のようにフィールド名を参照します。画面に表示される ₹35,989 や店舗名は、すべてバックエンドの元の JSON から直接流し込まれます。この例では、モデルは表示位置のレイアウトのみを担当し、数値はツール自体の結果から取得されます。これにより転記ミスのリスクは下がりますが、データの出典や選択が正しいことまでは保証されません。チームは天気検索の結果がモデルによって本文へと書き戻されている例も確認しており、すべてのデータがバイパスを通るわけではありません。

ChatGPT 内でインライン生成された Drum Lab ドラムマシンのデモ
AppBlock のエスケープハッチを通じて生成された独立したウェブアプリケーション「Drum Lab」:ネイティブコンポーネントライブラリの制約を突破し、Web Audio API を利用してブラウザ内でリアルタイムにビート音を合成している。(出典:Thesys リバースエンジニアリング採取事例)

AppBlock:コンポーネントライブラリに収まらない場合、もう1つの経路はどう動作するのか?

ネイティブコンポーネントライブラリがいかに豊富であっても、「音を合成してビートを鳴らせる16ステップのドラムマシンを作ってほしい」といったあらゆる要望に応えられるわけではありません。

このような要求には AppBlock という拡張経路が利用されます。大規模モデルは HTML、CSS、<script> を含むコードを直接生成します。DIL コンパイラはその内部コードを翻訳せず、コンパイル後のプログラムには __chatgptClientDefinedWidget というプレースホルダーのみが残されます。クライアントは元の回答テキストから直接ソースコードを読み取り、独立したドメインの2重 iframe(Skybridge サンドボックス:*.web-sandbox.oaiusercontent.com)内で実行します。このウェブページが周囲から浮いて不自然なパッチのように見えないよう、ホスト環境から Tailwind の基本仕様や公式 CSS 変数(--viz-text、--viz-panel など)が注入され、ChatGPT のライト/ダークテーマに自動的に馴染むよう調整されます。

前述のネイティブコンポーネント経路とは異なり、AppBlock は別の実行経路を採用しており、前述の Worker や GenUI パスをそのまま適用することはできません。ウェブページ自身が UI を描画し、状態はミニアプリ自身の JavaScript 変数内に保持されます。分析チームがキャプチャしたウェブ版のサンプルでは、AppBlock は ChatGPT 側へのコールバックを行っておらず、ページをリロードするとその内部状態も失われていました。

ChatGPT Intelligent UI が実際に生成した3組の事例
元著者によって示された3組の生成事例:左はデザインの選択肢を備えた犬用ベストのムードボード、中央は人数と材料の分量を動的に計算するディナーメニュー、右はシミュレーションスライダーを備えたプロダクト分析ダッシュボード。(注:右図のデータには Demo data と明記されており、実際のビジネス指標ではありません)

クリックできることと計算が正しいことは別であり、モデルが選択を記憶したことにもならない

Thesys のウェブ版サンプルは、実際の利用に影響を与えるいくつかの課題も浮き彫りにしています:

  1. ストリーミング更新によるコードの重複送信:該当の応答におけるパッチデータは約 275 KB であり、そのうち約 83% は重複して送信されたコンパイル済みプログラムでした。これは単一キャプチャでの転送オーバーヘッドであり、すべてのセッションを代表するものではありません。
  2. ページリロードによる状態の消失:クライアントはバックエンドへ view-state 状態イベントを送信しているものの、チームが調査したウェブ版サンプルでは、ページをリロードすると変更した数値が初期状態に戻ってしまいました。
  3. ローカル状態はモデルのコンテキストへ自動反映されない:すべてのインタラクションがゼロネットワーク、あるいはモデルを一切呼び出さずに完結できるわけではありません。コンポーネントが外部と通信する必要がある場合、ホスト側が提供する GenUI オブジェクトに依存します。このうち copy はテキストのコピー、openUrl は外部リンクのオープンを担当し、これらのアクションはホスト環境で処理されるためモデルが回答を再生成する必要はありません。一方、大規模モデルにユーザーの選択を実際に認識させる唯一の手段は、GenUI.issueNewTurn(...) を明示的に呼び出すことです。例えば犬用ベストのムードボードでは、ユーザーがスタイルとメインカラーを選んで「進める」をクリックした際、コンポーネント内部ではローカル状態をテキスト(例:「ムードボードに基づいてデザインを進める。スタイル:コンフォート、メインカラー:サンド……」)として組み立て、新しいユーザーの返答としてチャット欄に送信します。大規模モデルはその入力を受けて初めて新しい回答を生成します。こうした明示的な送信を行わない限り、フロントエンドでスライダーをいくら動かしても、何をチェックしても、大規模モデルはコンテキスト上で一切感知できず、その後の追加質問時にもユーザーが以前画面上でどのような調整を行ったかを把握できません。
  4. 追加の質問で元の UI がインプレース更新されることはない:ユーザーが次の修正要望を出しても、モデルは元のカード上でインプレースな差分更新を行うのではなく、その下にまったく新しいカードを再生成します。
  5. サイレントなロジックエラー:式が例外をスローした際は空白が表示されるだけで済む場合があります。しかし、計算式自体は実行可能でも計算結果が間違っている場合、例外は発生しない可能性があります。UI が正常にレンダリングされたからといって、ビジネス計算が正しい証明にはなりません(リバースエンジニアリングの過程で、あるダッシュボードの指標を切り替えた際に収益の数字が $6,930 から $256,410 へと急変する事象が確認されています)。
  6. エラー修復がコンテキストのフィードバックループを形成していない:サーバーサイドコンパイラは不正な属性を排除して診断ログを記録するものの、複数ターンの対話テストにおいて、モデルはまったく同じ属性の記述ミスを繰り返し続けました。これは診断結果がモデルにフィードバックされていない可能性を示唆していますが、これらのサンプルのみから内部フィードバック機構の全体像を断定することはできません。

さらに、リコンサイラが常に最小限の変更命令を生成するとは限りません。キャプチャされた価格コンポーネントのインタラクションサンプルでは、1回のクリックで 158 個の操作命令が生成されたものの、実際の属性変更を伴うものはわずか 4 個でした。

互換性やレイアウトの面でも現実的な妥協が存在します。コンパイル済みプログラムを実行できないクライアントに対応するため、サーバーはコンパイル時にプレーンな Markdown によるフォールバック版(fallbackMarkdown)を同時に生成します。これは本文を保持しつつ、画像、商品、引用、地図をクライアントが持つ既存のインラインマークアップで表現し、インタラクティブ部分は静的なコンテンツに置き換えます。チームがキャプチャしたフォールバックテキストでは、状態から算出される計算値や {#each} ループの内容が欠落していました。また、モデルは検索キーワードを出力するだけで検索結果の検証までは行わないため、画像検索で食い違いが発生することもあります(あるサンプルでは、自転車のシフターの接写を検索したのに対し、電動アシスト自転車の全体商品画像が返されました)。チームがキャプチャした UI は固定のカラム数とピクセル幅を採用しており、用意されているブレークポイントフック(Breakpoint hooks)を呼び出していなかったため、ネイティブコンポーネント描画であってもモバイル版のレイアウトが自動的に適切に解決されるわけではありませんでした。


OpenUI で同様の体験を構築可能、ただしデフォルトサンプルは依然クラウドに依存

このような体験を自身のアプリケーションに取り入れるには、OpenUI が提供するコンポーネントプロトコルとレンダラーから着手できます。 Thesys は OpenUI と呼ばれるオープンソースの Generative UI フレームワークを提供しており、このフレームワークに基づいて同様の操作体験を再現したデモプロジェクト Open Intelligent UI(サンフランシスコ日帰りツアールート:フォトギャラリー表示、地図連動、環境設定フォームをサポート)を公開しています。ただし、このデモプロジェクトで使用されているのは OpenUI 独自のストリーミングマークアップ言語(OpenUI Lang)であり、OpenAI 内部の DIL 仕様そのものが公開されたわけではない点には注意が必要です。

このオープンソースのアプローチは、開発者が自身のアプリ内で同様の体験を構築する助けとなりますが、具体的な実装やデプロイにおける依存関係については個別に整理しておく必要があります:

  • OpenUI フレームワークの位置づけと価値:軽量なストリーミングマークアップ言語(OpenUI Lang)を定義しており、プロジェクト自身による7つの UI シナリオでのテストでは、比較対象となった Vercel JSON 形式に比べて全体で約 52.8% のトークン削減、単一シナリオで最大約 67.1% の削減を達成しています(これはプロジェクトが選定したシナリオにおける独自テスト結果であり、一般的な処理速度の向上にそのまま換算できるものではありません)。また、React、Vue、Svelte、Angular を網羅するプログレッシブなストリーミングレンダラーを提供しています。
  • デモプロジェクトの動作環境とモデル依存:Open Intelligent UI はデフォルトで Node 24 とクラウドゲートウェイの API キー(THESYS_API_KEY)を必要とし、デフォルトのモデルは openai/gpt-5.5 に設定されています(これはデモプロジェクト用に設定された構成であり、本稿で取り上げた ChatGPT の実測で使われたモデルではありません)。開発者は互換性のあるインターフェースを持つ他のモデルやローカル LLM に切り替えることも可能ですが、プロジェクト自体はデフォルトで完全オフライン動作する設計ではありません。ローカル推論が円滑に成立するかどうかは、接続先モデルが準拠したコンポーネントプロトコルを安定して確実に出力できるかに大きく依存します。フレームワークがモデルの差し替えをサポートしているからといって、任意のローカルモデルで同等の体験が得られるとは限りません。
  • 外部データサービスとの連携と体験の境界:サンプル内の画像検索は Gateway ツールと Wikipedia のフォールバックに依存しています。地図のルートジオメトリデータは公開 OSRM サービスの自動車運転モード(driving profile)から取得されており、これは道路の大まかな道筋を示す概略図に過ぎず、徒歩や公共交通機関のナビゲーションではないため、実際の徒歩やバスのルートは別途調べる必要があります。これらの周辺データサービスから切り離して、ローカルで大規模モデルを単体で動かすだけでは、連動した完全な体験を直接再現することはできません。

前述の「モデルからはローカル状態が見えない」という点について、このデモプロジェクトでは、コンポーネントが useStateField を用いてユーザーによるスポットの追加・削除の履歴を回答状態の中に保存し、メッセージとともに保持します。バックエンドの /api/chat は describeRouteEdits を通じてその追加・削除履歴をプロンプトテキストへと変換し、次ターンのモデルコンテキストに引き渡します。選択中のスポット(routeSelected)も UI の状態に保存されますが、この変換関数はそれを読み取らないため、モデルへ送られるテキストには書き込まれません。

両者には画像データフローの違いもあります。OpenUI では Gateway 検索の後にモデルが結果を受け取り、URL をコンポーネント属性に流し込みますが、チームが観察した ChatGPT の AsyncImage 経路では、サーバーが検索を行った後に直接 appData へ結果をパッチします。操作体験が似ているからといって、このプロジェクトが OpenAI の DIL コンパイラや Worker サンドボックス、あるいはまったく同一のデータフローを再利用しているわけではありません。

モデルの生成からユーザーインタラクションに至るまで、Intelligent UI の本質は、要求を記述できるモデル、構造を検証できるコンパイラ、入力に応答できるクライアント、そして信頼できるデータソースを緊密に結びつけることにあります。これによって初めて、従来の一方通行なタイピングストリームが直接操作可能な実用的ツールへと変化します。オープンソースエコシステムによって類似の操作体験を再現できるようになったとしても、それは成熟したシステム全体をリバースエンジニアリングで手に入れたことと同義ではありません。Thesys が主張するコンパイル出力のバイトレベルでの一致も、実際にキャプチャされた特定のサンプルに限られたものであり、内部システムが完全に再現されたことを意味するものではありません。各層の役割分担と境界線を正しく見極めることこそが、生成 UI を理解し実務に導入するための現実的な第一歩となります。

出典
We reverse engineered ChatGPT Intelligent UI. Here's how it actually worksRabi Shanker Guha / Thesys·原文を見る
編集部注
実装の詳細は Thesys によるウェブ版のリバースエンジニアリングによる観察に基づいており、モバイル版の実装は直接検証されていません。
↑ 本文ここまで
✓ この記事は全文無料です

メンバーは全文 Markdown のコピー・保存に加え、自分の AI に購読・質問させることができます。

原文を読む ↗
📮 新着解説をメールでいち早く

役に立ちましたか?メールを登録すると、新着解説を公開と同時にお届けします。解除はいつでも。