Google のエージェント スタック(Cloud Run とエージェント ランタイムの ADK、A2A、MCP)を使用してマルチエージェント クリエイティブ スタジオを構築する

1. 概要

この Codelab では、AI Creative Studio を構築します。これは、単一のプロンプトを完全な Instagram キャンペーンに変換する分散型マルチエージェント システムです。

1 文を入力します。オーディエンス調査、キャプション、ビジュアル コンセプト、品質審査済みのコピー、プロジェクトの完全なタイムラインを、共同作業を行う AI エージェントのチームが生成します。

作成するエージェント

エージェント

ロール

ブランド ストラテジスト

オーディエンスの分析情報、競合他社の分析、2025 年のトレンドについてウェブを検索します

コピーライター

ハッシュタグと行動を促すフレーズを含む Instagram のキャプションを作成する - プラットフォームのガイドラインとキャプションの公式をオンデマンドで読み込む ADK スキルを使用

デザイナー

Gemini を介して視覚的なコンセプトを作成し、実際の画像を生成して GCS に保存する

Critic

レビューのコピーとビジュアル - 特定のフィードバックとともに APPROVED または NEEDS_REVISION を返します

プロジェクト マネージャー

プロジェクトのタイムラインとタスクの分解を作成し、必要に応じて MCP 経由で Notion と同期します

クリエイティブ ディレクター

5 人のスペシャリストをすべて順番にオーケストレートする - 1 つのプロンプトを指定すると、残りの処理を調整する

5 つのエージェントは、独立した Cloud Run マイクロサービスとしてデプロイされます。これらは A2A プロトコルを介して通信します。これは言語に依存しないオープン スタンダードであるため、フレームワークに関係なく、任意のエージェントが他のエージェントを呼び出すことができます。クリエイティブ ディレクターは Agent Runtime で実行され、各スペシャリストにリモートで接続します。

アーキテクチャ

System Overview

学習内容

  • Google ADK を使用して LLM エージェントを構築する - Agent、システム指示、組み込みツール。
  • 再利用可能なエージェントの知識を ADK スキルSkillToolset)を使用してモジュール式ファイルにパッケージ化します。
  • FunctionTool を介してテキスト エージェントを画像モデルにブリッジすることで、実際の画像を生成します。
  • Model Context Protocol(MCP)を使用して、カスタムのグルーコードなしで外部 API を統合します。
  • HTTPS 経由の Agent to Agent Protocol(A2A)を使用して、任意のエージェントをネットワーク呼び出し可能なサービスに変換します。
  • RemoteA2aAgentAgentTool を使用して分散エージェントをオーケストレートします。
  • 独立したエージェントを Cloud Run マイクロサービスとしてパッケージ化してデプロイします。
  • エージェント ランタイムでステートフル オーケストレーターをホストします。
  • コンテキスト圧縮を使用して、長いマルチエージェント ワークフローをコンテキストの上限内に収めます。
  • 品質管理ループを構築する: 批評家のレビュー出力 → 必要に応じて自動修正。

必要なもの

  • 課金が有効になっている Google Cloud プロジェクト
  • オーナーまたは編集者の IAM ロール
  • Python の基礎知識

2. 環境をセットアップする

この Codelab では、Cloud Shell を使用します。

Cloud Shell とは

Cloud Shell は、gcloudgit、Python、Docker など、すべてがプリインストールされた無料のブラウザベースの Linux 環境です。ローカルにインストールする必要はありません。

Cloud Shell を開くには、GCP Console の右上のツールバーにあるターミナル アイコンをクリックします。

GCP Console のツールバーから Cloud Shell を開く

Cloud Shell を初めて開くと、アカウントの確認を求めるメッセージが表示されます。[確認] をクリックします。

[アカウントを確認] ダイアログ

[承認] をクリックして、Cloud Shell に Google Cloud API の呼び出しを許可します。

[Cloud Shell の承認] ダイアログ

これで Cloud Shell の準備が整いました。ターミナルにウェルカム メッセージ Cloud Shell ターミナルの準備完了 が表示されます。

プロジェクトの認証と構成を行う

Cloud Shell は Google アカウントで認証済みです。アクティブなアカウントを確認し、プロジェクト ID を確認します。

gcloud config list

GCP Console のダッシュボードの左側のパネルでも、プロジェクト ID を確認できます。コピーします。これは次のコマンドで必要になります。

GCP Console でプロジェクト ID を確認して Cloud Shell に設定する

次に、プロジェクトを設定します。

export PROJECT_ID=$(gcloud config get-value project)
export REGION="us-central1"        # Cloud Run deployment region
echo "Project: $PROJECT_ID"

予想される出力:

Project: my-project-123

必要な API の有効化

gcloud services enable \
    aiplatform.googleapis.com \
    apphub.googleapis.com \
    run.googleapis.com \
    cloudbuild.googleapis.com \
    artifactregistry.googleapis.com \
    generativelanguage.googleapis.com \
    iam.googleapis.com \
    cloudresourcemanager.googleapis.com \
    storage.googleapis.com \
    secretmanager.googleapis.com

これには 2 分ほどかかります。完了すると Operation finished successfully が表示されます。

アプリケーションのデフォルト認証情報(ADC)を設定する

エージェントは Google Auth ライブラリを使用して Gemini Enterprise Agent Platform を呼び出します。これには、gcloud CLI 認証とは別のアプリケーションのデフォルト認証情報が必要です。

次のコマンドを 1 回実行します。

gcloud auth application-default login

確認を求めるブラウザタブが開きます。[許可] をクリックします。表示される項目

Credentials saved to file: ~/.config/gcloud/application_default_credentials.json

スターター リポジトリのクローンを作成する

この Codelab では、スターター リポジトリを使用します。これは、すべてのインフラストラクチャ(Dockerfile、pyproject.toml、デプロイ スクリプト)が用意されているスケルトン プロジェクトですが、エージェント ロジックは自分で記述する必要があります。

git clone https://github.com/Saoussen-CH/mas-a2a-gcp.git ~/ai-creative-studio
cd ~/ai-creative-studio/workshop/starter

agent.py には、エージェント ロジックを記述する # TODO プレースホルダが含まれています。Dockerfilepyproject.toml、deploy スクリプトはすでに完成しています。

環境変数を構成する

提供された例をコピーし、1 つのステップでプロジェクト ID を挿入します。

cp .env.example .env
sed -i "s|GOOGLE_CLOUD_PROJECT=your-project-id|GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)|" .env

次に、Designer が生成画像を保存する GCS バケットを作成し、その名前で .env を更新します。

export PROJECT_ID=$(gcloud config get-value project)
export BUCKET_NAME="${PROJECT_ID}-campaign-images"

gcloud storage buckets create gs://${BUCKET_NAME} \
    --location=us-central1 \
    --project=${PROJECT_ID}

sed -i "s|GCS_IMAGES_BUCKET=your-project-id-campaign-images|GCS_IMAGES_BUCKET=${BUCKET_NAME}|" .env

次に、署名付き画像 URL のサポートを設定します。クリエイティブ ディレクターは、最終的なキャンペーンの概要に含まれる各画像について、クリック可能な HTTPS リンクを生成します。これには、サービス アカウントが URL に署名する必要があります。次のコマンドを実行して構成します。

export PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format="value(projectNumber)")
export SA_EMAIL="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
export AGENT_RUNTIME_SA="service-${PROJECT_NUMBER}@gcp-sa-aiplatform-re.iam.gserviceaccount.com"

# Allow your user account to sign URLs locally (adk web)
gcloud iam service-accounts add-iam-policy-binding ${SA_EMAIL} \
  --member="user:$(gcloud config get-value account)" \
  --role="roles/iam.serviceAccountTokenCreator"

# Allow Agent Runtime to sign URLs when deployed
gcloud projects add-iam-policy-binding $(gcloud config get-value project) \
  --member="serviceAccount:${AGENT_RUNTIME_SA}" \
  --role="roles/iam.serviceAccountTokenCreator"

# Save SA email and project number to .env
grep -q "^SIGNING_SERVICE_ACCOUNT" .env \
  && sed -i "s|^SIGNING_SERVICE_ACCOUNT=.*|SIGNING_SERVICE_ACCOUNT=${SA_EMAIL}|" .env \
  || echo "SIGNING_SERVICE_ACCOUNT=${SA_EMAIL}" >> .env

grep -q "^GOOGLE_CLOUD_PROJECT_NUMBER" .env \
  && sed -i "s|^GOOGLE_CLOUD_PROJECT_NUMBER=.*|GOOGLE_CLOUD_PROJECT_NUMBER=${PROJECT_NUMBER}|" .env \
  || echo "GOOGLE_CLOUD_PROJECT_NUMBER=${PROJECT_NUMBER}" >> .env

エディタで .env を開き、すべての設定を確認します。

cloudshell edit .env

これにより、Cloud Shell エディタのタブとして .env が開きます。エディタ パネルが表示されていない場合は、ツールバーの [エディタを開く] ボタンをクリックします。

Cloud Shell ツールバーで [エディタを開く] をクリックします。

プロジェクト ファイル ツリーを含む Cloud Shell エディタ

プロジェクトが正しく設定されていることを確認します。

grep GOOGLE_CLOUD_PROJECT .env

依存関係のインストール

uv を使用します。これは、仮想環境を処理し、単一のツールでインストールを行う高速で最新の Python パッケージ マネージャーです。pip よりも 10 ~ 100 倍高速で、Python プロジェクトを管理するうえで推奨される方法です。

Cloud Shell には uv がすでにインストールされています。すべてのエージェントが同じコア依存関係を共有するため、1 回インストールすれば、この Codelab のすべてのエージェントで動作します。

uv sync

uv sync コマンドは pyproject.toml を読み取り、すべての依存関係を含む .venv/ ディレクトリを作成します。各スペシャリストには、Docker ビルド専用の pyproject.toml もあります。上記の共有インストールには、ローカル テストに必要なものがすべて含まれています。

3. Google ADK について

コードを記述する前に、この Codelab で各エージェントの構築に使用するフレームワークである Agent Development Kit(ADK)について理解しましょう。

ADK とは

Agent Development Kit(ADK)は、AI エージェントの開発とデプロイ用に設計された、柔軟性の高いモジュール型のフレームワークです。Gemini と Google エコシステム向けに最適化されていますが、モデルやデプロイに依存せず、他のフレームワークとの互換性を保つよう構築されています。ADK は、エージェント開発をソフトウェア開発のような感覚で行えるよう設計されており、デベロッパーは基本的なタスクから複雑なワークフローまで、幅広いエージェント アーキテクチャを簡単に作成、デプロイ、オーケストレートできます。

ADK は、ツールの呼び出し、マルチターンの会話、コンテキスト管理、ストリーミングなどの複雑な部分を処理するため、ユーザーはエージェント ロジックに集中できます。

