LiteLLM × LangGraphでGPTとClaudeを混在させたA2Aエージェント連携を構築してみる
はじめに
データ事業本部のkobayashiです。
LiteLLM × LangGraph シリーズの最終回です。今回はエージェント協調の総仕上げトピックである A2A (Agent-to-Agent) プロトコル を、公式 Python SDK a2a-sdk を使って LiteLLM × LangGraph で実装します。
A2A は Google が 2025 年に提唱した「エージェント同士が自律的に対話・協調する」ためのオープンプロトコルで、HTTP + JSON-RPC(message/send) をベースに動きます。MCP が「LLM ↔ ツール」の標準化なら、A2A は「エージェント ↔ エージェント」の標準化と言えます。本記事では LangChain の ChatLiteLLM を a2a-sdk の AgentExecutor で包み、別の LangGraph エージェントから JSON-RPC で呼び出す構成を組みます。
A2A の主要概念
| 概念 | 役割 |
|---|---|
| Agent Card | エージェントの自己紹介。/.well-known/agent-card.json で公開(camelCase) |
| Skill | エージェントが提供できる能力(翻訳・要約・コード生成など)。Card 内で宣言 |
| Message / Task | クライアントが message/send で送る依頼と、サーバー側で生成・進行する Task |
| Streaming / Push | タスクの進捗を SSE / push で受信可能(message/stream) |
これにより、組織や会社をまたいだエージェント連携が可能になります。
全体構成
オーケストレータ側は openai/gpt-5-mini、翻訳エージェント側は anthropic/claude-sonnet-4-6 を採用し、プロバイダー混在のエージェント協調 を A2A プロトコル上で成立させます。
環境
Python 3.13
a2a-sdk 0.3.26
litellm 1.83.14
langgraph 1.1.10
langchain 1.0.0
langchain-litellm 0.6.4
fastapi
uvicorn
httpx
$ uv pip install "a2a-sdk>=0.3,<1.0" litellm langgraph langchain langchain-litellm fastapi uvicorn httpx
公式 SDK は v1.x 系も公開されていますが、v0.3 系の方が A2AFastAPIApplication などの高レベル API が揃っており、LangChain と組み合わせる場合の記述量が少なく済みます。本記事は v0.3.26 を前提にしています。
A2A サーバー(翻訳エージェント)
a2a-sdk では AgentExecutor を実装し、A2AFastAPIApplication で FastAPI アプリにする という流れになります。Executor の execute() がリクエストごとに呼ばれるので、その中で LangChain の ChatLiteLLM.ainvoke() を呼ぶだけです。
"""A2A プロトコル準拠の翻訳エージェントサーバー。
公式 `a2a-sdk` の AgentExecutor で LangChain (ChatLiteLLM) を包み、
JSON-RPC `message/send` を `A2AFastAPIApplication` が処理する。
Agent Card は `/.well-known/agent-card.json` で公開される。
起動: uvicorn translator_server:app --port 8001
"""
import litellm
import uvicorn
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.apps.jsonrpc import A2AFastAPIApplication
from a2a.server.events import EventQueue
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
from a2a.types import (
AgentCapabilities,
AgentCard,
AgentSkill,
Part,
TextPart,
)
from langchain_litellm import ChatLiteLLM
litellm.modify_params = True
class TranslatorAgentExecutor(AgentExecutor):
"""LangChain (ChatLiteLLM) を A2A AgentExecutor で包む。
`execute()` が JSON-RPC `message/send` リクエストごとに呼ばれる。
成果物(翻訳結果)は TaskUpdater で artifact として登録する。
"""
def __init__(self) -> None:
# 翻訳品質を担保するため Claude Sonnet を採用
self._llm = ChatLiteLLM(model="anthropic/claude-sonnet-4-6")
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
user_input = context.get_user_input()
# クライアントが Message.metadata で指定した skill_id を取り出す
# 翻訳方向は LLM が入力言語から自動判定し、skill_id ではトーンを切り替える
skill_id = (context.message.metadata or {}).get("skill_id", "translate_natural")
updater = TaskUpdater(event_queue, context.task_id, context.context_id)
await updater.start_work()
if skill_id == "translate_natural":
prompt = (
"次の文を、日常会話やSNSで自然に通じる日本語/英語に翻訳してください。"
"口語表現・短い文・親しみやすい言い回しを優先します。"
"翻訳方向は入力言語から自動判定し、翻訳のみ出力してください。\n\n"
f"{user_input}"
)
elif skill_id == "translate_business":
prompt = (
"次の文を、ビジネスメールや社外文書としてそのまま使える"
"フォーマルな日本語/英語に翻訳してください。"
"敬語・丁寧表現・定型句を適切に使い、口語表現は避けます。"
"翻訳方向は入力言語から自動判定し、翻訳のみ出力してください。\n\n"
f"{user_input}"
)
else:
await updater.add_artifact(
parts=[Part(root=TextPart(text=f"未対応の skill_id: {skill_id}"))]
)
await updater.complete()
return
response = await self._llm.ainvoke(prompt)
await updater.add_artifact(
parts=[Part(root=TextPart(text=str(response.content)))]
)
await updater.complete()
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
updater = TaskUpdater(event_queue, context.task_id, context.context_id)
await updater.cancel()
# Agent Card: /.well-known/agent-card.json で公開される自己紹介データ
agent_card = AgentCard(
name="translator-ja-en",
description="日本語 ↔ 英語の双方向翻訳エージェント(Claude Sonnet 4.6)",
url="http://localhost:8001/",
version="1.0.0",
capabilities=AgentCapabilities(streaming=True, push_notifications=False),
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[
AgentSkill(
id="translate_natural",
name="自然訳(カジュアル)",
description="日本語 ↔ 英語の双方向翻訳。日常会話・SNS で自然に通じる口語スタイル。",
tags=["translation", "casual", "natural"],
),
AgentSkill(
id="translate_business",
name="ビジネス文体訳",
description="日本語 ↔ 英語の双方向翻訳。ビジネスメール・社外文書向けのフォーマルスタイル。",
tags=["translation", "business", "formal"],
),
],
)
task_store = InMemoryTaskStore()
executor = TranslatorAgentExecutor()
request_handler = DefaultRequestHandler(
agent_executor=executor,
task_store=task_store,
)
# A2AFastAPIApplication が JSON-RPC ルートと Agent Card 公開を組み立てる
app_builder = A2AFastAPIApplication(
agent_card=agent_card,
http_handler=request_handler,
)
app = app_builder.build()
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8001)
ポイント:
AgentExecutor で LangChain を包む
execute() 内では context.get_user_input() でユーザー入力を取り、LangChain ロジックで処理して updater.add_artifact() で結果を登録します。本サンプルでは LLM 呼び出しは単発ですが、ここを create_agent(...) の LangGraph エージェントに置き換えれば、ツール呼び出しを伴うエージェントを A2A 経由で公開できます。
skill_id でリクエストを分岐する
context.message.metadata には、クライアントが Message.metadata={"skill_id": "..."} で送ってきた値が入っています。本実装では translate_natural(カジュアル/口語)と translate_business(フォーマル/ビジネス)の2 skill それぞれに別プロンプトを割り当て、翻訳方向は LLM が自動判定 / トーンは skill_id で明示 という役割分担にしています。これにより、同じ翻訳エージェントでも依頼元のユースケース(チャット用 / メール用)に応じて skill を切り替えるだけで、出力の文体を制御できます。「Card に skill を載せただけで実装は自動判定」だと skill 宣言が形骸化するため、このようにメタデータで skill_id を渡してサーバー側で switch する形が A2A 仕様らしい使い方です。未対応 skill_id を受け取ったときの分岐を else 句で持たせている点も、本番サーバーでは必須です。
TaskUpdater で Task ライフサイクルを進める
A2A の Task は submitted → working → completed / failed / canceled という状態遷移を持ち、クライアントは tasks/get や message/stream でこの状態を観測します。TaskUpdater はこの 状態遷移とイベント発行をワンセットで処理する SDK ヘルパー で、AgentExecutor.execute() に渡される event_queue と紐付いて動きます。
主に使うメソッドは以下の4つです。
| メソッド | 役割 |
|---|---|
start_work() |
Task を working に遷移させ、状態変更イベントを発行 |
add_artifact(parts=...) |
エージェントの成果物(翻訳結果・生成テキスト・JSON など)を Task に紐付けて配信 |
complete() |
Task を completed に遷移させて終了 |
update_status(state, message=...) |
任意の中間ステータス(例: input_required)+ メッセージを発行。長時間タスクの進捗通知などに使う |
本サンプルは start_work() → add_artifact() → complete() の3ステップで Task を完結させていますが、長時間処理では合間に update_status() で進捗を流したり、ユーザー追加入力が必要なら input_required 状態に遷移させてクライアントに追加質問を促す、といった使い分けができます。エラー時は cancel()(canceled へ遷移)を呼ぶか、例外を投げると SDK 側で failed 状態に遷移します。
AgentCard は SDK の Pydantic モデル
a2a.types.AgentCard が camelCase シリアライゼーションを自動で行うため、Python では default_input_modes(snake_case)で書きつつ、JSON 出力では defaultInputModes になります。AgentCapabilities / AgentSkill も同様です。
A2AFastAPIApplication
JSON-RPC ルート(POST /)と Agent Card 公開ルート(GET /.well-known/agent-card.json)を1度に組み立ててくれます。中身の DefaultRequestHandler が Task ライフサイクル(pending → working → completed)と Task ストア(InMemoryTaskStore)を管理してくれるので、自前で実装する必要はありません。
起動 + Agent Card 確認:
$ export ANTHROPIC_API_KEY="sk-ant-..."
$ uvicorn translator_server:app --port 8001
INFO: Started server process [...]
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8001
# 別ターミナルで Agent Card を取得
$ curl -s http://localhost:8001/.well-known/agent-card.json | jq
{
"capabilities": {
"pushNotifications": false,
"streaming": true
},
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"text/plain"
],
"description": "日本語 ↔ 英語の双方向翻訳エージェント(Claude Sonnet 4.6)",
"name": "translator-ja-en",
"preferredTransport": "JSONRPC",
"protocolVersion": "0.3.0",
"skills": [
{
"description": "日本語 ↔ 英語の双方向翻訳。日常会話・SNS で自然に通じる口語スタイル。",
"id": "translate_natural",
"name": "自然訳(カジュアル)",
"tags": [
"translation",
"casual",
"natural"
]
},
{
"description": "日本語 ↔ 英語の双方向翻訳。ビジネスメール・社外文書向けのフォーマルスタイル。",
"id": "translate_business",
"name": "ビジネス文体訳",
"tags": [
"translation",
"business",
"formal"
]
}
],
"url": "http://localhost:8001/",
"version": "1.0.0"
}
preferredTransport: "JSONRPC" と protocolVersion: "0.3.0" が SDK 側で自動的に付与され、フィールドは仕様通りの camelCase で配信されます。
オーケストレータエージェント(A2A クライアント)
別の LangGraph エージェントから、上記の翻訳エージェントを ツール経由で呼び出す 形で連携します。a2a-sdk には Card 取得用の A2ACardResolver と、メッセージ送信用の ClientFactory.connect() が用意されているので、自前で JSON-RPC エンベロープを組む必要はありません。
"""A2A クライアント: LangGraph オーケストレータが A2A サーバーをツール経由で呼ぶ。
`A2ACardResolver` で Agent Card を取得(`/.well-known/agent-card.json`)し、
`ClientFactory.connect()` で JSON-RPC `message/send` で対話する。
"""
import asyncio
import uuid
import httpx
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
from a2a.types import Message, Part, Role, TaskState, TextPart
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_litellm import ChatLiteLLM
TRANSLATOR_URL = "http://localhost:8001"
async def discover_agent(base_url: str):
"""Agent Card を取得して能力を確認する。"""
async with httpx.AsyncClient() as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=base_url)
return await resolver.get_agent_card()
async def call_translator(skill_id: str, text: str) -> str:
"""A2A サーバーへ JSON-RPC で message/send し、結果を返す。
Card で宣言された skill_id を Message.metadata で指定することで、
サーバー側の TranslatorAgentExecutor が方向別に分岐できるようにする。
"""
config = ClientConfig(streaming=False, accepted_output_modes=["text/plain"])
async with await ClientFactory.connect(TRANSLATOR_URL, client_config=config) as client:
message = Message(
message_id=uuid.uuid4().hex,
role=Role.user,
parts=[Part(root=TextPart(text=text))],
metadata={"skill_id": skill_id},
)
async for event in client.send_message(message):
if isinstance(event, tuple):
task, _ = event
if task.status.state == TaskState.completed and task.artifacts:
for artifact in task.artifacts:
for part in artifact.parts:
if hasattr(part.root, "text"):
return part.root.text
else:
for part in event.parts:
if hasattr(part.root, "text"):
return part.root.text
return "(応答なし)"
@tool
async def translate_natural(text: str) -> str:
"""日本語 ↔ 英語の双方向翻訳(カジュアル/自然な口語スタイル)。
日常会話・SNS など親しみやすい文体が必要なときに使う。
A2A skill: translate_natural"""
return await call_translator("translate_natural", text)
@tool
async def translate_business(text: str) -> str:
"""日本語 ↔ 英語の双方向翻訳(ビジネスメール・社外文書向けのフォーマル文体)。
敬語・丁寧表現・定型句を伴う公式な文章が必要なときに使う。
A2A skill: translate_business"""
return await call_translator("translate_business", text)
async def main() -> None:
# まず Agent Card を取得して能力を確認
card = await discover_agent(TRANSLATOR_URL)
print(f"接続先エージェント: {card.name}")
print(f" 説明: {card.description}")
print(f" スキル: {[s.name for s in card.skills]}\n")
# オーケストレータ自身は安価な GPT、翻訳は Claude エージェントに委譲する構成
llm = ChatLiteLLM(model="openai/gpt-5-mini")
agent = create_agent(
model=llm,
tools=[translate_natural, translate_business],
system_prompt=(
"翻訳が必要なときは外部エージェントのツールを使ってください。"
"ユーザーの依頼から **カジュアル(日常会話・SNS)か、ビジネス(メール・社外文書)か** を読み取り、"
"translate_natural / translate_business のいずれかを選んでください。"
"自分で翻訳するのではなく、必ずツール呼び出しで処理してください。"
),
)
# ビジネスメール向けの翻訳依頼
result = await agent.ainvoke(
{
"messages": [
{
"role": "user",
"content": (
"次の日本語をビジネスメールとして送れる英語にして:"
"「打ち合わせの件、添付資料をご確認のうえフィードバックをお願いします」"
),
}
]
}
)
print("[business] " + result["messages"][-1].content)
# カジュアルな翻訳依頼
result = await agent.ainvoke(
{
"messages": [
{
"role": "user",
"content": "「明日ヒマだったらランチしようよ」を友達に送るくらいのカジュアルな英語にして",
}
]
}
)
print("[natural ] " + result["messages"][-1].content)
if __name__ == "__main__":
asyncio.run(main())
ポイント:
A2ACardResolver でディスカバリ
/.well-known/agent-card.json を取得して AgentCard Pydantic オブジェクトとして返してくれます。card.skills で利用可能なスキル一覧をプログラマブルに確認できます。
ClientFactory.connect() + send_message()
URL を渡すと内部で Card を取得して適切なトランスポート(JSON-RPC / REST / gRPC)を選び、send_message() で JSON-RPC message/send を投げてくれます。レスポンスは (Task, update) または Message の async iterator で返ります。本記事のように streaming=False を指定すると Task が completed になるまで待ってから artifact を取り出せます。Message の message_id は 必須フィールド なので、uuid.uuid4().hex などで明示的に渡しています。
A2A タスクをツール化
@tool で包んだ関数の中で call_translator(skill_id, text) を呼ぶことで、オーケストレータの LangGraph エージェントは A2A エージェントを 通常のツールと同じ感覚で利用 できます。translate_natural / translate_business の各ツールは 対応する skill_id を Message.metadata で送ることでサーバー側の switch 分岐を駆動 しているため、「Card に宣言された skill が、クライアントから明示的に指定され、サーバーで処理分岐される」という A2A 仕様らしい流れがそのまま完結します。tool の docstring に どのケースで使うべきか を書いておくことで、上位の GPT が依頼の文脈(カジュアル/ビジネス)から適切な skill を自動選択してくれます。エージェントロジック側には Anthropic 固有のコードも JSON-RPC のエンベロープも一切現れません。
実行結果は以下のようになります。
# 別ターミナルで A2A サーバーを起動済みとする
$ export OPENAI_API_KEY="sk-..."
$ python orchestrator.py
接続先エージェント: translator-ja-en
説明: 日本語 ↔ 英語の双方向翻訳エージェント(Claude Sonnet 4.6)
スキル: ['自然訳(カジュアル)', 'ビジネス文体訳']
[business] Regarding the matter of our upcoming meeting, we kindly request that you review the attached materials and provide us with your feedback at your earliest convenience.
[natural ] If you're free tomorrow, let's grab lunch!
オーケストレータが Card で能力を確認したあと、ユーザー依頼の文脈から 「ビジネスメールに送れる英語」→ translate_business 、「友達に送るカジュアル」→ translate_natural とツールを使い分けています。それぞれの A2A 呼び出しは内部で JSON-RPC message/send + metadata={"skill_id": ...} として送信され、サーバー側の TranslatorAgentExecutor が skill_id ごとのプロンプトに分岐して Claude に処理させ、artifact として返却します。
出力を見比べると差別化が明確で、ビジネス側は "kindly request" / "at your earliest convenience" のような フォーマルな定型句 が現れる一方、カジュアル側は "let's grab lunch!" のような 口語表現と感嘆符 で軽い文体になっています。「同じ翻訳エージェントでも、Card で宣言した skill 軸でトーンを切り替えられる」というのが本構成のポイントです。
認証
ここまでの実装は誰でも呼び出せる無認証サーバーでした。本番では (1) Card で要求方式を宣言 → (2) サーバーで検証 → (3) クライアントで送信 の3段で認証を組み込みます。a2a-sdk には認証用の型・インターセプターが揃っており、API キーや Bearer トークンを最小限のコードで導入できます。
Agent Card に認証スキームを宣言
AgentCard.security_schemes と AgentCard.security を埋めると、/.well-known/agent-card.json に securitySchemes / security が camelCase で配信されます。クライアントは Card を見るだけで「どの方式で認証すればよいか」を判断できます。
本記事では API キー認証を例に使います。Card は次のように security_schemes と security を埋めるだけで OK です(Bearer / OAuth2 を使う場合も同じ枠組みで HTTPAuthSecurityScheme / OAuth2SecurityScheme を入れ替えるだけで対応できます)。
from a2a.types import APIKeySecurityScheme, SecurityScheme
agent_card = AgentCard(
# ...既存のフィールドはそのまま...
security_schemes={
"api_key": SecurityScheme(
root=APIKeySecurityScheme(name="X-API-Key", in_="header")
),
},
# api_key を必須要件として宣言
security=[{"api_key": []}],
)
サーバー側で検証(FastAPI ミドルウェア)
A2AFastAPIApplication.build() は そのまま FastAPI インスタンスを返す ので、FastAPI 標準のミドルウェアで認証を挟み込むのが最も簡潔です。
import os
from fastapi import HTTPException, Request
API_KEY = os.environ["A2A_API_KEY"]
app = app_builder.build()
@app.middleware("http")
async def api_key_middleware(request: Request, call_next):
# Card 取得は無認証にする運用が一般的(クライアントが Card を見て認証方式を決めるため)
if request.url.path.startswith("/.well-known/"):
return await call_next(request)
if request.headers.get("X-API-Key") != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
return await call_next(request)
より A2A に従って書くなら CallContextBuilder を実装し、ServerCallContext.user に認証済みユーザーを載せる形にできます。AgentExecutor.execute(context, ...) の中で context.call_context.user を参照できるので、ユーザー単位の権限チェックやレート制限を Executor に組み込めます。下の例ではマルチテナントを想定し、API キー → ユーザー ID のマッピング を持たせて、キーごとに別ユーザーとして識別します。
from a2a.auth.user import User
from a2a.server.apps.jsonrpc.jsonrpc_app import CallContextBuilder
from a2a.server.context import ServerCallContext
# 本番ではこのマッピングを DB / シークレットマネージャーから取得する
API_KEYS: dict[str, str] = {
"key-team-alpha": "team-alpha",
"key-team-beta": "team-beta",
}
class APIKeyUser(User):
"""API キー認証済みユーザーの最小実装。"""
def __init__(self, user_id: str) -> None:
self._user_id = user_id
@property
def is_authenticated(self) -> bool:
return True
@property
def user_name(self) -> str:
return self._user_id
class APIKeyContextBuilder(CallContextBuilder):
def build(self, request) -> ServerCallContext:
key = request.headers.get("X-API-Key", "")
user_id = API_KEYS.get(key)
if user_id is None:
raise HTTPException(status_code=401, detail="Invalid API key")
return ServerCallContext(
user=APIKeyUser(user_id=user_id),
state={"headers": dict(request.headers)},
)
app_builder = A2AFastAPIApplication(
agent_card=agent_card,
http_handler=request_handler,
context_builder=APIKeyContextBuilder(), # ← ここで挿す
)
AgentExecutor.execute() 側では context.call_context.user で APIKeyUser インスタンスを取り出せ、user.user_name(team-alpha / team-beta など)で どのテナントからのリクエストか を識別できます。
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
user = context.call_context.user if context.call_context else None
if user is None or not user.is_authenticated:
# 通常は ContextBuilder で弾けているが、念のため
...
# 例: ユーザーごとのレート制限・課金カウント・監査ログ
audit_log.info("translate request from user=%s", user.user_name)
...
単一キー運用(API_KEY 1個)でユーザー識別が不要なら、最初の FastAPI ミドルウェア例で十分です。ユーザー単位の課金やレート制限が必要になった時点で CallContextBuilder 方式に移行する という段階的な選択ができます。
クライアント側で資格情報を送る
a2a-sdk の AuthInterceptor が、Card の security_schemes を読んで 適切なヘッダーを自動付与 してくれます。InMemoryContextCredentialStore に資格情報を登録し、ClientConfig.interceptors に渡すだけです。
import os
from a2a.client import ClientConfig, ClientFactory
from a2a.client.auth import AuthInterceptor, InMemoryContextCredentialStore
store = InMemoryContextCredentialStore()
await store.set_credential(
session_id="default",
security_scheme_name="api_key",
credential=os.environ["A2A_API_KEY"],
)
config = ClientConfig(
streaming=False,
accepted_output_modes=["text/plain"],
interceptors=[AuthInterceptor(credential_service=store)],
)
client = await ClientFactory.connect(TRANSLATOR_URL, client_config=config)
これで Card の api_key スキームが宣言されていれば X-API-Key ヘッダーに、bearer が宣言されていれば Authorization: Bearer ... に、自動でクレデンシャルが配置されます。クライアントロジックには認証ヘッダーの組み立てが一切現れない点がポイントです。
運用パターン
- 小規模・社内: API キーを環境変数(または AWS Secrets Manager / Google Secret Manager)で管理し、FastAPI ミドルウェアで検証する最小構成。
- 対外サービス:
OAuth2SecuritySchemeを Card で宣言し、IdP(Auth0 / Cognito / Keycloak など)と連携して JWT 検証。AuthInterceptorがトークンの自動更新・付与を担当。 - 本番: A2A サーバーを API Gateway / Cloudflare Access の背後 に置き、ネットワーク層で第一段階の認証、A2A 側で第二段階の認可、という二段構えにすると堅牢です。
LiteLLMでのモデル使い分け
| エージェント | モデル | 役割 |
|---|---|---|
| Orchestrator(A2Aクライアント側) | openai/gpt-5-mini |
ツール呼出の指揮役、コスト重視 |
| Translator(A2Aサーバー側) | anthropic/claude-sonnet-4-6 |
翻訳実務担当、品質を担保 |
GPT が指揮し、Claude が実務を担う 異プロバイダー協調が、A2A プロトコル越しに成立しています。Orchestrator 側のロジックには Anthropic 固有のコードは一切含まれず(HTTP 越しにブラックボックス化されている)、LiteLLM × A2A の組み合わせは「マルチプロバイダー × マルチエージェント」という現代の LLM システム設計の理想形に近い構造を実現できます。Translator 側の Executor を create_agent の LangGraph エージェントに置き換えれば、ツール利用や RAG を伴うエージェントもそのまま A2A で公開できます。
A2A vs MCP の使い分け
| 観点 | MCP | A2A |
|---|---|---|
| 標準化対象 | LLM ↔ ツール | エージェント ↔ エージェント |
| 通信粒度 | ツール呼び出し(細粒度) | タスク委譲(粗粒度) |
| 状態の主担当 | クライアント側 | サーバー(エージェント)側 |
| ストリーミング | レスポンス単位 | タスク進捗の段階的通知(message/stream) |
| 適したシナリオ | 1エージェント内のツール拡充 | 複数エージェントの協調・分業 |
両者は補完関係です。社内の各チームが自前のエージェントを A2A で公開し、各エージェントが内部で MCP サーバーを呼ぶ、という構成が現実的です。
まとめ
Google が提唱する A2A (Agent-to-Agent) プロトコル を、公式 SDK a2a-sdk の AgentExecutor + A2AFastAPIApplication で実装し、A2ACardResolver + ClientFactory で別エージェントから JSON-RPC message/send 経由で呼び出す構成を組みました。
Agent Card は /.well-known/agent-card.json で camelCase 配信され、Task ライフサイクル管理は SDK の DefaultRequestHandler + InMemoryTaskStore に任せることで、開発者は AgentExecutor.execute() の中で LangChain ロジックを書くだけ で仕様準拠の A2A エージェントを公開できます。本記事では openai/gpt-5-mini(オーケストレータ)と anthropic/claude-sonnet-4-6(翻訳エージェント)を混在させましたが、A2A レイヤーが HTTP プロトコルである以上、エージェントロジックには プロバイダー固有のコードが一切現れない のがポイントです。
最後まで読んでいただきありがとうございました。





