iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

A2UIの仕組みを整理して動かしてみる

はじめに

こんにちは。iimonでエンジニアをしている保田です。 最近Generative UIというキーワードを見かけることが増え、気になって調べてみました。 AIチャットの応答がテキストだけでなく、カードやフォームなどのUIで返ってくることもよく見かけるようになりました。 これを実現する仕組みの一つが、Googleが公開したA2UI(Agent to UI)プロトコルです。 この記事ではGenerative UIの実装パターンを整理したうえで、A2UIの仕組みを解説し、公式デモを動かしながら実際の通信内容を確認していきたいと思います。 今回はA2UIの概要とプロトコルの仕組みに焦点を当てているため、カタログの作成方法やレンダラーの実装など、アプリケーションに組み込む際の詳細には触れていません。

Generative UIとは何か?

Generative UIとは、AIが会話の中でUI(ユーザーインターフェース)を動的に生成するパターンです。 従来のAIとの対話はテキストベースが中心でしたが、テキストだけでは複雑な情報を伝えるのが難しいという課題があります。

例えば今日の天気をAIチャットで聞いた場合、テキストベースのやり取りでは以下のようになります。

ユーザー→「今日の東京都の天気を教えて?」 AI→「今日の東京都の天気は晴れのち曇りです。最高気温28℃、最低気温19℃、降水確率は午前10%・午後30%・夜60%です。午後から雲が増え、夜には雨が降る可能性があるので傘を持っていくのがおすすめです。」

このようにテキストだけで回答されると、情報量が多く把握しづらいです。 以下のように天気アイコン付きの時間帯別予報や降水確率のビジュアル表示などを動的に生成して表示できれば、ユーザーは視覚的に情報を理解しやすくなります。

Generative UIの3つの実装パターン

LLMにUIをどこまで生成させるかによって、制御型生成UI、宣言型生成UI、オープンエンド型生成UIの3つのパターンに分けることができます。

制御型生成UI

制御型生成UIとは、事前にUIコンポーネントを構築しておき、エージェントが「どのコンポーネントをいつ表示するか?」を選択するパターンです。 エージェントの役割はコンポーネントの選択とデータの受け渡しだけで、UI自体はあらかじめ開発者が用意したものを使用します。 制御しやすく品質も安定しますが、事前に用意したコンポーネント以外のUIは表示できないため多様性は限られます。

  • 関連技術:MCP-UI、Apps SDK
  • 具体例:天気を聞いたら事前に作っておいた「天気カード」コンポーネントを表示する、株価を聞いたら「株価チャート」コンポーネントを表示するなど

宣言型生成UI

宣言型生成UIは、エージェントがUIの構造をJSON等の構造化データで記述して返し、フロントエンドがそれを解釈してレンダリングするパターンです。 制御型のように事前定義されたコンポーネントの中から選ぶだけでなく、エージェントがコンポーネントの組み合わせやレイアウトを自由に決められるため、柔軟性が高くなります。

  • 関連技術:A2UI、Open-JSON-UI、Vercel AI SDK(AI SDK UI)、CopilotKit
  • 具体例:エージェントが「カード3枚を横並びに配置し、それぞれに画像・タイトル・ボタンを含む」といったレイアウトをJSONで指示する

オープンエンド型生成UI

オープンエンド型生成UIは、エージェントがHTML/CSS/JavaScriptなどのコードを直接生成し、UIの構造も見た目もすべてを自由に作り出すパターンです。 フロントエンドは生成されたコードをそのままレンダリングするだけなので、表現の自由度は最大ですが、セキュリティリスク(任意コード実行)や品質のばらつきが課題になります。

  • 関連技術:Claude Artifacts、Vercel v0
  • 具体例:「インタラクティブなダッシュボードを作って」と指示すると、Reactコンポーネントのコードごと生成されてプレビュー表示される

制御型は安全で安定していますが表現力に限界があり、オープンエンド型は自由度が高い反面セキュリティリスクを伴います。 今回解説する宣言型(A2UI)はその中間に位置し、カタログで使えるコンポーネントを制限しつつ、エージェントが自由に組み合わせることで安全性と柔軟性を両立しています。

A2UIの基本的な仕組み

A2UIとは何か

A2UI(Agent to UI)は、Googleが提唱したオープンソースのプロトコルです。 AIエージェントがUIを安全に生成するための仕様で、特徴は宣言的なJSONメッセージでUIを記述する点です。 エージェントはコードを実行するのではなく、「どんなコンポーネントをどう配置するか」をJSONで宣言して、実際のレンダリングはクライアント側が行います。

