前回はマルチターン会話の実装とシステムプロンプト設計を紹介しました。今回はシリーズ最終回として、Claude APIの目玉機能でもある Tool Use(ツール使用 / 関数呼び出し) をPythonで実装して実践的なアプリを組む話をします。あとよくハマるポイントと、ちょっとした速度・コスト改善Tipsも。
- Tool Use(ツール使用・関数呼び出し)の仕組みと基本フロー
- PythonでのエージェントループとTool Use実装コード
- よくある落とし穴3つとその対処法
- プロンプトキャッシュ・エラーハンドリングのチューニングTips
- MCPとの関係について
Tool Use(ツール使用・関数呼び出し)とは何か、なぜ使うのか
Tool Useは、Claude自身が「この処理は外部の関数に任せよう」と判断して呼び出してくれる仕組みです。「天気を教えて」と聞かれたときに自力で答えるのではなく、get_weather() を呼んで結果を使って答えてくれるイメージ。
ツール使用のリクエストに対してClaudeは stop_reason: tool_use を返し、1つ以上の tool_use コンテンツブロックを返します。このブロックには、後でツール結果を照合するための一意の id、ツール名、そして input_schema に準拠した入力値(input)が含まれます。
クライアント側でツールの結果を得たら、tool_use ブロックから name、id、input を抽出して対応する関数を実行します。そして tool_result タイプのコンテンツブロックを user ロールの新しいメッセージとして会話に追加して送り返します。このループを繰り返すのが基本的な実装パターンです。
実際に動くコードを書いてみる
まずシンプルなサンプルとして「天気取得ツール」を定義してみます。実際の外部APIは叩きませんが、関数呼び出しのフローを確認するには十分です。
import anthropic
import json
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "指定した都市の現在の天気を返す。",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "天気を知りたい都市名(例: 東京, 大阪)"
}
},
"required": ["city"]
}
}
]
def get_weather(city: str) -> str:
# ここは本来外部APIを叩く
return json.dumps({"city": city, "temp": "18°C", "condition": "晴れ"}, ensure_ascii=False)
def run_tool(name: str, tool_input: dict) -> str:
if name == "get_weather":
return get_weather(tool_input["city"])
raise ValueError(f"unknown tool: {name}")
次にエージェントループ。核心は while True ループと stop_reason のチェックで、これだけでも最小構成のエージェントが書けます。
注意: Claudeが tool_use を返した場合、ツール実行後は「同じ会話履歴 + tool_result を含む新しい user メッセージ」でもう一度 client.messages.create() を呼ぶ必要があります(1リクエスト内で完結はしない)。ここ、初見だとつい読み飛ばしがちです。
def agent_loop(user_message: str):
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
# まずassistantの返答(textやtool_useを含む)を履歴に追加
messages.append({"role": "assistant", "content": response.content})
# ツール呼び出しがなければ、そのままテキストを表示して終了
if response.stop_reason != "tool_use":
for block in response.content:
if getattr(block, "type", None) == "text":
print(block.text)
break
# tool_useがあれば、全部実行してtool_resultをまとめて返す
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # ここ注意: idの対応がズレると破綻します
"content": result,
})
# tool_resultは user ロールの新しいメッセージとして追加して次ループへ
messages.append({"role": "user", "content": tool_results})
agent_loop("東京と大阪の天気を教えて")
余談ですが、最初これを書いたとき tool_use_id と block.id の対応を間違えて30分溶かしました。ツールが複数返ってくる場合は特にIDの紐付けが大事です。
tool_choice でツール使用(関数呼び出し)を制御する
Tool Useを組み込んでいると「なんでここでツールを使わないんだ」とか逆に「毎回呼ぶな」という状況が出てきます。そこで使えるのが tool_choice パラメータです。
tool_choice には複数のオプションがあります。auto はClaudeが必要に応じて使うかどうかを判断するデフォルト動作で、any は提供したツールのうち必ず1つを使うよう強めに促します。さらに tool を指定すると「このツールを使ってね」を固定できます。
# ツールをいずれか必ず使わせたい場合
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "any"},
messages=messages,
)
# 特定のツールを必ず使わせたい場合
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"},
messages=messages,
)
注意: extended thinking と tool use を併用するケースでは、tool_choice: any や tool_choice: tool がサポートされずエラーになることがあるらしいです。そういうときは auto のままにして、ユーザーメッセージ側で「必ずこのツールを使って」と指示する方が安全っぽいです。
構造化データ抽出みたいな用途で「必ずこのスキーマで返してほしい」ときは tool_choice: tool が素直で便利です。個人的にはこの使い方が一番実用的かなと。
よくある落とし穴 3つ
① tool_result を返さずに次を送ってしまう
Claudeが tool_use を返したのに、それを無視して新しい user メッセージを送るとAPIがバリデーションエラーになることがあります。基本は「返ってきた tool_use に対して、対応する tool_result を返す」までが1セットです。
② 並列ツール呼び出しへの対応漏れ
「東京と大阪の天気を教えて」みたいなリクエストだと、Claudeが get_weather を2つ返してくることがあります。response.content の中に tool_use ブロックが複数入るイメージです。1つしか処理しない実装だと片方が抜け落ちます。
実装としては今回のサンプルみたいに response.content を走査して tool_use を全部拾う書き方が無難です。普通に実装していても複数の tool_use が返ってくることはあるので、最初からループで全部処理する形にしておくのが安全です。
③ input_schema の description が曖昧でハルシネーションが増える
description が短すぎると、Claudeが input に変な値を入れてくることがあります。「どんな値が入るか」「単位は何か」「例は何か」まで書いてあげると精度が上がります。
# 悪い例
"description": "都市名"
# 良い例
"description": "天気を取得したい都市名。日本語または英語で指定。例: 東京, Tokyo, 大阪"
地味ですが description の質がツール使用の精度にかなり効きます。正直ここは自分もまだ試行錯誤中で、「もっといい書き方があるはず」という感じがしています。
チューニングTips:トークンとエラー対応
ツール定義が多いときはプロンプトキャッシュを使う
ツールを10個とか定義していると、それだけでプロンプトが膨らみます。毎リクエストで同じツール定義を送るのはもったいないので、prompt caching を使うとコストとレイテンシが下がりやすいです。
キャッシュしたい”プロンプト側(例: ツール定義や固定の指示)”に cache_control を置くイメージです。ツール定義をキャッシュしたいなら、ツール定義のどこか(実運用だと最後のツール定義)に cache_control を付けて「ここまでをキャッシュしてね」の目印にします。
# ツール定義にキャッシュ制御を付ける例(ツール定義をキャッシュしたいとき)
tools_with_cache = [
{
"name": "get_weather",
"description": "...",
"input_schema": { ... },
"cache_control": {"type": "ephemeral"}
}
]
そういえば最近、ツール定義を増やしすぎて「この会話、ほぼJSON投げてるだけでは…?」って気持ちになりました。キャッシュ、精神衛生にも効きます。
エラーはClaudeに渡してリカバリさせる
ツールがエラーになった場合でも、tool_result で「エラーだったよ」を返すとClaude側が状況を理解して次の手を考えてくれます。早期にループを止めたい場合や、こちらで分岐したい場合はもちろんアプリ側で握ってもOKですが、黙って握りつぶすよりは”エラーとして返す”方が事故りにくい印象です。
tool_results = []
for block in response.content:
if block.type == "tool_use":
try:
result = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"エラーが発生しました: {str(e)}",
"is_error": True,
})
エラーを黙って握りつぶすと、Claudeが「ツールから何も返ってこない」という状態になってループが変な方向に進むことがあります。エラー内容をちゃんと渡してあげると、Claudeが「別の方法で試しましょうか」みたいな対応をしてくれます。
MCPとの関係について少しだけ
最近気になっているのがMCP(Model Context Protocol)です。MCPはAnthropicが提唱している「AIアプリがツールやデータソースに繋がるための標準プロトコル」みたいなやつで、Claude Desktop/Claude Codeみたいな”ホスト”が、MCPサーバーに接続してツールを呼び出す仕組みになっています。
MCPのツール定義とClaude APIの tools は似ている部分はあるものの、周辺フィールドややり取りのレイヤが異なるので「そのままコピペでOK」とは言いにくいです。「JSON Schemaを使う」という共通点が大きい、くらいの認識が安全かなと思っています。
Tool Useをちゃんと理解しておくと、MCPに触るときのメンタルモデル(モデルが”ツール呼び出し”を返して、ホストが実行して、結果を返す)がわりと共通なので、今のうちに基礎を押さえておくのはアリだと思っています。
※この記事にはプロモーションが含まれます
ちなみに、Aiarty Image Enhancer(AI画像高画質化ツール。ノイズ除去・8倍拡大に対応)も気になっています。Aiarty Image Enhancer![]()
まとめ
Claude API の Tool Use(ツール使用・関数呼び出し)は、「止まる → ツール実行 → 結果を返す → 続ける」というループを理解すれば、思ったより素直に動きます。Pythonでの実装ポイントをまとめると以下のとおりです。
- エージェントループ:
stop_reasonを見てtool_useならツール実行 →tool_resultを返して再リクエスト - IDの対応ズレに注意:
tool_use_idとblock.idを正しく紐付ける - 並列呼び出しを想定:
response.contentのtool_useを全部ループで処理する - description の質が精度に効く:例・単位・形式まで書くと安定しやすい
- エラーはClaudeに返す:握りつぶすとループが迷子になりやすい
4回にわたってClaude API × Pythonの基礎から実践まで書いてきましたが、正直まだ「これで完璧」とは全然思っていなくて、複数ツールを組み合わせた複雑なエージェントの設計や、AWS Lambdaに乗せてサーバーレスで動かす構成など、やりたいことはまだたくさんあります。

