私たち Alpic は、次世代の製品やサービスは AI ファーストの体験を中心に構築されると考えています。従来のあらかじめ決められた UI ワークフローをたどるのではなく、ユーザーがモデルと協力するインターフェースです。
OpenAI が Apps SDK を公開すると、私たちはすぐにそれを使った開発を始めました。3 か月の間に、社内向けと顧客向けを合わせて 24 個の ChatGPT アプリを開発しました。対象は、 旅行、小売、SaaS などの B2B および B2C 分野です。
早い段階でわかったのは、 ChatGPT アプリの開発は、従来のウェブアプリやモバイルアプリの開発とは根本的に異なるということです。必要になった時点でのデータ取得、UI 主導の状態管理、ユーザーによる明示的な設定など、ウェブではうまく機能するパターンが、エージェント型の環境では通用しなかったり、かえって使い勝手を損ねたりすることがよくあります。
この記事では、実際に使われる ChatGPT アプリを開発する中で学んだ、 特に重要な 15 の教訓 をまとめます。続いて、その教訓をコミュニティ向けのオープンソースフレームワーク Skybridge と Codex スキルに組み込み、開発者がアプリの構想、開発、テスト、リリースを大幅に速められるようにした方法を紹介します。
三体問題
従来のウェブアプリはシンプルでした。登場するのは ユーザー と UI だけです。ChatGPT アプリでは、ここに 3 つ目の存在である モデルが加わります。
ChatGPT 向けの開発で特に難しいのは、この三者の間で情報をどうやり取りするかを管理することです。ユーザーがウィジェットの「選択」ボタンをクリックすると、UI の表示は更新されます。しかし、そのコンテキストを明示的に伝えない限り、会話の頭脳であるモデルは変更に気づきません。その後、ユーザーが 「この商品についてもっと詳しく教えてください」 と尋ねても、モデルにはユーザーが実際に何を見ているのかわかりません。
私たちはこれを コンテキストの非対称性 と呼んでいます。それぞれがシステムについて一部の情報しか持たず、誰も全体像を把握していない状態です。優れた ChatGPT アプリを作るうえで大切なのは、すべてを同期し続けることではありません。 どの 情報を、 いつ 共有し、 誰 が把握する必要があるかを決めることです。この問題を解決できるかどうかが、使いづらいアプリとスムーズなエージェント型の体験を分けます。
1. 共有すべきコンテキストの見極め
最初は「とにかく、すべてをあらゆる場所で共有しよう」と考えました。これが、初期の失敗の 1 つでした。
実際には、ChatGPT アプリの各部分に、同じ状態について あえて異なる 情報を渡す必要がよくあります。なぜでしょうか。
- パフォーマンスのため: UI ウィジェットは、モデルに渡す必要のある量をはるかに超えるデータを必要とすることがよくあります。たとえば旅行予約アプリなら、画像、料金のバリエーション、事前に読み込んだ選択肢などです。これらをすべてモデルに送ると、トークン使用量やレイテンシが増え、モデルの判断を妨げる余計な情報も増えてしまいます。
- ロジックのため: 設計上、情報の非対称性を維持しなければならない場合もあります。初期に開発したアプリの 1 つである推理ゲーム Murder in the Valleys では、モデルが役を正しく演じるには犯人を知る必要がありますが、UI とユーザーには知らせてはいけません。 Time’s Up のようなゲームでは逆です。UI はお題の単語をユーザーに表示しますが、モデルには知らせてはいけません。
教訓は「常にすべてを同期する」ことではなく、 誰が何を知る必要があるのかを明確に決めることでした。私たちは、 ツール出力 のフィールドを使い分けることで、この方針を具体化しました。
| フィールド | 用途 | 参照できる対象 |
|---|---|---|
| structuredContent | ウィジェットとモデル向けの型付きデータ | ウィジェットとモデルの両方(toolOutput 関数と callTool 関数経由) |
| _meta | レスポンスのメタデータ | ウィジェットのみ。モデルには非公開 |
たとえば Time’s Up のゲームでは、_meta フィールドでお題の単語をウィジェットだけに渡し、モデルにはユーザーのヒントから単語を推測させていました。
2. AI アプリにはなじみにくい遅延読み込み
ウェブ開発の経験から、私たちは当たり前のように遅延読み込みを採用していました。ユーザーがクリックしたらデータを取得し、必要に応じて詳細を読み込み、初回のペイロードを最小限に抑えるよう最適化していたのです。
ChatGPT では、この考え方が逆になります。ツール呼び出しには待ち時間が伴い、セキュリティのためのサンドボックスやモデルの推論によって、数秒かかることもよくあります。
実践を通じて学んだのは、できる限り最初にまとめてデータを渡すことでした。最初のツールレスポンスに可能な限り多くのデータを含め、 window.openai.toolOutput 経由でウィジェットにデータを反映します。これにより、ほぼ常に速度と応答性が向上しました。
もちろん、ウィジェットが公開 API エンドポイントから安全にデータを取得でき、モデルとの情報共有も不要であれば、ウィジェット内で従来の XHR 呼び出しを使うこともできます。ただし、多くの場合、会話を通じて操作できる体験を保つには、モデルが自律的にツールを呼び出せることが望まれます。
3. モデルによる UI 状態の把握
ユーザーがウィジェットを操作した後、たとえばリストから特定の商品を選んでからチャットで質問すると、気づきにくいものの重大な問題が生じます。ユーザーが UI のどの部分について話しているのかをモデルが把握していなければ、正しく答えられません。
このために、私たちは window.openai.setWidgetState(state) を使いました。これを使うと、特定の状態データを保存し、次にユーザーとモデルがやり取りする際にモデルのコンテキストへ追加できます。
アプリが複雑になるにつれ、モデルが画面遷移を把握できるよう、あちこちに setWidgetState を追加するようになっていました。そこで、UI のコンテキストを宣言的に記述する方法を導入しました。操作のたびに命令的なコードでモデルに伝える情報を更新する代わりに、コンポーネントへ直接 data-llm 属性を付けます。
<div
data-llm={
selectedTab === "details"
? "User is viewing product details"
: "User is viewing reviews"
}
>
これを内部で動作させるため、これらの属性を収集して widgetState を自動更新する Vite プラグインを開発しました。開発者が操作のたびに手動で同期しなくても、モデルは必要な UI のコンテキストを適切なタイミングで受け取れます。
この Vite プラグインは、学んだことをコミュニティと共有するために作ったオープンソースフレームワークに含まれています。この記事で紹介するほかの多くのノウハウも、そこで確認できます。
4. やり取りに応じた API の使い分け
ChatGPT アプリには、ウィジェット、サーバー、モデルの間に複数の通信経路があります。これらは互いに代用できるものではありません。それぞれ異なる種類のやり取りを支えるために存在しています。
ChatGPT アプリの開発で得た重要な教訓の 1 つは、こうした通信経路を明確にし、体験のどの部分をどの仕組みが担うのかを意識して設計することです。
通信経路を図にすると、次のようになります。

