マルチエージェントAIシステム:GenkitでGoogle Mapsによるグラウンディング

Dev.to / 2026/4/8

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • この記事では、Genkitの「Google Mapsによるグラウンディング」を使って、Geminiが最新の場所情報(座標、詳細、レビュー)を取得し、それを応答に活用する方法を説明します。
  • 場所に関するテキストを返すだけでなく、この機能は「Contextual Token」を生成し、フロントエンドがアプリ内でインタラクティブなGoogle Mapsウィジェット(例:道案内付きのレストランカード)を表示できるようにします。
  • 必要なGoogle Maps PlatformのAPIを、関連するGoogle Cloudプロジェクトで有効化するための手順を段階的に案内します。そこにはMaps JavaScript APIに加えて、PlacesおよびMaps Embedのサブサービスが含まれます。
  • さらに、Google Maps APIキーの作成方法も取り上げ、悪用を防ぐためにアプリケーションの制限(application restrictions)で特定のWebサイトにのみ利用を許可することを強調します。
  • Google Mapsによるグラウンディングを、以前のマルチエージェントのツール呼び出しやGoogle Search Retrievalを用いたデータ・グラウンディングに続く、実用的な次のステップとして位置づけています。

前回の記事では、Angular と Google の Genkit を使ってマルチエージェント AI コンシェルジュを構築する方法を探りました。マルチエージェント・パターンのアーキテクチャ上の利点と、Google Search Retrieval を使ってエージェントを現実のデータに基づける方法について説明しました。

今日はさらに一歩進めます。Genkit の強力な機能である Grounding with Google Maps を取り上げます。これにより、AI は場所について語るだけでなく、アプリケーション内で直接インタラクティブで文脈に沿った地図体験を提供できるようになります。

Google Maps による Grounding とは?

Google Maps による Grounding により、Gemini はリアルタイムの施設情報、座標、口コミにアクセスできます。さらに重要なのは、フロントエンドが Contextual Token を使ってインタラクティブな Google Maps ウィジェットを描画できることです。

レストランの文章による説明を受け取るだけでなく、ユーザーは写真、評価、そして「行き方を取得」ボタンが付いたインタラクティブなカードを受け取れます。これはすべて、モデルが応答を生成するために使用したのと同じデータによって動作します。

Google Maps API の有効化

これらの機能を使う前に、アプリケーションに関連付けられた Google Cloud Project で必要な API を有効化する必要があります。これにより、Maps JavaScript API と関連する Places サービスがアプリケーションで利用可能になります。

  1. Google Cloud Console に移動し、アプリに関連付けられているプロジェクトを選択します。
  2. APIs & Services > Library に移動します。
  3. 検索して Maps JavaScript API を有効化します。
  4. 有効化したら、Maps JavaScript API のダッシュボード内の APIs & Services タブへ移動し、以下が有効になっていることを確認します(これらはメインの API のサブ API です)。
    • Places API
    • Places API (New)
    • Maps Embed API

API キーの取得と制限

API を有効化したら、プロジェクトのセキュリティを保ちながら、リクエストを認証する手段が必要です。

1. キーを作成する

  • Google Cloud Console で Google Maps Platform > Keys & Credentials に移動します。
  • Create Credentials をクリックし、API Key を選択します。
  • 新しい API キーをコピーします(次の手順で必要になります)。

2. キーを制限する(重要!)

