🐈
A2A を基礎から学ぶ (2) さくらのAI Engine / gpt-oss-120bでAgent間の通信を見る
この記事では前回に続いてA2Aを学んでいきます。
前回のおさらい
前回ではLLMを使わないシンプルなAgentを2つ作成し、Agentがお互いのAgentを知る仕組みを見ていきました。
今日はAgentがLLMと対話を行った後その結果をどのように別のAgentに引き渡しているのかを見ていきます。
さっそくやってみる
1. 基本準備
まずは前回の手順を終わらせておきます。
2. Agentスクリプトの入れ替え
agentA.js,agentB.jsを以下に入れ変えます。
agentA.js
// agentA-sakura.js - A2A Client (Planner Agent) for Sakura AI
// ========================================================
// さくらAI Engineを使うAgent Bに質問を投げるClient Agent
// ========================================================
import fetch from 'node-fetch';
const AGENT_B_URL = 'http://localhost:3001';
// ========================================================
// ユーティリティ: JSON整形表示
// ========================================================
function displayJson(label, data) {
console.log(`\n${label}`);
console.log('─'.repeat(60));
console.log(JSON.stringify(data, null, 2));
console.log('─'.repeat(60));
}
// ========================================================
// Agent Card の取得
// ========================================================
async function getAgentCard() {
try {
const response = await fetch(`${AGENT_B_URL}/.well-known/agent.json`);
const card = await response.json();
console.log('\n📄 Agent B のカード情報を取得:');
console.log(` 名前: ${card.name}`);
console.log(` 説明: ${card.description}`);
console.log(` プロバイダー: ${card.provider}`);
console.log(` モデル: ${card.model}`);
console.log(` 利用可能なスキル:`);
card.skills.forEach(skill => {
console.log(` - ${skill.name}: ${skill.description}`);
});
return card;
} catch (error) {
console.error('❌ Agent Card の取得に失敗:', error.message);
throw error;
}
}
// ========================================================
// タスクの作成
// ========================================================
async function createTask(instruction) {
try {
// リクエストボディを準備
const requestBody = {
jsonrpc: "2.0",
method: "tasks.create",
params: { instruction }
};
// 📤 リクエスト内容を表示
displayJson('📤 Agent B へのリクエスト:', requestBody);
const response = await fetch(`${AGENT_B_URL}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();
// 📥 レスポンス内容を表示
displayJson('📥 Agent B からのレスポンス:', data);
console.log('\n✓ Agent A: タスクを作成しました');
console.log(` タスクID: ${data.result.task_id}`);
return data.result.task_id;
} catch (error) {
console.error('❌ タスク作成に失敗:', error.message);
throw error;
}
}
// ========================================================
// タスク状態のポーリング
// ========================================================
async function pollTaskStatus(taskId, maxAttempts = 30) {
console.log('\n⏳ タスクの完了を待機中...\n');
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;
const statusEmoji = {
'submitted': '📋',
'working': '⚙️',
'completed': '✅',
'failed': '❌'
};
console.log(` ${statusEmoji[status] || '❓'} [${i + 1}/${maxAttempts}] 状態: ${status}`);
if (status === 'completed') {
console.log('\n✓ Agent A: タスク完了を確認');
// 📥 完了時の最終レスポンスを表示
displayJson('📥 Agent B からの最終レスポンス(completed):', data);
return data.result;
}
if (status === 'failed') {
throw new Error('タスクが失敗しました: ' + (data.result.result?.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('\n' + '='.repeat(60));
console.log('🚀 A2A プロトコル + さくらAI Engine デモ');
console.log('='.repeat(60));
try {
// 1. Agent B のカード情報を取得
const agentCard = await getAgentCard();
// 2. 質問を準備
const questions = [
"A2Aプロトコルとは何ですか?簡潔に説明してください。",
// 必要に応じて質問を追加
// "さくらのAI Engineの特徴は何ですか?",
// "JavaScriptとTypeScriptの違いを教えてください。"
];
// 3. 各質問をAgent Bに投げる
for (const question of questions) {
console.log('\n' + '-'.repeat(60));
console.log(`📝 質問: "${question}"`);
console.log('-'.repeat(60));
// タスク作成
const taskId = await createTask(question);
// 完了を待つ
const result = await pollTaskStatus(taskId);
// 結果を表示
console.log('='.repeat(60));
console.log('🎯 AI からの回答:');
console.log('='.repeat(60));
console.log(result.result.answer);
console.log('\n📊 メタ情報:');
console.log(` モデル: ${result.result.model}`);
console.log(` 使用トークン: ${JSON.stringify(result.result.usage)}`);
console.log(` タイムスタンプ: ${result.result.timestamp}`);
console.log('='.repeat(60));
}
console.log('\n✅ すべての質問が完了しました!\n');
} catch (error) {
console.error('\n❌ エラーが発生しました:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// 実行
main();
agentB.js
// agentB-sakura.js - A2A Server (Worker Agent) with Sakura AI Engine
// ========================================================
// さくらのAI Engineを使ってLLMで応答するWorkerエージェント
// ========================================================
import express from 'express';
import { v4 as uuidv4 } from 'uuid';
import fetch from 'node-fetch';
const app = express();
app.use(express.json());
const PORT = 3001;
// さくらAI Engineの設定
const SAKURA_API_URL = 'https://api.ai.sakura.ad.jp/v1/chat/completions';
const SAKURA_API_KEY = 'xxf3e60b-7c0a-4685-b5eb-148fbe845cc5:mFhWGnNfWmfvy4psmZgWKQ4EkrzEy5MQxu9vuyV5';
const SAKURA_MODEL = 'gpt-oss-120b';
// タスクの状態を保存(メモリ内)
const tasks = new Map();
// ========================================================
// Agent Card - エージェントのメタデータ
// ========================================================
const agentCard = {
name: "SakuraAIAgent",
version: "1.0.0",
description: "さくらのAI Engineを使って質問に答えるエージェント",
capabilities: ["text", "llm"],
skills: [
{
name: "answerQuestion",
description: "LLMを使って質問に答えます"
},
{
name: "generateText",
description: "指示に基づいてテキストを生成します"
}
],
endpoint: `http://localhost:${PORT}`,
provider: "Sakura AI Engine",
model: SAKURA_MODEL
};
// ========================================================
// GET /.well-known/agent.json - エージェントカードの公開
// ========================================================
app.get('/.well-known/agent.json', (req, res) => {
res.json(agentCard);
});
// ========================================================
// さくらAI Engineを呼び出す関数
// ========================================================
async function callSakuraAI(userMessage, systemPrompt = "あなたは親切なアシスタントです。") {
try {
// メッセージのバリデーション
if (!userMessage || typeof userMessage !== 'string') {
throw new Error('userMessageが不正です');
}
const messagePreview = userMessage.length > 50
? userMessage.substring(0, 50) + "..."
: userMessage;
console.log(`🤖 さくらAI Engine呼び出し: "${messagePreview}"`);
const response = await fetch(SAKURA_API_URL, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${SAKURA_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: SAKURA_MODEL,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage }
],
temperature: 0.7,
max_tokens: 500,
stream: false
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Sakura AI API error: ${response.status} - ${errorText}`);
}
const data = await response.json();
const aiResponse = data.choices[0].message.content;
const responsePreview = aiResponse && aiResponse.length > 50
? aiResponse.substring(0, 50) + "..."
: aiResponse || "(応答なし)";
console.log(`✅ AI応答取得: "${responsePreview}"`);
return {
content: aiResponse,
model: data.model,
usage: data.usage
};
} catch (error) {
console.error('❌ さくらAI Engine呼び出しエラー:', error.message);
throw error;
}
}
// ========================================================
// POST /tasks - タスクの作成
// ========================================================
app.post('/tasks', (req, res) => {
// JSON-RPC形式のparamsから取得
const { instruction } = req.body.params || req.body;
const taskId = uuidv4();
const task = {
id: taskId,
status: "submitted",
instruction,
createdAt: new Date().toISOString(),
result: null
};
tasks.set(taskId, task);
console.log(`\n📥 Agent B: タスク受信 [${taskId}]`);
console.log(` 指示: "${instruction || '(指示なし)'}"`);
// タスクを非同期で処理
setTimeout(() => {
processTask(taskId);
}, 100);
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
}
});
});
// ========================================================
// タスク処理(さくらAI Engineを使用)
// ========================================================
async function processTask(taskId) {
const task = tasks.get(taskId);
if (!task) return;
try {
// 状態を "working" に更新
task.status = "working";
console.log(`⚙️ Agent B: タスク処理中 [${taskId}]`);
// さくらAI Engineで回答を生成
const aiResult = await callSakuraAI(
task.instruction,
"あなたは親切で正確なAIアシスタントです。質問に対して簡潔で分かりやすい回答を提供してください。"
);
// 状態を "completed" に更新
task.status = "completed";
task.result = {
answer: aiResult.content,
model: aiResult.model,
usage: aiResult.usage,
timestamp: new Date().toISOString()
};
console.log(`✅ Agent B: タスク完了 [${taskId}]`);
const answerPreview = aiResult.content && aiResult.content.length > 100
? aiResult.content.substring(0, 100) + "..."
: aiResult.content || "(応答なし)";
console.log(` 回答: "${answerPreview}"`);
} catch (error) {
// エラーが発生した場合
task.status = "failed";
task.result = {
error: error.message,
timestamp: new Date().toISOString()
};
console.error(`❌ Agent B: タスク失敗 [${taskId}]`, error.message);
}
}
// ========================================================
// サーバー起動
// ========================================================
app.listen(PORT, () => {
console.log(`\n${'='.repeat(60)}`);
console.log(`🚀 Agent B (Sakura AI Worker) が起動しました`);
console.log(`${'='.repeat(60)}`);
console.log(`📍 エンドポイント: http://localhost:${PORT}`);
console.log(`📄 Agent Card: http://localhost:${PORT}/.well-known/agent.json`);
console.log(`🤖 LLMプロバイダー: Sakura AI Engine`);
console.log(`🔧 モデル: ${SAKURA_MODEL}`);
console.log(`${'='.repeat(60)}\n`);
});
前回同様に起動を行うとAgentA.js側で以下の出力が行われます。
npm run agent-a
> a2a-simple-demo@1.0.0 agent-a
> node agentA.js
============================================================
🚀 A2A プロトコル + さくらAI Engine デモ
============================================================
📄 Agent B のカード情報を取得:
名前: SakuraAIAgent
説明: さくらのAI Engineを使って質問に答えるエージェント
プロバイダー: Sakura AI Engine
モデル: gpt-oss-120b
利用可能なスキル:
- answerQuestion: LLMを使って質問に答えます
- generateText: 指示に基づいてテキストを生成します
------------------------------------------------------------
📝 質問: "A2Aプロトコルとは何ですか?簡潔に説明してください。"
------------------------------------------------------------
📤 Agent B へのリクエスト:
────────────────────────────────────────────────────────────
{
"jsonrpc": "2.0",
"method": "tasks.create",
"params": {
"instruction": "A2Aプロトコルとは何ですか?簡潔に説明してください。"
}
}
────────────────────────────────────────────────────────────
📥 Agent B からのレスポンス:
────────────────────────────────────────────────────────────
{
"jsonrpc": "2.0",
"id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"result": {
"task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"status": "submitted"
}
}
────────────────────────────────────────────────────────────
✓ Agent A: タスクを作成しました
タスクID: ed14c736-5a7b-4e9b-a4e4-047975d7b320
⏳ タスクの完了を待機中...
📋 [1/30] 状態: submitted
⚙️ [2/30] 状態: working
⚙️ [3/30] 状態: working
⚙️ [4/30] 状態: working
✅ [5/30] 状態: completed
✓ Agent A: タスク完了を確認
📥 Agent B からの最終レスポンス(completed):
────────────────────────────────────────────────────────────
{
"jsonrpc": "2.0",
"id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"result": {
"task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"status": "completed",
"result": {
"answer": "**A2A(Application‑to‑Application)プロトコル**は、異なるアプリケーション同士が直接データや機能をやり取りするための通信規約・ 方式のことです。主な特徴は以下の通りです。\n\n| 項目 | 内容 |\n|------|------|\n| **目的** | アプリケーション間で自動的に情報交換・連携を行う |\n| **利用シーン** | 企業システム統合、B2B取引、マイクロサービス間通信、IoTデバイス間連携など |\n| **代表的なプロトコル例** | - **REST/HTTP**(JSONやXMLでデータをやり取り)<br>- **SOAP**(XMLベースのWebサービス)<br>- **gRPC**(バイナリ形式で高速通信)<br>- **MQTT**(軽量メッセージング、IoT向け) |\n| **メリット** | - システム間の疎結合が実現できる<br>- 標準化されたインターフェースで拡張性が高い<br>- 自動化・リアルタイム連携 が可能 |\n| **課題** | - セキュリティ(認証・暗号化)の確保が必要<br>- 異なるデータフォーマットやバージョン管理の調整が必要 |\n\n要するに、A2A プロトコルは「アプリケーション同士が相互に通信し、機能やデータを共有するための標準的な手段」だと言えます。",
"model": "gpt-oss-120b",
"usage": {
"prompt_tokens": 124,
"total_tokens": 598,
"completion_tokens": 474,
"prompt_tokens_details": null
},
"timestamp": "2025-11-20T05:31:02.048Z"
}
}
}
────────────────────────────────────────────────────────────
============================================================
🎯 AI からの回答:
============================================================
**A2A(Application‑to‑Application)プロトコル**は、異なるアプリケーション同士が直接データや機能をやり取りするための通信規約・方式のことです。主な特徴は以下の通りです。
| 項目 | 内容 |
|------|------|
| **目的** | アプリケーション間で自動的に情報交換・連携を行う |
| **利用シーン** | 企業システム統合、B2B取引、マイクロサービス間通信、IoTデバイス間連携など |
| **代表的なプロトコル例** | - **REST/HTTP**(JSONやXMLでデータをやり取り)<br>- **SOAP**(XMLベースのWebサービス)<br>- **gRPC**(バイナリ形式で高速通信)<br>- **MQTT**(軽量メッセージング、IoT向け) |
| **メリット** | - システム間の疎結合が実現できる<br>- 標準化されたインターフェースで拡張性が高い<br>- 自動化・リアルタイム連携が可能 |
| **課題** | - セキュリティ(認証・暗号化)の確保が必要<br>- 異なるデータフォーマットやバージョン管理の調整が必要 |
要するに、A2Aプロトコルは「アプリケーション同士が相互に通信し、機能やデータを共有するための標準的な手段」だと言えます。
📊 メタ情報:
モデル: gpt-oss-120b
使用トークン: {"prompt_tokens":124,"total_tokens":598,"completion_tokens":474,"prompt_tokens_details":null}
タイムスタンプ: 2025-11-20T05:31:02.048Z
============================================================
✅ すべての質問が完了しました!
3. AgentA→AgentBのリクエストとレスポンス
Agent間の通信はJSON-RPCで行われておるため、内容はJSONです。
まずAgentAからAgentBに以下のリクエストが出されています。
📤 Agent B へのリクエスト:
────────────────────────────────────────────────────────────
{
"jsonrpc": "2.0",
"method": "tasks.create",
"params": {
"instruction": "A2Aプロトコルとは何ですか?簡潔に説明してください。"
}
}
────────────────────────────────────────────────────────────
その後AgentBから着手開始のレスポンスが戻ります。
📥 Agent B からのレスポンス:
────────────────────────────────────────────────────────────
{
"jsonrpc": "2.0",
"id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"result": {
"task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"status": "submitted"
}
}
────────────────────────────────────────────────────────────
AgentBが作業を行っている間AgentAはポーリングでそのジョブステータスを確認しています。
📋 [1/30] 状態: submitted
⚙️ [2/30] 状態: working
⚙️ [3/30] 状態: working
⚙️ [4/30] 状態: working
✅ [5/30] 状態: completed
最後にレスポンスがAgentBから戻ります。
📥 Agent B からの最終レスポンス(completed):
────────────────────────────────────────────────────────────
{
"jsonrpc": "2.0",
"id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"result": {
"task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
"status": "completed",
"result": {
"answer": "**A2A(Application‑to‑Application)プロトコル**は、異なるアプリケーション同士が直接データや機能をやり取りするための通信規約・ 方式のことです。主な特徴は以下の通りです。\n\n| 項目 | 内容 |\n|------|------|\n| **目的** | アプリケーション間で自動的に情報交換・連携を行う |\n| **利用シーン** | 企業システム統合、B2B取引、マイクロサービス間通信、IoTデバイス間連携など |\n| **代表的なプロトコル例** | - **REST/HTTP**(JSONやXMLでデータをやり取り)<br>- **SOAP**(XMLベースのWebサービス)<br>- **gRPC**(バイナリ形式で高速通信)<br>- **MQTT**(軽量メッセージング、IoT向け) |\n| **メリット** | - システム間の疎結合が実現できる<br>- 標準化されたインターフェースで拡張性が高い<br>- 自動化・リアルタイム連携 が可能 |\n| **課題** | - セキュリティ(認証・暗号化)の確保が必要<br>- 異なるデータフォーマットやバージョン管理の調整が必要 |\n\n要するに、A2A プロトコルは「アプリケーション同士が相互に通信し、機能やデータを共有するための標準的な手段」だと言えます。",
"model": "gpt-oss-120b",
"usage": {
"prompt_tokens": 124,
"total_tokens": 598,
"completion_tokens": 474,
"prompt_tokens_details": null
},
"timestamp": "2025-11-20T05:31:02.048Z"
}
}
}
────────────────────────────────────────────────────────────
次回予告
今のサンプルではAgentAがAgentBを明示的に呼び出す部分がコーディング化されています。本来複数のAgentをどう呼び出すかLLMが判断する、というのが商用環境におけるよくあるパターンですので、そのサンプルを作ってみていきたいと思います。
Discussion