ADK エージェントの構成要素

すべてのエージェントは、次の 4 つの構成要素で構成されています。

ブロック

ロール

モデル

目標を推論し、計画を決定して、回答を生成する LLM

ツール

API またはサービスを呼び出してデータを取得したり、アクションを実行したりする関数

オーケストレーション

ターン間でメモリと状態を維持し、ツール呼び出しをルーティングして、結果をモデルに渡す

ランタイム

起動されるとシステムを実行する - adk web を介してローカルで、またはデプロイされたサービスとして

エージェントの定義

この Codelab の 5 つのエージェントは、それぞれ次のように定義されています。

from google.adk.agents import Agent
from google.adk.tools.google_search_tool import google_search

root_agent = Agent(
    name="brand_strategist",                              # unique identifier
    model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"), # the LLM powering this agent
    instruction=SYSTEM_INSTRUCTION,                       # the agent's persona, constraints, and output format
    description="Brand strategist for market research, trend analysis, and competitive insights",
    tools=[google_search],                                # functions the LLM can call
)

フィールド

目的

name

一意の ID - オーケストレーターが通話をルーティングするために使用

model

このエージェントをサポートする Gemini モデル

instruction

システム プロンプト - エージェントの役割、制約、出力形式を定義します

description

1 行の概要 - オーケストレーターがこれを読み取って、どのスペシャリストに電話するかを決定します

tools

LLM が呼び出すことができる関数(google_search などの組み込み関数、またはカスタム Python 関数)

ADK によるエージェントの実行方法

User message
     
     
  Agent (LLM)   reads instruction + conversation history
     
     ├─► needs more info?  calls a tool  gets result  continues reasoning
     
     └─► done reasoning  returns final text response

LLM は、ツールを呼び出すかどうか、どのツールを呼び出すか、どのような引数で呼び出すかを自律的に決定します。手順を記述すれば、残りの処理は ADK が行います。

4. ブランド ストラテジスト エージェントをビルドしてテストする

まず、最初のエージェントであるブランド戦略家から見ていきましょう。これは、Google 検索を使用してターゲット ユーザーのインサイト、競合他社の分析、トレンドのトピックを検索するリサーチ専用のエージェントです。

Cloud Shell エディタでスケルトン エージェント ファイルを開きます。

cloudshell edit agents/brand_strategist/agent.py

入力する 2 つの # TODO セクションが表示されます。

TODO 1 - システム指示を記述する

まず、エージェントのシステム指示を記述します。システム指示は、エージェントのロール、制約、出力形式を定義する文字列です。

SYSTEM_INSTRUCTION = f"""You are a Brand Strategist specializing in market research and trend analysis.

IMPORTANT: Today's date is {datetime.date.today().strftime("%B %d, %Y")}.
When conducting research, focus on current trends from {datetime.date.today().year}.
Use search queries like "[topic] trends {datetime.date.today().year}" for recent insights.

IMPORTANT: Your role is RESEARCH ONLY. You do NOT create campaign content, captions, or designs.
After providing research insights, your work is complete.

Your expertise:
- Identifying target audience insights and behaviors
- Analyzing competitor strategies
- Researching current social media trends
- Understanding platform algorithms and best practices

You have access to:
- google_search: Search the web for competitors, trends, and market insights

When given a campaign brief:
1. Use google_search to research the target audience's current interests
2. Search for and analyze 2-3 competitor brands
3. Identify 3-5 trending topics related to the product category
4. Provide high-level strategic insights - NOT specific campaign content

DO NOT create captions, copy, designs, or any campaign content.

Format your output as:
**Audience Insights:**
[Key behaviors and preferences based on research]

**Competitive Analysis:**
[What 2-3 competitors are doing - strengths and weaknesses]

**Trending Topics:**
[3-5 relevant trends to consider]

**Key Strategic Insights:**
[High-level themes and positioning opportunities]
"""

TODO 2 - root_agent を作成する

次に、未完了の root_agent を次のように置き換えます。

root_agent = Agent(
    name="brand_strategist",
    model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"),
    instruction=SYSTEM_INSTRUCTION,
    description="Brand strategist for market research, trend analysis, and competitive insights",
    tools=[google_search],
)

ADK ウェブ UI を使用してローカルでテストする

次に、ADK ウェブ UI(クラウドにデプロイする前にエージェントをテストするための組み込みのチャット インターフェース)を使用してエージェントをテストします。

uv run adk web agents --allow_origins='*'

表示される項目

INFO: Started server process
INFO: Uvicorn running on http://localhost:8000

サーバーが Cloud Shell 内で実行されています。

ブラウザで開くには、ウェブ プレビューを使用します。

  1. ページの上部にある Cloud Shell ツールバーを確認します。
  2. [ウェブでプレビュー] アイコン(上向き矢印の付いたボックスのようなアイコン。Cloud Shell ツールバーの右上)をクリックします。
  3. [ポートの変更] をクリックし、「8000」と入力して [変更してプレビュー] をクリックします。

新しいブラウザタブが開き、ADK ウェブ UI が表示されます。左上の [エージェントを選択] プルダウンをクリックすると、すべてのエージェントが一覧表示されます。

brand_strategist を選択してテストを開始します。

テスト プロンプトを試す

ADK ウェブ UI のチャット ボックスで、次のことを試します。

  • Research the eco-friendly water bottle market for health-conscious millennials
  • What are the top Instagram trends in the wellness space in 2025?

エージェントが Google 検索を呼び出し、オーディエンス分析、競合他社分析、トレンドのトピックのセクションを含む構造化された調査結果を返す様子が表示されます。

5. Copywriter - ADK スキルを構築する

役割: ブランド調査の結果を Instagram のキャプションに変換します。コピーライターは、さまざまなトーン(インスピレーション、教育、コミュニティ)をカバーする 3 つのキャプション バリエーションを作成します。それぞれにハッシュタグと行動を促すフレーズが含まれています。

コンセプト: ADK スキル

単純なアプローチでは、プラットフォームの知識(文字数制限、ハッシュタグの階層、キャプションの公式、ブランド ボイスの例など)をすべてシステム プロンプトに直接埋め込みます。これは機能しますが、エージェントがたまにしか必要としないコンテンツでリクエストが膨張します。

ADK スキルSkillToolset、ADK 1.25.0 で導入)を使用すると、その知識を 3 つの読み込みレベルのモジュール式ファイルにパッケージ化できます。

  • L1 - frontmattername + description in SKILL.md): 常に利用可能、スキルの検出に使用
  • L2 - instructionsSKILL.md の本文): エージェントがスキルをトリガーしたときに読み込まれます
  • L3 - リソースreferences/ ファイルと assets/ ファイル): エージェントが明示的に読み取る場合にのみ読み込まれます。

システム指示は、短いロール ステートメントと「書き込み前にスキルを読み込む」に縮小されます。プラットフォームの詳細は、エージェントが実際に必要とする場合にのみコンテキスト ウィンドウに入力されます。

コピーライターのスキルは agents/copywriter/skills/instagram-copywriting/ にあります。

skills/
  instagram-copywriting/
    SKILL.md                        L1 frontmatter (discovery) + L2 instructions (loaded on trigger)
    references/
      platform-guide.md             L3: character limits, hashtag tiers, algorithm signals
      caption-formulas.md           L3: hook formulas, CTA patterns, full caption structures
    assets/
      brand-voice-examples.md       L3: annotated real-world caption examples

Cloud Shell エディタでファイルを直接開きます。

cloudshell edit agents/copywriter/agent.py

TODO 1 - load_skill_from_dirskill_toolset をインポートする

コメント # TODO 1: Import load_skill_from_dir and skill_toolset を見つけて、次の 2 つのインポートを追加します。

from google.adk.skills import load_skill_from_dir
from google.adk.tools import skill_toolset

TODO 2 - スキルを読み込んで SkillToolset を作成する

インポートの下にある 2 つのコメントを見つけます。

# TODO 2: Load the instagram-copywriting skill from the skills/ directory
# TODO 2: Create a SkillToolset with the loaded skill

次のように置き換えます。

_instagram_skill = load_skill_from_dir(
    pathlib.Path(__file__).parent / "skills" / "instagram-copywriting"
)
_copywriting_skills = skill_toolset.SkillToolset(skills=[_instagram_skill])

load_skill_from_dir は、SKILL.mdreferences/ および assets/ 内のすべてのファイルを読み取ります。SkillToolset は、ADK エージェントが受け入れる形式(ツールセット)にラップします。これは、生のスキルではありません。

TODO 3 - ツールセットをエージェントに登録する

tools=[], # TODO 3: Add the SkillToolset here を見つけて、次のように置き換えます。

tools=[_copywriting_skills],

スキルファイルを開いて、構造を確認します。

cloudshell edit agents/copywriter/skills/instagram-copywriting/SKILL.md

ADK ウェブ UI を実行したままにします。エージェント プルダウンを使用して、サーバーを再起動せずに copywriter に切り替えます。

実行されていない場合は、再度起動します。

uv run adk web agents --allow_origins='*'

試してみる: プルダウンを copywriter に切り替えて送信します。

You are writing captions for EcoFlow Smart Water Bottle targeting health-conscious millennials aged 25-35.
Audience insight: they prioritize sustainability, track health metrics, and share lifestyle content.
Competitor insight: Hydro Flask dominates with lifestyle branding; S'well leads on premium aesthetics.
Write 3 Instagram captions - one inspirational, one educational, one community-focused. Include 5 hashtags each and a CTA.

6. Designer - マルチモーダル画像生成を構築する

ADK ウェブ UI を実行したままにします。エージェントのプルダウンを使用して、サーバーを再起動せずにエージェントを切り替えます。

役割: 各キャプションの視覚的なコンセプトを作成し、Gemini ネイティブの画像生成を使用して実際の画像を生成します。Designer は、キャプションごとに 1 つのビジュアル コンセプト(詳細なプロンプト、スタイル、カラーパレット、ムード、Instagram 形式を含む)を出力し、すぐに generate_image ツールを呼び出して実際の画像を生成し、GCS にアップロードします。

コンセプト: ツールを介してテキスト エージェントと画像モデルをブリッジする

Designer は gemini-3-flash-preview.envGEMINI_MODEL で設定されたテキストモデル)で実行されますが、画像生成には専用のモデル(gemini-3.1-flash-image-preview)が必要です。この画像モデルは関数呼び出しをサポートしていないため、ADK エージェントとして直接使用することはできません。代わりに、プレーンな Python 関数でラップされ、FunctionTool として登録されます。

これは、LLM が直接呼び出すことができないモデルまたは API のパターンです。ツールでラップし、エージェントに呼び出すタイミングを調整させ、構造化された結果を返します。