これらの教訓は、ChatGPT アプリの基礎となるものです。コンテキストをどう共有し、モデルがどう状況を把握し、さまざまなやり取りをシステム内でどう伝えていくかを定めます。次のセクションでは、この基礎を踏まえ、UI 設計への影響に焦点を当てます。
AI に合わせた UI の再設計
ChatGPT アプリはまったく新しい環境です。そのため私たちは、UI に対する先入観を脇に置き、新しい機能を十分に活用する必要があるとすぐに学びました。このセクションでは、効果的なアプリを作るために新たに学んだインターフェース設計の考え方と、見直す必要があった従来の前提を紹介します。
5. 複数の表示モードとその制約への UI の対応
ChatGPT アプリのレイアウトは 1 つに固定されていません。呼び出し方やタイミングに応じて、同じウィジェットを 3 種類の表示モードで描画できます。
アプリは、会話の中に インライン で表示したり、会話に重ねて ピクチャーインピクチャー(PiP) で表示したり、より広い領域が必要なときに 全画面 で表示したりできます。PiP と全画面はより豊かなインターフェースを実現できる一方、ウィジェット側では制御できない UI オーバーレイも表示されます。コンテンツが見切れるのを防ぎ、操作性を最適化するには、モバイルで常時表示される閉じるボタンなど、デバイス固有のセーフゾーンを考慮することが欠かせません。
開発を重ねるうちに、表示モードとその使いどころについて、次のようなパターンが見えてきました。
| 表示の特徴 | 適した用途 | |
|---|---|---|
| インライン | デフォルトの表示モードです。ウィジェットは会話履歴に残ります。 | 短いやり取り |
| 全画面 | ウィジェットが画面全体に表示され、下部にチャットバーが配置されます。 | 地図など、複雑で広い表示領域が必要なウィジェット |
| ピクチャーインピクチャー | インラインと同じサイズですが、ウィジェットは会話に重なった状態で表示され続けます。 | 生成後の会話でも引き続き関連するウィジェット |
6. 埋め込み環境における UI の一貫性の重要性
初期に迷ったことの 1 つが、ChatGPT アプリの見た目にどこまで独自性を持たせるべきかでした。ユーザーにとって新しいインターフェースだからこそ、親しみやすく、自分たちのアプリ同士でも、周囲の ChatGPT エコシステムとも一貫している必要がありました。単独で動く製品とは異なり、ウィジェットは既存のインターフェース内に置かれるため、見た目の不整合がすぐに目立ってしまいます。
幸い、OpenAI Apps SDK UI Kit が明確な基準を示してくれました。
Tailwind CSS を基盤とし、ChatGPT のデザインシステムに沿った、すぐに使えるコンポーネント、アイコン、デザイントークンを提供しています。これを使うことで、Mapbox 連携などのカスタムコンポーネントを作る場合も、周囲のインターフェースに自然になじみ、見た目に一貫性のあるウィジェットをすばやく開発できました。
7. 自然言語を中心とした絞り込み
従来のダッシュボードでは、チェックボックスや範囲スライダーを並べたサイドバーが中心です。しかし、エージェント型 UI では、こうした設計がかえって使い勝手を悪くすることがあります。たとえば「200 ドル未満で行ける、ヨーロッパの晴れた旅行先」のように、ユーザーが意図を自然言語で直接伝えられるのに、いくつもの UI コントロールを操作させるのは余計な手間です。言葉で伝えるだけで済むようにすべきです。
そこで、ほとんどのアプリでは「フィルターを設けない」方針にしました。絞り込みや並べ替えのオプションをサイドバーに並べる代わりに、ツールのパラメーターに指定できる 値の一覧(LOV) をモデルに渡しています。
これにより、モデルは利用可能な選択肢を「推測」することなく、ユーザーのメッセージをそのまま入力として扱えます。つまり、自然言語をバックエンドの API 要件に直接対応付けられます。ユーザーが「晴れ」と言えば、モデルは weather="sunny" を指定してツールを呼び出せばよいと判断できます。
8. ファイルによる、より豊かなインタラクション
より複雑なアプリを開発する中で得た教訓の一つは、ファイルを補助的な入力として扱うべきではないということです。ChatGPT アプリでは、ファイルによって新しいインタラクションが可能になります。フォームやフィルターから始めるのではなく、ユーザーがすでに持っているものを体験の出発点にできます。
たとえば EC アプリなら、ユーザーはチャットに商品の写真をアップロードし、モデルに商品を識別してもらった後、そのままウィジェット内で一致する商品を見つけたり、新しい商品を探したりできます。
これは、システムの両側でファイルを扱えるようにすることで実現できます。モデル側では、ツールが openai/fileParams を介してチャットにアップロードされたファイルを直接扱えるため、モデルは画像など、ユーザーが提供した素材をもとに推論できます。UI 側でも、ウィジェットが window.openai.uploadFile と window.openai.getFileDownloadUrl を使ってファイルを直接扱えます。これにより、UI の操作の流れの中でアップロードを求めたり、ユーザーがダウンロードして再利用できるファイルを生成したりできます。
本番環境への移行
次に、アプリがローカル開発の段階を超えると、セキュリティ、構成、開発ツールについて新たな検討事項が出てきます。第 3 のテーマでは、これらの教訓を紹介します。
9. CORS に代わる新たな課題、CSP
セキュリティ上の理由から、OpenAI は二重にネストされた iframe 内でアプリをレンダリングします。コンテンツセキュリティポリシー(CSP)は iframe を分離するための標準的な仕組みで、この構成では厳格に適用されます。そのため、「ローカルでは動くのに本番環境では動かない」というおなじみの問題がよく発生します。
従来のウェブ開発では緩いポリシーでも済む場合がありますが、Apps SDK では必要な範囲を厳密に指定しなければなりません。
具体的には、アプリのマニフェストで、操作の種類ごとに許可するドメインを慎重に宣言します。
| フィールド | 目的 | 例 | よくある間違い |
|---|---|---|---|
| connectDomains | API リクエストと XHR リクエスト | https://api.weather.com | ステージング環境と本番環境で API が異なることの見落とし |
| resourceDomains | 画像、フォント、スクリプト | https://cdn.jsdelivr.net | delivr.net のような汎用 CDN を許可リストに登録せずに使用 |
| frameDomains | iframe の埋め込み | https://www.youtube.com | YouTube 動画や Mapbox インスタンスを許可リストに登録せずに埋め込み |
| redirectDomains | 警告なしで開く外部リンク | https://app.alpic.ai | 決済ページや OAuth コールバックのドメインの登録漏れ |
早い段階から CSP の構成を重要な検討事項として扱うことで、後の本番環境でのデバッグ作業を大幅に減らせました。
10. 小さなウィジェットフラグが及ぼす大きな影響
CSP に加え、ウィジェット単位のいくつかの設定によって、ウィジェット、モデル、ホスト環境の間で制御をどう分担するかが決まります。こうしたフラグは見落としがちですが、ナビゲーション、ツールへのアクセス、公開に関する重要な境界を定義しています。
ホストとナビゲーションの境界
widgetDomainは申請に必須です。全画面モードの「<App> で開く」ボタンのデフォルトのリンク先を定義します。また、ウィジェットは<widgetDomain>.web-sandbox.oaiusercontent.com配下でレンダリングされるため、オリジンの許可リストにも関わります。私たちはsetOpenInAppUrlを使い、コンテキストに応じて適切なパスにユーザーを誘導しました。
モデルとツールの境界
- ツールアノテーション は公開ガイドラインに従う必要があります。
readOnly、destructiveHint、openWorldHintなどのフラグは必須で、申請時に検証されます。 - ツールの可視性 も重要です。モデルから呼び出せないようにすべきツールは、明示的に非公開として指定する必要があります。
ウィジェットの実行範囲
widgetAccessibleは、ウィジェットがcallToolを使って自らツールを呼び出せるかどうかを制御します。
一つひとつは小さな設定ですが、これらの組み合わせによって、公開後にアプリが正しく動作するかどうかが決まります。
開発サイクルを速めるための最適化
Apps SDK は急速に進化しており、私たちもその進化とともに開発を進めることを楽しんできました。スムーズで効率的な開発ワークフローを支えるため、独自のオープンソースフレームワークを開発し、コミュニティと共有することにしました。ここでは、初期に直面した開発体験上の問題を避けるために役立つ教訓を紹介します。
11. 素早い開発サイクルに欠かせないホットリロード
最初に取り組んだことの一つが、開発サイクルの高速化でした。リソースキャッシュの TTL が長いうえ、リソースの転送に JSON-RPC を使うため、Vite や Next.js にある標準的なホットモジュールリロードは、そのままでは ChatGPT アプリで動作しません。
Vite の内部構造を理解するためにかなりの時間を費やした後、ChatGPT 内でウィジェットを直接ライブリロードできる Vite プラグインを開発しました。このプラグインは MCP サーバーへのリソースリクエストを捕捉し、ChatGPT の iframe にリアルタイムの更新を差し込みます。IDE での変更が ChatGPT 内に即座に反映されるようになり、フィードバックループが劇的に短縮されました。

