Function Calling を使ってエージェントを組んでみたら、ツールを複数呼ぶたびに体感速度が遅くなっていく——そんな経験はありませんか。私も最初に Gemini API で調査系エージェントを組んだとき、検索・計算・RAG・外部 API 呼び出しをすべて逐次で回していて、ユーザーが投稿ボタンを押してから最終回答が出るまでに十数秒かかっていました。
この「逐次で遅い」を一段飛ばしで解決するのが、Gemini API の Parallel Function Calling(並列ツール実行)です。モデルが一度のターンで複数の関数呼び出しを発行し、アプリケーション側でそれらを並列に実行してから結果をまとめて返す、というパターンです。
ここではParallel Function Calling の仕様と、単に「並列に呼ぶ」だけでは本番で通用しない理由、そして順序依存・部分失敗・レート制限・監視まで含めた設計パターンを、動作するコードとともに解説します。「とりあえず並列化したら逆に不安定になった」という段階でつまずいている方にも、そのまま現場に持ち帰れる内容を目指しました。
Parallel Function Calling とは何か — 仕様の整理
Gemini API の Parallel Function Calling は、モデルが 1 ターンの応答で複数の functionCall パートを返す機能です。従来の逐次的な Function Calling では「ツール A を呼ぶ → 結果をモデルに返す → ツール B を呼ぶ → ...」という往復が必要でしたが、並列実行では次のような応答が一度に返ってきます。
説明文に「いつ呼ぶべきか」「何と並列に呼んで良いか」を明示すると、Gemini 2.5 Pro は高い確率で DAG に沿った順序を守ります。ツールを増やす前に「このツールは、他のどのツールと同時に走らせて安全か」を 1 行で書けるか自問してみてください。書けないツールは設計がまだ固まっていない証拠です。
# pip install tenacityfrom tenacity import retry, stop_after_attempt, wait_exponential_jitterclass ToolError(Exception): ...@retry( stop=stop_after_attempt(3), wait=wait_exponential_jitter(initial=0.2, max=2.0), reraise=True,)async def fetch_shipping_with_retry(zip: str) -> dict[str, Any]: try: return await asyncio.wait_for(fetch_shipping(zip), timeout=2.0) except asyncio.TimeoutError as exc: raise ToolError(f"fetch_shipping timeout for zip={zip}") from excasync def safe_dispatch(fc: types.FunctionCall) -> types.Part: impl = RETRY_REGISTRY.get(fc.name) or TOOL_REGISTRY[fc.name] try: result = await impl(**(fc.args or {})) except Exception as exc: # 失敗したことを明示的にモデルに伝える result = { "error": "tool_unavailable", "message": str(exc), "retry_allowed": False, } return types.Part.from_function_response(name=fc.name, response=result)
エラー情報を error キーで返すと、Gemini 2.5 Pro は「配送情報は取得できませんでしたが、注文履歴と在庫はご案内できます」のように部分的な成功を組み立てた回答を返してくれます。これが本記事で最も強調したい点です。エラーをモデルから隠さありません。
パターン 2:critical / optional の区別
ツールをビジネス上「必須」と「補助」に区別し、補助ツールの失敗では処理を継続する方式です。
CRITICAL_TOOLS = {"search_orders"} # 失敗したら回答不可OPTIONAL_TOOLS = {"get_inventory", "fetch_shipping"} # 失敗しても回答可能async def run_parallel_with_criticality(calls: list[types.FunctionCall]) -> list[types.Part]: results = await asyncio.gather(*(safe_dispatch(c) for c in calls)) # critical が失敗していたら即エラー応答に切り替える for c, r in zip(calls, results): if c.name in CRITICAL_TOOLS: resp = r.function_response.response if r.function_response else {} if "error" in resp: raise RuntimeError(f"critical tool {c.name} failed: {resp}") return results
MAX_PARALLEL_PER_TURN = 5async def run_bounded(calls: list[types.FunctionCall]) -> list[types.Part]: if len(calls) > MAX_PARALLEL_PER_TURN: # モデルに「多すぎるので絞って呼び直して」とエラーを返す return [ types.Part.from_function_response( name=c.name, response={"error": "too_many_parallel_calls", "limit": MAX_PARALLEL_PER_TURN}, ) for c in calls[MAX_PARALLEL_PER_TURN:] ] + [await safe_dispatch(c) for c in calls[:MAX_PARALLEL_PER_TURN]] return await asyncio.gather(*(safe_dispatch(c) for c in calls))
層 3:予算メーター
コスト上限(トークン数ベース)に近づいたら並列化をあえて諦めて逐次実行に切り替える、というアプローチも実務では有効です。Gemini API は入力トークンが並列実行でも基本的に増えませんが、ツール結果を再入力する 2 ターン目の入力トークンが膨らむため、長いツール応答(例:全文 RAG の結果 20KB)を複数並列で返すと 1 ターンあたりのコストが急騰します。ツールの戻り値サイズに上限を設け、超えたら要約したりサマリだけ返したりする工夫が必要になります。
よくある落とし穴 — 現場で遭遇した3つのバグ
ここからは、実際に本番で遭遇した並列 Function Calling 特有のバグを紹介します。公式ドキュメントには書かれていない、実装者だけが痛い目を見る領域です。
def dedupe_calls(calls: list[types.FunctionCall]) -> list[types.FunctionCall]: seen: set[tuple] = set() uniq: list[types.FunctionCall] = [] for c in calls: key = (c.name, tuple(sorted((c.args or {}).items()))) if key not in seen: seen.add(key) uniq.append(c) return uniq
落とし穴②:ツール応答の順序をモデルが混同する
Gemini にツール結果を戻すとき、Part.from_function_response の name をモデルの functionCall の順序と対応させる必要があります。asyncio.gather は引数の順序で結果を返すため通常は問題ありませんが、asyncio.as_completed を使って完了順に処理してしまうと順序がズレ、モデルが結果をどの呼び出しに紐づけるか迷って幻覚を生み出します。
対処:必ず asyncio.gather または自前のインデックス管理で「呼び出し順 = 結果順」を保つ。Part.from_function_response で渡される name はあくまで補助情報であり、順序が主であることを意識します。
落とし穴③:toolConfig の強制呼び出しモードが並列を阻害する
デバッグのために tool_config.function_calling_config.mode = ANY を設定し、allowed_function_names=["search_orders"] のように 1 つに絞ると、その時点でモデルは並列呼び出しをやめます。つまり「強制モードに切り替えたら急に遅くなった」というのは期待通りの挙動です。
対処:強制モードは「特定のツールを絶対に呼ばせたい初期段階」だけに使い、会話が進んだらすぐ AUTO に戻す。私はこれを「エージェントの助走期間だけハンドルを握る」と呼んでいます。