はじめに
前回は、ローカル LLM を OpenAI Agents SDK に接続し、単一の Agent を実行しました。
今回は、Agent が別の Agent に仕事を依頼する仕組みである A2A(Agent2Agent)を試します。売上分析を受け付ける Agent A と、分析を専門に行う Agent B を用意し、Agent A から Agent B へ分析を依頼します。
ただし、Agent A のコードから Agent B を直接 import したり、同じ Runner 内の Python の Tool として扱ったりはしません。Agent A と Agent B を別プロセスとして起動し、A2A プロトコルで連携させます。
ローカル LLM サーバーは、両 Agent から利用する共通の推論基盤です。Agent A / B 自身は別々のアプリケーションとして存在します。
A2A とは
A2A は、独立して動く AI Agent 同士が通信・協働するためのオープンプロトコルです。Agent を作る Framework や、利用する LLM、実行する場所が異なっていても、共通の方法で能力を確認し、仕事を依頼して結果を受け取れます。
プロトコルの仕様や公式チュートリアルは、A2A Protocol 公式サイト を参照してください。
今回試す構成
今回の例では、Agent A は Agent B を「売上データを分析できる外部 Agent」として扱います。Agent B の内部実装や Instructions は Agent A に公開しません。Agent B は Agent Card という能力説明を公開し、Agent A はそれを取得してから A2A のタスクとして依頼を送ります。
「Agent を Tool として呼び出す」と表現するとイメージしやすい一方で、A2A の Agent は単純な Tool そのものではありません。Tool は通常、決められた入力に対して処理結果を返す機能です。A2A の Agent は、Task の進捗、複数回のやり取り、長時間処理などを扱いながら、自律的に仕事を進められます。今回の記事では最小構成として、Agent A が Agent B へ1回の分析タスクを依頼し、完了結果を受け取るところまでを確認します。
完成イメージ
ターミナルを2つ開いて Agent B と Agent A を別々に起動します。
ユーザーからの依頼は Agent A が受け取ります。Agent A は分析が必要だと判断した場合、公開されている Agent B の Agent Card を取得し、A2A のタスクとして依頼を送ります。Agent B は自分の Agent として処理し、結果を A2A のレスポンスとして返します。Agent A はその結果を使ってユーザーへ回答します。
今回確認すること
- Agent A / B を別ディレクトリ、別仮想環境、別プロセスとして起動できる
- Agent B が A2A Server として Agent Card を公開できる
- Agent A が Agent Card を取得して、Agent B の Skill を確認できる
- Agent A が A2A Client として Agent B にタスクを送信できる
- Agent B の結果を Agent A が受け取り、最終回答を返せる
- 両 Agent が同じローカル LLM サーバーを利用できる
SDK 内の Agent 連携と何が違うのか
OpenAI Agents SDK の as_tool() や handoff を使えば、1つの Python プロセス内でも複数 Agent を連携できます。しかしその場合、Agent A は Agent B の Python オブジェクトを直接持ち、同じ SDK の実行管理下に置かれます。
今回の構成で Agent A が知るのは、Agent B の URL と、そこから取得する Agent Card だけです。Agent B は別プロセスで停止・更新・再起動でき、将来は別 PC や別言語の実装へ置き換えることもできます。
| 観点 | SDK 内の Agent 連携 | 今回の A2A 連携 |
|---|---|---|
| プロセス | 同一 | 別々 |
| 起動 | 1つのプログラム | Agent ごとに別ターミナル |
| Agent B の参照 | Python オブジェクト | URL と Agent Card |
| 通信 | SDK 内部の呼び出し | A2A プロトコル |
| Agent B の実装 | 同じアプリに依存 | Agent A から独立 |
| LLM | 共有してもよい | 共有してもよい |
「LLM を共用する」ことと「Agent を独立させる」ことは矛盾しません。LLM サーバーは共通インフラであり、Agent A / B のプロセス、Instructions、依存関係、実行ライフサイクルはそれぞれ独立しています。
実装する
ディレクトリ構成
最初から Agent ごとに分けます。
a2a-sample/
├─ agent-a/
│ ├─ .venv/
│ ├─ pyproject.toml
│ ├─ main.py # Agent A の CLI と A2A Client
│ └─ a2a_client.py # Agent Card の取得とタスク送信
│
└─ agent-b/
├─ .venv/
├─ pyproject.toml
├─ server.py # A2A Server のエントリーポイント
├─ executor.py # A2A の依頼を Agent B へ橋渡しする
└─ analysis_agent.py # OpenAI Agents SDK で定義する Agent B
両方のプロジェクトに a2a-sdk、openai-agents、openai を追加します。記事では動作を最小化するため、A2A の JSON-RPC バインディングとテキスト入出力だけを使います。
Agent B を A2A Server として公開する
Agent B は分析専門の Agent です。外部へ公開するのは Agent B の Python クラスではなく、A2A Server と Agent Card です。
Agent Card に能力を記述する
Agent Card は通常 /.well-known/agent-card.json で公開されます。Agent A はこの JSON を取得して、Agent B の名前、説明、接続先、対応する入出力形式、Skills を確認します。
今回の Agent B には、例えば次の Skill を定義します。
id: sales_analysis
name: Sales analysis
description: 売上データを集計し、売上・件数・傾向を分析する
input: text/plain
output: text/plain
Skill の説明は、Agent A が委譲判断に使える粒度で書きます。「いろいろできます」のような説明では、後から Agent が増えたときに適切な委譲先を選べません。
A2A の依頼を Agent B に渡す
Agent B 側では、A2A SDK の AgentExecutor を実装します。Executor は A2A のリクエストを受け取り、メッセージ本文を取り出して OpenAI Agents SDK の Agent B へ渡し、完了結果を A2A の artifact として返す橋渡し役です。
A2A Request
↓
AgentExecutor.execute()
↓
Runner.run(analysis_agent, input=...)
↓
local LLM
↓
A2A artifact
Agent B の Instructions は分析に限定します。completed のレコードだけを対象にし、集計値・根拠・注意点を返すようにします。ユーザー向けの会話や委譲先の選択は Agent A の責務です。
Agent A から Agent B を呼び出す
Agent A はユーザーとの窓口です。Agent B の URL(例: http://127.0.0.1:9999)を設定として持ち、開始時または委譲直前に Agent Card を取得します。
GET http://127.0.0.1:9999/.well-known/agent-card.json
取得した Agent Card に sales_analysis の Skill があることを確認したうえで、A2A Client が Agent B にメッセージを送ります。
Agent A
├─ ユーザーの依頼を確認
├─ Agent Card を取得
├─ Skill / 入出力形式を確認
├─ A2A task を作成して Agent B へ送信
└─ 完了した task の artifact から分析結果を取得
最初の実装では、売上データの分析依頼は必ず Agent B に送るルールにします。これにより、ローカルモデルの Tool Calling の安定性に依存せず、A2A の通信・タスク・結果取得を検証できます。
次の段階で、Agent A 自身に「分析が必要なら Agent B に委譲する」という判断をさせます。このときも A2A Client を Agent A の Tool として包めばよく、Agent B が独立プロセスである点は変わりません。
実行する
事前に、共通のローカル LLM サーバーを起動しておきます。Agent A / B はそれぞれ LOCAL_LLM_BASE_URL と LOCAL_LLM_MODEL を同じ値に設定します。
Terminal 1: Agent B を起動する
cd agent-b
.venv\Scripts\Activate.ps1
$env:LOCAL_LLM_BASE_URL = "http://127.0.0.1:11434/v1"
$env:LOCAL_LLM_MODEL = "Qwen3.5-9B-Q5_K_M.gguf"
python server.py
起動後、Agent Card を取得できることを確認します。
Invoke-RestMethod http://127.0.0.1:9999/.well-known/agent-card.json
Terminal 2: Agent A を起動する
cd agent-a
.venv\Scripts\Activate.ps1
$env:AGENT_B_URL = "http://127.0.0.1:9999"
$env:AGENT_B_TIMEOUT_SECONDS = "300"
$env:LOCAL_LLM_BASE_URL = "http://127.0.0.1:11434/v1"
$env:LOCAL_LLM_MODEL = "Qwen3.5-9B-Q5_K_M.gguf"
Get-Content -Raw -Encoding utf8 request.txt | python main.py
入力には、agent-a/request.txt を使用しました。以下は動作確認用に作成した架空の売上データです。
status が completed の決済だけを対象に、日付・商品別の決済件数と売上金額を Markdown 表として返すよう依頼します。cancelled のレコードを2件混ぜ、指示どおりに除外されることも確認します。
以下は1レコード1決済の売上データです。
- `status` が `completed` のレコードだけを対象にしてください。
- 日付・商品ごとに決済件数と売上金額を集計してください。
- 分析担当者へ渡せる Markdown 表だけを返してください。
取引ID,日付,商品,金額(円),status
P001,2026-08-01,ノート,300,completed
P002,2026-08-01,ノート,300,completed
P003,2026-08-01,ボールペン,150,completed
P004,2026-08-01,マグカップ,1800,completed
P005,2026-08-02,ノート,300,completed
P006,2026-08-02,ボールペン,150,completed
P007,2026-08-02,ボールペン,150,completed
P008,2026-08-02,マグカップ,1800,completed
P009,2026-08-03,ノート,300,completed
P010,2026-08-03,ノート,300,completed
P011,2026-08-03,ボールペン,150,completed
P012,2026-08-03,マグカップ,1800,cancelled
P013,2026-08-04,ノート,300,completed
P014,2026-08-04,ボールペン,150,completed
P015,2026-08-04,ボールペン,150,completed
P016,2026-08-04,ボールペン,150,completed
P017,2026-08-05,ノート,300,completed
P018,2026-08-05,ノート,300,completed
P019,2026-08-05,ノート,300,completed
P020,2026-08-05,マグカップ,1800,completed
P021,2026-08-06,ボールペン,150,completed
P022,2026-08-06,ボールペン,150,completed
P023,2026-08-06,マグカップ,1800,completed
P024,2026-08-07,ノート,300,completed
P025,2026-08-07,ノート,300,completed
P026,2026-08-07,ボールペン,150,completed
P027,2026-08-07,マグカップ,1800,completed
P028,2026-08-08,ノート,300,cancelled
P029,2026-08-08,ボールペン,150,completed
P030,2026-08-08,ボールペン,150,completed
P031,2026-08-08,ボールペン,150,completed
P032,2026-08-08,ボールペン,150,completed
P033,2026-08-08,マグカップ,1800,completed
確認したいログの流れは次のとおりです。
Agent A: Agent Card を取得した
Agent A: sales_analysis Skill を見つけた
Agent A: Agent B に A2A task を送信した
Agent B: task を受信した
Agent B: local LLM に分析を依頼した
Agent B: artifact を返した
Agent A: artifact を受信し、最終回答を表示した
データは Agent A がそのまま A2A task の入力として渡します。Agent B は completed のレコードだけを集計して artifact を返し、Agent A はその artifact を受けてユーザーへ最終出力を返します。
実行結果
Agent A は Agent B の Agent Card を取得し、sales_analysis Skill が公開されていることを確認してから A2A task を送信しました。その後、Agent B が返した artifact を受け取れました。
[Agent A] Retrieved Agent B's Agent Card
Name : Sales Analysis Agent
Interface : http://127.0.0.1:9999 (JSONRPC 1.0)
Skill : sales_analysis
[Agent A] Sending an A2A task to Agent B
[Agent A] Received Agent B's analysis artifact
[Agent A] Final answer
| 日付 | 商品 | 決済件数 | 売上金額(円) |
| --- | --- | ---: | ---: |
| 2026-08-01 | ノート | 2 | 600 |
| 2026-08-01 | ボールペン | 1 | 150 |
| 2026-08-01 | マグカップ | 1 | 1,800 |
| 2026-08-02 | ノート | 1 | 300 |
| 2026-08-02 | ボールペン | 2 | 300 |
| 2026-08-02 | マグカップ | 1 | 1,800 |
| 2026-08-03 | ノート | 2 | 600 |
| 2026-08-03 | ボールペン | 1 | 150 |
| 2026-08-04 | ノート | 1 | 300 |
| 2026-08-04 | ボールペン | 3 | 450 |
| 2026-08-05 | ノート | 3 | 900 |
| 2026-08-05 | マグカップ | 1 | 1,800 |
| 2026-08-06 | ボールペン | 2 | 300 |
| 2026-08-06 | マグカップ | 1 | 1,800 |
| 2026-08-07 | ノート | 2 | 600 |
| 2026-08-07 | ボールペン | 1 | 150 |
| 2026-08-07 | マグカップ | 1 | 1,800 |
| 2026-08-08 | ボールペン | 4 | 600 |
| 2026-08-08 | マグカップ | 1 | 1,800 |
cancelled のレコードに対応する 2026-08-03 のマグカップと、2026-08-08 のノートは、最終出力に含まれていません。
この時点では扱わないこと
- Agent B の自動発見(URL は
AGENT_B_URLで明示する) - 認証、署名、認可
- ストリーミング結果や長時間タスクの再開
- 複数の Agent Card からの委譲先選択
- MCP 接続
これらを省くことで、今回の記事では「独立プロセスの Agent が Agent Card を公開し、A2A task で連携する」という核心を確認します。
まとめ
- Agent A と Agent B は別ディレクトリ・別仮想環境・別プロセスで起動する
- 共通なのはローカル LLM サーバーだけで、Agent の実装やライフサイクルは独立している
- Agent B は A2A Server と Agent Card を公開し、Agent A は A2A Client として利用する
- 同一 SDK 内の複数 Agent 連携ではなく、A2A による Agent 間通信を検証する