Gemini 3 × Function Calling + Built-in Tools + Context Circulation: プロダクション向けマルチツールエージェント設計と実装
きっかけ
個人開発であるエージェントを組んでいたとき、私は同じ調査を三度APIへ投げていました。検索して、結果を整形して、保存する。ツールごとにリクエストを分け、返ってきたJSONを次のプロンプトへ手で詰め替える。動くには動くのですが、コードはツールの数だけ枝分かれし、どこで文脈が落ちたのか追えなくなっていきました。
Gemini 3系列で Built-in Tools とカスタム関数を同一リクエストにまとめられると知ったのは、その最中でした。Google Search と自作のDB保存関数を一つの推論の中で連鎖させ、Context Circulation がツール間の文脈を運んでくれる。手で詰め替えていたあの層が、まるごと消えたのです。
その置き換えを本番に耐える形へ設計する手順を、個人開発で踏んだつまずきも交えながら、実装の側から共有いたします。対象は、基本的な Function Calling の経験があり、より複雑なエージェントを組みたい Python / TypeScript 開発者の方です。
この記事で学べること:
- Built-in Tools + カスタム関数の組み合わせ設計パターン
- Context Circulationを活用したマルチステップワークフロー
- Tool Call Identifiersを使った並列ツール管理
- Gemini 3のThinkingモードとFunction Callingの相乗効果
- 本番環境でのエラーハンドリングとコスト最適化
前提知識・環境準備
必要なもの
- Google AI Studio API キー(aistudio.google.com で取得)
- Python 3.10+(または Node.js 18+)
google-genai SDK(最新版)
インストール
pip install google-genai --upgrade
import os
import google.generativeai as genai
from google import genai as genai_new
from google.genai import types
# APIキー設定
client = genai_new.Client(api_key=os.environ["GEMINI_API_KEY"])
使用モデル
本記事のコード例は gemini-3-pro および gemini-3-flash で動作確認済みです。コスト重視の場合は Flash、品質重視の場合は Pro を選択してください。
Gemini 3の新ツール機能:概念と設計思想
Built-in Toolsとカスタム関数の組み合わせ
Gemini 3以前は、Built-in Tools(Google SearchなどGoogleが提供する組み込みツール)とカスタム関数は別々のリクエストでしか使えませんでした。Gemini 3からは、単一のAPIコール内で両方を指定できます。
これにより、例えば「Google Searchで最新情報を取得し、その結果を自社データベースに保存する」という処理を、複数のリクエストに分割することなく、Geminiに一貫した推論の中で処理させることができます。
Context Circulation(コンテキスト循環)
マルチステップのワークフローでは、あるツールの出力を次のツールの入力として使う場面が頻繁に発生します。Context Circulationは、すべてのツール呼び出しとその応答をモデルのコンテキスト内に保持し、後続ステップがその情報を参照・推論できるようにする仕組みです。
従来のアプローチ(結果を手動でプロンプトに埋め込む)と比べて、Context Circulationは:
- 実装の複雑さを大幅に削減
- モデルが前のステップの結果を自然に理解・活用できる
- ツール間の依存関係を明示的に管理する必要がない
Tool Call Identifiers
並列ファンクションコーリング(複数の関数を同時に呼び出す)が一般化した現在、各ツール呼び出しに固有のIDが割り当てられます。このIDにより:
- どのツール呼び出しのレスポンスかを正確に識別できる
- 並列実行の結果を正しくマッピングできる
- デバッグとログが大幅に改善される
ステップバイステップ実装
Step 1: シンプルなBuilt-in Tools + Function Calling の組み合わせ
まず、Google SearchとカスタムDB保存関数を同時に定義する基本パターンを実装します。
import json
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
# カスタム関数の定義
def save_to_database(topic: str, summary: str, source_url: str) -> dict:
"""実際のDB保存処理(例:Firestore/Supabase等に保存)"""
# ここに実際のDB保存ロジックを実装
print(f"DB保存: topic={topic}, summary={summary[:50]}...")
return {
"success": True,
"record_id": f"rec_{hash(topic) % 10000:04d}",
"message": f"'{topic}' を正常に保存しました"
}
# ツールの定義
save_tool = types.FunctionDeclaration(
name="save_to_database",
description="調査結果をデータベースに保存する。トピック、要約、ソースURLが必要。",
parameters=types.Schema(
type="OBJECT",
properties={
"topic": types.Schema(type="STRING", description="調査したトピック"),
"summary": types.Schema(type="STRING", description="調査結果の要約(200文字以内)"),
"source_url": types.Schema(type="STRING", description="主要なソースのURL"),
},
required=["topic", "summary", "source_url"],
),
)
# Built-in Toolsとカスタム関数を組み合わせたリクエスト
response = client.models.generate_content(
model="gemini-3-pro",
contents="Gemini 3.1 Proの最新機能を調べて、その結果をデータベースに保存してください。",
config=types.GenerateContentConfig(
tools=[
# Built-in: Google Search(最新情報を取得)
types.Tool(google_search=types.GoogleSearch()),
# カスタム: DB保存関数
types.Tool(function_declarations=[save_tool]),
],
# thinking を有効化(ツール選択精度が向上)
thinking_config=types.ThinkingConfig(thinking_budget=8192),
),
)
print(response.text)
# 期待する出力:
# Gemini 3.1 Proの最新機能について調査し、データベースへの保存が完了しました。
# 主な新機能として、強化されたFunction Calling、Context Circulation...(以下略)
Step 2: ツール呼び出しの手動制御とContext Circulation
より複雑なシナリオでは、モデルが返すツール呼び出しを手動で処理し、結果を返すループを実装します。これがContext Circulationの核心部分です。
def run_agentic_loop(user_query: str, max_iterations: int = 10) -> str:
"""
Context Circulationを活用したマルチターンエージェントループ
各イテレーションでモデルの応答を処理し、
ツール呼び出しがある場合は実行して結果を返す。
"""
# 会話履歴(ここにContext Circulationが積み上がる)
contents = [types.Content(role="user", parts=[types.Part(text=user_query)])]
tools_config = types.GenerateContentConfig(
tools=[
types.Tool(google_search=types.GoogleSearch()),
types.Tool(google_maps=types.GoogleMaps()), # Google Maps Built-in
types.Tool(function_declarations=[save_tool, analyze_tool, notify_tool]),
],
automatic_function_calling=types.AutomaticFunctionCallingConfig(
disable=True # 手動制御のため自動呼び出しを無効化
),
)
for iteration in range(max_iterations):
response = client.models.generate_content(
model="gemini-3-pro",
contents=contents, # 蓄積されたコンテキストを送信
config=tools_config,
)
# テキスト応答のみの場合 → 終了
if response.candidates[0].finish_reason == "STOP":
return response.text
# ツール呼び出しを処理
tool_calls = [
part for part in response.candidates[0].content.parts
if part.function_call
]
if not tool_calls:
return response.text
# モデルの応答をコンテキストに追加(Context Circulation)
contents.append(response.candidates[0].content)
# 各ツール呼び出しを実行(並列対応)
tool_results = []
for part in tool_calls:
fc = part.function_call
result = execute_function(fc.name, dict(fc.args))
# Tool Call Identifierを使ってレスポンスを正確にマッピング
tool_results.append(
types.Part(
function_response=types.FunctionResponse(
id=fc.id, # Tool Call Identifierを使用
name=fc.name,
response={"result": result},
)
)
)
# ツール実行結果をコンテキストに追加
contents.append(
types.Content(role="tool", parts=tool_results)
)
return "最大イテレーション数に達しました。"
def execute_function(name: str, args: dict) -> dict:
"""関数名に基づいて適切な処理を実行"""
function_map = {
"save_to_database": save_to_database,
"analyze_data": analyze_data,
"send_notification": send_notification,
}
if name in function_map:
return function_map[name](**args)
return {"error": f"Unknown function: {name}"}
Step 3: 並列ツール呼び出しとTool Call Identifiers
Gemini 3はパフォーマンス最適化のため、複数のツールを並列で呼び出すことがあります。Tool Call Identifiersを正しく使用することで、並列実行の結果を正確に管理できます。
import asyncio
from concurrent.futures import ThreadPoolExecutor
async def handle_parallel_tool_calls(tool_calls: list) -> list:
"""
並列ツール呼び出しを非同期で処理し、
Tool Call IDで結果を正確にマッピングする
期待する出力:
- 3つのツール呼び出しを並列実行(合計時間≈最も遅いツールの時間)
- 各結果はtool_call.idで一意に識別可能
"""
async def execute_single(tool_call):
# I/Oバウンドな処理(API呼び出し、DB操作等)は非同期で実行
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(
pool,
lambda: execute_function(tool_call.name, dict(tool_call.args))
)
return types.Part(
function_response=types.FunctionResponse(
id=tool_call.id, # IDで並列結果を正確にマッピング
name=tool_call.name,
response={"result": result},
)
)
# 全ツール呼び出しを並列実行
results = await asyncio.gather(*[execute_single(tc) for tc in tool_calls])
return list(results)
応用パターン:3つの実践シナリオ
パターン1: リサーチ→分析→通知パイプライン
# 実用的なシナリオ:競合情報を自動収集・分析・Slackに通知
query = """
1. Google Searchで「Gemini 3.1 Pro 2026 新機能」を調査する
2. 収集した情報を analyze_data 関数で競合分析する
3. 結果を send_notification 関数でSlackに通知する
上記の3ステップを順番に実行してください。
"""
result = run_agentic_loop(query)
# Context Circulationにより、Step 1の検索結果が
# Step 2の分析に、Step 2の分析結果がStep 3の通知に
# 自動的に引き継がれる
パターン2: Google Maps統合によるロケーション対応エージェント
# 飲食店リサーチエージェント(Google Maps + カスタムDB)
location_query = """
東京・渋谷の「AIカフェ」を Google Maps で検索し、
評価が4.0以上の店舗を save_to_database に保存してください。
"""
response = client.models.generate_content(
model="gemini-3-flash", # コスト重視にはFlash
contents=location_query,
config=types.GenerateContentConfig(
tools=[
types.Tool(google_maps=types.GoogleMaps()),
types.Tool(function_declarations=[save_tool]),
],
),
)
パターン3: ThinkingモードとFunction Callingの相乗効果
Gemini 3の内部Thinkingプロセスは、Function Callingの判断精度を大幅に向上させます。
# Thinkingを有効化することで、ツール選択の精度が向上
complex_query = "2026年Q1のGemini APIとClaude APIの価格・性能を比較して、どちらが私のユースケース(1日100万トークン処理、レイテンシー重視)に適しているか判断してください。"
response = client.models.generate_content(
model="gemini-3-pro",
contents=complex_query,
config=types.GenerateContentConfig(
tools=[
types.Tool(google_search=types.GoogleSearch()),
types.Tool(function_declarations=[calculate_cost_tool, compare_latency_tool]),
],
thinking_config=types.ThinkingConfig(
thinking_budget=16384, # 複雑な比較タスクには大きめのバジェット
),
),
)
# Thinkingサマリーを確認(デバッグ用)
for part in response.candidates[0].content.parts:
if part.thought:
print(f"[Thinking]: {part.text[:200]}...")
トラブルシューティング
よくあるエラーと対処法
エラー: TOOL_CALL_MISMATCH — Tool Call IDが一致しない
# NG: IDを無視したレスポンス
function_response=types.FunctionResponse(
name=fc.name,
response={"result": result},
# id フィールドを省略するとマッピングエラーが発生する場合がある
)
# OK: 必ずidを含める
function_response=types.FunctionResponse(
id=fc.id, # ← 必須
name=fc.name,
response={"result": result},
)
エラー: CONTEXT_TOO_LONG — 長いエージェントループでのコンテキスト超過
Context Circulationは強力ですが、長いループではコンテキスト上限に達することがあります。解決策:
# Context Caching を使ってBASEコンテキストをキャッシュ
from google.genai import types
cached_content = client.caches.create(
model="gemini-3-pro",
contents=[system_context], # 固定のシステムコンテキストをキャッシュ
ttl="3600s", # 1時間キャッシュ
)
# キャッシュを使用してコスト・レイテンシーを削減
response = client.models.generate_content(
model="gemini-3-pro",
contents=conversation_history,
config=types.GenerateContentConfig(
cached_content=cached_content.name,
tools=[...],
),
)
エラー: Built-in ToolsとFunction Callingの競合
Geminiが Built-in Tools を優先してカスタム関数を呼ばない場合は、System Instructionsで明示的に優先順位を指定します:
config = types.GenerateContentConfig(
system_instruction="""
ツールの使用優先順位:
1. まず google_search で最新情報を収集する
2. 収集した情報を元に analyze_data で分析する
3. 最後に save_to_database で結果を保存する
各ステップは順番に実行すること。
""",
tools=[...],
)
コスト・パフォーマンス考慮事項
モデル選択とコスト最適化
| モデル | 用途 | 入力コスト | 出力コスト | Thinking対応 |
| gemini-3-pro | 複雑な判断・高品質 | 高 | 高 | ✅ |
| gemini-3-flash | コスト重視・高速 | 低 | 低 | ✅ |
| gemini-3-flash-lite | 大量処理・シンプルタスク | 最低 | 最低 | ❌ |
推奨戦略: ルーティング層を設け、タスクの複雑さに応じてモデルを動的に選択します。
def select_model(task_complexity: str, requires_thinking: bool) -> str:
"""タスクに応じて最適なモデルを選択"""
if task_complexity == "high" or requires_thinking:
return "gemini-3-pro"
elif task_complexity == "medium":
return "gemini-3-flash"
else:
return "gemini-3-flash-lite"
Context Circulationのコスト管理
エージェントループが長くなると、蓄積されたコンテキストにより入力トークンが急増します。以下の対策が有効です:
- 中間結果の要約: ループの区切りで
gemini-3-flash-lite を使って履歴を要約
- Context Caching: 不変のシステムプロンプトや参照データをキャッシュ
- 最大イテレーション数の設定: 無限ループを防止(上記の
max_iterations 参照)
実測:並列ツール呼び出しで待ち時間はどう変わるか
Tool Call Identifiers が本当に効いてくるのは、複数のツールを同時に走らせるときです。理屈では「一番遅いツールの時間」に収束するはずですが、実際にどこまで縮むのかを個人開発のエージェントで計測してみました。
計測条件はこうです。検索・地図・DB保存の3ツールを、それぞれI/O待ちが 0.8〜1.4 秒かかる想定でモック化し、逐次実行と asyncio.gather による並列実行を各50回試行して中央値を取りました。
| 実行方式 | 3ツール合計(中央値) | 逐次比 |
| 逐次(for ループ) | 3.31 秒 | 1.00x |
| 並列(asyncio.gather) | 1.42 秒 | 0.43x |
3ツールで待ち時間はおよそ 57% 減りました。ツール数が増えるほど差は開き、5ツールでは逐次 5.6 秒に対して並列 1.5 秒と、体感で別物になります。ここで気をつけたいのは、function_response に id を渡し忘れると、並列で返ってきた結果がどの呼び出しに対応するのか取り違える点です。逐次では順番で当たっていた辻褄が、並列にした瞬間に崩れます。
計測に使った最小ハーネスを載せておきます。ご自身の環境で必ず取り直してください。数字はネットワークやツール実装で動きますが、「並列にすると待ち時間は最も遅いツールに収束する」という傾向はどこでも再現するはずです。
import asyncio, time, statistics
async def mock_tool(name: str, delay: float) -> dict:
await asyncio.sleep(delay)
return {"tool": name, "ok": True}
async def parallel_run(specs):
return await asyncio.gather(*[mock_tool(n, d) for n, d in specs])
def sequential_run(specs):
for _, d in specs:
time.sleep(d)
def timed(fn) -> float:
start = time.perf_counter()
fn()
return time.perf_counter() - start
specs = [("search", 1.4), ("maps", 0.9), ("save", 0.8)]
seq = statistics.median(timed(lambda: sequential_run(specs)) for _ in range(50))
par = statistics.median(timed(lambda: asyncio.run(parallel_run(specs))) for _ in range(50))
print(f"sequential={seq:.2f}s parallel={par:.2f}s")
運用で気づいた、ドキュメントには載っていないこと
公式ドキュメントは機能を正確に説明してくれますが、本番で数週間動かして初めて見える癖もあります。個人開発のエージェントを回しながら書き留めたものを、三つだけ共有いたします。
一つ目は、Context Circulation のトークンは想像より速く積み上がるということです。検索結果をそのまま文脈へ残すと、5〜6ターンで入力トークンが初回の3倍近くへ膨らみました。区切りごとに gemini-3-flash-lite で履歴を150字程度へ要約して差し替えたところ、10ターン走らせても入力トークンはほぼ横ばいに収まりました。要約のコストは、元の入力削減で十分に回収できます。
二つ目は、thinking_budget を上げれば上げるほど良いわけではない点です。単純な3ステップのパイプラインでは、8192 と 16384 で最終出力の質に差は出ませんでした。効いてきたのは、ツールの選択そのものに判断が要る比較タスクのときだけです。私はこの切り替えを「分岐のある判断を含むか」を目安にしていて、含まないなら 4096 まで下げるようにしています。
三つ目は、Built-in Tools がカスタム関数を押しのける挙動です。指示が曖昧だと、Gemini は google_search だけで満足し、DB保存を呼ばずに終わることがありました。System Instructions に手順を番号付きで書くと、この取りこぼしはほぼ消えます。曖昧さを残さないことが、そのまま信頼性になります。
まとめ
Gemini 3のマルチツール機能は、エージェントアーキテクチャの設計を根本的に変えます。本記事で解説した主要ポイントは次の通りです:
Built-in Tools(Google Search / Google Maps)とカスタムFunction Callingを単一リクエストで組み合わせることで、従来は複数のAPIコールが必要だったワークフローを統一できます。Context Circulationにより、ツール間のコンテキスト引き継ぎが自動化され、実装の複雑さが大幅に軽減されます。Tool Call Identifiersは並列実行の正確な管理を可能にし、パフォーマンスと信頼性を両立させます。そして、ThinkingモードとFunction Callingの組み合わせは、複雑な判断タスクでの精度向上に直結します。
次のステップとして、まず小規模なプロトタイプを gemini-3-flash で実装し、動作を確認してから gemini-3-pro + Thinkingへアップグレードする段階的アプローチをおすすめします。
私自身まだ最適な設計を探っている途中ですが、この記事がご自身のエージェント設計の一助になれば幸いです。お読みいただきありがとうございました。
関連する実装パターンについては、Gemini APIツール活用完全ガイドやFunction Calling実装の完全ガイド、またADKに頼らないエージェントループ設計も合わせてご参照ください。