LiteLLM × LangGraphでGPTとClaudeを混在させたA2Aエージェント連携を構築してみる

LiteLLM × LangGraphでGPTとClaudeを混在させたA2Aエージェント連携を構築してみる

2026.05.22

はじめに

データ事業本部の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 の ChatLiteLLMa2a-sdkAgentExecutor で包み、別の 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() を呼ぶだけです。

translator_server.py
"""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 は submittedworkingcompleted / failed / canceled という状態遷移を持ち、クライアントは tasks/getmessage/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 エンベロープを組む必要はありません。

orchestrator.py
"""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 を取り出せます。Messagemessage_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_schemesAgentCard.security を埋めると、/.well-known/agent-card.jsonsecuritySchemes / security が camelCase で配信されます。クライアントは Card を見るだけで「どの方式で認証すればよいか」を判断できます。

本記事では API キー認証を例に使います。Card は次のように security_schemessecurity を埋めるだけで 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.userAPIKeyUser インスタンスを取り出せ、user.user_nameteam-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-sdkAuthInterceptor が、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-sdkAgentExecutor + 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 プロトコルである以上、エージェントロジックには プロバイダー固有のコードが一切現れない のがポイントです。

最後まで読んでいただきありがとうございました。


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事