🙆

A2A (Agent2Agent) プロトコルを基礎から学ぶ (1) LLM を使わない複数Agentのサンプル

に公開

2025年4月、バラバラに動いていたAIエージェントたちに共通言語を与える画期的な取り組みとして、Googleが「A2A(Agent2Agent)プロトコル」を発表しました。

AI Agentとは?

実は明確な定義がありません。これがまずわかりづらさを生み出しています。
RAGやMCPなどにより生成AIの検索能力は拡張され、様々なベンダーが多くの機能をリリースしてきました。LLMと対話するLLMクライアントはその総評としてAgent / エージェントと呼ばれます。

Agentは一般的に以下の機能(の一部ないしは全て)を保有しています。
・LLMとの対話
・外部ナレッジへの検索(RAG)
・外部APIへの検索や操作(MCP)

またAWSがリリースしたオープンソースのAgent開発用フレームワークであるStrands AgentsではAgentic Loopという機能を備え、一度のプロンプトで複数の指示を与えることが可能となり、反復的に回答が出そろうまでAgentはLLMとの対話、RAG/MCPを用いた検索やデータ操作を繰り返します。

これらの機能によりAgentは明確な定義がないまま発展を続け企業内部での利用が進んできました。

A2A の基本的な考え方

それに呼応する形で今度はサイロ化の問題が発生してきました。例えば、
人事部門:採用プロセスを支援するエージェント
経理部門:経費処理を行うエージェント
ITエージェント:アカウントを管理するエージェント
開発部門:コード生成を支援するエージェント
このような場合、それぞれのエージェントはばらばらに開発され提供されているため、横で情報を連携させる仕組みがありません。これ連携させるための共通規格として定義されたのがA2Aです。

これにより例えば上記4つのエージェントに対して「新入社員の入社手続きをしたい」という指示を与えると、
人事部門:採用情報や社内ポータルへの登録
経理部門:給与や税金などの設定
ITエージェント:社内アカウント発行
開発部門:開発用GitHubアカウントの発行
を一気通貫で行うことができるようになります。A2Aとしてそれぞれのエージェントが共通言語を話すようになれば、それぞれ別々に開発されたエージェントでも組み込みが簡単に行えるようになります。

MCPと同じようにオープンなプロトコルとしてアナウンスされ、発表後すでに50社以上がこの取り組みに参加しています。

MCP と A2A

A2AはAnthropicが提唱したMCP(Model Context Protocol)を補完するもの、とよく表現されます。

一般的には以下の相関関係を持ちます。
MCP(Model Context Protocol)
LLMとツール・データを接続
例:「AIがデータベースを使う」「AIがAPIを呼ぶ」
関数呼び出しの標準化

A2A(Agent2Agent Protocol)
エージェント同士を接続
例:「営業AIが人事AIに依頼する」「複数AIが協力する」
エージェント間通信の標準化

MCPにより単独のAgentはLLM未対応のAPIなどを検索範囲に加えることができたり、時にはデータ操作なども行います。つまり一つのAgentをより高度なタスクをこなせるように進化させるものです。
一方A2Aはその進化したAgentをさらに複数横連携させるもの、という関係性です。

前述のAWSが開発したStrands Agentsなども独自の考え方で複数Agentの連携などをサポートしており、今後すべてが A2Aに統一されるわけではありません。一方Amazon Bedrock AgentCoreもA2Aのサポートをアナウンスしており、重要なポジションに位置することは間違いなさそうです。
(参考)Amazon Bedrock AgentCore の A2A サポート
https://serverless.co.jp/blog/g_ovyb3qdf/

(参考)Strands Agents の複数Agents連携
https://serverless.co.jp/blog/w5-9mcx11/
https://serverless.co.jp/blog/hyc-05g-4g/
https://serverless.co.jp/blog/i6c2xe--ltm/

LLM を使わない A2A サンプル!?

は?といったところです。A2AはAgent間の連携を定義したプロトコルであり、AgentはLLMを活用することが前提の物です。このためLLMを使うことは大前提です。

ただしA2Aの学習ということにフォーカスをあてると、Agentが他のAgentの存在をどのように知るのか?、どういうときに他のAgentに処理を依頼するのか?等はLLMが無くても実装可能です。

むしろサンプルソースにLLM呼び出しが加わると長大となり少し学習がしずらくなってしまいます。このためこのブログではまずLLMを使わない複数AgentをA2Aで連携させるサンプルを使って中身を見ていきたいと思います。

通信方式と Agent Card

A2AではMCPと同じように通信方式はJSON-RPCです。
https://zenn.dev/kameoncloud/articles/7b663daf3c4fad

エージェントカード(Agent Card)は、AIエージェントの「名刺」や「プロフィール」 のようなものです。A2Aプロトコルでは、エージェント同士が連携する前に、互いに「自己紹介」をする必要があります。この自己紹介の情報をまとめたJSONファイルが、エージェントカードです。
エージェントカードのURLは、標準的に以下のパスで公開されます:

https://example.com/.well-known/agent.json

以下の様なフォーマットになります。

