メインコンテンツへスキップ
kt-tech.blog

【学習メモ】MCP / Function Calling / Agno の違いと使い分け — 既存 Web サービスに外部ツール連携を入れるときの選択肢整理

学習メモ12分で読めます

この記事でわかること

  • MCP / Function Calling / Agno は競合ではなく、レイヤーが違うという整理
  • MCP は AI ではなく、LLM もツール選択も自分で持ち込む必要があるということ
  • ユースケース別の選択指針(単一 API 連携なら Function Calling だけで十分)
  • 公式 MCP Server の多くが stdio のみで、Web サービスからはそのまま使えないこと

Function Calling API の基本的な理解と、Python / asyncio の基礎知識。

概要: 既存のWebサービス(FastAPI + Azure OpenAI)に「外部APIを叩いて回答するLLM機能」を追加する際、MCP・Function Calling・Agno のうちどれを採用すべきか悩んだ。三者の関係を整理し、ユースケース別の選択指針を実例ベースでまとめる。

はじめに

社内の生成AIアプリケーションに「外部の公開APIを叩いて、その結果をもとに回答する」機能を追加することになった。要件を読んだ瞬間に思い浮かんだ選択肢は次の3つだった。

  • Function Calling をそのまま使う
  • MCP (Model Context Protocol) を使う
  • Agno などの Agent フレームワークを使う

名前は聞いたことがあっても「結局これって何が違うの?」「重複してない?」と感じる人は多いと思う。実際に検討してみた結果、三者は競合関係ではなく、レイヤーが違うことが分かった。本記事ではその関係性と、用途別の使い分けを整理する。


三者の役割を一言でまとめると

結論から書くと、それぞれの担当レイヤーはこうなる。

名前 レイヤー ざっくり何をするか
Function Calling LLM API の機能 LLM が「このツールをこの引数で呼びたい」と JSON で返してくれる
MCP プロトコル 「LLM ↔ 外部ツール」のやり取りを標準化する仕様
Agno フレームワーク LLM 呼び出し・ツール選択・状態管理などを束ねた高レベル抽象

図にするとこういう包含関係になる。

JavaScript
┌──────────────────────────────────────────────────┐
│  Agno / LangChain (Agent フレームワーク)         │
│  ┌────────────────────────────────────────────┐  │
│  │  MCP (プロトコル仕様)                       │  │
│  │  ┌──────────────────────────────────────┐  │  │
│  │  │  Function Calling (LLM API の機能)   │  │  │
│  │  └──────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────┘

外側になるほど抽象度が高く、内側になるほど低レイヤーになる。

それぞれを掘り下げる

Function Calling — LLM API の生の機能

Function Calling は OpenAI / Azure OpenAI の API が提供する機能そのもの。tools=[...] パラメータでツールスキーマを渡すと、LLM が「このツールをこの引数で呼びたい」というメッセージ (tool_calls) を返してくれる。

Python
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "東京の天気は?"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "指定都市の現在の天気を返す",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    }],
)

LLM 自身は実際にツールを実行しない。「呼んでほしい」という意思表示を返すだけで、実行はアプリ側の責任。返ってきた tool_calls をパースして、対応する Python 関数を実行し、結果を messages に append してもう一度 LLM を呼ぶ — このループが「Function Calling Loop」と呼ばれる。

このレイヤーには「ツールがどこにあるか」「どう発見するか」といった概念は無い。すべてアプリ側が手書きで tools=[...] を組み立てる。

MCP — ツール側を標準化するプロトコル

Function Calling だけでは、ツールごとに「スキーマを書く」「実行関数を書く」「結果を整形する」を毎回ベタ書きする必要がある。アプリ A とアプリ B が同じ外部 API を呼びたくても、それぞれが独立して同じことをやることになる。

ここで MCP (Model Context Protocol) が登場する。MCP は Anthropic が策定した「LLM とツール群をやり取りするためのプロトコル仕様」。