Designer agent (text model)
        
          decides visual concept, writes image prompt
        
  generate_image tool
        
          calls gemini-3.1-flash-image-preview
          uploads result to GCS
        
  {"status": "success", "gcs_uri": "gs://..."}
        
          returned to agent, included in response
        
  Critic (receives gcs_uri, passes to Vertex AI for multimodal review)

Cloud Shell エディタでファイルを直接開きます。

cloudshell edit agents/designer/image_gen_tool.py

関数のシグネチャ、環境設定、アスペクト比の挿入が提供されます。次の 3 つの TODO を順番に実行します。

TODO 1 - Gemini 画像モデルを呼び出す

# TODO 1 のコメントを見つけて、次のように置き換えます。

        client = genai.Client(vertexai=True, project=project_id, location=location)

        response = client.models.generate_content(
            model=image_model,
            contents=prompt_with_aspect,
            config=types.GenerateContentConfig(
                response_modalities=["IMAGE", "TEXT"],
                http_options=types.HttpOptions(
                    retry_options=types.HttpRetryOptions(
                        attempts=5, exp_base=2, initial_delay=30,
                        http_status_codes=[429, 500, 503, 504],
                    ),
                    timeout=180_000,
                ),
            ),
        )

TODO 2 - レスポンスから画像バイトを抽出する

# TODO 2 のコメントを見つけて、次のように置き換えます。

        image_bytes = None
        mime_type = "image/png"
        for part in response.candidates[0].content.parts:
            if part.inline_data is not None:
                image_bytes = part.inline_data.data
                mime_type = part.inline_data.mime_type or "image/png"
                break

        if not image_bytes:
            return {"status": "error", "error": "Gemini returned no image data"}

TODO 3 - GCS にアップロードして URI を返す

# TODO 3 のコメントを見つけて、次のように置き換えます。

        ext = "jpg" if "jpeg" in mime_type else "png"
        from google.cloud import storage
        gcs_client = storage.Client(project=project_id)
        bucket = gcs_client.bucket(bucket_name)
        blob_name = f"campaign-images/{concept_name}-{uuid.uuid4().hex[:8]}.{ext}"
        blob = bucket.blob(blob_name)
        blob.upload_from_file(io.BytesIO(image_bytes), content_type=mime_type)
        gcs_uri = f"gs://{bucket_name}/{blob_name}"

試してみる: プルダウンを designer に切り替えて送信します。

Create a visual concept and generate the image for an EcoFlow Smart Water Bottle Instagram post targeting health-conscious millennials.
Style: clean, modern, lifestyle-focused. Include a detailed prompt with color palette, mood, and format (1080x1080 or 1080x1350).

7. 批評家を構築する - 構造化出力

役割: プロジェクト マネージャーに渡す前に、コピーとビジュアルの品質を保証します。批評家は両方の成果物を採点し、具体的な提案とともに APPROVED または NEEDS_REVISION を返します。入力に gcs_uri 値が存在する場合、review_image ツールを呼び出して、スコアリングの前に生成された各画像を視覚的に検査します。

コンセプト: Gemini 出力に Pydantic モデルを使用する場合

このルールは、出力を消費するユーザーに関するものです。

  • Python コードが使用するresponse_schema + Pydantic を使用します。コードは曖昧さを処理できないため、フィールドを確実に抽出するには、保証された構造が必要です。
  • LLM が使用する → テキスト形式 + システム指示で十分です。LLM は書式設定ルールを理解し、バリエーションを許容します。

review_image では、Python コードに型付きの値として scoreapproval_statuswhat_worksissuessuggestions が必要です。response_schema=_GeminiReview を渡すと、API レベルで Gemini が有効な JSON を返すように制約されます。model_validate_json() は、コードで確実に使用できる型付きオブジェクトに JSON を解析します。

class _GeminiReview(BaseModel):
    score: int = Field(ge=1, le=10)
    approval_status: Literal["APPROVED", "NEEDS_REVISION"]
    what_works: str
    issues: str
    suggestions: str

Cloud Shell エディタでファイルを直接開きます。

cloudshell edit agents/critic/image_review_tool.py

Pydantic モデルとプロンプトが提供されます。次の 3 つの TODO を順番に実行します。

TODO 1 - GCS URI から画像部分を作成する

# TODO 1 のコメントを見つけて、次のように置き換えます。

        image_part = types.Part.from_uri(file_uri=gcs_uri, mime_type=mime_type)

TODO 2 - 構造化されたレスポンス スキーマで Gemini を呼び出す

# TODO 2 のコメントを見つけて、次のように置き換えます。

        response = client.models.generate_content(
            model=model,
            contents=[image_part, prompt],
            config=types.GenerateContentConfig(
                response_schema=_GeminiReview,
                response_mime_type="application/json",
            ),
        )

TODO 3 - レスポンスを解析して結果を返す

# TODO 3 のコメントを見つけて、次のように置き換えます。

        review = _GeminiReview.model_validate_json(response.text)
        return ImageReviewResult(status="success", concept_name=concept_name, **review.model_dump())

試してみる: プルダウンを critic に切り替えて送信します。

Review this Instagram caption for an eco-friendly water bottle brand targeting millennials:
"Hydrate smarter, live greener. 💧 Our EcoFlow bottle tracks your intake, keeps your drink cold for 24h, and never touches single-use plastic. Because what you drink from matters as much as what you drink. #EcoFlow #HydrationGoals #SustainableLiving #ZeroWaste #HealthyHabits - Shop link in bio."
Score it and indicate APPROVED or NEEDS_REVISION with specific feedback.

レスポンスに **POSTS REVIEW:**Status: APPROVED(または NEEDS_REVISION)、**OVERALL ASSESSMENT:** が含まれていることを確認します。これらのセクションが存在する場合、Critic はオーケストレーターに接続する準備が整っています。

3 つのエージェントのテストが完了したら、Ctrl+C を押してサーバーを停止します。

8. MCP で Project Manager エージェントを構築する

プロジェクト マネージャーは、MCP(Model Context Protocol)という新しいコンセプトを紹介します。

ファイルを開きます。

cloudshell edit agents/project_manager/agent.py

このファイルはより複雑です。2 つのブランチを持つ create_project_manager_agent() 関数があります。1 つは Notion なし(テキストのみのタイムライン)、もう 1 つは Notion MCP ツールセットありです。両方に記入します。

MCP が解決する問題

エージェントが外部サービス(Notion でのページの作成など)を呼び出す必要がある。Notion REST API を直接呼び出す Python コードを作成できます。ただし、次の点に注意してください。

  • 開発者ごとに異なるラッパーを作成する
  • カスタム統合コードを維持する必要がある
  • すべてのエンドポイントを手動で記述しない限り、LLM は API の存在を認識しません

MCP は、この問題を解決します。外部サービスが機能を ツールとして公開するための標準的な方法を定義することで、LLM が自動的に検出して呼び出すことができます。

MCP とは

MCP(Model Context Protocol)は、AI エージェントを外部のツールやデータソースに接続するためのオープン標準(Anthropic が公開)です。ユニバーサル アダプタのように機能します。

MCP サーバーは、次の処理を行う小さなプログラムです。

  1. 外部 API(Notion、GitHub、データベース、ファイル システムなど)をラップします。
  2. その API を型付けされ、文書化されたツールのリストとして公開します。
  3. シンプルなプロトコル(stdio または HTTP)を介してエージェントと通信する

エージェントは MCP サーバーに接続し、利用可能なツールを自動的に検出して、他のツールと同じように呼び出すことができます。LLM は API-post-page(...) を呼び出し可能な関数として認識します。

A2A と MCP の違い

これはよくある混乱の原因です。重要な違いは次のとおりです。

A2A

MCP

接続するもの

エージェント ↔ エージェント

エージェント ↔ 外部ツール/サービス

もう一方の面は

別の LLM エージェント

API ラッパー(LLM なし)

クリエイティブ ディレクターがブランド ストラテジストに電話する

プロジェクト マネージャーが Notion API を呼び出す

プロトコル

HTTPS 経由の JSON-RPC

stdio または HTTP ストリーム

定義元

Google

Anthropic

たとえば、このように考えることができます。

  • A2A = エージェントが他のエージェントと通信する方法
  • MCP = エージェントがツールやサービスと通信する方法

このプロジェクトでは、両方が一緒に使用されています。

Creative Director
    
      (A2A)  Brand Strategist ─── (google_search tool built into ADK)
      (A2A)  Copywriter
      (A2A)  Designer
      (A2A)  Critic
      (A2A)  Project Manager
                   
                     (MCP)  notion-mcp-server ──► Notion REST API

このプロジェクトでの MCP の仕組み

エージェントが実行されると、ADK は notion-mcp-server を子プロセスとして起動します。このプロセスにより、これらのツールが LLM に直接公開されます。

ツール

機能

API-retrieve-a-database

スキーマ(プロパティ名、型、有効な値)を取得します

API-post-database-query

既存のページをクエリする

API-post-page

新しいページを作成する

API-patch-page

既存のページを更新する

LLM は、他の関数と同様にこれらの関数を呼び出します。これらの関数が MCP を介して Notion REST API にアクセスしていることは認識していません。

stdio を使用する理由HTTP ではなく HTTPS を使用する理由

MCP サーバーはエージェントの子プロセスとして実行され、stdin/stdout を介して通信します。これは次のことを意味します。

  • 追加のネットワーク ポートは不要
  • ライフサイクルはエージェントによって管理される(オンデマンドで開始、終了時に停止)
  • すべてが 1 つの Docker イメージで提供されるため、デプロイする個別のサービスはありません。

(省略可)Notion 統合を有効にする

このセクション全体をスキップできます。プロジェクト マネージャー エージェントは、Notion の有無にかかわらず、常に完全なテキストベースのキャンペーン タイムラインを生成します。この設定をスキップすると、エージェントはインメモリ モードにフォールバックし、タイムラインをチャットにプレーン テキストとして出力します。何も壊れません。Notion データベースにタスクが表示されなくなるだけです。スキップする場合は、TODO 1 に直接移動してください。

Notion アカウントをお持ちで、MCP 統合の動作を確認したい場合は、今すぐ以下の設定を完了してください。以降の TODO は Notion データベース ID を参照しています。ここで取得します。

ステップ 1 - テンプレートから Notion データベースを作成する

データベースとして、公式の Notion プロジェクトとタスク テンプレートを使用します。このテンプレートは、複雑な現実世界の状況を示すために意図的に選択しました。このテンプレートには、わかりにくい名前の複数のプロパティ タイプ(ステータス、日付範囲、関係、選択)があります。これは、MCP の動的スキーマ検出の優れたテストです。エージェントは、プロパティ名をハードコードするのではなく、実行時に正確なプロパティ名を特定する必要があります。

下のリンクをクリックして、テンプレートを Notion ワークスペースに追加します。