{
  "name": "WorkerAgent",
  "version": "1.0.0",
  "description": "固定値を返すシンプルなWorkerエージェント",
  "capabilities": ["text"],
  "skills": [
    {
      "name": "getFixedValue",
      "description": "固定値を返すスキル"
    }
  ],
  "endpoint": "http://localhost:3001"
}

こうすることにとってこのAgentは何をやってくれるAgentか?というものを相手に伝えます。このブログではLLMを使わず2つのAgentをまずは連携させますが、LLMを使うAgent環境の場合、いつどのAgentを呼び出すか?はdescriptionskillsの内容によってLLMが判断しますので丁寧に書いておくことが重要です。

Agent の状態管理

A2AではAgentを以下7つの種別で状態管理します。

type TaskState = 
  | "submitted"      // タスクが送信され、実行待ち
  | "working"        // エージェントが積極的に作業中
  | "input-required" // ユーザーからの入力待ち
  | "completed"      // 正常に完了
  | "canceled"       // ユーザーによりキャンセル
  | "failed"         // エラーにより失敗
  | "unknown"        // 不明な状態

シンプルな非同期処理呼び出しの場合
submitted → working → completed と呼び出された側のAgentは状態遷移します。
なおA2Aは非同期呼び出しをデフォルトとしているようです。つまり呼び出した側が呼び出された側をポーリングします。

さっそくやってみる

1. 環境構築

以下2つのファイルを作成します。

agentA.js
// agentA.js - A2A Client (Planner Agent)
// ========================================================
// A2Aプロトコルに準拠したAgent A(タスク依頼側)
// ========================================================

import fetch from 'node-fetch';

const AGENT_B_URL = 'http://localhost:3001';

// ========================================================
// Agent Card の取得
// ========================================================
async function getAgentCard() {
  try {
    const response = await fetch(`${AGENT_B_URL}/.well-known/agent.json`);
    const card = await response.json();
    console.log('📄 Agent B のカード情報を取得:', card.name);
    return card;
  } catch (error) {
    console.error('❌ Agent Card の取得に失敗:', error.message);
    throw error;
  }
}

// ========================================================
// タスクの作成
// ========================================================
async function createTask(instruction) {
  try {
    const response = await fetch(`${AGENT_B_URL}/tasks`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: "2.0",
        method: "tasks.create",
        params: { instruction }
      })
    });
    
    const data = await response.json();
    console.log('✓ Agent A: タスクを作成しました [' + data.result.task_id + ']');
    return data.result.task_id;
  } catch (error) {
    console.error('❌ タスク作成に失敗:', error.message);
    throw error;
  }
}

// ========================================================
// タスク状態のポーリング
// ========================================================
async function pollTaskStatus(taskId, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const response = await fetch(`${AGENT_B_URL}/tasks/${taskId}`);
      const data = await response.json();
      
      const status = data.result.status;
      console.log(`⏳ Agent A: タスク状態確認 [${i + 1}/${maxAttempts}] - ${status}`);
      
      if (status === 'completed') {
        console.log('✓ Agent A: タスク完了を確認');
        return data.result;
      }
      
      if (status === 'failed') {
        throw new Error('タスクが失敗しました');
      }
      
      // 1秒待機
      await new Promise(resolve => setTimeout(resolve, 1000));
      
    } catch (error) {
      console.error('❌ タスク状態取得に失敗:', error.message);
      throw error;
    }
  }
  
  throw new Error('タスク完了のタイムアウト');
}

// ========================================================
// メイン処理
// ========================================================
async function main() {
  console.log('=== A2A プロトコル デモ開始 ===\n');
  
  try {
    // 1. Agent B のカード情報を取得
    const agentCard = await getAgentCard();
    console.log(`   利用可能なスキル: ${agentCard.skills.map(s => s.name).join(', ')}\n`);
    
    // 2. Agent B にタスクを依頼
    console.log('▶ Agent A: 「固定値を教えてください」というタスクを Agent B に依頼します\n');
    const taskId = await createTask('固定値を教えてください');
    
    // 3. タスクの完了を待つ
    console.log('');
    const result = await pollTaskStatus(taskId);
    
    // 4. 結果を表示
    console.log('\n=== 最終結果 (Agent B → Agent A) ===');
    console.log('タスクID:', result.task_id);
    console.log('ステータス:', result.status);
    console.log('結果:', JSON.stringify(result.result, null, 2));
    
  } catch (error) {
    console.error('\n❌ エラーが発生しました:', error.message);
    process.exit(1);
  }
}

// 実行
main();
agentB.js
// agentB.js - A2A Server (Worker Agent)
// ========================================================
// A2Aプロトコルに準拠したAgent B(タスク実行側)
// ========================================================

import express from 'express';
import { v4 as uuidv4 } from 'uuid';

const app = express();
app.use(express.json());

const PORT = 3001;

// タスクの状態を保存(メモリ内)
const tasks = new Map();

