Alpic では、次世代の製品やサービスの中心になるのは AI を前提としたユーザー体験だと考えています。従来の UI では、ユーザーはあらかじめ決められた手順に沿って操作します。これからのインターフェースでは、ユーザーがモデルと協力して目的を達成します。
OpenAI が Apps SDK をリリースすると、私たちはすぐに開発に取りかかりました。3 か月で、社内向けと顧客向けを合わせて 24 個の ChatGPT アプリを開発しました。旅行、小売、SaaS などの分野で、B2B と B2C の両方を手がけました。
早い段階で気づいたのは、ChatGPT アプリの開発は、従来の Web アプリやモバイルアプリの開発とは根本的に異なるということです。必要になった時点でデータを取得する、UI を起点に状態を管理する、ユーザー自身に設定してもらうといった手法は、Web ではうまく機能します。しかし、エージェントが自律的に動く環境ではうまく機能しないことが多く、かえって使い勝手を損なうこともあります。
この記事では、実際に使われる ChatGPT アプリの開発を通じて学んだ 特に重要な 15 の教訓を紹介します。さらに、開発者がアプリの構想から実装、テスト、リリースまでにかかる時間を大幅に短縮できるよう、これらの教訓をコミュニティ向けのオープンソースフレームワーク Skybridge と Codex スキル に組み込んだ方法も紹介します。
三体問題
従来の Web アプリでは、考えるべき関係は単純でした。登場するのは ユーザーと UI だけです。ChatGPT アプリには、そこに第 3 の存在である モデルが加わります。
ChatGPT 向けの開発で特に難しいのは、この三者の間で情報がどう伝わるかを管理することです。たとえば、ユーザーがウィジェットの「選択」ボタンをクリックすると、UI の表示は変わります。しかし、そのコンテキストを明示的に伝えない限り、会話を担うモデルは変化に気づきません。その状態でユーザーが 「この商品についてもう少し詳しく教えて」 と頼んでも、モデルにはユーザーがどの商品を見ているのか分かりません。
私たちはこれを コンテキストの非対称性と呼んでいます。三者がそれぞれシステムの一部を把握していても、どの立場からも全体像は見えません。使いやすい ChatGPT アプリを作るには、どの情報 を共有するか、いつ 共有するか、誰が その情報を見る必要があるかを見極めます。その判断次第で、操作に手間取るアプリにも、エージェントと自然にやり取りできるアプリにもなります。
1. 共有するコンテキストの取捨選択
当初は「ひとまずすべての情報を全員に共有しよう」と考えました。これは、開発の初期に犯した失敗の一つでした。
実際には、同じ状態でも、ChatGPT アプリの各部分から見える情報を 意図的に変える 必要がよくあります。それには次の理由があります。
- パフォーマンスのため: UI ウィジェットが必要とするデータは、モデルに必要なデータよりはるかに多いことがあります。たとえば旅行予約アプリでは、画像、条件ごとの料金、先に読み込んでおく選択肢などが該当します。これらをすべてモデルに渡すと、トークン使用量と待ち時間が増え、推論に無関係な情報も増えてしまいます。
- アプリのロジックのため: 設計上、情報を一方だけに見せる必要もあります。初期に開発した推理ゲーム Murder in the Valleys では、モデルが役を正しく演じるには犯人を知っている必要がありますが、UI やユーザーに犯人を明かしてはいけません。Time’s Up 形式のゲームでは逆です。UI にはユーザー向けにお題の単語を表示しますが、モデルにはその単語を知らせてはいけません。
ここで学んだのは、「常にすべてを同期する」という方針を見直し、誰にどの情報が必要かを明確に決めることです。そこで、ツール出力 のフィールドを次のように使い分けました。
| フィールド | 用途 | 参照できる対象 |
|---|---|---|
| structuredContent | ウィジェットとモデル向けの型付きデータ | ウィジェットとモデルの両方(toolOutput と callTool 関数経由) |
| _meta | レスポンスのメタデータ | ウィジェットのみ。モデルには非公開 |
たとえば Time’s Up ゲームでは、_meta フィールドを使ってお題の単語をウィジェットだけに渡し、モデルにはユーザーのヒントから単語を当ててもらいました。
2. AI アプリに遅延読み込みが向かない理由
Web 開発に慣れていた私たちは、最初は当然のように遅延読み込みを採用しました。ユーザーがクリックした時点でデータを取得し、詳細は必要に応じて読み込み、最初に送るデータ量を最小限に抑えるという考え方です。
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"
}
>
Product details
</div>この仕組みを支えるために、これらの属性を読み取って widgetState を自動更新する Vite プラグインを作りました。開発者が操作のたびに手動で同期しなくても、モデルには必要なタイミングで関連する UI コンテキストが届きます。
この Vite プラグインや、この記事で紹介するさまざまな工夫は、私たちが知見をコミュニティと共有するために作ったオープンソースフレームワークで公開しています。
4. やり取りに応じた API の使い分け
ChatGPT アプリでは、ウィジェット、サーバー、モデルの間に複数の通信経路があります。それぞれが異なる種類のやり取りを担うため、相互に置き換えることはできません。
ChatGPT アプリの開発で学んだ大切なことの一つは、これらの通信経路を明確にし、アプリ内のどのやり取りをどの仕組みに任せるかを意識して決めることです。
通信経路を図にすると、次のようになります。