12. すべてのテストを ChatGPT 内で行う必要はない
ChatGPT 上でのテストが最も確実ですが、初期の試行錯誤にはローカルエミュレーターを使うと、よりすばやく進められます。特に、開発者モードでアプリの再読み込みが必要になるツール定義を調整している場合に有効です。
初期の開発サイクルを速めるため、ChatGPT のホスト環境を模擬する軽量なローカルエミュレーターを開発し、デバッグツールやアプリ固有のログも備えました。これにより、React の状態やレイアウトの変更をミリ秒単位で試しながら、実際の ChatGPT でのテストはモデルとのやり取りやエッジケースの検証に集中できました。
13. モバイルテストに必要な専用の対応
モバイルでのテストでは、別の課題が生じました。ChatGPT でテストするにはローカルサーバーへのトンネル接続が必要ですが、Vite はデフォルトで localhost を使うため、同じ URL にほかのデバイスからアクセスできません。
この問題に対処するため、Vite プラグインを拡張し、トンネル接続したポートでのドメイン転送に対応させました。これにより iOS と Android の両方のデバイスでテストできるようになり、モバイルでの検証を通常のワークフローに組み込めました。
14. React フックなどの使い慣れた抽象化によるフロントエンド開発の高速化
Apps SDK は強力な機能を提供していますが、その多くは低レベルの JavaScript API を通じて利用します。長年 React を使ってきた私たちは、すでに使い慣れた概念に近い形で扱いたいと考えました。
そこで、React で使いやすい抽象化を導入しました。useCallTool、useWidgetState、useLocale などのフックに加え、複雑なデータフローに対応するため、Zustand を基盤とした createStore などの高度な状態管理も用意しました。使い慣れたフロントエンドのパターンを取り入れ直すことで、定型コードが減り、ウィジェット開発を現代的なウェブ開発のワークフローに近い感覚で進められるようになりました。
教訓を Codex スキルへ
15. 教訓を再利用可能な開発ツールへ
複数のアプリでこうしたパターンが見えてくるにつれ、同じことを何度も発見し直す作業が開発を遅らせているとわかりました。ChatGPT App の開発をより速く、見通しよく進められるように、これらの教訓を開発ツールに直接組み込むことにしました。自分たちだけでなく、コミュニティにも役立ててもらうためです。
こうして、互いを補完する 2 つの取り組みが生まれました。
- Skybridge Framework: この記事で紹介した多くのパターンを、再利用可能な構成要素としてまとめたオープンソースの React フレームワークです。私たちが開発したフック(
useCallTool、useToolInfo)、開発ツール(HMR とローカルエミュレーター)、data-llm 属性などが含まれています。 - Codex スキル「chatgpt-apps-builder」: このフレームワークを基盤に、アプリのライフサイクル全体を支援する専用の Codex スキルを開発しました。以下の作業を支援します。
- アイデアの検討: 単なるウェブアプリの移植にとどまらず、エージェント型のアプリにするためのブレインストーミング
- コード生成: 適切な UX と UI のパターンをすべてあらかじめ組み込んだ、React フロントエンドと MCP サーバーのバックエンドの同時作成
- ローカルテスト: 開発サーバーを起動し、ローカルアプリを ChatGPT に接続して、ホットリロードで変更をリアルタイムに確認しながら開発
- QA と公開: CSP の検証、セーフゾーンへの配慮、本番環境でのテストなど、OpenAI の申請ガイドラインに沿った体系的なチェックの実施
- アプリのデプロイ: アプリのリリースと継続的な改善に必要な最終工程の支援
このスキルをインストールして使うには、次のコマンドを実行するだけです。
npx skills add alpic-ai/skybridge
まとめ
ChatGPT アプリの開発では、コンテキストの流れ、インターフェースの動作、ユーザーとモデルの連携のあり方を見直す必要があります。この記事で紹介した教訓の多くは、慣れ親しんだウェブ開発のパターンと、エージェント型システムの実態との隔たりから得られたものです。
これらの教訓を共有し、オープンソースのフレームワークと Codex スキルに組み込むことで、各チームが同じ問題の発見に繰り返し費やす時間を減らし、この新しい対話モデルで何ができるのかを探る時間を増やせればと考えています。最も魅力的な ChatGPT アプリは、既存製品を単に移植したものではなく、AI を中心に据えたこの新しい体験を軸に、意図を持って設計されたものになるでしょう。