悪意ある第三者による不正利用を防ぐために、あなたのキーを特定のウェブサイトだけで使用可能にするよう必ず制限する必要があります。

  • 新しく作成したキーをクリックして設定を編集します。
  • Application restrictions の下で、Websites (HTTP referrers) を選択します。
  • 自分のウェブサイトの URL を追加します(例: 開発用の https://your-app.web.app/*http://localhost:4200/*)。
  • API restrictions の下で Restrict key を選択し、先ほど有効化した Maps と Places API を指定します。
  • Save をクリックします。

バックエンド実装:「Find & Navigate」エージェント

コンシェルジュ・システムには findAndNavigateAgentTool という専用のエージェントがあります。このツールは、特に Google Maps による Grounding を使うように構成されています。

1. Google Maps ツールの有効化

Genkit では、ai.generate の設定で tools 配列に追加することで Maps による Grounding を有効化します。

export const _findAndNavigateAgentToolLogic = ai.defineTool(
  {
    name: 'findAndNavigateAgentTool',
    description: '最適なルートと交通手段の選択を支援します',
    inputSchema: z.object({
      input: z.string(),
      history: z.array(conversationMessageSchema).optional(),
    }),
    outputSchema: z.object({
      text: z.string(),
      mapsWidgetToken: z.string().optional(),
    }),
  },
  async ({input, history}) => {
    const response = await ai.generate({
      system: TRANSPORT_AGENT_PROMPT,
      messages: [
        ...toGenkitMessages(history ?? []),
        {role: 'user', content: [{text: input}]},
      ],
      config: {
        tools: [
          {
            googleMaps: {enableWidget: true} // これが魔法の行です
          }
        ]
      },
    });

    // ... 抽出ロジック
  }
);

2. Maps ウィジェットトークンの抽出

返却形式: {"translated": "翻訳されたHTML"}

enableWidget: true が設定されていると、Gemini はグラウンディングのメタデータ内に googleMapsWidgetContextToken を返します。これを抽出し、Angular のフロントエンドに渡し返す必要があります。

const mapsWidgetToken = (response.custom as any)
  ?.candidates?.[0]
  .groundingMetadata
  ?.googleMapsWidgetContextToken as string | undefined;

return { text: response.text, mapsWidgetToken };

Frontend: インタラクティブウィジェットのレンダリング

Angular 側では、この mapsWidgetToken を受け取ります。このトークンを視覚的な地図に変換するために、Google Maps JavaScript API と、特に PlaceContextualElement を使用します。

Maps ウィジェットコンポーネント

ライブラリの読み込みと要素の作成を行う、スタンドアロンの MapsWidget コンポーネントを作成しました。

@Component({
  selector: 'app-maps-widget',
  template: `<div #mapElement class="map-container"></div>`,
  schemas: [CUSTOM_ELEMENTS_SCHEMA], // カスタム要素に必要
})
export class MapsWidget implements AfterViewInit {
  @ViewChild('mapElement') container!: ElementRef<HTMLElement>;
  readonly token = input<string>(''); // Genkit からのトークン

  async ngAfterViewInit() {
    await this.mapsLoader.importLibrary('places');
    this.renderWidget(this.token());
  }

  private renderWidget(token: string) {
    const places = (window as any)['google']?.maps?.places;

    // トークンを使ってコンテキスト要素を作成する
    const el = new places.PlaceContextualElement({ 
      contextToken: token 
    });

    this.container.nativeElement.appendChild(el);
  }
}

なぜ PlaceContextualElement を使うのか?

PlaceContextualElement(または <gmp-place-contextual>)は Google Maps が提供する専用の Web コンポーネントです。これらのグラウンディングトークンと連携して動作するように設計されています。地図上に表示される場所が、AI が話していた内容であることを正確に保証し、情報に対して「チェーン・オブ・カストディ(受け渡しの連続性)」を維持します。

Maps API を安全に読み込む

PlaceContextualElement をレンダリングするには、Google Maps JavaScript API を読み込む必要があります。フロントエンドに API キーを直書きする代わりに、安全な Firebase 関数から動的に取得します。これにより、そのキーをFirebase Secretとして保持し、必要な場合に認証済みユーザーにだけ公開できます。

1. バックエンド:API キーの提供

API キーがリポジトリにコミットされないようにするため、これらを Firebase Secrets として保存します。これによりソースコードからはキーを排除し、関数の実行時の環境にのみ注入します。

手順 1:Secret のデプロイ

Firebase CLI を使って API キーを安全にアップロードします。

firebase functions:secrets:set MAPS_API_KEY

プロンプトが表示されたら、Google Cloud Console で生成した API キーを貼り付けます。

手順 2:コードで Secret にアクセスする

functions/src/index.ts では、secret を定義し、その値を onCall 関数内で取得します。

import { defineSecret } from 'firebase-functions/params';

// Secret を定義する
const MAPS_API_KEY = defineSecret('MAPS_API_KEY');

返却形式: {"translated": "翻訳されたHTML"}export const loadGoogleMaps = onCall(
  {
    ...GENKIT_FUNCTION_CONFIG,
    secrets: [MAPS_API_KEY], // このシークレットへの関数のアクセスを明示的に許可する
  },
  () => {
    // 値を安全に取得する
    return { key: MAPS_API_KEY.value() };
  }
);

2. フロントエンド:GoogleMapsLoaderService

Angularで、Mapsライブラリの非同期読み込みを扱うための中央集約型のサービスを作成しました。@googlemaps/js-api-loader パッケージを使うことで、クリーンでPromiseベースの初期化を実現しています。

@Injectable({ providedIn: 'root' })
export class GoogleMapsLoaderService {
  private readonly functions = inject(Functions);
  private initialized = false;

  private async ensureInitialized() {
    if (this.initialized) return;

    // 1. Firebase関数からAPIキーを取得する
    const loadGoogleMaps = httpsCallable<unknown, { key: string }>(
      this.functions,
      'loadGoogleMaps'
    );
    const { data } = await loadGoogleMaps();

    // 2. キーでJS APIローダーを設定する
    setOptions({ 
      key: data.key, 
      v: 'alpha', // 文脈に応じた要素は、alpha/beta版を必要とすることが多い
      libraries: ['places'] 
    });

    this.initialized = true;
  }

  async importLibrary(library: string) {
    await this.ensureInitialized();
    return importLibrary(library);
  }
}

なぜこのアプローチなのか?

  1. セキュリティ:APIキーは environment.ts に保存されず、index.html にハードコードされてもいません。Firebase Secretsに保持されます。
  2. オンデマンド読み込み:Maps JS SDKは、本当に必要になったときだけ読み込みます(例:最初のマップウィジェットが描画されようとするとき)。
  3. 一貫性:ローダーを中央集約することで、すべてのコンポーネントが同じAPIバージョンと設定を使用できます。

主要な機能とメリット

  1. 文脈に基づく正確さ:ウィジェットは単なるランダムな地図検索ではありません。AIが特定した特定の場所に結び付いています。
  2. インタラクティブなUX:ユーザーはチャットインターフェースを離れずに、評価、営業時間、写真を確認できます。
  3. 低い手間:Place IDや座標を手動で管理する必要はありません。GenkitとMaps APIが、トークンを介してハンドシェイクを処理します。
  4. 最新のAngular連携:Signalsとinput()を使うことで、会話中に生成された新しいトークンに対してコンポーネントが即座に反応します。

結論

Google Mapsによる「グラウンディング」により、あなたのAIはテキストベースのアシスタントから、豊かな空間的ガイドへと変わります。Genkitの強力なツール呼び出しオーケストレーションと、Angularのコンポーネントベースのアーキテクチャを組み合わせることで、実際の物理世界に本当にネイティブに感じられるコンシェルジュ体験を構築できます。

楽しくコーディングを!