ここまでに紹介した、コンテキストの共有方法、モデルが UI の状態を把握する仕組み、操作がシステム内で伝わる経路が、ChatGPT アプリの基盤になります。次は、この基盤を踏まえて UI をどう設計するかを見ていきます。
AI 向けの UI 設計
ChatGPT アプリはまったく新しい環境です。新しい機能を十分に生かすには、UI に関する先入観をいったん脇に置く必要があると、私たちはすぐに気づきました。ここでは、使いやすいアプリを作るために新たに学んだ設計の考え方と、見直す必要があった従来の前提を紹介します。
5. 複数の表示モードとそれぞれの制約に対応する UI
ChatGPT アプリのレイアウトは一つではありません。呼び出し方やタイミングに応じて、同じウィジェットを 3 種類のモードで表示できます。
アプリは、会話内に インラインで表示することも、ピクチャーインピクチャー(PiP) で会話に重ねることも、より広いスペースが必要なときに 全画面で表示することもできます。PiP と全画面では表現の幅が広がりますが、ウィジェット側で制御できない UI 要素も画面に重なります。コンテンツが見切れず、操作しやすい配置にするには、モバイルで常時表示される閉じるボタンの位置などを踏まえ、デバイスごとに安全に表示できる領域を考慮する必要があります。
開発を重ねるうちに、各表示モードの特徴と使いどころが見えてきました。
| 表示の特徴 | 適した場面 | |
|---|---|---|
| インライン | 既定の表示モードです。ウィジェットは会話履歴に残ります。 | 短い操作で完結する場合 |
| 全画面 | ウィジェットが画面全体に広がり、下部にチャットバーが表示されます。 | 地図など、ウィジェットが複雑で広い表示領域が必要な場合 |
| ピクチャーインピクチャー | サイズはインラインと同じですが、ウィジェットが会話の上に表示され続けます。 | 応答の生成後も、続く会話でウィジェットを参照する場合 |
6. 埋め込み環境での UI の一貫性
開発当初に迷ったのは、ChatGPT アプリの見た目にどこまで独自性を持たせるかでした。ユーザーにとって新しいインターフェースだからこそ、私たちのアプリ同士でも、周囲の ChatGPT の画面とも一貫性があり、慣れた感覚で使えることが重要です。単独で使う製品と違い、ウィジェットは既存のインターフェースの中に表示されるため、見た目のずれがすぐに目につきます。
幸い、OpenAI Apps SDK UI Kit が明確な基準になりました。
Tailwind CSS をベースにしたこのキットには、ChatGPT のデザインシステムに沿った、すぐに使えるコンポーネント、アイコン、デザイントークンが揃っています。これを使うことで、Mapbox 連携用などの独自コンポーネントを作る場合でも、開発を素早く進めながら、ウィジェットを周囲の画面に自然になじませることができました。
7. 自然言語による絞り込み
従来のダッシュボードには、チェックボックスや範囲指定のスライダーが並ぶサイドバーがあります。エージェントとやり取りする UI では、これがかえって使い勝手を損なうことがあります。「ヨーロッパで、200 ドル未満で行ける晴れた旅行先を探して」のように、ユーザーが希望を言葉で直接伝えられるのに、いくつもの UI 要素を操作してもらうのは余計な手間です。希望を伝えるだけで済むようにするのが自然です。
そこで、ほとんどのアプリでは「絞り込み用の UI を置かない」という方針を取りました。絞り込みや並べ替えの選択肢をサイドバーに並べる代わりに、ツールのパラメーターで指定できる 値の一覧(LOV) をモデルに渡します。
こうすれば、モデルは利用可能な選択肢を「推測」せずに、ユーザーのメッセージをそのまま入力として受け取れます。つまり、自然言語で伝えられた希望を、バックエンド API が受け付けるパラメーターに直接対応付けられます。ユーザーが「晴れ」と言えば、モデルは weather=“sunny” を指定してツールを呼び出せばよいと分かります。
8. ファイルから広がるやり取り
より複雑なアプリを開発する中で学んだのは、ファイルを補助的な入力として扱うだけではもったいないということです。ChatGPT アプリでは、ファイルを起点に新しいやり取りが可能になります。フォームへの入力や絞り込みから始める代わりに、ユーザーがすでに持っているものから始められます。
たとえば EC アプリでは、ユーザーがチャットに商品の写真をアップロードしてモデルに商品を特定してもらい、そのままウィジェットで一致する商品を探したり、ほかの商品を見つけたりできます。
これを可能にするのは、モデル側と UI 側の両方でファイルを扱える仕組みです。モデル側では、ツールが openai/fileParams 経由でチャットにアップロードされたファイルを直接受け取れるため、モデルはユーザーが提供した画像などのデータをもとに推論できます。UI 側では、ウィジェットも window.openai.uploadFile と window.openai.getFileDownloadUrl を使ってファイルを直接扱えます。画面操作の途中でアップロードを求めたり、ユーザーがダウンロードして再利用できるファイルを生成したりできます。
本番環境への移行
ローカル開発から本番環境に進むと、セキュリティ、設定、開発ツールについて新たに考えるべきことが出てきます。第 3 部では、その段階で学んだ教訓を紹介します。
9. CORS に続く新たな関門、CSP
セキュリティ上の理由から、OpenAI は二重にネストした iframe 内にアプリを表示します。コンテンツセキュリティポリシー(CSP)は iframe の分離を支える標準の仕組みで、この環境では厳格に適用されます。そのため、「ローカルでは動いたのに本番では動かない」という典型的な問題に直面しがちです。
従来の Web 開発では緩いポリシーでも動作することがありますが、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. 反復開発を速めるホットリロード
最初に取り組んだ課題の一つは、変更してから結果を確認するまでの時間でした。ChatGPT アプリでは、リソースのキャッシュの有効期間(TTL)が長く、リソースの転送には JSON-RPC が使われます。この組み合わせのため、Vite や Next.js に備わっている標準のホットモジュールリロードは、そのままでは使えません。
Vite の内部動作をじっくり調べたうえで、ChatGPT 内でウィジェットを直接ライブリロードできる Vite プラグインを作りました。このプラグインは MCP サーバーへのリソースリクエストをインターセプトし、ChatGPT の iframe に更新内容をリアルタイムで反映します。IDE で変更した結果をすぐに ChatGPT 内で確認できるようになり、フィードバックを得るまでの時間が大幅に短くなりました。
12. ChatGPT とローカル環境でのテストの使い分け
テストには実際の ChatGPT を使うのが最も確実ですが、開発の初期段階ではローカルエミュレーターを使うと作業が速くなります。特に、変更のたびに ChatGPT でカスタム MCP サーバーの接続を更新する必要があるツール定義を扱うときに役立ちます。
初期の試行錯誤を速めるため、ChatGPT のホスト環境を模擬する軽量なローカルエミュレーターを作り、デバッグツールとアプリ固有のログも用意しました。これにより、React の状態やレイアウトの変更はミリ秒単位で確認し、モデルとのやり取りやエッジケースの検証は実際の ChatGPT で行えるようになりました。
13. モバイルテストに必要な対応
モバイルテストでは別の課題が生じました。ChatGPT でテストするにはローカルサーバーへのトンネルが必要ですが、Vite は既定で localhost を使うため、ほかのデバイスから同じ URL にアクセスできません。
そこで、トンネルを通すポートでドメインを転送できるように Vite プラグインを拡張しました。これにより、iOS と Android の両方の実機でテストできるようになり、普段の開発手順にモバイルでの検証を組み込めました。
14. React フックなど、使い慣れた抽象化によるフロントエンド開発の効率化
Apps SDK は強力な機能を提供していますが、その多くは低レベルの JavaScript API を通じて利用するものです。長く React を使ってきた私たちは、使い慣れた考え方でこれらの機能を扱いたいと思いました。
そこで、useCallTool、useWidgetState、useLocale などのフックを導入し、React から機能を扱いやすくしました。複雑なデータフロー向けには、Zustand をベースにした createStore など、より高度な状態管理の仕組みも用意しました。使い慣れたフロントエンドのパターンを取り入れることで、定型コードが減り、普段の Web 開発に近い感覚でウィジェットを開発できるようになりました。
開発の教訓を Codex スキルへ
15. 教訓を再利用できるツールに
複数のアプリに共通するパターンが見えてくると、アプリを作るたびに同じことを一から学び直すのが開発を遅らせていると分かりました。ChatGPT アプリの開発を速め、見通しを立てやすくするために、これらの教訓をツールに直接組み込むことにしました。自分たちだけでなく、コミュニティでも活用できるようにするためです。
そこから、互いを補う 2 つの取り組みが生まれました。
- Skybridge フレームワーク: この記事で紹介した多くのパターンを、再利用できる部品にまとめたオープンソースの React フレームワークです。私たちが作ったフック(
useCallTool、useToolInfo)、開発ツール(HMR とローカルエミュレーター)、data-llm 属性などが含まれます。 - chatgpt-apps-builder Codex スキル: フレームワークを基盤に、アプリのライフサイクル全体を支援する専用の Codex スキルを作りました。
- 構想: 既存の Web アプリの単純な移植にとどまらず、エージェントの自律性を生かす方法を検討
- コード生成: 適切な UX と UI のパターンをあらかじめ組み込み、React フロントエンドと MCP サーバーのバックエンドを並行して実装
- ローカルテスト: 開発サーバーを起動してローカルアプリを ChatGPT に接続し、ホットリロードで変更をリアルタイムに確認
- QA と公開: CSP の検証、安全に表示できる領域への配慮、本番環境でのテストなど、OpenAI の提出ガイドラインに沿って体系的にチェック
- アプリのデプロイ: アプリをリリースし、その後も改善を重ねるために必要な最終手順を支援
スキルをインストールして使い始めるには、次のコマンドを実行します。
npx skills add alpic-ai/skybridge
おわりに
ChatGPT アプリを開発するには、コンテキストの伝わり方、インターフェースの振る舞い、ユーザーとモデルの協力の仕方を見直す必要があります。この記事で紹介した教訓の多くは、慣れ親しんだ Web 開発のパターンと、エージェントが動くシステムの実際の振る舞いとの違いから生まれました。
これらの教訓を共有し、オープンソースフレームワークと Codex スキルに組み込むことで、各チームが同じ問題を一から調べ直す時間を減らし、この新しいやり取りの形で何ができるかを探ることに、より多くの時間を使えるようになればと考えています。とりわけ魅力的な ChatGPT アプリは、既存の製品を単に移植するだけでは生まれません。AI を前提とした新しいユーザー体験を中心に据え、そこから設計することで生まれます。