MCP では「ツール群」を MCP Server として外部に切り出す。MCP Server は次の能力を公開する。

  • list_tools() — 提供しているツールの一覧とスキーマを返す
  • call_tool(name, args) — 名前と引数を渡すとツールを実行して結果を返す

クライアント側 (アプリ) は MCP Client として MCP Server に接続し、list_tools() で得たスキーマをそのまま LLM の tools=[...] に渡せる。LLM が tool_calls を返してきたら、対応する call_tool() を呼ぶだけで済む。

Python
# 概念コード
async with mcp_client.connect("http://mcp-server:3001/mcp") as session:
    tools = await session.list_tools()  # スキーマを動的取得
    
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=convert_to_openai_format(tools),
    )
    
    for tool_call in response.choices[0].message.tool_calls:
        result = await session.call_tool(
            name=tool_call.function.name,
            arguments=json.loads(tool_call.function.arguments),
        )
        # 結果を messages に append して再度 LLM 呼び出し

重要なポイント: MCP は「LLM」ではない。LLM の機能を内蔵しているわけでも、ツール選択を行うわけでもない。あくまで「ツールを公開するためのプロトコル」であり、ツール選択や実際の LLM 呼び出しは引き続きアプリ (MCP Client) 側の責任。

MCP には大きく2つの通信方式がある。

トランスポート 接続方式 主なユースケース
stdio サーバーをサブプロセスとして起動し、標準入出力で通信 Claude Desktop など「単一クライアントが MCP Server を独占的に起動」
Streamable HTTP HTTP/SSE で接続。複数クライアントが同じサーバーに接続可能 Web サービスが MCP Server を共有する構成

GitHub に公開されている公式 MCP Server の多くは Claude Desktop 想定で stdio のみ実装されている。これを Web サービスから使いたい場合、後述する mcp-proxy などで HTTP に変換する必要がある。

Agno — ツール選択と状態管理を抽象化するフレームワーク

Agno (旧 PhiData) は、Function Calling Loop や状態管理を高レベル API でラップするフレームワーク。LangChain や LlamaIndex の仲間。

Python
# 概念コード
from agno.agent import Agent
from agno.models.openai import OpenAIChat

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[my_tool_1, my_tool_2],
    instructions="...",
)

response = agent.run("東京の天気は?")

これだけで Function Calling Loop も、ツール実行も、結果のフィードバックも全部やってくれる。MCP Server を tools として登録する機能もある。

便利に見えるが、トレードオフもある。

  • Pro: ボイラープレートが激減する。プロトタイピングや個人プロジェクトに向く。
  • Con: 内部のプロンプト・状態管理がブラックボックス化する。既存サービスのストリーミング実装やエラーハンドリングと噛み合わないことが多い。

ユースケース別の選択指針

ケース1: 既存の Web サービスに、特定の外部 API 連携を1つ追加したい

Function Calling のみで十分。MCP も Agno も不要。

外部 API のクライアントを Python で書き、スキーマと一緒に tools=[...] に渡す。「動的にツールが増減する」「他のアプリでも同じツールを使い回す」予定が無いなら、MCP のレイヤーを足すメリットは薄い。

ケース2: 公式 MCP Server を既存 Web サービスから使いたい

MCP Client + Function Calling。Agno は不要。

例えば「政府機関や OSS コミュニティが公式 MCP Server を提供している」「自社で同じものを作るのは無駄」というケース。MCP Client として接続し、list_tools() で得たスキーマを Function Calling で使う。

公式 MCP Server が stdio のみの場合、mcp-proxy でラップして HTTP 化し、サイドカーコンテナとして配置する構成が現実的。

flowchart LR
    A["API Container<br/>(MCP Client)"] -- "HTTP" --> B["mcp-proxy<br/>(sidecar)"]
    B -- "stdio" --> C["公式 MCP Server<br/>(subprocess)"]

この構成の詳細は 別記事 で解説した。

ケース3: 複数の独自ツールを管理し、新規 LLM アプリをゼロから作る

