はじめに
今回、3D空間上でアバターと対話しながら、必要に応じてUIを生成するAIエージェントアプリを作りました。(まだ試作ですが...)
Generative UIでは、チャット欄の中にテキスト応答とUIを一緒に表示することが多いと思います。今回は少し変えて、会話はチャット欄、UIはホワイトボードに分けてみました。
そのあたりの構成や、作ってみて感じたことを書いていきます。
作ったアプリデモ
試しにスライドを表示してみてます。
一応、他にもクイズ機能や用途不明な入力フォームも表示・操作できます。
Generative UI について
前提として「Generative UI」という言葉には、主に2つの意味があります。
- AIがフロントエンドのソースコードそのものを生成する(例:v0とか)
- 生成AIやAIエージェントが、会話の文脈に応じてUIを動的に生成する
この記事で扱うのは後者です。
Generative UIとは
従来のAIエージェントとのやり取りは、テキストだけで行うことが多いと思います。
ただ、テキストだけだと表現しづらい場面もあります。
- 長文の文字だらけで読みにくい
- 他のWebサイトと横断して操作する必要がある
- テキスト入力が手間
Generative UIは、こうした課題を解決するための考え方です。
AIエージェントが会話の文脈やタスクに応じてUIを生成することで、テキストだけでは扱いづらい情報を、より分かりやすく表現できます。
- 長文の文字だらけで読みにくい ⇒ カードやグラフでグラフィカルに表現
- 他のWebサイトと横断して操作する必要がある ⇒ 必要な情報や操作を1つのUIに集約
- テキスト入力が手間 ⇒ 入力フォームからユーザーが直接入力
Generative UIをアプリで実現する選択肢
Generative UIを実現する方法には、大きく以下の3つの方式があります。
| 方式 | 概要 | 代表例 |
|---|---|---|
| 事前定義したUIを使用する | あらかじめ用意したUIコンポーネントを、AIエージェントのツール呼び出しや状態に応じて表示する | CopilotKit(標準機能) / Vercel AI SDK など |
| 宣言的にUIを定義する | AIエージェントがJSON形式のUI定義を返し、フロントエンド側で解釈して描画する | A2UI / Open JSON UI |
| アプリを埋め込む | MCPサーバーがUIリソースを提供し、ホスト側がiframeとして表示する | MCP Apps |
次に、今回の用途に対して見たときのメリット・デメリットを考えてみます。
| 方式 | メリット | デメリット |
|---|---|---|
| 事前定義UIを使用する |
UI品質が安定しやすい アプリ側でコンポーネントや引数を管理するため、表示されるUIを制御しやすい |
柔軟性には限界がある AIが使えるUIは、基本的に事前に用意したコンポーネントに限られる |
| 宣言的にUIを定義する |
柔軟にUIを組み立てられる AIが複数のコンポーネントを組み合わせて、UI構造を定義できる |
出力の制御が難しい 自由度が高い分、UI構造の一貫性や適切さを担保しにくい |
| アプリを埋め込む |
疎結合で再利用しやすい UIをMCPサーバー側で管理できるため、複数のホストやエージェントから使いやすい |
アプリ全体との統一感を出しにくい UIがiframe内で独立するため、デザインやレイアウトをそろえにくい |
なぜ今回はA2UIを使ったのか
今回は最初から、生成されたUIをチャット欄ではなく、ホワイトボード上に表示するというコンセプトで考えていました。
ホワイトボードを想像してみると、決まった情報を表示する場所というより、必要に応じて要素を自由に配置できる領域です。
そのため、会話の内容に応じてUIの構造を柔軟に変えられる仕組みが合いそうだということで、「A2UI」を使ってみました。(元々、使ってみたかったということもありますが....)
2. コンセプト
元々やりたかったこと
元々、作りたかったのは、チャット型ではなく3D空間のアバターと対話するAIエージェントアプリです。
- チャット欄は入力・履歴の補助として位置づけ、体験の中心はアバターとの対話に置くこと
- 会話の内容に応じて、カード・一覧・比較表などのUIも生成できること
感じた課題
Generative UIをチャット欄に表示するにあたり、いくつか課題を感じました。
没入感の欠如
- チャット欄でテキストをやり取りしながらも、3Dアバターがいることで「誰かと話している」感覚が生まれる。
- しかし、その流れの中に突然グラフやフォームなどのUIが現れると、その感覚が一気に冷めてしまう
操作性
- チャット欄に表示したUIは会話の流れとともに上へ流れていってしまい、固定して参照し続けることができない
3Dアバターとの対話体験を壊さない設計
考えたのは、「Generative UIの表示場所をチャット欄から分離し、3D空間の世界観に溶け込ませる」というアプローチです。
チャット欄へのUIの割り込みをなくしながら、アバターがいる空間の中でUIを見せることで、対話の流れを保ったまま情報を伝えられます。
現実世界で誰かがホワイトボードを使いながら説明してくれる体験に近いイメージです。
今回は、適当にホワイトボードを使いましたが、用途に応じて、ディスプレイ・パネルなど、別の形に置き換えることもできると思います
3. システム構成
実際に構築したシステムの構成を記載したいと思います。
今回は主にAWSを使用しています。
機能概要
今回のアプリでできることは以下のとおりです。
- 3Dアバターとテキストでやり取りできる
- 会話の内容に応じて、スライド・グラフなどのUIをホワイトボードに描画できる
- AIの返答を音声で読み上げられる
- ユーザー認証などログイン・ログアウトができる
前提
- A2UIバージョン:v0.9を使用
- フロントエンドとAIエージェント間の通信:SSE + AG-UIを使用
AG-UI
AIエージェントとフロントエンド間のリアルタイム通信を標準化するオープンプロトコルです。
エージェントの状態・ツール実行・テキストストリームなどをサーバー送信イベント(SSE)として送受信します。
アーキテクチャ図
これらの機能は、フロントエンド・バックエンド・AIエージェントの3層で実現しています。
フロントエンド
フロントエンドを構想する主な要素は以下になります。
| 分類 | 構成 | 役割 |
|---|---|---|
| アプリ | React | アプリケーション本体を構成する |
| アプリ | Three.js | 3D空間を描画する |
| アプリ | @pixiv/three-vrm | VRMアバターをThree.js上でレンダリングする |
| インフラ | AWS Amplify Hosting | WebアプリをAWS上でホスティングする |
| インフラ | AWS Amplify Auth | Amazon Cognitoを構築し、ユーザー認証を担う |
3DモデルはVRM*を使用
VRMは、人型アバター向けの3Dモデル形式です。
.vrm というファイル形式で、VTuber、メタバース、3Dアバターアプリなどでよく使われます。
今回はデフォルトのモデルを使ってるので、自分でモデル作りたいですね。
バックエンド
バックエンドを構想する主な要素は以下になります。
| 分類 | 構成 | 役割 |
|---|---|---|
| インフラ | Amazon API Gateway | ストリーミング通信のHTTPエンドポイントを提供する |
| インフラ | AWS Lambda | CopilotKit Runtimeを動作させる実行環境を提供する |
| アプリ | CopilotKit Runtime | フロントエンドとAgentCore上の Strands Agentsをつなぐブリッジ |
| インフラ | AWS Lambda Web Adapter | VOICEVOXをコンテナとしてLambda上で動作させる |
| アプリ | VOICEVOX | 音声合成を担う |
| インフラ | Amplify Data | AWS AppSync + DynamoDBを構築し、 ユーザー設定などのデータを管理する |
音声読み上げはVOICEVOXを使用
VOICEVOXは、テキストを入力すると音声を生成できる、無料の音声合成ソフトウェアです。
ずんだもん、四国めたん、春日部つむぎ などの声が使えます。(ずんだもん最高!)
AIエージェント
AIエージェントを構想する主な要素は以下になります。
※ Amazon Bedrock AgentCoreは長いので、Agent Coreと略してます。
| 分類 | 構成 | 役割 |
|---|---|---|
| アプリ | Strands Agents | AIエージェント本体として動作する |
| アプリ | AG-UIアダプター | Strands AgentsをCopilotKit経由のAG-UI通信に対応させる |
| インフラ | AgentCore Runtime | Strands Agentsをホスティングする |
| インフラ | AgentCore Memory | AIエージェントの会話コンテキストを記憶・管理する |
| インフラ | AgentCore Identity | Tavily検索APIなど外部サービスのAPIキーを管理する |
AIエージェントがA2UIでUIを表示する仕組み
CopilotKit RuntimeがAIエージェントへRender A2UIツールを注入する
CopilotKit Runtimeの設定で a2ui: { injectA2UITool: true } を指定すると、render_a2ui というツールがエージェントに自動的に注入されます。
現時点で Strands Agents 用の AG-UI アダプター(ag_ui_strands)には、RunAgentInput.context をモデル入力に反映しないという制約があります。render_a2ui ツール自体はエージェントに注入されますが、A2UIのカタログスキーマや使用ガイドラインといったコンテキストはエージェントに伝わりません。ツールの存在は認識できても、どのコンポーネントをどう使うかを把握できない状態です。
この問題についてIssueを提出しています。
https://github.com/ag-ui-protocol/ag-ui/issues/1636
この制約への対策として、Strands Agents の「スキル」機能を利用し、A2UI 用のスキルを定義しました。スキルにカタログの定義や使用ガイドラインを記述することで、エージェントがコンポーネントの使い方を把握できるようにしています。
エージェントがrender_a2uiツールを呼び出す
Strands Agentsは会話の文脈に応じてUIが有効と判断したとき、render_a2ui ツールを呼び出し、UIコンポーネントの組み合わせをJSON形式でフロントエンドへ渡します。
このとき参照できるコンポーネントには2種類あります。
- デフォルトコンポーネント: A2UIが標準で提供するもの
- カスタムコンポーネント: 独自カタログに定義するもの。今回はスライド表示やグラフ表示など、ホワイトボード向けのコンポーネントを追加しています
フロントエンドがUIをホワイトボードへ描画する
フロントエンドでは、以下の2つのパッケージを使用しています。
-
@copilotkit/react-core: CopilotKitのコア機能を提供するパッケージ。CopilotKitProviderでアプリ全体をラップし、エージェントとの通信基盤を担います -
@copilotkit/a2ui-renderer: A2UI専用の独立したパッケージ。カタログの構築とUI定義のレンダリングを担います
CopilotKitProvider に a2ui={{ catalog: appCatalog }} を指定することで、カスタムカタログを登録します。
エージェントが render_a2ui を呼び出すと、その結果はAG-UIメッセージとしてフロントエンドに届きます。フロントエンドはこのメッセージを検知してZustandストアに保持し、ホワイトボード側へ渡します。
ホワイトボード側は createA2UIMessageRenderer で作成したレンダラーを持っており、ストアからメッセージを受け取るとカタログを参照してUIを描画します。描画先は @react-three/drei の Html コンポーネントで、ホワイトボードの表面にUIを埋め込んでいます。
Zustand
Reactアプリ向けの軽量な状態管理ライブラリです。今回はAG-UIメッセージをホワイトボードへ橋渡しするためのストアとして使用しています。
A2UIActivitiesHost
AG-UIメッセージを監視し、A2UI用のメッセージを検知してZustandストアに格納するコンポーネントです。エージェントからのA2UIレスポンスをホワイトボードへ橋渡しする役割を担います。
createA2UIMessageRenderer
A2UIのカタログとテーマを渡すことでA2UIメッセージをReactコンポーネントとして描画するレンダラーを生成する関数です。
@react-three/drei の Html
3D空間の任意の座標にDOMを配置できるコンポーネントです。Three.js内に通常のReactコンポーネントをそのまま表示できます。
まとめ
ということで、Generative UIの実現パターンのひとつとして、独自のUI表示領域を持つAIエージェントを作ってみました。とはいえ、まだまだ道半ばです。
Generative UIを「どこに表示するか」という設計は、アプリの体験にかなり影響するポイントだと感じています。本アプリでは、その体験を少しでも良くできそうな感触があり、Generative UIの自由度も一段広がった気がしています。
A2UIがベストかというとケース次第ですが、今後はより多様なUIを試しつつ、検証を深めていきたいです。
3D部分もまだほぼデフォルト状態なので、アプリ全体としてもう少し作り込んでいければと思っています。