A2UIは、AIエージェントがUIを安全に送信する際の技術的課題を解決します。 テキストだけの応答では表現力が不足する一方、コードを自由に実行させると生成されたコードがアプリと同じ権限で動作するため、XSSと同様のセキュリティリスクがあります。 A2UIはその中間として、宣言的なデータ形式でUIを記述することで安全性と表現力を両立しています。 この記事ではA2UIの最新ドラフトのv0.9の仕様を解説しています。

github.com

A2UIの設計思想

A2UIは以下の3つのコアコンセプトで構成されています。

ストリーミングメッセージ

UIの更新はJSONメッセージとしてエージェントからクライアントにストリーミングされます。 レスポンス全体の生成を待たずに、チャンクごとに順次レンダリングされるため、ユーザーはUIが組み上がっていく様子をリアルタイムで見ることができます。

宣言的コンポーネント

A2UIではコンポーネントの階層構造を「隣接リストモデル」で構成しています。 UIの階層構造をJSONで表現しようとすると普通はネストで表現するのが自然ですが、A2UIではフラットなリストにしてIDで親子関係を参照しています。

[
  {
    "id": "root",
    "component": "Column",
    "children": ["greeting", "buttons"]
  },
  {
    "id": "greeting",
    "component": "Text",
    "text": "Hello"
  },
  {
    "id": "buttons",
    "component": "Row",
    "children": ["cancel-btn", "ok-btn"]
  },
  {
    "id": "cancel-btn",
    "component": "Button",
    "child": "cancel-text",
    "action": { "event": { "name": "cancel" } }
  },
  {
    "id": "cancel-text",
    "component": "Text",
    "text": "Cancel"
  },
  {
    "id": "ok-btn",
    "component": "Button",
    "child": "ok-text",
    "action": { "event": { "name": "ok" } }
  },
  {
    "id": "ok-text",
    "component": "Text",
    "text": "OK"
  }
]

各コンポーネントは一意のidを持ち、childrenchildで他のコンポーネントのIDを指定して親子関係を構築します。 すべてのコンポーネントが同じ階層にフラットに並んでいるのが特徴です。

フラットにしている理由としては、LLMにとって生成しやすい点と、コンポーネントを1つずつ順番に生成でき、途中で特定のコンポーネントだけをIDで更新することも簡単にできるためです。

データバインディング

A2UIではUIの構造(コンポーネント)と表示するデータ(データモデル)を分離しています。

各サーフェスはJSONオブジェクトとしてデータモデルを持ちます。 サーフェスとは、1つのまとまったUI単位のことをいいます。例えば、レストラン検索の結果一覧や予約フォームなどがサーフェスにあたります。

{
  "user": { "name": "Alice", "email": "alice@example.com" }
}

コンポーネントはJSON Pointerのパスでデータモデルの値を参照します。 例えば{"path": "/user/name"}と指定すると、データモデルから「Alice」が取得されて表示されます。

{
  "id": "username",
  "component": "Text",
  "text": { "path": "/user/name" }
}

データモデルの値が変更されると、UIも自動的に更新されます。例えば/user/nameを「Bob」に更新すると、コンポーネントを再送信しなくても画面の表示が「Bob」に変わります。

入力系コンポーネント(TextFieldなど)では逆方向にも動作します。 ユーザーがフォームに値を入力すると、データモデルも自動的に更新されます。この双方向バインディングにより、エージェントはデータモデルを通じてユーザーの入力内容を把握できます。

データの流れ

ユーザーのプロンプト送信をトリガーに、エージェントがJSONメッセージを生成し、トランスポート(SSE、WebSocket、A2Aなど)を通じてクライアントに送信、クライアントがレンダリングするという流れです。

エージェントはレスポンスとして、以下のメッセージをストリーミングで順番にクライアントへ送信します。

  1. サーフェスを作成するcreateSurface):サーフェス(1つのまとまったUI単位)の描画領域を作成し、使用するカタログ(エージェントが使えるコンポーネントの一覧)を指定する
  2. UIの構造を定義するupdateComponents):どんなコンポーネントをどう配置するか
  3. データを投入するupdateDataModel):UIに表示するデータを設定する

クライアントはこれらのメッセージを受信すると、コンポーネントをネイティブのウィジェット(Web ComponentsやReactコンポーネントなど)に変換して描画します。

A2UIを動かしてみる

A2UI公式リポジトリのQuickstartを使って、レストラン予約デモを動かしてみます。

github.com

Step 1: リポジトリのクローン

git clone https://github.com/google/a2ui.git
cd a2ui

Step 2: APIキーの設定

今回はGemini APIを使用しました。