→ Notion に「プロジェクトとタスク」テンプレートを追加する

マーケットプレイスの Notion Projects & Tasks テンプレート

追加すると、ProjectsTasks の 2 つのリンクされたデータベースが作成されます。テンプレートにはサンプル エントリが含まれています。エージェントがクリーンなワークスペースから開始できるように、続行する前にすべて削除してください(すべて選択 → 削除)。

ステップ 2 - Notion 統合を作成する

統合を作成します。

  1. notion.so/my-integrations にアクセスします。
  2. [New Integration] をクリック → AI Creative Studio という名前を付けます
  3. ワークスペースに関連付ける
  4. [設定を構成] をクリック → [コンテンツの読み取り]、[コンテンツの更新]、[コンテンツの挿入] の各機能がすべてオンになっていることを確認します。

Notion 統合の設定 - 「AI Creative Studio」という名前を付けてトークンをコピーします。

  1. 内部統合トークンntn_...)をコピーして、.env ファイルに貼り付けます。
NOTION_TOKEN=ntn_your-token-here

インテグレーションをデータベースに接続します。

  1. 複製したテンプレート ページを開き、[Projects] データベースをクリックします。
  2. ... メニュー(右上)→ [接続] → [接続を追加] をクリック → AI Creative Studio を選択

データベース メニューの [接続] をクリックして、統合と共有します

AI Creative Studio がアクティブな接続として表示される

  1. Tasks データベースについても同様の手順を行います。

データベース ID を取得します。

  1. [Projects] データベース リンクをクリックして開きます。このリンクは、次のような URL の独自のページで開きます。
https://www.notion.so/9887b6a94f7f83f68f8581e038d1aaa4?v=2c37b6a94f7f838685f1086e312c7278

テンプレート ページからプロジェクト データベースを開く

データベース ID は、URL の最初の UUID(?v= の前の部分)です。

https://www.notion.so/{DATABASE_ID}?v=...
                       ^^^^^^^^^^^^^^^^
                       9887b6a94f7f83f68f8581e038d1aaa4  ← this is your DATABASE_ID
  1. Tasks データベース リンクについても同様に、データベース ID を取得します。
  2. .env に 3 つの値をすべて追加します。
NOTION_TOKEN=ntn_your-token-here
NOTION_PROJECT_DATABASE_ID=9887b6a94f7f83f68f8581e038d1aaa4   # <-- your Projects DB ID
NOTION_TASKS_DATABASE_ID=your-tasks-db-id                      # <-- your Tasks DB ID

ステップ 3 - Notion MCP サーバーをインストールする

Project Manager は、公式の @notionhq/notion-mcp-server Node.js パッケージを介して Notion に接続します。グローバルにインストールします。

npm install -g @notionhq/notion-mcp-server@1.9.1

インストールを検証します。

npm list -g @notionhq/notion-mcp-server

予想される出力:

└── @notionhq/notion-mcp-server@1.9.1

notion-mcp-server: command not found

? Node.js がインストールされていること(node --version)と、npm グローバル bin が PATH に含まれていること(export PATH=$PATH:$(npm bin -g))を確認します。

ステップ 4 - .env を確認する

.env を開き、3 つの Notion 値がすべて設定されていることを確認します(手順 2 で追加しました)。

cloudshell edit .env
NOTION_TOKEN=ntn_...                           # integration token
NOTION_PROJECT_DATABASE_ID=...                 # Projects database ID
NOTION_TASKS_DATABASE_ID=...                   # Tasks database ID

Project Manager エージェントは、起動時にこれらの変数を自動的に検出し、Notion MCP ツールセットを有効にします。

スキーマ検出の仕組み

Project Manager は動的スキーマ検出を使用します。Notion のプロパティ名をハードコードすることはありません。

Step 1: Call API-retrieve-a-database to discover exact property names
Step 2: Read the "properties" object in the response
Step 3: Use ONLY discovered property names (case-sensitive) in API calls
Step 4: For select/status fields, use only values from the options array

つまり、エージェントは Notion データベースの構造に自動的に適応します。プロパティの名前をフランス語やアラビア語などに変更しても、エージェントは引き続き機能します。

TODO 1 - システム指示を記述する

スターターはすでに notion_section を計算しています。Notion が構成されていない場合は空の文字列、構成されている場合はデータベース ID とツールの完全なガイダンスを含むブロックです。これにより、Notion の手順が Notion なしエージェントのプロンプトから完全に除外されます。LLM は、持っていないツールのルールを認識しません。

プレースホルダ return を、{notion_section} を使用する実際のシステム指示に置き換えてください。

    return f"""You are a Project Manager specializing in creative campaign execution.

Today's date is {datetime.date.today().strftime("%B %d, %Y")}.
Use this as the starting point for all timelines.

Your goal: create a complete project plan for the campaign.
{notion_section}
**Project Timeline:**
Phase 1: Strategy & Research | [date]  [date] | [key activities]
Phase 2: Content Creation    | [date]  [date] | [key activities]
Phase 3: Review & Revision   | [date]  [date] | [key activities]
Phase 4: Launch & Monitoring | [date]  [date] | [key activities]

**Task List:**
| Task | Owner | Deadline | Status |
[list each task with realistic deadlines from today; set Owner to TBD]

**Budget Breakdown:**
[by category with approximate allocations]

**Milestones:**
[3-5 key checkpoints with dates]

**Notion Status:**
[What happened - e.g. "Project created (ID: xxx), 8 tasks linked" or "Notion not configured - text timeline only"]
"""

TODO 2 - Notion を使用しないエージェント

create_project_manager_agent()if not notion_token ブランチで、不完全なエージェントを次のように置き換えます。

        return Agent(
            name="project_manager",
            model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"),
            generate_content_config=GENERATE_CONTENT_CONFIG,
            instruction=get_system_instruction(),
            description="Project manager that creates campaign timelines and task breakdowns",
        )

TODO 3 - Notion MCP を使用したエージェント

注: スターター ファイルには、create_project_manager_agent() の上に handle_notion_error コールバックがすでに記述されています。Notion API エラー(400/404)をインターセプトし、未加工のエラー ペイロードをクリーンで実用的なメッセージに置き換えて、LLM が自己修正できるようにします。after_tool_callback を介して配線するだけです。

まず、create_project_manager_agent() の上部にある両方のデータベース ID を読み取ります。

    notion_token           = os.getenv("NOTION_TOKEN")
    notion_project_db_id   = os.getenv("NOTION_PROJECT_DATABASE_ID")
    notion_tasks_db_id     = os.getenv("NOTION_TASKS_DATABASE_ID")

次に、else ブランチで MCP ツールセットとエージェントを作成します。

        from google.adk.tools.mcp_tool import McpToolset, StdioConnectionParams
        from mcp import StdioServerParameters

        server_params = StdioServerParameters(
            command="notion-mcp-server",
            env={
                "NOTION_TOKEN": notion_token,
                "PATH": os.environ.get("PATH", ""),
            }
        )
        notion_toolset = McpToolset(
            connection_params=StdioConnectionParams(
                server_params=server_params,
                timeout=30.0
            )
        )

        return Agent(
            name="project_manager",
            model=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"),
            generate_content_config=GENERATE_CONTENT_CONFIG,
            after_tool_callback=handle_notion_error,
            instruction=get_system_instruction(
                project_database_id=notion_project_db_id,
                tasks_database_id=notion_tasks_db_id,
            ),
            description="Project manager with Notion integration for task tracking",
            tools=[notion_toolset],
        )

ベスト プラクティス: オプションのインテグレーションでハードフェイルしないようにします。テキスト タイムラインは常に主要な成果物であり、Notion は補足的なものです。

ADK ウェブを使用してプロジェクト マネージャーをローカルでテストする

uv run adk web agents --allow_origins='*'

ポート 8000 でウェブ プレビューを開きます。エージェントのプルダウンproject_manager を選択し、次の操作を試します。

Create a project plan for a GreenBrew organic coffee brand Instagram campaign.
Budget: $2,500. Launch in 3 weeks. Target audience: eco-conscious millennials aged 22-30.
Include phases, tasks with deadlines from today, and milestones.

フェーズ、タスクリスト、マイルストーンを含む構造化されたテキストのタイムラインが表示されます。Notion の認証情報が .env で設定されている場合、エージェントは Notion ワークスペースにもエントリを作成します。

9. A2A プロトコルについて

エージェント間プロトコル(A2A)を使用して、システム内のさまざまなエージェントを接続します。その仕組みを見てみましょう。

A2A が解決する問題

ADK で構築されたブランド ストラテジスト エージェントと、LangGraph で構築されたコピーライター エージェントがあるとします。どのように呼び出すのでしょうか?内部言語が異なるため、毎回カスタムのグルーコードを記述する必要があります。

A2A は、フレームワークに関係なく、どのエージェントも話すことができるユニバーサル言語を定義することで、この問題を解決します。これはエージェントの世界の HTTP です。誰もが同意する標準であるため、誰もが誰とでも話すことができます。

A2A とは何ですか?

Agent-to-Agent(A2A)は、Google が公開したエージェント通信のオープン スタンダードです。次のものを定義します。

  1. エージェントの自己紹介 - /.well-known/agent.json のエージェント カード
  2. 別のエージェントによる呼び出し方法 - HTTPS 経由の JSON-RPC
  3. 結果の返され方 - ストリーミングまたは単一のレスポンス

A2A が柔軟である理由:

  • 言語に依存しない - Python エージェントは TypeScript エージェントと通信できる
  • フレームワークに依存しない - ADK エージェントは LangGraph エージェントまたは CrewAI エージェントと通信できる
  • インフラストラクチャに依存しない - ローカル エージェントはクラウド エージェントと通信できる

仕組み - 手順

Creative Director                  Brand Strategist
      │                                  │
      │  1. GET /.well-known/agent.json  │
      │ ────────────────────────────────►│
      │  ◄──── agent card (name, url,    │
      │         skills, capabilities) ───│
      │                                  │
      │  2. POST /                       │
      │     {"method": "tasks/send",     │
      │      "params": {"message": ...}} │
      │ ────────────────────────────────►│
      │                                  │  LLM does
      │                                  │  the work...
      │  3. streaming response chunks    │
      │  ◄───────────────────────────────│
      │  ◄───────────────────────────────│
      │  ◄───────────────────────────────│

ステップ 1 - 検出: オーケストレーターは、エージェントの名前、URL、機能を学習するために、エージェント カードを 1 回取得します。

ステップ 2 - 呼び出し: オーケストレーターが JSON-RPC POST を介してタスクを送信します。本文にはメッセージ(スペシャリストへのプロンプト)が含まれます。

ステップ 3 - レスポンス: スペシャリストは、通常の LLM 呼び出しと同様に、レスポンスをチャンク単位でストリーミングします。

エージェント カード

