なぜ高度なツール使用が必要なのか
Claude API でエージェントを構築していると、ツールの数が増えるにつれて深刻な問題が発生します。100個のツール定義をすべてプロンプトに含めると、それだけで 55,000トークン以上 を消費してしまいます。1リクエストあたりのコストが跳ね上がり、レスポンス速度も遅くなります。これは大規模なエージェントシステムにとって致命的な欠点です。
Anthropic が 2025年11月24日に公開した3つのツール使用機能が、この問題に正面から答えています。
Tool Search Tool — ツールを動的に検索・ロードし、初期コンテキストからツール定義を追い出す
Programmatic Tool Calling — Python コードでツールをオーケストレーションし、中間結果をコンテキスト外に保持する
Tool Use Examples — ツール定義に入力例を添え、スキーマだけでは伝わらない使い方の作法を示す
いずれも本記事執筆時点(2026年8月)でベータ提供です。ベータヘッダーの指定が必須 で、ここを落とすと defer_loading も allowed_callers も input_examples も、そもそも受け付けられません。最初につまずくのは、たいていこの一行です。
以下では3機能を組み合わせた実装を、動くコードとともに示します。あわせて、Anthropic が公表している削減率が「何を測った数字なのか」も切り分けておきます。ここを取り違えたまま見積もりを立てると、期待していた効果と実測がずれます。
対象読者は、Claude API の基本的なツール使用(入門ガイド)をすでに理解している中〜上級のエンジニアです。
公表されている数字が、それぞれ何を測ったものか
この3機能を紹介する記事では「85%削減」「37%削減」「72%→90%」という数字が並びます。私も最初は全部まとめて「速くなる」と受け取っていました。ところが実際に組んでみると、期待した箇所が縮まない。数字の出どころを読み直して、ようやく取り違えに気づきました。
いずれも Anthropic が Introducing advanced tool use on the Claude Developer Platform で公表した社内計測値です。測っている対象は、機能ごとにまるで違います。
数字 機能 実際に測っているもの
85%削減 Tool Search Tool 初期コンテキストのトークン量 。50以上の MCP ツールで約77Kトークンだった消費が、約8.7Kトークンになった比較
37%削減 Programmatic Tool Calling トークン量 。複雑なリサーチタスクの平均で 43,588 → 27,297 トークン
72%→90% Tool Use Examples 複雑なパラメータ処理の正確性 。ネストした入力を持つツールでの社内評価
ここで一番間違えやすいのが 37% です。これはレイテンシではなく、トークンの削減率 です。Programmatic Tool Calling のレイテンシについて Anthropic が述べているのは、「20回以上のツール呼び出しを1つのコードブロックにまとめると、19回以上の推論パスがなくなる」という別の説明です。往復が減るぶん結果として速くはなりますが、「37%速くなる」と読むのは誤りです。
私はこの取り違えのまま、自分の環境で応答時間を計って「37%も縮まないではないか」と首をひねっていました。縮んでいたのはトークンの方で、そちらは実際に相応の削減が出ていたのです。測る対象を間違えると、効いている改善まで見落とします。
精度についても補助線を引いておきます。Tool Search Tool を有効にした MCP 評価では、Opus 4 が 49% から 74%、Opus 4.5 が 79.5% から 88.1% へ改善したと報告されています。トークンが減るだけでなく、候補が絞られることで選択ミスそのものが減る という筋です。似た名前のツールが並ぶカタログでは、こちらの効果の方が体感に近いかもしれません。
そのうえで前提を一つ。これらは Anthropic の社内テスト環境での数字であり、ツールカタログの構成にも作業内容にも強く依存します。自分の環境で見積もりを立てるときは、比率をそのまま当てはめるのではなく、「現在ツール定義に何トークン払っているか」を先に測る 方が確実です。usage.input_tokens をツールなし・ツールありで一度ずつ取れば、その差が削減の上限になります。
前提知識・環境構築
必要な環境
Python 3.10 以上
anthropic SDK 最新版(pip install anthropic --upgrade)
Anthropic API キー(platform.claude.com から取得)
SDK のインストールと初期化
# インストール
# pip install anthropic>=1.50.0
import anthropic
import asyncio
import json
from typing import Any
# クライアント初期化
client = anthropic.Anthropic( api_key = "YOUR_API_KEY" )
# 使用するモデル(2026年3月時点の推奨)
MODEL = "claude-opus-4-6-20260205"
# 3機能に共通のベータヘッダー(本記事のコードは全てこれを前提とします)
ADVANCED_TOOL_USE_BETA = "advanced-tool-use-2025-11-20"
ベータヘッダーを忘れると何が起きるか
3機能はいずれもベータ提供です。呼び出しは client.messages.create ではなく client.beta.messages.create を使い、betas にヘッダーを渡します。
response = client.beta.messages.create(
betas = [ ADVANCED_TOOL_USE_BETA ],
model = MODEL ,
max_tokens = 4096 ,
tools = [ ... ],
messages = [{ "role" : "user" , "content" : user_query}],
)
ヘッダーを付け忘れた場合、返ってくるのは「機能が無効です」という親切なメッセージではありません。defer_loading や allowed_callers、input_examples といったフィールドが未知のキーとして扱われ、リクエスト自体が検証エラーで弾かれます。
厄介なのは、tools の中身が長くなるほど、エラーメッセージのどのフィールドが原因なのか読み取りにくくなる点です。ツールカタログを100個並べた状態で最初の実行に失敗すると、スキーマの書き方を疑って何時間も潰しかねません。私は実際、defer_loading の綴りを何度も見直したあとで、原因が呼び出し側の一行だったと気づきました。
新しい機能を試すときは、ツールを1つだけにした最小構成で通してから カタログを広げる。この順番を守るだけで、切り分けにかかる時間が大きく変わります。
ベータ機能である以上、フィールド名や挙動は今後変わり得ます。本番に載せる際は、ヘッダー名を定数に切り出しておくと、更新時の差し替え箇所が一箇所で済みます。
概念と設計思想
従来のツール使用の問題点
従来のアプローチでは、すべてのツール定義を最初のリクエストに含める必要がありました:
# ❌ 非効率なアプローチ(ツール数が増えるとトークンが爆発的に増加)
response = client.messages.create(
model = MODEL ,
max_tokens = 4096 ,
tools = [tool_1, tool_2, ... , tool_100], # 全100ツールを毎回送信
messages = [{ "role" : "user" , "content" : user_query}]
)
100ツールの定義だけで 55,000 トークン以上を消費。これは Claude に実際の作業に使えるコンテキストを大幅に圧迫します。
新しいアプローチの全体像
ユーザークエリ
↓
[Tool Search Tool] Claude がクエリに関連するツールを検索
↓
必要なツールのみ動的ロード(初期コンテキストの圧迫を回避)
↓
[Programmatic Tool Calling] コード内で複数ツールを並列実行
↓
中間結果をコンテキスト外で処理(推論パスの往復を削減)
↓
最終結果のみを Claude に返す
ステップバイステップ実装
Step 1:Tool Search Tool の実装
Tool Search Tool は、Claude がツールカタログから必要なツールを動的に検索できるようにする仕組みです。defer_loading: true でマークしたツールは初期ロード時には展開されず、Claude が必要に応じて検索してロードします。
import anthropic
import json
client = anthropic.Anthropic()
# ── ツールカタログ定義(大規模環境を想定)──
TOOL_CATALOG = {
"get_weather" : {
"name" : "get_weather" ,
"description" : "指定された都市の現在の天気情報を取得する" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"city" : { "type" : "string" , "description" : "都市名(例: Tokyo, Osaka)" },
"unit" : { "type" : "string" , "enum" : [ "celsius" , "fahrenheit" ], "default" : "celsius" }
},
"required" : [ "city" ]
}
},
"search_database" : {
"name" : "search_database" ,
"description" : "製品データベースを検索して関連情報を返す" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"query" : { "type" : "string" , "description" : "検索クエリ" },
"limit" : { "type" : "integer" , "description" : "返す結果の最大数" , "default" : 10 }
},
"required" : [ "query" ]
}
},
"send_email" : {
"name" : "send_email" ,
"description" : "指定の宛先にメールを送信する" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"to" : { "type" : "string" },
"subject" : { "type" : "string" },
"body" : { "type" : "string" }
},
"required" : [ "to" , "subject" , "body" ]
}
},
# ... 実際には100以上のツールが存在する想定
}
def tool_search_handler (query: str , limit: int = 5 ) -> list[ dict ]:
"""
BM25 または正規表現でツールカタログを検索する関数
本番環境では Elasticsearch や pgvector を使うことを推奨
"""
results = []
query_lower = query.lower()
for tool_name, tool_def in TOOL_CATALOG .items():
# シンプルなキーワードマッチング(本番では意味検索を推奨)
score = 0
if query_lower in tool_def[ "description" ].lower():
score += 2
if any (word in tool_def[ "name" ] for word in query_lower.split()):
score += 1
if score > 0 :
results.append({ "tool" : tool_def, "score" : score})
# スコア順にソートして上位 N 件を返す
results.sort( key =lambda x: x[ "score" ], reverse = True )
return [r[ "tool" ] for r in results[:limit]]
def run_agent_with_tool_search (user_query: str ) -> str :
"""
Tool Search Tool を使ったエージェントループ
"""
# Tool Search Tool の定義
tool_search_tool = {
"name" : "tool_search" ,
"description" : "利用可能なツールカタログを検索して関連ツールを発見する。大量のツールがある場合に使用。" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"query" : {
"type" : "string" ,
"description" : "どんなツールを探しているか(例: 天気を取得, データベースを検索)"
},
"limit" : {
"type" : "integer" ,
"description" : "返すツール数の上限" ,
"default" : 5
}
},
"required" : [ "query" ]
}
}
messages = [{ "role" : "user" , "content" : user_query}]
available_tools = [tool_search_tool] # 最初は Tool Search Tool だけ提供
loaded_tools = {} # 動的にロードされたツールを追跡
max_iterations = 10
for _ in range (max_iterations):
response = client.beta.messages.create(
betas = [ ADVANCED_TOOL_USE_BETA ], # defer_loading を有効にするために必須
model = "claude-opus-4-6-20260205" ,
max_tokens = 4096 ,
tools = available_tools,
messages = messages
)
if response.stop_reason == "end_turn" :
# 最終テキストレスポンスを返す
for block in response.content:
if hasattr (block, "text" ):
return block.text
return "完了"
# ツール呼び出しを処理
tool_results = []
for block in response.content:
if block.type != "tool_use" :
continue
tool_name = block.name
tool_input = block.input
if tool_name == "tool_search" :
# ツール検索を実行して新しいツールをロード
found_tools = tool_search_handler(
tool_input[ "query" ],
tool_input.get( "limit" , 5 )
)
# 見つかったツールを available_tools に追加
for tool in found_tools:
if tool[ "name" ] not in loaded_tools:
loaded_tools[tool[ "name" ]] = tool
available_tools.append(tool)
result = f " { len (found_tools) } 個のツールを発見してロードしました: { [t[ 'name' ] for t in found_tools] } "
elif tool_name in loaded_tools:
# 実際のツール実行(本番では外部APIやDBに接続)
result = execute_tool(tool_name, tool_input)
else :
result = f "ツール ' { tool_name } ' が見つかりません"
tool_results.append({
"type" : "tool_result" ,
"tool_use_id" : block.id,
"content" : str (result)
})
# メッセージ履歴を更新
messages.append({ "role" : "assistant" , "content" : response.content})
messages.append({ "role" : "user" , "content" : tool_results})
return "最大イテレーション数に達しました"
def execute_tool (tool_name: str , tool_input: dict ) -> Any:
"""ツールを実際に実行する(本番では外部APIに接続)"""
if tool_name == "get_weather" :
return { "city" : tool_input[ "city" ], "temp" : 22 , "condition" : "晴れ" }
elif tool_name == "search_database" :
return { "results" : [ f "製品 { i } " for i in range (tool_input.get( "limit" , 3 ))]}
elif tool_name == "send_email" :
return { "status" : "sent" , "message_id" : "msg_001" }
return { "error" : "未実装のツール" }
# 実行例
if __name__ == "__main__" :
result = run_agent_with_tool_search( "東京の天気を調べて、製品データベースを検索してください" )
print (result)
# 期待する出力例:
# 東京の天気は22°C、晴れです。
# データベースの検索結果: 製品0, 製品1, 製品2 が見つかりました。
Step 2:Programmatic Tool Calling の実装
Programmatic Tool Calling は、Claude が Python コードを書いてツールを内部から呼び出す仕組みです。中間結果がコンテキストに蓄積されないため、複雑な多段階ワークフローでもトークン効率が大幅に向上します。
import anthropic
from anthropic.types import ToolResultBlockParam
import json
client = anthropic.Anthropic()
def run_programmatic_tool_calling (task: str ) -> str :
"""
Programmatic Tool Calling を使った複雑なデータ処理ワークフロー
Claude がコードを書いてツールを内部から呼び出し、
中間結果はコンテキストに含めずに最終結果のみを返す。
"""
# ツール定義(関数として実装)
tools = [
{
"name" : "fetch_sales_data" ,
"description" : "指定期間の売上データをCSV形式で取得する" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"start_date" : { "type" : "string" , "description" : "開始日 (YYYY-MM-DD)" },
"end_date" : { "type" : "string" , "description" : "終了日 (YYYY-MM-DD)" },
"region" : { "type" : "string" , "description" : "地域(all, north, south, east, west)" , "default" : "all" }
},
"required" : [ "start_date" , "end_date" ]
}
},
{
"name" : "calculate_statistics" ,
"description" : "数値リストの統計(平均、中央値、標準偏差)を計算する" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"values" : { "type" : "array" , "items" : { "type" : "number" }, "description" : "数値のリスト" },
"metrics" : { "type" : "array" , "items" : { "type" : "string" }, "description" : "計算する指標" }
},
"required" : [ "values" ]
}
},
{
"name" : "generate_report" ,
"description" : "分析結果からレポートを生成してSlackに送信する" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"title" : { "type" : "string" },
"summary" : { "type" : "string" },
"data" : { "type" : "object" }
},
"required" : [ "title" , "summary" ]
}
}
]
messages = [{ "role" : "user" , "content" : task}]
while True :
response = client.beta.messages.create(
betas = [ ADVANCED_TOOL_USE_BETA ], # allowed_callers を有効にするために必須
model = "claude-opus-4-6-20260205" ,
max_tokens = 8192 ,
tools = tools,
messages = messages,
# tools には code_execution ツールも含める
# これにより Claude がコード内でツールを呼び出せる
)
if response.stop_reason == "end_turn" :
for block in response.content:
if hasattr (block, "text" ):
return block.text
return "タスク完了"
if response.stop_reason != "tool_use" :
break
# ツール呼び出しを処理(並列実行対応)
tool_results = []
for block in response.content:
if block.type != "tool_use" :
continue
result = dispatch_tool(block.name, block.input)
tool_results.append({
"type" : "tool_result" ,
"tool_use_id" : block.id,
"content" : json.dumps(result, ensure_ascii = False )
})
messages.append({ "role" : "assistant" , "content" : response.content})
messages.append({ "role" : "user" , "content" : tool_results})
return "処理完了"
def dispatch_tool (name: str , inputs: dict ) -> dict :
"""ツール実行ディスパッチャー(本番では各サービスに接続)"""
import statistics
import random
if name == "fetch_sales_data" :
# 本番では実際のDBやAPIから取得
data = [{ "date" : f "2026-03- { i :02d } " , "amount" : random.randint( 10000 , 100000 )}
for i in range ( 1 , 20 )]
return { "data" : data, "total_rows" : len (data)}
elif name == "calculate_statistics" :
values = inputs[ "values" ]
metrics = inputs.get( "metrics" , [ "mean" , "median" , "stdev" ])
result = {}
if "mean" in metrics:
result[ "mean" ] = statistics.mean(values)
if "median" in metrics:
result[ "median" ] = statistics.median(values)
if "stdev" in metrics and len (values) > 1 :
result[ "stdev" ] = statistics.stdev(values)
return result
elif name == "generate_report" :
# 本番では Slack API や Email に送信
print ( f "📊 レポート生成: { inputs[ 'title' ] } " )
return { "status" : "sent" , "channel" : "#sales-reports" }
return { "error" : f "未知のツール: { name } " }
# 実行例
if __name__ == "__main__" :
result = run_programmatic_tool_calling(
"2026年3月1日〜19日の売上データを取得して統計を計算し、"
"結果をレポートにまとめてSlackに送信してください"
)
print (result)
# 期待する出力例:
# 3月の売上分析が完了しました。
# 平均売上: ¥55,234, 中央値: ¥52,000, 標準偏差: ¥18,432
# レポートを #sales-reports に送信しました。
Step 3:Tool Use Examples で精度を向上
Tool Use Examples は、ツール定義に具体的な使用例を追加することで、Claude のツール呼び出し精度を大幅に向上させる機能です。特に複雑なパラメータ構造を持つツールに効果的です。
import anthropic
client = anthropic.Anthropic()
def create_tool_with_examples () -> dict :
"""
Tool Use Examples を含むツール定義の作成
input_examples フィールドに最小・部分・完全仕様のパターンを定義
"""
return {
"name" : "create_chart" ,
"description" : "データからグラフを生成してファイルに保存する。"
"折れ線グラフ、棒グラフ、円グラフに対応。" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"chart_type" : {
"type" : "string" ,
"enum" : [ "line" , "bar" , "pie" ],
"description" : "グラフの種類"
},
"data" : {
"type" : "array" ,
"description" : "グラフデータ。各要素は {label : string, value: number} 形式" ,
"items" : {
"type" : "object" ,
"properties" : {
"label" : { "type" : "string" },
"value" : { "type" : "number" }
}
}
},
"title" : { "type" : "string" , "description" : "グラフのタイトル" },
"output_path" : { "type" : "string" , "description" : "出力ファイルパス(省略可)" },
"options" : {
"type" : "object" ,
"description" : "追加オプション(色、サイズ等)" ,
"properties" : {
"colors" : { "type" : "array" , "items" : { "type" : "string" }},
"width" : { "type" : "integer" },
"height" : { "type" : "integer" },
"legend" : { "type" : "boolean" }
}
}
},
"required" : [ "chart_type" , "data" ]
},
# ── Tool Use Examples: 最小・部分・完全 の3パターン ──
"input_examples" : [
{
# 最小仕様(必須パラメータのみ)
"description" : "シンプルな棒グラフ" ,
"input" : {
"chart_type" : "bar" ,
"data" : [
{ "label" : "Q1" , "value" : 150 },
{ "label" : "Q2" , "value" : 230 },
{ "label" : "Q3" , "value" : 190 }
]
}
},
{
# 部分仕様(タイトルあり、オプションなし)
"description" : "タイトル付き折れ線グラフ" ,
"input" : {
"chart_type" : "line" ,
"data" : [
{ "label" : "Jan" , "value" : 100 },
{ "label" : "Feb" , "value" : 120 },
{ "label" : "Mar" , "value" : 90 }
],
"title" : "月次売上推移"
}
},
{
# 完全仕様(すべてのオプションを含む)
"description" : "カスタマイズされた円グラフ" ,
"input" : {
"chart_type" : "pie" ,
"data" : [
{ "label" : "製品A" , "value" : 40 },
{ "label" : "製品B" , "value" : 35 },
{ "label" : "製品C" , "value" : 25 }
],
"title" : "製品別シェア(2026年Q1)" ,
"output_path" : "/reports/q1_share.png" ,
"options" : {
"colors" : [ "#FF6B6B" , "#4ECDC4" , "#45B7D1" ],
"width" : 800 ,
"height" : 600 ,
"legend" : True
}
}
}
]
}
def run_with_tool_examples (user_request: str ) -> str :
"""Tool Use Examples を含むツールでエージェントを実行"""
chart_tool = create_tool_with_examples()
response = client.beta.messages.create(
betas = [ ADVANCED_TOOL_USE_BETA ], # input_examples を有効にするために必須
model = "claude-opus-4-6-20260205" ,
max_tokens = 4096 ,
tools = [chart_tool],
messages = [{ "role" : "user" , "content" : user_request}]
)
# ツール呼び出しを処理
for block in response.content:
if block.type == "tool_use" and block.name == "create_chart" :
print ( f "✅ グラフ生成: { block.input } " )
# 実際のグラフ生成処理(matplotlib 等)
return f "グラフを生成しました: { block.input.get( 'title' , '無題' ) } "
# テキスト応答
for block in response.content:
if hasattr (block, "text" ):
return block.text
return "処理完了"
# 実行例
if __name__ == "__main__" :
result = run_with_tool_examples(
"2026年第1四半期の売上データ(製品A:40%, 製品B:35%, 製品C:25%)を"
"カスタムカラーの円グラフで可視化して、/reports/q1_share.png に保存してください"
)
print (result)
# 期待する出力例:
# ✅ グラフ生成: {'chart_type': 'pie', 'data': [...], 'title': '2026年Q1 製品別売上シェア',
# 'output_path': '/reports/q1_share.png', 'options': {'colors': [...], ...}}
# グラフを生成しました: 2026年Q1 製品別売上シェア
応用パターン・拡張方法
3つの機能を組み合わせた完全な本番パターン
実際の本番環境では、3つの機能を組み合わせて使うことが最も効果的です:
class ProductionAgentWithAdvancedTools :
"""
本番環境向けエージェント:3つのツール使用機能を統合
- Tool Search で動的ツール検索(初期コンテキストのトークンを削減)
- Programmatic Tool Calling で中間処理(中間結果をコンテキスト外へ)
- Tool Use Examples で入力例を提示(パラメータ誤りを削減)
"""
def __init__ (self, tool_catalog: dict ):
# ベータ機能を使うため beta 名前空間で呼び出す(後述の _call を参照)
self .client = anthropic.Anthropic()
self .tool_catalog = tool_catalog
self .loaded_tools = {}
self .model = "claude-opus-4-6-20260205"
def run (self, user_query: str , max_iterations: int = 15 ) -> str :
messages = [{ "role" : "user" , "content" : user_query}]
# Tool Search Tool を最初から提供
available_tools = [ self ._get_tool_search_definition()]
for iteration in range (max_iterations):
response = self .client.beta.messages.create(
betas = [ ADVANCED_TOOL_USE_BETA ],
model = self .model,
max_tokens = 8192 ,
tools = available_tools,
messages = messages
)
if response.stop_reason == "end_turn" :
return self ._extract_text(response)
tool_results = self ._process_tool_calls(
response.content,
available_tools
)
messages.append({ "role" : "assistant" , "content" : response.content})
messages.append({ "role" : "user" , "content" : tool_results})
return "最大イテレーション到達"
def _process_tool_calls (self, content, available_tools: list ) -> list :
results = []
for block in content:
if block.type != "tool_use" :
continue
if block.name == "tool_search" :
# ツールを動的ロード
found = self ._search_and_load_tools(
block.input[ "query" ],
available_tools
)
result = f "ロード完了: { [t[ 'name' ] for t in found] } "
else :
result = self ._execute_tool(block.name, block.input)
results.append({
"type" : "tool_result" ,
"tool_use_id" : block.id,
"content" : json.dumps(result, ensure_ascii = False )
})
return results
def _search_and_load_tools (self, query: str , available_tools: list ) -> list :
found = []
for name, tool in self .tool_catalog.items():
if query.lower() in tool[ "description" ].lower():
if name not in self .loaded_tools:
self .loaded_tools[name] = tool
available_tools.append(tool)
found.append(tool)
return found
def _execute_tool (self, name: str , inputs: dict ) -> Any:
# 実際のツール実行ロジック
if name in self .tool_catalog:
return { "result" : f " { name } を実行しました" , "inputs" : inputs}
return { "error" : f "ツール ' { name } ' が見つかりません" }
def _get_tool_search_definition (self) -> dict :
return {
"name" : "tool_search" ,
"description" : "利用可能なツールを検索してロードする" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"query" : { "type" : "string" , "description" : "検索クエリ" }
},
"required" : [ "query" ]
}
}
def _extract_text (self, response) -> str :
for block in response.content:
if hasattr (block, "text" ):
return block.text
return "完了"
パフォーマンス最適化のヒント
大規模なツールカタログを持つ場合、Tool Search に 意味検索(Semantic Search) を組み込むことで検索精度が大幅に向上します。sentence-transformers と FAISS を使った例:
# 意味検索ベースの Tool Search(本番推奨)
# pip install sentence-transformers faiss-cpu
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np
class SemanticToolSearch :
def __init__ (self, tool_catalog: dict ):
self .model = SentenceTransformer( "paraphrase-multilingual-MiniLM-L12-v2" )
self .tools = list (tool_catalog.values())
# ツール説明文をベクトル化してインデックス構築
descriptions = [t[ "description" ] for t in self .tools]
embeddings = self .model.encode(descriptions)
self .index = faiss.IndexFlatL2(embeddings.shape[ 1 ])
self .index.add(embeddings.astype(np.float32))
def search (self, query: str , k: int = 5 ) -> list[ dict ]:
query_vector = self .model.encode([query]).astype(np.float32)
distances, indices = self .index.search(query_vector, k)
return [ self .tools[i] for i in indices[ 0 ] if distances[ 0 ][ list (indices[ 0 ]).index(i)] < 1.5 ]
トラブルシューティング
よくあるエラーと解決策
エラー1:tool_use ブロックの後に tool_result がない
# ❌ 間違い:ツール呼び出しの後に tool_result を返さない
messages.append({ "role" : "assistant" , "content" : response.content})
# ここで直接 user メッセージを追加するとエラー
# ✅ 正解:必ず tool_result を返す
tool_results = [{ "type" : "tool_result" , "tool_use_id" : block.id, "content" : "..." }
for block in response.content if block.type == "tool_use" ]
messages.append({ "role" : "user" , "content" : tool_results})
エラー2:Tool Search が空の結果を返す
# 検索クエリが具体的すぎる場合に発生
# ❌ 具体的すぎるクエリ
tool_search_handler( "東京の天気を摂氏で取得する関数" )
# ✅ 汎用的なクエリで検索
tool_search_handler( "天気" ) # 短いキーワードの方が効果的
エラー3:Programmatic Tool Calling でコンテキストが増えすぎる
# 大量のツール結果をそのまま返すとコンテキストが膨らむ
# ✅ 結果を要約してから返す
def truncate_result (result: dict , max_chars: int = 500 ) -> str :
result_str = json.dumps(result, ensure_ascii = False )
if len (result_str) > max_chars:
return result_str[:max_chars] + "...[省略]"
return result_str
パフォーマンス・セキュリティ考慮事項
コスト最適化
アプローチ トークン使用量 コスト
従来(全ツール定義) 100% 基準
Tool Search + 動的ロード 15% -85%
+ Programmatic Tool Calling 10% -90%
この表は Anthropic の公表値から導いた目安 であり、私の環境での実測値ではありません。実際の削減幅はカタログの大きさとツール定義の冗長さで変わります。定義が最初から10ツール・数千トークン程度なら、Tool Search Tool を挟むぶんの往復が増えて、かえって遅くなることもあります。導入の判断は、次節の条件と照らして決めてください。
セキュリティのベストプラクティス
入力バリデーション :ツール入力は必ず JSON Schema でバリデーション
サンドボックス実行 :コード実行は必ずサンドボックス環境で(Claude API の code execution tool を活用)
レート制限 :エージェントループに最大イテレーション数を設定(max_iterations)
ログ記録 :すべてのツール呼び出しと結果を監査ログに記録
入れない方が速い場面を先に決めておく
3機能とも、ただで効くわけではありません。Tool Search Tool は呼び出しの前に検索の一手を挟みます。Programmatic Tool Calling はコード実行環境の起動を挟みます。Tool Use Examples はツール定義そのものを長くします。どれも、削減の前に何かを足している という構造です。
私が個人開発でエージェントを組むとき、最初に決めるのは「入れる条件」ではなく「入れない条件」の方です。先に線を引いておかないと、機能が新しいというだけで全部載せてしまい、あとから遅さの原因を切り分けられなくなります。
機能 入れる価値が高い 入れない方が速い
Tool Search Tool
ツール定義が1万トークンを超える/MCP サーバーを複数つないでいる/似た名前のツールで選択ミスが起きている
ツールが10個未満/毎セッションでほぼ全ツールを使う/定義がもともと短い
Programmatic Tool Calling
集計や要約だけが欲しい大きなデータを扱う/依存関係のあるツール呼び出しが3つ以上連なる/多数の対象へ並列に問い合わせる
単発のツール呼び出しで済む/途中経過そのものを Claude に読ませて判断させたい/応答が小さい単純な参照
Tool Use Examples
ネストした入力構造を持つ/任意パラメータが多く組み合わせに作法がある/独自の ID 体系や日付書式がある
引数が1つで用途が自明/URL やメールアドレスのような標準的な書式/JSON Schema の制約で十分に縛れる
一つだけ補足しておきたいのは、「途中経過そのものを Claude に読ませて判断させたい」場合です。Programmatic Tool Calling は中間結果をコンテキストから追い出す仕組みなので、判断材料まで一緒に消えます 。ログの異常検知のように「生データを見た上での気づき」が価値になる処理では、この機能はむしろ邪魔になります。トークンが減ることと、判断の質が保たれることは別の話です。
導入の順番についても、Anthropic は「最も大きなボトルネックから一つずつ」と勧めています。ツール定義でコンテキストが埋まっているなら Tool Search Tool、中間結果が膨らんでいるなら Programmatic Tool Calling、パラメータの誤りが多いなら Tool Use Examples。同時に3つ入れると、効いた機能と効かなかった機能の区別がつかなくなります 。私も一度まとめて入れて、切り分けに余計な時間を使いました。
大規模エージェントで避けて通れない——ツール実行時エラーの伝え方と入力バリデーション
ここまでは、ツールが正しく呼ばれる前提で設計を進めてきました。けれども数百のツールを束ねるエージェントでは、外部 API のタイムアウトや権限エラーが日常的に起こります。私自身、個人開発でエージェントを常時走らせていると、失敗そのものより「失敗を Claude にどう伝えるか」でつまずくことが多くありました。
ツール実行が失敗したとき、その事実を握りつぶすと、エージェントは誤った前提のまま平然と回答を続けます。tool_result の is_error フィールドは、この分岐を Claude に明示するための仕組みです。
def handle_tool_execution (tool_use_block):
"""ツールを実行し、失敗は is_error で Claude に伝える"""
try :
result = call_external_api(tool_use_block.input)
return {
"type" : "tool_result" ,
"tool_use_id" : tool_use_block.id,
"content" : json.dumps(result),
}
except ExternalAPIError as e:
return {
"type" : "tool_result" ,
"tool_use_id" : tool_use_block.id,
"content" : f "APIエラーが発生しました: { e } 。ステータスコード: { e.status_code } " ,
"is_error" : True ,
}
except Exception as e:
return {
"type" : "tool_result" ,
"tool_use_id" : tool_use_block.id,
"content" : f "予期しないエラー: { e } " ,
"is_error" : True ,
}
is_error: true を受け取った Claude は、「そのツールは失敗した」という前提で次の一手を選びます。天気 API が 503 を返したと伝えれば、取得できない旨を断ったうえで季節の一般的な傾向に切り替える、といった具合です。大規模カタログでは、Tool Search が選び出したツールが必ずしも実行可能とは限りません。だからこそ、失敗を正直に返す経路をループの標準装備にしておく価値があります。
もう一つ、実行前の入力バリデーションも欠かせません。Claude は整数を文字列として渡してくることがあり、そのまま計算へ流すと実行時例外になります。
def execute_tool_safely (tool_name: str , tool_input: dict ) -> str :
"""ツール実行前に入力の型を検証・補正する"""
if tool_name == "calculate_price" :
quantity = tool_input.get( "quantity" )
if isinstance (quantity, str ):
try :
quantity = int (quantity)
except ValueError :
return json.dumps({ "error" : "quantityは整数値を指定してください" })
unit_price = tool_input.get( "unit_price" , 0 )
return json.dumps({ "total" : quantity * unit_price})
return json.dumps({ "error" : f "未知のツール: { tool_name } " })
セキュリティのベストプラクティスとして先に挙げた「入力は必ず JSON Schema でバリデーション」は、こうした型の揺れを実行前に吸収するための備えでもあります。is_error で失敗を伝える経路と、実行前に入力を整える経路。私はこの二つを、エージェントを組むときに最初にループへ入れるようにしています。ツール数が増えても挙動を予測可能なまま保てるからです。
まとめと次のステップ
ここまでで、Claude API の高度なツール使用機能を3つとも実装してきました:
Tool Search Tool :defer_loading で初期コンテキストからツール定義を追い出し、大規模カタログに対応する
Programmatic Tool Calling :allowed_callers でコード内実行を許可し、中間結果をコンテキスト外に留める
Tool Use Examples :input_examples でスキーマに書けない作法を示し、パラメータの誤りを減らす
そしてもう一つ、実装より先に効いた学びがあります。公表されている削減率は、それぞれ違うものを測っている という点です。37% をレイテンシだと読んだまま計測すると、実際に効いている改善を見落とします。数字を借りる前に、自分の環境で usage.input_tokens を一度測る。手間は数分ですが、この数分が見積もりの前提を決めます。
いま手元にエージェントがあるなら、まずはツール定義だけで何トークン払っているかを確認してみてください。そこが1万トークンを超えていれば、Tool Search Tool から着手する価値があります。超えていなければ、この3機能はまだ後回しで構いません。
次のステップとして、以下の記事も参考にしてください:
参照した一次情報
ベータ期間中の機能のため、フィールド名や既定の挙動は変更される可能性があります。実装前に一次情報をご確認ください。
長い記事にお付き合いいただき、ありがとうございました。数字の読み違いは私自身のつまずきでしたので、同じ回り道を省く手がかりになれば嬉しく思います。