export GEMINI_API_KEY="your_gemini_api_key_here"

Step 3: Litクライアントのディレクトリに移動

cd samples/client/lit

Step 4: インストールと起動

npm run demo:restaurant

以下の画面が表示されるはずです。

すでにデフォルトで入力されている「Top 5 Chinese restaurants in New York.」のまま送信してみると、テキストでの回答ではなく、レストランの写真や住所、評価などがカード形式で表示されるはずです。 ここで表示されているUIはすべてAIエージェントがA2UI JSONメッセージを生成してブラウザに送信し、クライアント側でレンダリングされたものになります。

実際のリクエストとレスポンスを見てみる

「Top 5 Chinese restaurants in New York.」を送信した際に、DevToolsのNetworkタブからリクエストとレスポンスの中身を確認してみます。 先ほど「データの流れ」で説明したcreateSurfaceupdateComponentsupdateDataModelの流れが、実際のレスポンスでどのように表現されているかを見ていきます。

リクエストは、A2AプロトコルのJSON-RPC 2.0形式で送信されます。

{
  "jsonrpc": "2.0",
  "method": "message/send",
  "params": {
    "message": {
      "messageId": "832717ac-a042-4db7-869b-474b16813fc3",
      "role": "user",
      "parts": [
        {"kind": "text", "text": "Top 5 Chinese restaurants in New York."},
        {"kind": "data", "data": {"useStreaming": false}, "mimeType": "application/json"}
      ],
      "kind": "message"
    }
  },
  "id": 1
}

レスポンスも同じ形式で返ってきます。 resultの中にタスクの状態とhistory(メッセージ履歴)が含まれています。

{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "contextId": "034be778-...",
    "id": "bbb1f516-...",
    "kind": "task",
    "status": { "state": "input-required" },
    "history": [ ... ]
  }
}

historyの中にはエージェントからの複数のメッセージが含まれており、先ほど説明した「データの流れ」の各ステップが具体的なJSONとして表現されています。 履歴を順に見ていくと、A2UIのデータの流れを知ることができます。

レスポンス全体

{
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "contextId": "1f80fed1-6109-4e39-87a8-824105491fb0",
        "history": [
            {
                "contextId": "1f80fed1-6109-4e39-87a8-824105491fb0",
                "kind": "message",
                "messageId": "64a1f2cc-fdc7-497c-be39-0df6a4f28ac6",
                "parts": [
                    {
                        "kind": "text",
                        "text": "Top 5 Chinese restaurants in New York."
                    },
                    {
                        "data": {
                            "useStreaming": false
                        },
                        "kind": "data"
                    }
                ],
                "role": "user",
                "taskId": "91a9ce98-7bfd-48de-b3d3-c96659a9f9dc"
            },
            {
                "contextId": "1f80fed1-6109-4e39-87a8-824105491fb0",
                "kind": "message",
                "messageId": "247bf31b-9091-423d-978e-1545a29dd835",
                "parts": [
                    {
                        "kind": "text",
                        "text": "I've found the top 5 Chinese restaurants in New York for you! Here are some great options:\n\n"
                    }
                ],
                "role": "agent",
                "taskId": "91a9ce98-7bfd-48de-b3d3-c96659a9f9dc"
            },
            {
                "contextId": "1f80fed1-6109-4e39-87a8-824105491fb0",
                "kind": "message",
                "messageId": "87c17d9e-0f44-497d-9e94-7520793a6a8a",
                "parts": [
                    {
                        "data": {
                            "version": "v0.9",
                            "createSurface": {
                                "surfaceId": "default",
                                "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json",
                                "theme": {
                                    "primaryColor": "#FF0000",
                                    "font": "Roboto"
                                }
                            }
                        },
                        "kind": "data",
                        "metadata": {
                            "mimeType": "application/json+a2ui"
                        }
                    }
                ],
                "role": "agent",
                "taskId": "91a9ce98-7bfd-48de-b3d3-c96659a9f9dc"
            }
        ],
        "id": "91a9ce98-7bfd-48de-b3d3-c96659a9f9dc",
        "kind": "task",
        "status": {
            "state": "input-required"
        }
    }
}

historyの最初のメッセージはユーザーのリクエストなので、エージェントからの応答を順に見ていきます。

1.テキスト応答

まずエージェントが通常のテキストメッセージを返します。

{
  "role": "agent",
  "parts": [
    {"kind": "text", "text": "I've found the top 5 Chinese restaurants in New York for you!..."}
  ]
}

「ニューヨークの中華レストランTOP5を見つけました!」というテキスト応答です。