各エージェントは、/.well-known/agent.json で自己記述を公開します。これは名刺のようなもので、エージェントの機能と連絡先を世界に知らせます。

{
  "name": "brand_strategist",
  "description": "Market research and competitive analysis",
  "url": "https://brand-strategist-xyz.run.app",
  "capabilities": { "streaming": true },
  "skills": [
    {
      "id": "market_research",
      "description": "Research target audiences, competitors, and trends"
    }
  ]
}

オーケストレーターはこのカードを読み取って RemoteA2aAgent オブジェクトを構築します。スペシャリストの内部構造に関するハードコードされた知識は必要ありません。

ADK で A2A を介してエージェントを公開する

to_a2a() は、ADK エージェントを A2A 準拠の FastAPI アプリでラップします。1 行で記述します。

from google.adk.a2a.utils.agent_to_a2a import to_a2a

# root_agent = your normal ADK Agent(...)
a2a_app = to_a2a(root_agent, host=PUBLIC_HOST, port=PUBLIC_PORT, protocol=PROTOCOL)
uvicorn.run(a2a_app, host=HOST, port=PORT)

これにより、次のものが自動的に作成されます。

  • /.well-known/agent.json - エージェント カード
  • / - JSON-RPC エンドポイント(すべての A2A タスク リクエストはルートパスに送信されます)

10. エージェントを A2A サービスとして公開する

エージェントを A2A サービスとして公開するには、ADK の to_a2a() ユーティリティ関数を使用します。

to_a2a() の仕組み

from google.adk.a2a.utils.agent_to_a2a import to_a2a

a2a_app = to_a2a(root_agent, host=PUBLIC_HOST, port=PUBLIC_PORT, protocol=PROTOCOL)
uvicorn.run(a2a_app, host=HOST, port=PORT)

to_a2a() は、ADK エージェントを FastAPI アプリケーションでラップし、次のものを自動的に公開します。

  • /.well-known/agent.json - エージェント カード(名前、説明、機能)
  • /a2a/{agent_name} - タスクを受信する JSON-RPC エンドポイント

各エージェントのスケルトン コードには、to_a2a() を使用してエージェントを A2A サーバーにラップする __main__ ブロックがすでに含まれています。このコードは提供されるため、記述する必要はありません。

デュアル URL 構成について

python agent.py を実行すると、__main__ ブロックは 2 つの別々の URL 構成を使用します。

# Where the server actually listens (network interface):
HOST = "0.0.0.0"
PORT = 8082  # Brand Strategist (others use 80838086 locally)

# What gets advertised in the agent card (the address other agents use to reach it):
PUBLIC_HOST = os.getenv("PUBLIC_HOST", "localhost")
PUBLIC_PORT = int(os.getenv("PUBLIC_PORT", str(PORT)))
PROTOCOL    = os.getenv("PROTOCOL", "http")

a2a_app = to_a2a(root_agent, host=PUBLIC_HOST, port=PUBLIC_PORT, protocol=PROTOCOL)
uvicorn.run(a2a_app, host=HOST, port=PORT)

環境

HOST:PORT(リスン)

PUBLIC_HOST:PUBLIC_PORT(エージェント カードに表示)

ローカル

0.0.0.0:8082

http://localhost:8082

Cloud Run

0.0.0.0:8080

https://brand-strategist-xyz.run.app:443

ローカルでは、どちらも同じマシンを指します。Cloud Run では、コンテナは内部で 8080 をリッスンしますが、エージェント カードはパブリック HTTPS URL をアドバタイズする必要があります。そうしないと、クリエイティブ ディレクターがコンテナの外部からスペシャリストにアクセスできません。

5 台の専門 A2A サーバーをすべて起動する

5 人のスペシャリストをすべて A2A サーバーとして同時に実行し、それらを指すようにローカルでクリエイティブ ディレクターをテストします。

5 つの別々の Cloud Shell ターミナルを開き(ターミナル タブバーの + アイコンをクリック)、ターミナルごとに 1 つのエージェントを実行します。

uv run.venv を自動的に有効にします。各ターミナルで手動で source を実行する必要はありません。

ターミナル 1 - ブランド戦略担当者(ポート 8082):

cd ~/ai-creative-studio/workshop/starter
PORT=8082 uv run agents/brand_strategist/agent.py

ターミナル 2 - コピーライター(ポート 8083):

cd ~/ai-creative-studio/workshop/starter
PORT=8083 uv run agents/copywriter/agent.py

ターミナル 3 - デザイナー(ポート 8084):

cd ~/ai-creative-studio/workshop/starter
PORT=8084 uv run agents/designer/agent.py

ターミナル 4 - Critic(ポート 8085):

cd ~/ai-creative-studio/workshop/starter
PORT=8085 uv run agents/critic/agent.py

ターミナル 5 - プロジェクト マネージャー(ポート 8086):

cd ~/ai-creative-studio/workshop/starter
PORT=8086 uv run agents/project_manager/agent.py

.env で localhost URL を設定する

ターミナル 6 で、.env をローカル エージェントの URL で更新して、クリエイティブ ディレクターが URL を見つけられるようにします。

cd ~/ai-creative-studio/workshop/starter

sed -i \
  -e 's|STRATEGIST_AGENT_URL=.*|STRATEGIST_AGENT_URL=http://localhost:8082|' \
  -e 's|COPYWRITER_AGENT_URL=.*|COPYWRITER_AGENT_URL=http://localhost:8083|' \
  -e 's|DESIGNER_AGENT_URL=.*|DESIGNER_AGENT_URL=http://localhost:8084|' \
  -e 's|CRITIC_AGENT_URL=.*|CRITIC_AGENT_URL=http://localhost:8085|' \
  -e 's|PM_AGENT_URL=.*|PM_AGENT_URL=http://localhost:8086|' \
  .env

A2A インスペクタでエージェントを検査する

A2A Inspector は、A2A プロトコルをネイティブにサポートするオープンソースのデベロッパー ツールです。これにより、クライアント コードを記述することなく、実行中の A2A エージェントに直接接続し、エージェントカードを読み取り、タスクを送信できます。

表示される情報:

  • エージェントカード - エージェントがアドバタイズする構造化されたメタデータ。エージェントの名前、説明、サポートされている入出力モード、エンドポイント URL など。これは、クリエイティブ ディレクターがスペシャリストを発見したときに読み取るものです。
  • チャット インターフェース - A2A 経由でエージェントにメッセージを送信し、未加工のレスポンスを確認します。エージェントを接続する前に、プロンプトを個別にテストできます。
  • プロトコルの検証 - インスペクタは、エージェント カードが A2A 仕様に準拠していることを確認し、欠落しているフィールドや形式が正しくないレスポンスを早期に検出します。

重要性: 後で Cloud Run にデプロイするときに、クリエイティブ ディレクターは /.well-known/agent.json からエージェント カードを取得して、各スペシャリストを検出します。そのカードが間違っている場合(URL が正しくない、機能がないなど)、オーケストレーターは通知なしで失敗します。インスペクタを使用すると、クラウドにデプロイする前にこれらの問題をローカルで検出できます。

ブランド戦略担当者のエージェント カード

エージェント カードには、他のエージェントに表示されるのと同じように、スペシャリストの身元と機能が表示されます。

Agent カード情報

インスペクタをインストールして起動する

cd ~/ai-creative-studio/workshop
./setup_inspector.sh

.env の更新は 1 回限りのコマンドです。次に、ターミナル 6 を使用してインスペクタを起動します。

cd ~/a2a-inspector
bash scripts/run.sh

インスペクタ UI を開くには、[ウェブでプレビュー] → [ポートを変更] を選択し、「5001」と入力します。

ブランド ストラテジストに連絡する

インスペクタの URL フィールドに「http://localhost:8082」と入力して、[接続] をクリックします。インスペクタはエージェント カードを取得し、スペシャリストのメタデータを表示します。

A2A インスペクタがブランド戦略担当者に接続されている

エージェント カードに表示される情報

エージェントカードはメタデータ以上のものです。エージェントがネットワークに宣伝する完全な機能契約です。プロジェクト マネージャーhttp://localhost:8086)に接続して、最も豊富な例を確認します。

{
  "name": "project_manager",
  "description": "Project manager with Notion integration for task tracking",
  "protocolVersion": "0.3.0",
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "project_manager",
      "name": "model",
      "tags": ["llm"],
      "description": "... full system instruction including today's date and Notion database IDs ..."
    },
    {
      "id": "project_manager-API-post-page",
      "name": "API-post-page",
      "tags": ["llm", "tools"],
      "description": "Notion | Create a page"
    },
    {
      "id": "project_manager-API-retrieve-a-database",
      "name": "API-retrieve-a-database",
      "tags": ["llm", "tools"],
      "description": "Notion | Retrieve a database"
    }
  ]
}

注目すべき点は次の 3 つです。

1. MCP ツールが A2A スキルになる - プロジェクト マネージャーがアクセスできるすべての Notion ツール(API-post-pageAPI-retrieve-a-database など)が、エージェント カードに個別のスキルとして表示されます。ネットワーク上の他のエージェントは、コードを読まなくても、このエージェントが使用できるツールを正確に検出できます。

2. システム指示が埋め込まれている - 最初のスキルの description に、今日の日付や Notion データベース ID など、システム指示全体が含まれています。これにより、クリエイティブ ディレクターはプロジェクト マネージャーを呼び出すときに何を渡すかを把握できます。

3. URL はライブ エンドポイントです - url フィールドは、クリエイティブ ディレクターがこのスペシャリストを呼び出すときに RemoteA2aAgent が使用するものとまったく同じです。カード内の URL が間違っている場合、オーケストレーターはエージェントにアクセスできません。

これが、インスペクタが強力なデバッグツールである理由です。エージェント カードを一目見れば、エージェントが実行されているかどうか、どのようなツールがあるか、エンドポイントが正しいかどうかを確認できます。

テスト メッセージの送信

接続したら、チャット パネルにプロンプトを入力して送信します。検査ツールは、これを A2A タスクとして送信し、レスポンスをストリーミングで返します。これは、クリエイティブ ディレクターが本番環境でこのエージェントを呼び出すのと同じ方法です。

A2A Inspector を介してブランド ストラテジストとチャットする

インスペクタをローカルポート(8082 ~ 8086)に向けると、各スペシャリストを個別にテストできます。

11. クリエイティブ ディレクター オーケストレーターを構築する

クリエイティブ ディレクターは、オーケストラの指揮者のような役割を果たします。環境変数からスペシャリスト URL を読み取り、それぞれを RemoteA2aAgent としてラップし、LLM が呼び出すことができる AgentTool として公開します。

5 つのスペシャリスト エージェントがまだ実行されていることを確認します(ステップ 10 のターミナル 1 ~ 5)。

