Agentic AI on EKS 実践 — A2Aプロトコルでマルチエージェント連携
目次
はじめに
前回の記事では、EKS 上に Weather Agent と MCP Server をデプロイし、エージェントがツールを自動発見して外部 API を呼び出すフローを検証した。
今回は A2A(Agent-to-Agent)プロトコルによるマルチエージェント連携を検証する。Travel Agent が旅行の質問を受け、天気情報の取得を Weather Agent に自動委譲する構成だ。単体エージェントとは異なる「エージェント間の信頼と発見」という新しい課題に直面した。
アーキテクチャの全体像
ワークショップ全体のアーキテクチャを再掲する。前回(Part 1)は Weather Agent + MCP Server の 2 コンポーネントを検証した。
今回の検証範囲
本記事では Travel Agent → Weather Agent の A2A 連携を検証した。前回デプロイ済みの Weather Agent + MCP Server はそのまま利用する。
前回は curl → Weather Agent の直接呼び出しだったが、今回は curl → Travel Agent → (A2A) → Weather Agent → (MCP) → NWS API という 2 段階の委譲フローを確認した。
A2A プロトコルとは
A2A は Google が提唱するエージェント間通信のオープンプロトコルだ。MCP が「エージェント ↔ ツール」の接続規格であるのに対し、A2A は「エージェント ↔ エージェント」の協調を標準化する。
A2A の通信フローは以下のように進む。
- Travel Agent が Weather Agent の エージェントカード(
/.well-known/agent-card.json)を取得 - カードからスキル一覧(
get_forecast,get_alerts)と接続先 URL を確認 - A2A プロトコルでメッセージを送信し、Weather Agent が処理結果を返却
MCP との大きな違いは、A2A ではツール単位ではなくエージェント単位で委譲すること。Travel Agent は「天気情報を教えて」と自然言語でリクエストし、Weather Agent が内部でどのツールを使うかは Weather Agent 自身が判断する。
Travel Agent の設計
Weather Agent との構成比較
前回の Weather Agent は MCP ツール(get_forecast 等)を直接呼び出す構成だった。Travel Agent はこれと対照的に、他のエージェントを「ツール」として利用する構成を取る。
| Weather Agent | Travel Agent | |
|---|---|---|
| ツール取得元 | MCP Server(mcp.json) | 他のエージェント(a2a_agents.json) |
| ツールの種類 | get_forecast, get_alerts | a2a_send_message, a2a_list_discovered_agents |
| 通信プロトコル | MCP over HTTP | A2A (JSON-RPC) |
| 委譲の粒度 | ツール単位 | エージェント単位(自然言語) |
a2a_agents.json — 接続先エージェントの定義
Weather Agent が mcp.json で MCP Server の接続先を宣言するのと同様に、Travel Agent は a2a_agents.json で A2A 接続先を宣言する。
{
"urls": [
"http://weather-agent.agents:9000/"
]
}起動時に A2AClientToolProvider がこの URL のエージェントカードを取得し、相手の名前・スキル・接続先を自動発見する。接続先を増やす場合は、この配列に URL を追加するだけだ。
A2AClientToolProvider — エージェント間通信の抽象化
Travel Agent のコードは驚くほどシンプルだ。A2AClientToolProvider が A2A 通信の複雑さをすべて抽象化している。
from strands_tools.a2a_client import A2AClientToolProvider
provider = A2AClientToolProvider(
known_agent_urls=["http://weather-agent.agents:9000/"]
)
agent = Agent(
model=bedrock_model,
system_prompt=system_prompt,
tools=provider.tools
)provider.tools が返すのは以下の 3 つのツールだ。
| ツール | 役割 |
|---|---|
a2a_list_discovered_agents | 発見済みエージェントの一覧と URL を返す |
a2a_discover_agent | 指定 URL のエージェントカードを取得・キャッシュ |
a2a_send_message | 指定エージェントに自然言語でメッセージを送信 |
LLM はまず a2a_list_discovered_agents で接続先の URL を取得し、次に a2a_send_message で天気の質問を送る。Weather Agent のコードを一切知らなくても、カードの情報だけで連携できる設計だ。
システムプロンプト — LLM の「やってはいけないこと」を定義する
Travel Agent の agent.md は約 100 行にわたる詳細なシステムプロンプトを含む。Weather Agent の 10 行程度のプロンプトと比べると圧倒的に長い。これはオーケストレーター型エージェントに特有の設計だ。
プロンプトの中核は「何をすべきでないか」の制約にある。
CORE PRINCIPLES:
1. NEVER invent or fabricate specialized information
that should come from other agents
2. ALWAYS use the appropriate tool to query specialized agents
WEATHER INFORMATION PROTOCOL:
- Use ONLY the tools from the Weather Agent to obtain weather info
- NEVER attempt to predict, estimate, or generate weather yourself
- Clearly attribute: "According to the Weather Agent, Miami will..."なぜここまで厳密に禁止する必要があるのか。LLM は天気に関する一般知識を持っており、ツール呼び出しなしでも「マイアミは温暖で...」と回答できてしまう。しかしそれは不正確な情報であり、エージェント連携の意味がなくなる。LLM が「知っていても答えるな」という制約を明示的に書くのが、オーケストレーターのプロンプト設計の要点だ。
ハマりどころ 1: エージェントカードの URL 問題
Travel Agent のデプロイには、A2A 接続先を Helm values で指定する。
Travel Agent のデプロイ手順
Travel Agent 用の S3 セッションバケットと Pod Identity を作成し、コンテナイメージをビルドして Helm でデプロイする。
# Travel Agent 用 ECR リポジトリ
aws ecr create-repository --repository-name agents-on-eks/travel-agent --region $AWS_REGION
# Travel Agent 用 S3 セッションバケット
aws s3 mb s3://travel-agent-session-${ACCOUNT_ID} --region $AWS_REGION
# Travel Agent 用 IAM ロール(Bedrock + S3)
cat > /tmp/travel-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BedrockAccess",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": "*"
},
{
"Sid": "S3Access",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::travel-agent-session-${ACCOUNT_ID}/*"
},
{
"Sid": "S3List",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::travel-agent-session-${ACCOUNT_ID}"
}
]
}
EOF
aws iam create-role --role-name travel-agent-pod-role \
--assume-role-policy-document file:///tmp/pod-identity-trust.json
aws iam put-role-policy --role-name travel-agent-pod-role \
--policy-name bedrock-s3 --policy-document file:///tmp/travel-policy.json
TRAVEL_ROLE_ARN=$(aws iam get-role --role-name travel-agent-pod-role \
--query 'Role.Arn' --output text)
aws eks create-pod-identity-association \
--cluster-name $CLUSTER_NAME --region $AWS_REGION \
--namespace agents --service-account travel-agent \
--role-arn $TRAVEL_ROLE_ARNコンテナイメージをビルドする。ポイントは a2a.a2a_agents.json で Weather Agent の A2A エンドポイントを指定すること。
# Travel Agent のビルドコンテキストを S3 にアップロード
cd agents/travel
tar czf /tmp/travel-agent-context.tar.gz .
aws s3 cp /tmp/travel-agent-context.tar.gz s3://kaniko-build-${ACCOUNT_ID}/build/
cd ..apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-travel-agent
namespace: build
spec:
backoffLimit: 1
template:
spec:
serviceAccountName: kaniko
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- "--context=s3://kaniko-build-${ACCOUNT_ID}/build/travel-agent-context.tar.gz"
- "--destination=${ECR_HOST}/agents-on-eks/travel-agent:latest"
restartPolicy: Neverkubectl apply -f kaniko-travel.yaml
kubectl wait --for=condition=complete \
job/kaniko-travel-agent -n build --timeout=600sHelm でデプロイする。
# Travel Agent 用 values ファイル
cat > /tmp/travel-values.yaml << EOF
image:
repository: ${ECR_HOST}/agents-on-eks/travel-agent
tag: latest
env:
DISABLE_AUTH: "1"
SESSION_STORE_BUCKET_NAME: travel-agent-session-${ACCOUNT_ID}
serviceAccount:
name: travel-agent
agent:
agent.md: null
mcp:
mcp.json: null
a2a:
a2a_agents.json: |
{
"urls": [
"http://weather-agent.agents:9000/"
]
}
EOF
helm upgrade travel-agent manifests/helm/agent \
--install -n agents -f /tmp/travel-values.yaml
kubectl rollout status deployment travel-agent -n agents --timeout=180sagent.md: null と mcp.json: null を指定すると ConfigMap が作成されず、Dockerfile に埋め込まれたデフォルトの設定ファイルが使われる。Travel Agent は MCP ツールを直接使わないため mcp.json は不要だ。
最初にデプロイしたとき、Travel Agent は Weather Agent への A2A 通信に失敗した。
A2AClientHTTPError: HTTP Error 503: Network communication error
fetching agent card from https://weather-agent.example.com/.well-known/agent-card.json原因は Weather Agent のエージェントカードだ。A2A サーバーは A2A_URL 環境変数が未設定の場合、カードの url フィールドにデフォルト値(0.0.0.0 やプレースホルダー)を記載する。Travel Agent の初回ディスカバリー(a2a_agents.json の URL でのカード取得)自体は成功するが、LLM がカード内の url フィールドを読んで a2a_send_message の target_agent_url に渡すため、クラスター内で到達不能な URL への接続を試みて失敗する。
解決策は Helm values で a2a.http_url を Kubernetes Service の FQDN に設定すること。
# Weather Agent の Helm values
a2a:
http_url: "http://weather-agent.agents:9000/"これにより、エージェントカードの url が http://weather-agent.agents:9000/ に書き換わり、クラスター内の他のエージェントから正しく到達できるようになる。Kubernetes 上で A2A を使う場合、各エージェントのカードにサービスディスカバリ可能な URL を設定するのが必須だ。
ハマりどころ 2: S3 セッション履歴によるコンテキスト汚染
エージェントカードの URL を修正した後も、Travel Agent は依然として到達不能な URL に接続しようとした。
原因は S3 セッション履歴だった。Travel Agent は S3 にユーザーごとの会話履歴を保持しており、古い URL を含む過去のメッセージが LLM のコンテキストに復元されていた。LLM はそのコンテキストから到達不能な URL を学習し、a2a_send_message ツールの target_agent_url パラメータにそのまま渡していたのだ。
# S3 セッションデータをクリア
aws s3 rm s3://travel-agent-session-${ACCOUNT_ID}/ --recursive
# Pod を再起動してメモリ内キャッシュもクリア
kubectl rollout restart deployment travel-agent -n agents
kubectl rollout status deployment travel-agent -n agents --timeout=120sセッションクリア後、Travel Agent は a2a_list_discovered_agents でエージェントカードから正しい URL を取得し、Weather Agent への A2A 通信に成功した。
これは AI エージェント特有の問題だ。従来のマイクロサービスなら設定変更は即座に反映されるが、セッション状態を持つ AI エージェントでは、過去の会話履歴が LLM の判断を汚染する可能性がある。エージェントの接続先を変更した際はセッションのクリアも忘れてはならない。
注意: 上記 2 つの対策を行っても、LLM が
a2a_send_messageのtarget_agent_urlにディスカバリー結果ではなく推測した URL を渡し続ける場合がある。a2a_send_messageの内部実装はディスカバリー済み URL へのフォールバックを持たないため、LLM がa2a_list_discovered_agentsを呼ばずに URL を推測すると到達不能な宛先に接続を試みて失敗する。この問題が発生した場合は、Travel Agent のagent.mdのシステムプロンプトに「a2a_send_messageを呼ぶ前に必ずa2a_list_discovered_agentsで接続先 URL を取得すること」という指示を追加すると安定する。
動作確認
すべての問題を解決した後、Travel Agent に旅行の相談を送ると、Weather Agent への A2A 委譲が正常に動作した。
# port-forward でローカルからアクセスする場合
kubectl port-forward svc/travel-agent -n agents 3000:80 &
curl -X POST http://localhost:3000/prompt \
-H "Content-Type: application/json" \
-d '{"text":"I am planning a trip to Miami, Florida next week. What will the weather be like?"}'User: "I'm planning a trip to Miami, Florida next week.
What will the weather be like?"
Travel Agent の応答(抜粋):
## Miami, Florida - 7-Day Weather Forecast
According to the Weather Agent:
**Monday** - High: 83°F, Showers and thunderstorms possible (50%)
**Tuesday** - High: 71°F, Chance of showers (40%)
...
**Saturday** - High: 77°F, Sunny ☀️ - Excellent for outdoor activities
**Sunday** - High: 78°F, Sunny ☀️ - Ideal weather
Summary: Plan indoor activities for Monday-Thursday,
save beach and water activities for the weekend!Travel Agent がシステムプロンプトのルール通り「According to the Weather Agent」と出典を明記し、天気データを自分で生成せずに Weather Agent から取得していることが確認できた。
まとめ
- エージェントカードの URL は Kubernetes Service の FQDN に設定する — A2A サーバーのデフォルト
0.0.0.0ではクラスター内通信が不可能。a2a.http_urlの設定が必須だ。 - セッション履歴は LLM の判断を汚染する — 従来のサービスと異なり、AI エージェントは過去の会話コンテキストに基づいて判断する。設定変更後のセッションクリアを運用手順に組み込む必要がある。
- A2A はエージェント単位の委譲 — MCP がツール単位で接続するのに対し、A2A では自然言語でリクエストを送り、受け手のエージェントが内部で判断する。オーケストレーター型の設計に適している。