2.UIサーフェスの作成(createSurface

次に、UIを描画するためのサーフェスを作成します。 サーフェスとは、レストラン検索の結果一覧や予約フォームなど、1つのまとまったUI全体を表す単位です。 サーフェスIDは後続のupdateComponentsupdateDataModelで、どのサーフェスに対する操作かを指定するために使います。 また、カタログ(エージェントが使えるコンポーネントの一覧を定義したJSON)でコンポーネントの種類を制限し、テーマでUIの見た目を設定します。

{
  "role": "agent",
  "parts": [
    {
      "kind": "data",
      "metadata": {"mimeType": "application/json+a2ui"},
      "data": {
        "version": "v0.9",
        "createSurface": {
          "surfaceId": "default",
          "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json",
          "theme": {
            "primaryColor": "#FF0000",
            "font": "Roboto"
          }
        }
      }
    }
  ]
}

3.コンポーネントの定義(updateComponents

UIの構造をフラットな隣接リストで定義します。各コンポーネントはIDで参照し合います。

{
  "data": {
    "version": "v0.9",
    "updateComponents": {
      "surfaceId": "default",
      "components": [
        {"id": "root", "component": "Column", "children": ["title-heading", "item-list"]},
        {"id": "title-heading", "component": "Text", "variant": "h1", "text": {"path": "/title"}},
        {
          "id": "item-list",
          "component": "List",
          "direction": "vertical",
          "children": {"componentId": "item-card-template", "path": "/items"}
        },
        {"id": "item-card-template", "component": "Card", "child": "card-layout"},
        {"id": "card-layout", "component": "Row", "children": ["card-image", "card-details"]},
        {
          "id": "card-image",
          "component": "Image",
          "variant": "mediumFeature",
          "url": {"path": "imageUrl"}
        },
        {
          "id": "card-details",
          "component": "Column",
          "children": [
            "template-name",
            "template-rating",
            "template-detail",
            "template-link",
            "template-book-button"
          ]
        },
        {"id": "template-name", "component": "Text", "variant": "h3", "text": {"path": "name"}},
        {"id": "template-rating", "component": "Text", "text": {"path": "rating"}},
        {
          "id": "template-book-button",
          "component": "Button",
          "child": "book-now-text",
          "variant": "primary",
          "action": {
            "event": {"name": "book_restaurant", "context": {"restaurantName": {"path": "name"}}}
          }
        },
        {"id": "book-now-text", "component": "Text", "text": "Book Now"}
      ]
    }
  }
}

コンポーネントがネストではなくフラットに並んでいる点が特徴です。 childrenchildでIDを参照して親子関係を表現し、{ "path": "name" }のようにデータバインディングでデータモデルの値を参照しています。 また、childrencomponentIdpathを指定すると、配列の各要素に対してテンプレートが繰り返し描画されます。

4.データの投入(updateDataModel

最後に、UIに表示するデータを投入します。

{
  "data": {
    "version": "v0.9",
    "updateDataModel": {
      "surfaceId": "default",
      "path": "/",
      "value": {
        "title": "Top 5 Chinese Restaurants in NY",
        "items": [
          {
            "name": "Xi'an Famous Foods",
            "rating": "★★★★☆",
            "detail": "Spicy and savory hand-pulled noodles.",
            "imageUrl": "http://localhost:10002/static/shrimpchowmein.jpeg",
            "address": "81 St Marks Pl, New York, NY 10003"
          },
          {
            "name": "Han Dynasty",
            "rating": "★★★★☆",
            "detail": "Authentic Szechuan cuisine.",
            "imageUrl": "http://localhost:10002/static/mapotofu.jpeg",
            "address": "90 3rd Ave, New York, NY 10003"
          }
        ]
      }
    }
  }
}

このように、UIの構造(3)とデータ(4)が完全に分離されているため、同じUI構造で異なるデータを表示したり、データだけを更新してUIを再描画したりすることが簡単にできます。

まとめ

今回Generative UIやA2UIの基本的な仕組みを整理してみて、今後はテキストだけでなくカードやフォームなどのUIで応答が返ってくる場面がますます増えていくだろうと感じました。 また、どのサービスでも同じようなUIではなく、サービスやユーザーのニーズに合わせて最適なUIが動的に生成される世界もありえるのではないかと思います。

A2UIのReact向けレンダラーも公開されているようなので、次はより実装に近い形でA2UIを試してみたいと思います。

この記事を読んで少しでもiimonに興味を持ってくださった方がいらっしゃいましたら、まずはカジュアルにお話しましょう! ぜひお気軽にご応募ください!

iimon採用サイト / Wantedly / Green

参考

a2ui.org

github.com