Claude API の Tool Use で外部データを取得する

概要

Claude API 単体では、モデルは学習時点までの知識しか持たず、リアルタイムの天気・在庫・社内DBの値といった外部データを答えられません。この壁を越える仕組みが Tool Use(ツール使用) です。

この記事では、Python SDK を使って「Claude がツールを呼び出す → アプリ側で外部データを取得 → 結果を返す → Claude が最終回答を作る」という往復ループをハンズオンで組み立てます。複数ツール・エラー処理・プロンプトキャッシュまで扱います。内容は公開情報に基づく一般的な実装パターンのみです。

環境

  • Python 3.10+
  • anthropic SDK(1.x 系)/外部 API 用に requests
  • モデル: claude-sonnet-4-6
pip install anthropic requests
export ANTHROPIC_API_KEY="(自分のキー)"

Tool Use の全体像

Tool Use は「Claude が関数を実行してくれる」機能ではありません。実際にコードを動かすのはアプリ側で、Claude は「このツールを、この引数で呼んでほしい」という要求を返すだけです。

  1. アプリがツール定義を添えてメッセージを送る
  2. Claude が stop_reason: "tool_use" と tool_use ブロックを返す
  3. アプリがツールを実行し、tool_result を会話履歴に追加して再送する
  4. Claude が結果を踏まえて最終回答を返す

この往復をアプリ側のループで回すのが肝です。1回の messages.create では完結しません。

まず最小構成:ツール定義と呼び出し要求

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "指定した都市の現在の天気を取得する。気温は摂氏で返る。",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "都市名。英語表記(例: Tokyo, Osaka)",
                }
            },
            "required": ["city"],
        },
    }
]

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "東京の天気を教えて"}],
)

for block in response.content:
    if block.type == "tool_use":
        print(block.name, block.input, block.id)
        # get_weather {'city': 'Tokyo'} toolu_...

description は Claude がツールを選ぶ判断材料そのものです。何を返すツールか・単位・前提・使いどころまで具体的に書くと精度が安定します。

外部データ取得を実装する

import requests

def get_weather(city: str) -> dict:
    geo = requests.get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params={"name": city, "count": 1}, timeout=10,
    ).json()
    if not geo.get("results"):
        return {"error": f"都市が見つかりません: {city}"}

    loc = geo["results"][0]
    w = requests.get(
        "https://api.open-meteo.com/v1/forecast",
        params={
            "latitude": loc["latitude"],
            "longitude": loc["longitude"],
            "current": "temperature_2m,weather_code",
        }, timeout=10,
    ).json()["current"]

    return {
        "city": loc["name"],
        "temperature_c": w["temperature_2m"],
        "weather_code": w["weather_code"],
    }

社内 API でも DB クエリでも、関数の中身が変わるだけで構造は同じです。

往復ループを組む

import json

def run_conversation(user_message: str, max_turns: int = 10) -> str:
    messages = [{"role": "user", "content": user_message}]

    for _ in range(max_turns):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=tools,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            return "".join(b.text for b in response.content if b.type == "text")

        tool_results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            try:
                if block.name == "get_weather":
                    result = get_weather(**block.input)
                else:
                    result = {"error": f"unknown tool: {block.name}"}
            except Exception as e:
                result = {"error": f"tool execution failed: {e}"}

            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(result, ensure_ascii=False),
                "is_error": "error" in result,
            })

        messages.append({"role": "user", "content": tool_results})

    return "(最大ターン数に到達しました)"


print(run_conversation("東京とニューヨークの天気を比べて"))

実装で押さえるべき3点です。

  • assistant の content は丸ごと履歴に戻す。分解して入れ直すと tool_use_id の対応が壊れます。
  • tool_result は role: "user" で送る。assistant にすると API エラーになります。
  • 1応答に複数の tool_use が入りうる(「東京とニューヨーク」で天気APIを2回)。全部処理して全部まとめて返します。返し漏らすとエラーになります。

エラーの扱いと tool_choice

ツール実行の失敗は例外で落とさず、エラー内容を tool_result として返すのが実務的です。is_error: true を付けると、Claude は別の引数で再試行したり、ユーザーに確認したりと自己修正できます。

tool_choice の既定は auto(Claude が判断)。必ず呼ばせたいときは {"type": "any"}、特定ツール固定は {"type": "tool", "name": "..."}、禁止は {"type": "none"} です。エラーが続くと往復が止まらないため、最大反復回数の上限は必須です。

プロンプトキャッシュでコストを抑える

ツール定義やシステムプロンプトが大きい場合、tools の末尾要素に cache_control を付けると定義部がキャッシュされ、往復のたびのトークンコストと遅延を抑えられます。

tools = [
    {"name": "get_weather", "description": "...",
     "input_schema": {...},
     "cache_control": {"type": "ephemeral"}}
]

確認方法

  • 1周目で stop_reason == "tool_use"、block.input が schema 通りの型になること
  • 2周目以降で stop_reason == "end_turn" に変わり最終テキストが得られること
  • 存在しないツール名や引数不足のとき、is_error を見て Claude が自己修正すること

毎周の messages を出力すると、履歴の積み方の誤り(tool_result 付け忘れ・ロール違い)がすぐ見えます。

まとめ

  • Tool Use は Claude が要求し、実行はアプリが担う仕組み
  • tool_use → 実行 → tool_result 再送 を stop_reason が変わるまでループで回す
  • assistant 応答は丸ごと戻す/tool_result は user ロール/複数呼び出しは全部返す
  • エラーは is_error で返すと自己修正が効く/大きな定義は cache_control でキャッシュ

まず1ツール1往復を動かし、そこから複数ツールとエラー処理を足していくのが、拡張しやすく確実です。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。