Agno (or LangChain / LlamaIndex) を検討する価値あり。

Agent ループ・状態管理・メモリなどを自前で書くと結構な量になる。プロトタイピングや MVP 段階ならフレームワークの恩恵が大きい。ただし本番運用に耐えうるかは別問題で、内部のプロンプトを覗き込めないことが負債になりやすい。

ケース4: 自分で MCP Server を作って公開したい

MCP SDK を使ってサーバーを実装する。これは「Function Calling」「Agno」とは別の話。

ツールを多数の LLM クライアント (Claude Desktop, Cursor, 自社アプリ等) から使ってほしい場合、MCP Server として公開すれば全クライアントが同じインタフェースで使える。


よくある誤解

  • MCP = AI ではない: 「MCP Server をホストすれば適当に質問を投げるだけで AI が良い感じに答えてくれる」と誤解しがちだが、MCP は単なる「ツール公開プロトコル」。LLM は別途自分で持ち込み、Function Calling Loop も自分で回す必要がある。
  • MCP Server は LLM を内蔵していない: ツール (関数) の集合体に過ぎない。call_tool("get_xxx", {...}) を呼ぶと指定ツールを実行して結果を返すだけ。質問解釈・引数生成・結果の自然言語化は全て LLM 側 (= MCP Client 側) の責任。
  • Function Calling と MCP は競合しない: MCP Client は内部で Function Calling を使う。「MCP を採用したら Function Calling は不要」ではなく、「MCP は Function Calling の上にツール公開のレイヤーを足したもの」。
  • Agno を使うと MCP / Function Calling を意識しなくて良い、わけではない: 抽象化されているだけで、内部では同じことをしている。デバッグ時には結局生のレイヤーを理解する必要が出てくる。
  • 「公式 MCP Server があるから採用」は早計: 多くの OSS MCP Server は Claude Desktop 想定で stdio のみ。Web サービスで使うなら HTTP 化や同時接続制御が追加で必要。

トラブルシューティング

LLM がツールを呼んでくれない

  • 原因: ツールの description が曖昧 / parameters の説明不足。
  • 対策: 「いつ呼ぶべきか」「引数の意味」を具体的に書く。プロンプトに「○○の質問が来たらこのツールを使え」と明示するのも有効。

ツール結果が長すぎて context overflow

  • 原因: 外部 API が大量データを返してくる。
  • 対策: ツール結果を LLM に渡す前にアプリ側で要約・抜粋する。MAX_TOOL_RESULT_CHARS のような上限を設けて切り詰める。

MCP Server で同時実行が壊れる

  • 原因: MCP の ClientSession は単一接続のステートフル通信。複数リクエストが同じセッションを共有すると競合する。
  • 対策: asyncio.Lockcall_tool をシリアライズする。スループットが必要なら接続プールを実装する。

参考リンク

まとめ

  • Function Calling: LLM API の生の機能。アプリは tool_calls を受け取って自前で実行する。
  • MCP: ツール側を標準化するプロトコル。LLM 機能は持たない。「外部にツール群を公開する」「他人が作ったツール群を取り込む」が主目的。
  • Agno: Function Calling Loop や状態管理をラップする高レベルフレームワーク。プロトタイピング向き。
  • 三者は 競合ではなくレイヤーが違う。Agno が MCP を内包し、MCP が Function Calling を内包する関係。
  • 既存サービスへの「特定 API 連携の追加」程度なら Function Calling で十分。「公式 MCP Server を再利用したい」「自分でツール群を公開したい」場合に MCP を選ぶのが基本方針。
  • フレームワーク採用時は「内部が見えなくなることのコスト」を意識すること。

更新履歴

  1. 包含関係の図のコードブロックの言語指定を javascript から text に修正。「よくある誤解」の下に不自然に入っていた「Tips」見出しを削除。参考リンクの表記を揃えた。「この記事でわかること」「対象読者」「前提条件」を概要プロパティへ移動。
  2. 記事内リンクの修正