// ========================================================
// Agent Card - エージェントのメタデータ
// ========================================================
const agentCard = {
  name: "WorkerAgent",
  version: "1.0.0",
  description: "固定値を返すシンプルなWorkerエージェント",
  capabilities: ["text"],
  skills: [
    {
      name: "getFixedValue",
      description: "固定値を返すスキル"
    }
  ],
  endpoint: `http://localhost:${PORT}`
};

// ========================================================
// GET /.well-known/agent.json - エージェントカードの公開
// ========================================================
app.get('/.well-known/agent.json', (req, res) => {
  res.json(agentCard);
});

// ========================================================
// POST /tasks - タスクの作成
// ========================================================
app.post('/tasks', (req, res) => {
  const { instruction } = req.body;
  
  const taskId = uuidv4();
  const task = {
    id: taskId,
    status: "submitted",
    instruction,
    createdAt: new Date().toISOString(),
    result: null
  };
  
  tasks.set(taskId, task);
  
  console.log(`✓ Agent B: タスク受信 [${taskId}]`, instruction);
  
  // タスクを即座に処理(非同期シミュレーション)
  setTimeout(() => {
    processTask(taskId);
  }, 500);
  
  res.status(201).json({
    jsonrpc: "2.0",
    id: taskId,
    result: {
      task_id: taskId,
      status: "submitted"
    }
  });
});

// ========================================================
// GET /tasks/:taskId - タスク状態の取得
// ========================================================
app.get('/tasks/:taskId', (req, res) => {
  const { taskId } = req.params;
  const task = tasks.get(taskId);
  
  if (!task) {
    return res.status(404).json({
      jsonrpc: "2.0",
      error: {
        code: -32001,
        message: "Task not found"
      }
    });
  }
  
  res.json({
    jsonrpc: "2.0",
    id: taskId,
    result: {
      task_id: task.id,
      status: task.status,
      result: task.result
    }
  });
});

// ========================================================
// タスク処理(ダミー処理)
// ========================================================
function processTask(taskId) {
  const task = tasks.get(taskId);
  if (!task) return;
  
  // 状態を "working" に更新
  task.status = "working";
  console.log(`⚙ Agent B: タスク処理中 [${taskId}]`);
  
  // 固定値を返す(ダミー)
  setTimeout(() => {
    task.status = "completed";
    task.result = {
      message: "これはAgent Bが返す固定値です。",
      value: 42,
      timestamp: new Date().toISOString()
    };
    
    console.log(`✓ Agent B: タスク完了 [${taskId}]`, task.result);
  }, 1000);
}

// ========================================================
// サーバー起動
// ========================================================
app.listen(PORT, () => {
  console.log(`🚀 Agent B (Worker) が http://localhost:${PORT} で起動しました`);
  console.log(`📄 Agent Card: http://localhost:${PORT}/.well-known/agent.json`);
});

合わせてpackage.jsonを作成してnpm installを実行すれば環境構築は完了です。

package.json
{
  "name": "a2a-simple-demo",
  "version": "1.0.0",
  "description": "A2Aプロトコルに準拠したシンプルな2エージェント連携デモ",
  "type": "module",
  "scripts": {
    "agent-b": "node agentB.js",
    "agent-a": "node agentA.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "node-fetch": "^3.3.2",
    "uuid": "^9.0.1"
  }
}

2. 実行

まずはAgentBを起動します。

npm run agent-b

> a2a-simple-demo@1.0.0 agent-b
> node agentB.js

🚀 Agent B (Worker) が http://localhost:3001 で起動しました
📄 Agent Card: http://localhost:3001/.well-known/agent.json

次に別のターミナルでAgentAを起動します。

npm run agent-a

> a2a-simple-demo@1.0.0 agent-a
> node agentA.js

=== A2A プロトコル デモ開始 ===

📄 Agent B のカード情報を取得: WorkerAgent
   利用可能なスキル: getFixedValue

▶ Agent A: 「固定値を教えてください」というタスクを Agent B に依頼します

✓ Agent A: タスクを作成しました [cc05b21b-d5a5-47bf-8ffe-a76b6b452ad9]

⏳ Agent A: タスク状態確認 [1/10] - submitted
⏳ Agent A: タスク状態確認 [2/10] - working
⏳ Agent A: タスク状態確認 [3/10] - completed
✓ Agent A: タスク完了を確認

=== 最終結果 (Agent B → Agent A) ===
タスクID: cc05b21b-d5a5-47bf-8ffe-a76b6b452ad9
ステータス: completed
結果: {
  "message": "これはAgent Bが返す固定値です。",
  "value": 42,
  "timestamp": "2025-11-19T05:54:21.204Z"
}

AgentAは無事AgentBのタスクカードを入手し、AgentBに依頼できる処理を理解しました。次のステップでAgentAはタスクをAgentBに依頼し、実行結果をポーリングしながら入手し処理を完了させています。

次回予告

次回の記事ではちゃんとLLMを使います。本来AgentAがAgentBをよびすのは決め打ちで指定するのではなく、LLMが複数あるAgentからAgentBを選ぶのが正しい形です。そちらを実装していきます。

さくらインターネット株式会社

Discussion