ターミナル 6(A2A インスペクタ ターミナル)で、Ctrl+C を使用してインスペクタを停止します。

ファイルを開きます。

cd ~/ai-creative-studio/workshop/starter
cloudshell edit agents/creative_director/agent.py

このファイルには 3 つの TODO があります。順番に確認します。

TODO 1 - すでに作成済みのシステム指示を確認する

システム指示は同じディレクトリの prompt.py にあり、自動的にインポートされます。

from .prompt import SYSTEM_INSTRUCTION_TEMPLATE

prompt.py を開いて、先に進む前に確認してください。

cloudshell edit agents/creative_director/prompt.py

オーケストレーションの動作全体を制御するため、この概念を理解することが重要です。

オーケストレーター プロンプトがすべてを制御する理由

このセクションと並行して prompt.py を開いてください。以下の例では、その特定の箇所を参照しています。

prompt.py のプロンプトは単なるドキュメントではなく、システム全体のコントロール プレーンです。オーケストレーターのプロンプトの構造が適切でないと、エージェントが順序どおりに呼び出されない、専門家ではなくオーケストレーターによってコンテンツが生成される、失敗後もワークフローが継続される、エージェント間でコンテキストがサイレントにドロップされるなどの問題が発生します。これら 9 つの要素により、最も一般的な障害を防ぐことができます。

要素 0 - 計画してから実行する

これが最も重要な要素です。オーケストレーターは、スペシャリストに電話をかける前に、番号付きのプランを出力するように指示されます。

I'll create your campaign by coordinating the specialist agents in sequence:
1. Brand Strategist - develop positioning and audience insights
2. Copywriter - write captions using those insights
3. Visual Designer - create image prompts aligned with the copy
4. Critic - review and score the full package
5. Project Manager - build the timeline and task breakdown

このステップがないと、LLM はツール呼び出しに直接ジャンプし、ワークフローのどこにいるかを把握できなくなります。特に、スペシャリストから長い回答を受け取った後は、その傾向が強くなります。最初にプランを概説することで、オーケストレーターがアンカーされます。オーケストレーターは、どのステップを実行しているか、次のステップは何か、完全な実行がどのようなものかを把握します。この手順をスキップすると、オーケストレーターがワークフローの途中で停止したり、手順を繰り返したりします。

要素 1 - 明示的なロール定義

❌ "You are a helpful creative assistant."
✅ "You orchestrate specialists. You do NOT write captions, designs, or timelines yourself."

明示的な禁止がない場合、LLM はスペシャリストの呼び出しをスキップしてコンテンツを直接生成することがあります。これは、より高速で、LLM がその方法を「知っている」ためです。この指示は間違っている必要があります。

要素 2 - 誤ったパターンがリストされているツール呼び出し構文

正しい構文だけを表示するだけでは十分ではありません。LLM は、もっともらしいが失敗する呼び出しを生成する可能性があります。プロンプトには、正しいパターンと使用してはならないパターンが明示的に記載されています。

✅ copywriter(request="...")          ← correct
❌ print(copywriter(...))             ← breaks silently
❌ default_api.copywriter(...)        ← breaks silently
❌ copywriter.run(...)                ← breaks silently
❌ agents.copywriter(...)             ← breaks silently

誤ったパターンを明示的にリストすることで、本番環境での不正なツール呼び出しが約 95% 削減されました。

要素 3 - 逐次実行をステップごとに説明する

a) Call the tool
b) Wait for tool_output
c) Verify the output is not an error
d) Confirm to the user: "✓ Brand Strategist complete"
e) Then move to the next agent

ステップ(b)と(c)がないと、LLM が 2 つのエージェントを同時に呼び出したり、成功を前提として応答を受け取る前に次のステップに進んだりすることがあります。

要素 4 - エラー ディレクティブ: STOP、report、do not proceed

初期のバージョンでは、オーケストレーターは 1 人のスペシャリストからエラーを受け取ると、それに対する妥当な出力をでっち上げて、次のエージェントに進んでいました。ユーザーは、ハルシネーションの基盤に基づいて構築された、完全に見えるキャンペーンを取得しました。修正は明示的です。すぐに停止します。正確なエラーを報告します。続行しないでください。

要素 5 - コンテキストの受け渡しルール

リモート エージェントには会話履歴がありません。オーケストレーターが A2A 経由でコピーライターを呼び出すと、コピーライターはその単一のリクエストのメッセージのみを確認します。ブランド戦略家が何を言ったかはわかりません。オーケストレーターは、以前の出力を明示的にバンドルして、後続の各呼び出しに含める必要があります。

