【第4回】Claude API Python 実践入門 — 実践アプリ構築とよくある落とし穴・チューニングTips

AI

前回はマルチターン会話の実装とシステムプロンプト設計を紹介しました。今回はシリーズ最終回として、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 ブロックから nameidinput を抽出して対応する関数を実行します。そして 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_idblock.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: anytool_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_idblock.id を正しく紐付ける
  • 並列呼び出しを想定response.contenttool_use を全部ループで処理する
  • description の質が精度に効く:例・単位・形式まで書くと安定しやすい
  • エラーはClaudeに返す:握りつぶすとループが迷子になりやすい

4回にわたってClaude API × Pythonの基礎から実践まで書いてきましたが、正直まだ「これで完璧」とは全然思っていなくて、複数ツールを組み合わせた複雑なエージェントの設計や、AWS Lambdaに乗せてサーバーレスで動かす構成など、やりたいことはまだたくさんあります。

📚 シリーズ「Claude API Python 実践入門」(第4回 / 全4回)

← 前回の記事: 前回の記事はこちら

🎉 このシリーズは今回で完結です!

参考になったらクリックしてもらえると嬉しいです!

Blogmura AIAI Ranking
タイトルとURLをコピーしました