この記事では、Agent同士をつなぐ共通仕様 A2A(Agent2Agent Protocol) と、A2A通信を中継する agentgateway を使って2つのAgentを連携させ、その挙動を検証します。
文章を書くDraft Agentと内容を確かめるReview Agentを用意し、Agent Cardが示す接続先、3つのTaskのつながり、gatewayに残るログを追いました。そこから、Agent同士を直接つなぐ場合との違いを整理します。
A2Aとは#
A2A(Agent2Agent Protocol) は、異なるAgent同士が仕事を頼み、結果を受け取るための共通仕様です。Agentが別のフレームワークや言語で作られていても、相手の内部実装を知らずに連携できることを目指しています。
今回使ったA2A 1.0のJSON-RPCバインディングは、HTTP(S)上でJSON-RPC 2.0をやり取りします。A2Aでは、「相手が何をできるか」「どのように仕事を頼むか」「進み具合や結果をどう返すか」を共通の形式で決めています。現行仕様には、gRPCとHTTP+JSONのバインディングもあります。
| 要素 | 役割 | 今回の例 |
|---|---|---|
| Agent Card | Agentの名前、得意な仕事、接続先、認証方式などを伝える | Draft AgentとReview Agentの接続先を取得する |
| Message | Agentへ渡す依頼や、Agentから返す応答 | テーマやレビュー対象の文章を送る |
| Task | 状態を持つ仕事の単位 | 初稿作成、初回レビュー、再レビューを別々に記録する |
| Artifact | Taskから生まれた成果物 | 生成した文章やレビュー結果を返す |
| contextId | 関連するMessageやTaskを同じ文脈にまとめる | 3つのTaskが同じ会話に属することを示す |
応答はMessageとして直接返すことも、状態を持つTaskとして受け付けることもできます。Taskを使うと、処理の進み具合や成果物を追跡できます。ストリーミングやPush通知もA2Aの仕様に含まれます。
agentgatewayとは#
agentgateway は、Agent、MCPサーバー、LLM(大規模言語モデル)などへの通信を中継するgatewayです。
通常のHTTPやgRPCの通信も扱えます。スタンドアロンとKubernetesの両方に対応しています。現在はLinux FoundationのAgentic AI Foundation(AAIF)に参加するオープンソースプロジェクトです。
MCPとA2Aは役割が異なります。MCPはAgentやアプリケーションがツールやデータを使うための仕組みです。A2Aは、Agentが別のAgentへ仕事を頼むための仕組みです。両者は併用でき、A2Aで呼ばれたAgentが内部でMCPツールを使う構成もあります。
agentgatewayでMCP通信を中継する構成は、以前の記事「MCPゲートウェイって何? agentgatewayをDockerで最小構成から触ってみた 」で紹介しました。今回は、A2Aサーバーとして動くAgentの前段に置き、Agent Cardの取得とMessage送信を中継します。
A2Aクライアントは、A2Aサーバーへ直接接続できます。この記事でいう「A2A gateway」は、その通信をagentgatewayで中継する構成です。gatewayを置く場合は、そのURLを入口として公開し、そこからAgentへ転送します。
今回使った機能と、必要に応じて追加できる機能を分けると次のようになります。
| agentgatewayでできること | 得られること | 今回の扱い |
|---|---|---|
| A2AのAgent CardとMessageをAgentへ転送する | クライアントが使う入口とAgent内部の接続先を分けられる | 2つの固定ルートで確認した |
| Agent Cardの接続先をgatewayのURLに書き換える | Cardを読んだクライアントもgateway経由で通信できる | Draft用とReview用のCardで確認した |
| route、HTTP status、A2A methodなどをログへ残す | どの入口を通り、どこで失敗したかを追いやすくなる | アクセスログで確認した |
| 認証、認可、rate limit(通信量の制限)、timeout(待ち時間の上限)、retry(再試行)などのポリシーを入口に置く | 認証や通信制御を、共通の入口でそろえられる | 今後の検証対象 |
| metricsやtraceを外部の観測基盤へ送る | 複数のAgentをまたぐ通信を共通の観点で監視できる | 本記事ではアクセスログのみ使用した |
agentgatewayは、A2A通信をAgentへ中継し、入口でURLや通信ポリシー、アクセスログを扱います。文章の生成とレビューは各Agentが行います。Taskやその識別子、contextIdはA2Aの形式に沿って各Agentのアプリ側で扱います。
gatewayを置くと、Agent間通信の入口をまとめられる#
A2Aを使うことと、A2A通信をgatewayへ通すことは別の選択です。
| 観点 | A2Aで直接接続 | A2A + agentgateway |
|---|---|---|
| クライアントが接続する先 | 各AgentのURL | gatewayのURL |
| Agent Cardに載せるURL | Agent自身の公開URL | gatewayが公開するURL |
| 通信経路のログ | 各Agentや既存基盤で集める | gatewayの入口でも記録できる |
| 認証や通信制御 | Agentまたは別の基盤で実装する | gatewayのポリシーとして共通化できる |
TaskとcontextId | A2Aとアプリ側で扱う | A2Aとアプリ側で扱う(直接接続時と同じ) |
| 構成要素 | 少ない | gatewayの設定と運用が増える |
大きく変わるのは、Agent間通信の共通部分を誰が受け持つかです。直接接続では、公開URL、認証、通信制御、アクセスログを各Agentか既存の基盤で扱います。gatewayを置くと、その責任をAgentの前段へまとめられます。
たとえばReview Agentの配置先が変わった場合、gatewayの公開URLを保ったまま転送先だけを変更する設計にできます。認証やrate limitを追加するときも、各Agentへ同じ処理を実装する代わりにgatewayのポリシーとして設定できます。障害調査では、まずgatewayのログで到達したrouteとHTTP statusを確認し、その後にAgent側のTaskを追えます。
gatewayを置くと、設定、監視、更新、可用性を管理する責任も増えます。Agentが少なく、接続先も変わらず、既存のAPI基盤で同じ機能をまかなえているなら、gatewayを追加しない方が運用は軽くなります。
検証した構成#
利用者がagentgatewayの:3000へテーマを送ると、Draft AgentがGemini Enterprise Agent PlatformのGeminiで初稿を作ります。Draft Agentは:3001経由でReview Agentへ初稿を渡し、指摘を受けて修正したあと、もう一度レビューを依頼します。
flowchart TD
U[利用者] -->|テーマ| G1[agentgateway
:3000]
G1 --> D[Draft Agent]
D -->|初稿・修正版を生成| V[Gemini Enterprise
Agent Platform
Gemini]
D -->|レビュー依頼| G2[agentgateway
:3001]
G2 --> R[Review Agent]
R -->|文章を評価| V
R -->|判定・コメント| G2
G2 --> D
D -->|最終稿・実行結果| G1
G1 --> U
図の:3000と:3001は、同じagentgatewayの2つのポートです。
Draft Agentは、利用者から見ればA2Aサーバーです。同時に、Review Agentへ仕事を頼むA2Aクライアントでもあります。
| 通信 | A2Aクライアント | A2Aサーバー |
|---|---|---|
| テーマを送る | 利用者側のクライアント | Draft Agent |
| レビューを頼む | Draft Agent | Review Agent |
agentgatewayには入口を2つ用意しました。
| 入口 | 転送先 | 受け取る依頼 |
|---|---|---|
:3000 | Draft Agent | 利用者が送るテーマ |
:3001 | Review Agent | Draft Agentが送るレビュー依頼 |
2つの入口は、それぞれ決められたAgentへ固定転送します。依頼文を読んで転送先を選ぶオーケストレーションは、今回の構成に含めていません。
Agent Cardで、レビュー依頼の送り先を確認できた#
今回のA2Aクライアントは、通信前に相手のAgent Cardを取得します。Draft AgentがReview Agentを呼ぶときの取得先は次のURLです。agentgatewayはDocker Compose内のサービス名です。
GET http://agentgateway:3001/.well-known/agent-card.jsonReview Agentへ直接アクセスして取得したCardでは、Messageの送信先はhttp://review-agent:9999/a2a/jsonrpcでした。gateway経由で取得したCardでは、同じ送信先が次のように書き換わっていました。
{
"supportedInterfaces": [
{"url": "http://agentgateway:3001/a2a/jsonrpc", "protocolBinding": "JSONRPC", "protocolVersion": "1.0"},
{"url": "http://agentgateway:3001/a2a/jsonrpc", "protocolBinding": "JSONRPC", "protocolVersion": "0.3"}
]
}Draft AgentはCardに書かれたURLへレビュー対象の文章をPOSTしました。
POST http://agentgateway:3001/a2a/jsonrpc接続先をAgent Cardから得る設計では、Cardの取得に加えて、そこに示されたURLへ実際に送信できるかまで確認する必要があります。
Cardには0.3互換のurlフィールドも含まれていますが、agentgateway 1.5.0はこの値を書き換えず、http://review-agent:9999/a2a/jsonrpcのまま返しました。今回のクライアントはsupportedInterfacesを読むため、影響はありませんでした。0.3仕様のurlを読むクライアントは、gatewayを通らずにreview-agent:9999へ直接向かいます。この名前はDocker Composeのネットワーク外からは到達できません。
Agent CardはA2Aの仕組みであり、agentgatewayがなくても使えます。A2Aで直接接続する場合はAgent自身のURLを載せられます。今回gatewayを加えたことで、CardのsupportedInterfacesに載るURLをgatewayの入口にそろえられることを確認しました。
初稿作成と2回のレビューを、ひとつの流れとして追えた#
1回の実行で作られるTaskは3つです。
| Task | 内容 |
|---|---|
| Draft Task | テーマを受け取り、最終稿を返す |
| Review Task 1 | 初稿をレビューする |
| Review Task 2 | 修正版をレビューする |
3つのTaskには、それぞれ異なるタスクIDが付きます。Taskオブジェクトでは、この値をidフィールド(Task.id)に持ちます。どのレビュー結果かを調べるときは、この値で仕事を区別できます。
Draft Task contextId=C1 id=T1
Review Task 1 contextId=C1 id=T2
Review Task 2 contextId=C1 id=T3MessageやTaskの更新イベントからTaskを参照する場合は、taskIdフィールドに同じタスクIDを入れます。
contextIdは3つとも同じです。利用者側のクライアントが作った値をDraft Agentが2回のレビュー依頼へ引き継ぎ、Review Agentは依頼ごとに新しいタスクIDを作りました。これにより、初稿レビューと再レビューを別々の仕事として見ながら、元のDraft Taskと同じ会話にまとめられます。
このcontextIdの引き継ぎは、A2Aが自動で行うものではありません。両Agentは、Messageに入ったcontextIdをそのまま使ってTaskを作るよう実装しています。A2A仕様では、クライアントが指定したcontextIdを受け入れるかどうかはサーバー側の任意です。また、IDをそろえても本文や会話履歴が共有されるわけではないので、レビュー対象の文章は毎回Messageで渡しています。
この対応関係は、アプリが返したTask情報で確認しました。今回取得したgatewayのログにはcontextIdもタスクIDも含まれていなかったため、Taskの相関にはアプリ側の証跡が必要です。6回の実測とは別に、同じ構成でストリーミングを使わないSendMessageを1回送ったところ、ログにはa2a.context.idとTaskの状態が記録されました。タスクIDはこのときも記録されませんでした。
独自のHTTP APIでも、同様の識別子は設計できます。今回A2Aが役立ったのは、MessageとTaskの共通形式を使い、contextIdとTask.idで3つの仕事を記録できた点です。この形を複数のAgentでそろえたいかどうかが、A2Aを選ぶ判断材料になります。
レビューで、検証結果からは言えない説明を見つけて修正できた#
Review Agentが誤りを指摘できるかは、通信結果とは別に確認しました。最初に試した題材では、生成文に目立った誤りがなく、レビューはすべてapprovedでした。
そこで、直接接続とgateway経由の構成を比べる題材へ変えました。測っていない性能や運用上の利点まで書いたり、A2Aとgatewayの役割を混同したりしやすいテーマだからです。
A2Aとagentgatewayを使うべき場合と、Agent同士の直接接続で十分な場合を、今回の検証結果から比較するReview Agentには、今回の検証で確認した事実も渡しました。gatewayが固定転送であることと、Taskの相関はアプリ側で確認したことです。あわせて、直接接続、認証、再送、性能は比較していないと伝えました。
Review Agentは、テーマとの関連性、初心者にも分かる明瞭さ、具体性、構成、技術的な正確さの5つの観点を1〜5点で採点します。すべて4点以上ならapproved、それ以外はneeds_revisionを返すようGeminiに指示し、点数と判定が一致しているかをPython側で検証しました。一致しなければそのTaskを失敗にする実装で、6回の実行ではすべて一致しました。approvedとneeds_revisionは今回のアプリで決めた判定で、A2AのTaskの状態とは別物です。
同じテーマで6回生成すると、5回の初稿に実測からは言えない主張が入りました。Review Agentはいずれも該当箇所と理由を挙げてneeds_revisionと判定し、修正後はapprovedになりました。
| 初回レビューで見つけた問題 | Review Agentの指摘 | 修正版 |
|---|---|---|
gatewayがcontextIdやタスクIDを管理すると説明した | Taskの相関はアプリ側の証跡で確認しており、gatewayログだけでは分からない | gatewayとアプリの役割を分けて記述した |
| 直接接続の開発速度や運用コストを実測結果として扱った | 直接接続は実行していないため、検証結果と設計上の判断を分ける必要がある | 実測した構成と選択条件を分けた |
| 認証、負荷分散、再送への効果まで広げた | 今回の設定と実測では確認していない | 今回観測したCardのURL、固定転送、ログに絞った |
残る1件は、初稿から実測した事実と設計上の判断を分けて書けており、初回からapprovedでした。
たとえば1回目の初稿には「gatewayが通信の中継や識別子の管理を抽象化することで、エージェント間が直接依存し合う密結合を防ぎます」という一文がありました。Review Agentは「今回の検証ではgatewayログだけではTask相関を確認できておらず、アプリ側で確認しているため表現を修正してください」と指摘しました。修正版では、該当箇所が「3つのTaskのcontextIdとtaskIdの対応はアプリ側のTask情報で確認しました」に変わっています。ログに識別子が出ないことだけでは、gatewayが内部で識別子を扱っていないとまでは言えません。この初稿の問題は、gatewayが識別子を管理すると言える根拠が、今回の検証に見当たらない点です。
これで、少なくとも今回現れた3種類の誤りに対して、Review Agentが問題を特定し、修正後の文章を承認できることを確認できました。指摘と修正版は筆者も読み、妥当であることを確かめています。ただし、生成とレビューには同じモデルを使っているため、指摘されなかった誤りが残っている可能性はあります。
6回の実測結果#
1回の実行につき、初稿生成、初回レビュー、修正版の生成、再レビューの順にGeminiを4回呼びました。
| 指標 | 実測結果 |
|---|---|
| 実行回数 | 6 |
| モデル呼び出し | 24回(1回の実行あたり4回) |
| レビューの推移 | needs_revision → approvedが5件、approved → approvedが1件 |
| 異なる初稿 | 6種類 |
| 異なる最終稿 | 6種類 |
| 修正版で本文が変わった実行 | 6 / 6 |
| 入力トークン | 30,238 |
| 出力トークン | 10,216 |
| 総トークン数 | 40,454 |
| モデル応答時間 | 合計59,644.6ms、1呼び出し平均2,485.2ms |
モデル応答時間は、各Agentがgenerate_contentを呼んでから応答全体を受け取るまでの時間で、gatewayの中継遅延は含みません。トークン数は、Gemini Enterprise Agent Platformの応答に含まれるusage_metadataのうち、入力をprompt_token_count、出力をcandidates_token_count、総数をtotal_token_countから合計しました。コードではtemperatureを初稿0.8、レビュー0.2、修正版0.6と指定しましたが、gemini-3.5-flash-liteはtemperatureなどのカスタム値を無視する仕様のため、実際には効いていません。thinking_levelは指定しておらず、既定のMINIMALで動いています。
各実行では、初回がapprovedでも修正版を作り、2回目のReview Taskまで進めました。これはDraft AgentからReview Agentへの2回のA2A呼び出しとTaskのつながりを毎回同じ条件で見るためです。実運用では、初回approvedで終了する、重要な指摘だけ再レビューするなど、目的と費用に合わせて停止条件を決めます。
通信経路はgatewayログ、仕事のつながりはTask情報で確認する#
agentgatewayのアクセスログには、route、HTTPメソッド、パス、ステータス、A2A methodが記録されました。代表的な行を簡略化すると次の形です。
route=default/draft-agent method=GET path=/.well-known/agent-card.json status=200 protocol=a2a
route=default/draft-agent method=POST path=/a2a/jsonrpc status=200 a2a.method=SendStreamingMessage
route=default/review-agent method=GET path=/.well-known/agent-card.json status=200 protocol=a2a
route=default/review-agent method=POST path=/a2a/jsonrpc status=200 a2a.method=SendStreamingMessage6回の実行では、Draft AgentへのPOSTが6回、Review AgentへのPOSTが12回ありました。1回につき、テーマ送信が1回、レビュー依頼が2回です。Agent CardのGETはDraft側18回、Review側30回でした。各実行の開始時に、計測スクリプトが両AgentのCardを1回ずつ確認します。さらに、Messageを送るクライアントが送信のたびに、URLの検査用とresolver(接続先を解決する処理)用で2回取得します。そのため、GET数は実行回数と一致しません。
このログから、どの入口を通り、どのA2A methodがHTTP 200で応答したかを追えます。3つのTaskが同じ会話に属することは、アプリ側に保存したcontextIdとタスクIDを照合して確認しました。6回すべてでcontextIdが一致し、3つのタスクIDは異なっていました。
gatewayログとアプリ側のTask情報を組み合わせることで、「通信がHTTP 200で応答した」と「期待した仕事が完了した」を分けて確認できます。
直接接続、A2A、gatewayをどう選ぶか#
この記事では、独自HTTPで直接つなぐ、A2Aで直接つなぐ、A2A通信をgateway経由にする、の3つを比べます。
flowchart TD
Q1{Agent CardやTaskの形式を
Agent間でそろえたいか}
Q1 -->|いいえ| H[独自HTTPで直接接続]
Q1 -->|はい| Q2{公開入口・経路ログ・通信ポリシーを
共通化したいか}
Q2 -->|いいえ| A[A2Aで直接接続]
Q2 -->|はい| Q3{既存のAPI基盤で
その要件を満たせるか}
Q3 -->|はい| A2[A2A + 既存のAPI基盤]
Q3 -->|いいえ| G[A2A + agentgateway]
| 接続方法 | 向いている条件 | 設計で得たいもの |
|---|---|---|
| 独自HTTPで直接接続 | 相手が固定され、独自のリクエスト形式とアプリログで管理できる | 構成要素を増やさず接続する |
| A2Aで直接接続 | Agent CardとTaskの形式はそろえたいが、共通の入口やgatewayログは不要 | 接続先の提示と仕事の追跡をA2Aの形にそろえる |
| A2A + agentgateway | Agent Cardに載せる送信先、外部へ出す入口、A2Aの経路ログをgatewayへ集めたい | Agentの前段に共通の通信境界を置く |
今回の構成では、Draft AgentがReview Agentのコンテナへ直接送る場合のベースURLはhttp://review-agent:9999です。gatewayを通す場合はhttp://agentgateway:3001へ送り、そこからReview Agentへ固定転送します。
Draft Agent
└─ http://agentgateway:3001
└─ http://review-agent:9999認証や通信制御のポリシーを入口へ追加する設計も可能です。採用理由にする場合は、必要なポリシーを実際に設定し、拒否時や障害時の挙動まで確かめてから判断します。
Agentが少なく、同じチームやネットワーク内で接続先が固定され、認証やログを既存基盤で管理できるなら、agentgatewayを追加しない方が構成は小さくなります。Agent CardとTaskも必要なければ、独自のHTTP APIで十分です。
反対に、複数のAgentを同じ入口から公開したい場合や、Agentの追加・移動が多い場合は、gatewayへまとめる価値が出ます。認証や通信制御を入口でそろえたい、Agentの実装担当と通信基盤の担当を分けたい、といった運用上の要件も判断材料になります。
検証環境と設定#
linux/arm64版agentgatewayコンテナを含むDocker Composeのローカル環境で測定しました。
ここでは、結果を解釈するための環境とagentgateway設定の抜粋を示します。
| 要素 | バージョンと条件 |
|---|---|
| agentgateway | 1.5.0、linux/arm64コンテナ(sha256:f64c27c8bd8aa25f07864dab07072026e375f22c6298c2c2d64f7d6c08a24130) |
| A2Aプロトコル | 1.0、JSON-RPCバインディング |
| A2A Python SDK | a2a-sdk[http-server]==1.1.2 |
| Google Gen AI SDK | google-genai==2.19.0 |
| Geminiモデル | gemini-3.5-flash-lite、Gemini Enterprise Agent Platform、global |
| Python | 3.12.13 |
| 起動方法 | Docker Compose |
agentgatewayでは、2つのA2Aリスナーと転送先を次のように対応させました。
binds:
- port: 3000
listeners:
- routes:
- name: draft-agent
policies:
a2a: {}
backends:
- host: draft-agent:9999
- port: 3001
listeners:
- routes:
- name: review-agent
policies:
a2a: {}
backends:
- host: review-agent:9999共通化したいものから、接続方法を考える#
選択の基準は、Agent間でA2Aの共通形式を使う必要があるか、さらに公開入口や経路ログをgatewayへ集約する必要があるかです。
次に測るものも、採用理由から決まります。性能を理由に選ぶなら直接接続との遅延比較、入口の統制を理由に選ぶなら認証と拒否時の挙動、処理継続を重視するならtimeoutと再送です。
次にやりたいこと#
今回はReview Agentを自前で用意したので、どのモデルを使い、どんな指示でレビューしたかまで確認できました。しかし、A2Aでつながる相手が他社のAgentなら、Draft Agentに届くのはapprovedかneeds_revisionかと、指摘の内容だけです。そのレビューが正しいのか、何トークン使ったのかは分かりません。
次は、返ってきたレビューの正しさとコストを、頼んだ側からどこまで確かめられるかを試したいと思います。Review Agentを別のモデルに差し替えたとき、同じ方法で比べられるかも確かめます。