copywriter(request="Create 3 posts for EcoFlow water bottle targeting millennials.
Use these insights from the Brand Strategist: [paste full strategist output here].
Create engaging captions with hashtags.")

この指示には、「リモート エージェントは共有メモリを持たないため、以前の出力を明示的に渡す必要があります」と明記されています。これがないと、各エージェントは手探りで作業することになります。

要素 6 - リクエストの分類: 単純か複雑か

すべてのリクエストに 5 人のエージェントが必要なわけではありません。このプロンプトは、計画を立てる前にリクエストを分類するようオーケストレーターに指示します。

SIMPLE  → one agent needed
  "Research the eco-friendly water bottle market" → brand_strategist only
  "Write 3 Instagram captions"                    → copywriter only

COMPLEX → all agents sequentially
  "Create a complete campaign with timeline"      → all 5 agents

この分類がないと、オーケストレーターは「3 つの投稿アイデアを教えて」などのリクエストを含め、すべてのリクエストに対して 5 つのエージェントすべてを実行するため、不要なレイテンシとコストが発生します。

要素 7 - 通信ルール: 出力をすべて表示し、フィルタリングしない

オーケストレーターは、スペシャリストが返した内容を要約したり編集したりしてはならないことが明示的に示されています。

- DO NOT summarize unless the output exceeds 2000 words
- DO NOT filter or edit agent responses
- Show the user exactly what each specialist produced
- NEVER say results are ready unless you received them in tool_output

これがないと、オーケストレーターは専門家の出力を独自の言葉で書き換え、詳細が失われ、エラーが発生し、専門家を配置する目的が達成されません。

要素 8 - ワークフローの完了: 途中で停止しない

微妙ながらも重大な障害モード: オーケストレーターが 5 ステップのプランを発表し、3 ステップを完了してから、完了したかのように結果を表示します。このプロンプトでは、オーケストレーターが完了する前に合格する必要がある明示的なチェックリストを使用して、この問題を回避します。

✓ Did I announce a plan with N agents?
✓ Have I called ALL N agents from my plan?
✓ Did each agent respond successfully?
✓ Am I presenting complete results from ALL agents?

If any answer is NO → continue executing the remaining agents.

これにより、オーケストレーターが部分的な実行を完了として扱うことがなくなります。

品質検証(QC)ループ

リビジョン ワークフローは、prompt.py の最も複雑な部分です。## REVISION WORKFLOW セクションを開き、手順に沿って操作します。

仕組み

批評家が回答した後、クリエイティブ ディレクターはプロジェクト マネージャーに盲目的に進むことはありません。Critic の出力を読み取って分岐します。

Critic output
      │
      ├── "All Approved: YES"
      │         └──► proceed to Project Manager
      │
      └── "Status: NEEDS_REVISION"
                │
                ├── posts fail   → call copywriter again with feedback
                ├── visuals fail → call designer again with feedback
                └── both fail    → call copywriter, then designer
                          │
                          └──► revised output → Project Manager
                               (1 revision max per deliverable)

これはコードドリブンではなく、LLM ドリブンです

この Codelab で前述したように、オーケストレータは Critic のレスポンスを「解析」します。この解析を行う Python コードはありません(正規表現も文字列照合もありません)。クリエイティブ ディレクターは、独自の指示を読み取る LLM です。その指示には次のように記載されています。

Look for "Status: NEEDS_REVISION" in the critic's response.
Posts need revision  → call copywriter
Visuals need revision → call designer

LLM は、Critic の出力でこれらの文字列を読み取り、分岐をたどります。そのため、批評家形式は交渉の余地がありません。批評家が NEEDS_REVISION の代わりに「needs some work」と書くと、LLM は指示に一致するものがなく、修正ステップをスキップします。

リビジョン呼び出しでコンテキストが転送される仕組み

リビジョン呼び出しは、要素 5 と同じコンテキスト渡しのルールに従います。コピーライターには最初のバージョンの記憶がないため、オーケストレータはすべてを明示的に含める必要があります。

"I need you to revise the Instagram posts based on critic feedback.

ORIGINAL BRIEF:
[the original user request]

YOUR FIRST VERSION:
[the posts the copywriter created]

CRITIC FEEDBACK (Score: 6/10 - NEEDS_REVISION):
[the critic's specific suggestions]

Please revise the posts addressing this feedback while maintaining
the strengths the critic identified."

「YOUR FIRST VERSION」セクションがないと、コピーライターはすでに作成されたものを改善するのではなく、ゼロから作成することになります。

1 回の修正制限とその重要性

1 回の修正ラウンドの後、スコアに関係なく、オーケストレーターはプロジェクト マネージャーに進みます。この命令は、この情報を内部的に追跡します。

After calling copywriter for revision once:
→ mark "copywriter_revised = true" in context
→ even if the critic still suggests changes, proceed to PM

この上限がないと、ループが無限に実行される可能性があります(批評家が問題を指摘 → コピーライターが修正 → 批評家が再度指摘 → コピーライターが再度修正)。各ラウンドにはトークンと時間がかかります。1 回のリビジョンで、暴走サイクルのリスクを伴うことなく品質を改善できます。

プロジェクト マネージャーに渡されるもの

プロジェクト マネージャーは、常に元のバージョンではなく、最終承認バージョンを受け取ります。修正が行われた場合、オーケストレーターは修正されたコピーとビジュアルを渡します。最初のパスですべてが承認された場合は、それらが直接渡されます。PM は不承認のドラフトを見ることはありません。

TODO 2 - 各スペシャリストを RemoteA2aAgent + AgentTool として登録する

# TODO 2: For each specialist URL... のコメントを見つけて、次のように置き換えます。

    if strategist_url:
        available_agents_list.append(
            "- **brand_strategist**: Market research, competitor analysis, trend identification"
        )
        strategist_agent = RemoteA2aAgent(
            name="brand_strategist",
            description="Researches markets, competitors, and trends using Google Search",
            agent_card=f"{strategist_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=strategist_agent))

    if copywriter_url:
        available_agents_list.append(
            "- **copywriter**: Instagram captions, hashtags, and CTAs"
        )
        copywriter_agent = RemoteA2aAgent(
            name="copywriter",
            description="Creates Instagram captions with hashtags and CTAs",
            agent_card=f"{copywriter_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=copywriter_agent))

    if designer_url:
        available_agents_list.append(
            "- **designer**: Visual concepts and real images generated via Gemini (GCS URIs returned)"
        )
        designer_agent = RemoteA2aAgent(
            name="designer",
            description="Creates visual concepts and generates real images via Gemini, stored in GCS",
            agent_card=f"{designer_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=designer_agent))

    if critic_url:
        available_agents_list.append(
            "- **critic**: Quality review with APPROVED/NEEDS_REVISION scoring"
        )
        critic_agent = RemoteA2aAgent(
            name="critic",
            description="Reviews campaign materials and returns structured quality feedback",
            agent_card=f"{critic_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=critic_agent))

    if pm_url:
        available_agents_list.append(
            "- **project_manager**: Project timelines, task breakdowns, Notion integration"
        )
        pm_agent = RemoteA2aAgent(
            name="project_manager",
            description="Creates project timelines and task breakdowns, optionally in Notion",
            agent_card=f"{pm_url}/.well-known/agent.json",
        )
        agent_tools.append(AgentTool(agent=pm_agent))

TODO 3 - コンテキスト圧縮を使用して App でラップする

圧縮が必要な理由

会話内のすべてのメッセージ(ユーザーのプロンプト、すべてのツール呼び出し、すべてのツール レスポンス)は、次のターンで LLM が読み取るコンテキスト ウィンドウに追加されます。5 人のエージェントのワークフローでは、この累積が急速に進みます。

Turn 1:  user prompt                           ~200 tokens
Turn 2:  orchestrator plan                     ~300 tokens
Turn 3:  brand_strategist tool_call            ~150 tokens
Turn 4:  brand_strategist tool_output          ~1,500 tokens   full research report
Turn 5:  copywriter tool_call                  ~300 tokens     must include strategist output
Turn 6:  copywriter tool_output                ~2,000 tokens   3 captions
Turn 7:  designer tool_call                    ~500 tokens
Turn 8:  designer tool_output                  ~1,500 tokens
...

エージェント 4(批評家)のコンテキスト ウィンドウには、前の 3 人のエージェントの完全な出力が含まれます。多くの場合、ツール レスポンスだけで 8,000 ~ 12,000 個のトークンになります。Gemini 2.5 Pro の大きなコンテキスト ウィンドウを使用しても、オーケストレーターは増え続ける履歴を処理する必要があるため、推論の品質が低下します。圧縮を行わないと、長いワークフローは Agent 4 付近で実用的な上限に達します。

コンパクションの機能

ADK は、すべてのイベントを完全に保持するのではなく、LLM を定期的に呼び出して古いイベントをコンパクトな表現に要約します。過去のイベントの概要と最新のエージェントの完全な出力のみがコンテキストに保持されます。

Without compaction:
  [full strategist output] + [full copywriter output] + [full designer output] + → Critic

With compaction (interval=3, overlap=1):
  [summary of strategist + copywriter] + [full designer output] + → Critic

要約では、冗長な形式、各エージェントに渡される繰り返しのコンテキスト、中間的な推論が破棄され、重要な事実(重要な分析情報、承認済みのキャプション、視覚的なコンセプト)が保持されます。Critic は評価に必要なすべての情報を取得できます。3 つの完全なレポートではなく、要約を読み取るだけです。

コード

# TODO 3: Wrap the agent in an App... コメントを見つけて、プレースホルダ App(...) を次のように置き換えます。

    from google.adk.apps import App
    from google.adk.apps.app import EventsCompactionConfig
    from google.adk.apps.llm_event_summarizer import LlmEventSummarizer
    from google.adk.models import Gemini

    compaction_config = EventsCompactionConfig(
        summarizer=LlmEventSummarizer(llm=Gemini(model_id=os.getenv("GEMINI_MODEL", "gemini-2.5-flash"))),
        compaction_interval=3,   # Summarize after every 3 agent completions
        overlap_size=1,          # Keep the most recent agent's output in full
    )

    app = App(
        name="creative_director",
        root_agent=agent,
        events_compaction_config=compaction_config,
        plugins=[LoggingPlugin()],
    )
    return agent, app

compaction_interval=3 - 3 つのエージェントが完了するたびに圧縮が実行されます。5 エージェントのパイプラインの場合、これは 1 回(エージェント 1 ~ 3 の後)トリガーされ、批評家と PM は 1 ~ 3 の概要と、前のエージェントの完全な出力を確認します。

overlap_size=1 - エージェントの最新の完全な出力は常にそのまま保持され、要約されることはありません。これは、評論家が実際の画像を読み込んでレビューするために、デザイナーの完全な出力(gcs_uri 値を含む)を必要とするためです。概要ではこれらの URI が失われます。

キャンペーン全体でどのように機能するか:

Agent 1 (Strategist)  → full context
Agent 2 (Copywriter)  → full context
Agent 3 (Designer)    → full context
                        ↓ compaction fires: summarizes agents 1-2, keeps 3 in full
Agent 4 (Critic)      → sees [summary of 1-2] + [full output of 3]
Agent 5 (PM)          → sees [summary of 1-3] + [full output of 4]

RemoteA2aAgentAgentTool について

RemoteA2aAgent("brand_strategist", agent_card=url)
     
       wraps the remote service so ADK can call it
     
AgentTool(agent=strategist_agent)
     
       exposes it as a callable tool to the LLM
     
Agent(tools=[...])
     
       LLM calls tool("brand_strategist", message=...) when needed
     
brand-strategist-xxxx.run.app   actual HTTP A2A call happens here

LLM は、システム指示とユーザーのリクエストに基づいて、各ツールを呼び出すタイミングを決定します。オーケストレーターはコード内でエージェントを直接呼び出すことはありません。すべて LLM の推論によって駆動されます。

クリエイティブ ディレクターをローカルでテストする

uv run adk web agents --allow_origins='*'

ポート 8000 でウェブ プレビューを開きます。エージェントのプルダウンcreative_director を選択し、次の操作を試します。

Research the eco-friendly water bottle market for health-conscious millennials

クリエイティブ ディレクターがこの件をブランド戦略担当にのみ転送し、ブランド戦略担当から回答が届くことがわかります。

キャンペーン全体については、次の方法をお試しください。

Create a complete Instagram campaign for SolarPack portable solar charger targeting
outdoor enthusiasts and digital nomads aged 22-35.
Budget $2,000, launch in 2 weeks.

クリエイティブ ディレクターが 5 人のスペシャリストを順番に調整し、各エージェントの出力が次のエージェントに流れていく様子を確認できます。

デモ: エンドツーエンドのキャンペーン実行

続行する前に Creative Director(Ctrl+C)を停止します。A2A インスペクタもポート 8000 を使用します。

ローカル テストが完了したら、5 台のスペシャリスト サーバー(各ターミナルで Ctrl+C)を停止します。

12. スペシャリスト エージェントをデプロイしてテストする

これで、エージェントを Google Cloud にデプロイする準備が整いました。Cloud Run は、エージェントのデプロイに最適なサービスです。サーバーレスでスケーラブルであり、使いやすいです。各スペシャリスト エージェントは、独立した Cloud Run サービスとしてデプロイされます。

デプロイ構成

各スペシャリストの Dockerfile は次のパターンに従います。

FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc curl

# Fast dependency install with uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml .
RUN uv sync --no-install-project --no-dev

COPY . .
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

ENV PYTHONUNBUFFERED=1 PORT=8080 HOST=0.0.0.0
EXPOSE 8080
CMD ["uv", "run", "python", "agent.py"]

5 人のスペシャリストを順番にデプロイする

cd ~/ai-creative-studio/workshop/starter
source .env

uv run deploy/deploy_all_specialists.py

このスクリプトは、5 つのエージェントを 1 つずつデプロイします(合計で約 10 ~ 12 分かかります)。順次デプロイでは、Cloud Build のポーリング割り当て(60 リクエスト/分)が回避されます。完了すると、各エージェントの Cloud Run URL が .env に書き戻されます。

Designer がデプロイされると、スクリプトは自動的に Cloud Run サービス アカウントに GCS バケットに対する roles/storage.objectCreator を付与し、生成された画像をアップロードできるようにします。

.env で Notion の認証情報を構成した場合、スクリプトはそれらを Secret Managernotion-tokennotion-project-db-idnotion-tasks-db-id として)に安全に保存し、プレーンな環境変数ではなく --set-secrets を介して Project Manager サービスに挿入します。つまり、トークンは Cloud Run の [環境] タブや gcloud コマンド履歴に表示されません。

デプロイを確認する

デプロイが完了すると、スクリプトは Cloud Run URL を .env に自動的に書き戻し、前の手順の localhost URL を置き換えます。

source .env

echo "Deployed URLs:"
echo "  Brand Strategist: $STRATEGIST_AGENT_URL"
echo "  Copywriter:       $COPYWRITER_AGENT_URL"
echo "  Designer:         $DESIGNER_AGENT_URL"
echo "  Critic:           $CRITIC_AGENT_URL"
echo "  Project Manager:  $PM_AGENT_URL"

クリエイティブ ディレクターは、次のステップで Agent Runtime にデプロイされるときに、これらの Cloud Run URL を自動的に使用します。

エージェント カードを確認する

デプロイされた各エージェントは、/.well-known/agent.json でエージェントカードを公開します。それらを取得して、すべてが公開されていることを確認します。

source .env

for agent_url in $STRATEGIST_AGENT_URL $COPYWRITER_AGENT_URL $DESIGNER_AGENT_URL $CRITIC_AGENT_URL $PM_AGENT_URL; do
    echo "=== Agent Card: $agent_url ==="
    curl -s "${agent_url}/.well-known/agent.json" | python3 -m json.tool | grep -E '"name"|"url"|"description"'
    echo ""
done

各エージェントの想定される出力:

"name": "brand_strategist",
"url": "https://brand-strategist-xxxx.run.app",
"description": "Brand strategist for market research and competitive insights"

A2A インスペクタ(Cloud Run)でテストする

A2A インスペクタはステップ 10 でインストール済みです。開始します。

cd ~/a2a-inspector
bash scripts/run.sh

[ウェブでプレビュー] を開き、[ポートを変更] → [5001] を選択します。接続フィールドに Cloud Run URL を入力します。

https://brand-strategist-xxxx.us-central1.run.app

[接続] をクリックします。サービスは --allow-unauthenticated でデプロイされるため、認証トークンは必要ありません。

インスペクタが接続し、エージェント カードを検証して、A2A を介してインタラクティブにチャットできるようになります。

Cloud Run にデプロイされたエージェントを検査する

Cloud Run にデプロイしたら、パブリック HTTPS URL をインスペクタで指定して、クラウド デプロイが機能していることを確認します。

Cloud Run エージェントに接続された A2A インスペクタ

ワークフローは同じです。Cloud Run の URL を貼り付けて接続し、テスト メッセージを送信します。エージェント カードが読み込まれ、チャットが応答した場合は、スペシャリストが正しくデプロイされ、連絡可能であることを示します。

13. Creative Director を Agent Runtime にデプロイする

オーケストレーターは Agent Runtime にデプロイされます。これにより、マネージド セッション状態、自動スケーリング、組み込みのトレースが提供されます。

オーケストレーターに Agent Runtime を使用する理由

5 人のスペシャリストは、軽量でステートレスな Cloud Run にデプロイされ、それぞれが 1 つのタスクを処理します。クリエイティブ ディレクターには、次のような要件があります。

要件

利点

セッションの状態

複数ステップのワークフローに 45 秒以上かかる。Agent Runtime は、オーケストレーターのツール呼び出し間の会話の状態を維持するため、パイプラインの途中で情報が失われることはありません。

可変負荷

1 時間に 1 つのキャンペーンを実行することも、複数のキャンペーンを並行して実行することもあります。エージェント ランタイムは、アイドル状態になるとゼロにスケールダウンし、自動的にスケールアウトします。アイドル状態の容量に対して料金は発生しません。

オブザーバビリティ

Cloud Logging、Cloud Monitoring、Cloud Trace が組み込まれています。計測手法を追加することなく、すべての A2A 呼び出し、使用されたすべてのトークン、すべてのレイテンシ スパイクを確認できます。

長時間実行ワークフロー

Cloud Run のリクエスト タイムアウトは 3,600 秒です。Agent Runtime は、数分かかるワークフロー向けに設計されており、再試行と状態の永続性が管理されています。

Cloud Run は、ステートレスの専門家にとって最適なプラットフォームです。Agent Runtime は、ステートフル オーケストレーターに適したプラットフォームです。

オーケストレーターをデプロイする

cd ~/ai-creative-studio/workshop/starter
source .env

uv run deploy/deploy_orchestrator.py --action deploy

これには 5 ~ 10 分ほどかかります。完了すると、AGENT_ENGINE_IDAGENT_ENGINE_RESOURCE_NAME.env に保存されます。

source .env
echo "Agent Engine ID: $AGENT_ENGINE_ID"
echo "Resource: $AGENT_ENGINE_RESOURCE_NAME"

デプロイの仕組み

client.agent_engines.create()App オブジェクトをパッケージ化し、依存関係とともにアップロードして、マネージド インフラストラクチャにデプロイします。各パラメータの機能は次のとおりです。

import vertexai
from vertexai import Client, agent_engines

vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=STAGING_BUCKET)

# Wrap the App in an AdkApp adapter - enables tracing in Cloud Trace
adk_app = agent_engines.AdkApp(app=root_app, enable_tracing=True)

# Initialize client and deploy
client = Client(project=PROJECT_ID, location=LOCATION)

agent_engine_resource = client.agent_engines.create(
    agent=adk_app,
    config={
        "staging_bucket": STAGING_BUCKET,   # GCS bucket for packaging artifacts
        "display_name": "Creative Director",
        # Python packages installed in the managed runtime - pin for reproducibility
        "requirements": [
            "google-cloud-aiplatform[agent_engines]>=1.132.0,<2.0.0",
            "google-adk[a2a]==1.31.1",
            "google-genai>=1.70.0",
            "google-cloud-storage>=2.10.0",
            "python-dotenv>=1.0.0",
            "pydantic>=2.0.0",
            "cloudpickle>=3.0.0",
        ],
        # Specialist URLs passed as env vars - the orchestrator reads these at runtime
        "env_vars": {
            "COPYWRITER_AGENT_URL": COPYWRITER_URL,
            "DESIGNER_AGENT_URL":   DESIGNER_URL,
            "STRATEGIST_AGENT_URL": STRATEGIST_URL,
            "CRITIC_AGENT_URL":     CRITIC_URL,
            "PM_AGENT_URL":         PM_URL,
        },
    },
)

resource_name = agent_engine_resource.api_resource.name
agent_engine_id = resource_name.split("/")[-1]

バックグラウンドで行われる処理:

1. Agent Engine packages your App + requirements into a container
2. Uploads it to the staging bucket in your project
3. Deploys to managed compute (you never see or manage the VM)
4. Returns a resource name: projects/.../locations/.../reasoningEngines/<id>
5. That ID is saved to .env as AGENT_ENGINE_ID

デプロイ後、オーケストレーターは環境変数の URL を介して 5 人の Cloud Run スペシャリストに接続します。

  • これらは、デプロイ スクリプトが実行される前に .env を介して渡されます。

14. エンドツーエンドのキャンペーンを実施する

システム全体がデプロイされます。Agent Runtime プレイグラウンドから完全なキャンペーンを実行する。

エージェント ランタイムの Playground を開く

  1. https://console.cloud.google.com/agent-platform/runtimes に移動します。[Agent Platform] > [エージェント] > [デプロイ] から Agent Runtime に移動することもできます。
  2. デプロイされたエージェント ランタイム(creative-director)を選択します。
  3. 左側のサイドバーで [Playground] をクリックします。
  4. [新しいセッション] をクリックして、新しい会話を開きます。

キャンペーン全体を実施する

このブリーフをチャットに貼り付けて送信します。

Create a complete Instagram campaign for:
- Product: EcoFlow Smart Water Bottle (tracks hydration, keeps drinks cold 24h)
- Target Audience: Health-conscious millennials, 25-35 years old
- Platform: Instagram
- Goal: Brand awareness + drive website traffic
- Brand Voice: Motivational, clean, science-backed
- Budget: $3,000
- Timeline: Launch in 2 weeks

クリエイティブ ディレクターは、5 つのエージェントをすべて順番に実行します。

  1. ブランド戦略担当者 → 市場調査、競合他社の分析、オーディエンス分析
  2. コピーライター → キャプション、ハッシュタグ、行動を促すフレーズを含む 3 件の Instagram 投稿
  3. デザイナー → 各投稿の Gemini(GCS URI)で生成された視覚的なコンセプトと実際の画像
  4. Critic → 承認 / 要修正のスコアが付いた品質レビュー
  5. (必要に応じて修正)→ コピーライターまたはデザイナーにフィードバックを伝えて再度依頼
  6. プロジェクト マネージャー → 2 週間のタイムライン、タスクの分解、予算の割り当て

デモ: Notion との統合で実行されるキャンペーン

単一エージェントのルーティングをテストする

新しいセッションで、この短いリクエストを送信します。

Research the luxury skincare market - top brands and trends in 2025

クリエイティブ ディレクターがこのリクエストをブランド ストラテジストのみに転送していることに注目してください。他のエージェントは呼び出されません。これは、システム指示の要求分類ロジックが正しく機能していることを示しています。

実行トレースを検査する

コンソールで次の操作を行います。

  1. 左側のサイドバー(Playground の横)で [Traces] をクリックします。
  2. [Trace View] で、実行したセッションのトレースを選択します。
  3. トレースツリーを展開して、各エージェント呼び出し、その入力/出力、レイテンシ、トークン使用量を確認する

スペシャリストへの A2A 呼び出しは、それぞれ個別のスパンとして表示されます。クリエイティブ ディレクターが各エージェントに渡したコンテキストと、各エージェントから返されたコンテキストを正確に確認できます。

省略可: ターミナルから実行する

スターターにすでに含まれている run_campaign.py スクリプトを使用して、キャンペーンをプログラムで実行することもできます。

cd ~/ai-creative-studio/workshop/starter
uv run run_campaign.py

15. クリーンアップ

継続的な課金を回避するために、Google Cloud リソースをクリーンアップします。

破棄スクリプトを実行します。このスクリプトは .env を読み取り、この Codelab で作成したものをすべて削除します。

bash deploy/teardown_gcp.sh

スクリプトは、削除する内容を正確に示し、処理を行う前に確認を求めます。

リソース

削除されるデータ

Cloud Run サービス

brand-strategist、copywriter、designer、critic、project-manager

エージェント ランタイム

クリエイティブ ディレクターの推論エンジン + すべてのセッション

Artifact Registry

cloud-run-source-deploy リポジトリ + すべての Docker イメージ

GCS バケット

{PROJECT_ID}-campaign-images{PROJECT_ID}-agent-stagingrun-sources-{PROJECT_ID}-{REGION}

Secret Manager

notion-tokennotion-project-db-idnotion-tasks-db-id(作成されていない場合はスキップ)

すべてが削除されたことを確認する

gcloud run services list --region=us-central1
gcloud storage buckets list --project=$GCP_PROJECT_ID

予想される出力: 空のリストまたは独自の既存のリソースのみ。

16. まとめ

おめでとうございます!Google Cloud に本番環境グレードのマルチエージェント AI システムを構築してデプロイした。

構築した内容

エージェント

能力

デプロイ

ブランド戦略担当者

Google 検索を使った市場調査

Cloud Run

コピーライター

Instagram のキャプションの作成

Cloud Run

デザイナー

Gemini + GCS アップロードによる画像生成

Cloud Run

批評家

スコア付きの品質レビュー

Cloud Run

プロジェクト マネージャー

タイムライン + Notion MCP

Cloud Run

クリエイティブ ディレクター

A2A による完全なオーケストレーション

エージェント ランタイム

学習した主なパターン

  1. ADK Agent - 指示とオプションのツールを使用して LLM エージェントを定義する
  2. adk web - 組み込みのチャット UI を使用して、任意の ADK エージェントをローカルで実行してテストします
  3. SkillToolset - 再利用可能な知識をオンデマンドで読み込まれるモジュール式ファイルにパッケージ化する
  4. FunctionTool - 任意の Python 関数(または外部モデル)を呼び出し可能なエージェント ツールとしてラップします。
  5. to_a2a() - ADK エージェントを A2A 準拠の HTTPS サービスとして公開する
  6. RemoteA2aAgent + AgentTool - 呼び出し可能なツールとしてリモート エージェントをオーケストレート
  7. McpToolset - MCP stdio サーバーを介して外部サービスに接続する
  8. EventsCompactionConfig - 長いマルチエージェント ワークフローでトークン上限を処理
  9. 構造化された批評家出力 - 自動修正による機械可読の品質管理
  10. Cloud Run - コンテナ化されたエージェントを大規模にデプロイする
  11. エージェント ランタイム - マネージド セッションとトレースを使用してオーケストレーターをホストする

次のステップ

  • gemini-3.1-flash-image-preview の編集機能を使用して、デザイナーにマルチターンの画像編集を追加
  • Cloud Run サービスに IAM 認証を追加する(--allow-unauthenticated を削除)
  • 1 人のスペシャリストを LangGraph または CrewAI エージェントに置き換える - A2A はフレームワークに依存しない
  • ユーザー フィードバックをツールとして追加し、参加者がアウトプットを評価して反復できるようにします。
  • Cloud コンソールで Agent Runtime トレースを確認する

リソース