【第3回】Claude API Python 実践入門 Season2 — ツール呼び出し(Function Calling)で外部連携を実装する

AI

前回はマルチターン会話とシステムプロンプトの使い方を紹介しました。今回はいよいよ Tool Use(ツール呼び出し)です。個人的にこれが一番「Claude APIっぽいな」と感じる機能で、使いこなせると一気にできることが広がります。

きっかけはシンプルで、LambdaとDynamoDBを使った処理をClaudeに判断させたかったんですよね。「今日の注文件数を調べて」みたいな自然言語の指示で、ClaudeがDB照会まで動いてくれたら最高だな、と。それがそのままこの記事のネタになっています。

この記事でわかること

  • Claude APIのTool Use(ツール呼び出し)の仕組み
  • Pythonを使った最小構成の実装方法
  • 実装時のハマりポイント3つ
  • 複数ツールの定義と使い分け
  • tool_choiceパラメータの活用
  • DynamoDBとの連携実例

Tool Useとは何か、まず仕組みを整理する

「AIが関数を実行する」という説明をよく見かけますが、厳密にはちょっと違います。ClaudeはAI単体でコードを実行するわけではなくて、「この関数を呼んでほしい」という意思(tool_useブロック)を返してくるだけ。実際に関数を動かすのはこちら側(Python)の役目です。

流れをざっくり書くとこうなります。

  1. ツールの定義(名前・説明・引数のスキーマ)をAPIリクエストに含めて送る
  2. Claudeが「このツールを使うべき」と判断したら stop_reason: "tool_use" で返ってくる
  3. レスポンスのツール名と引数を使って、こちら側で関数を実行する
  4. 実行結果を tool_result としてClaudeに送り返す
  5. Claudeが結果を踏まえて最終回答を生成する

最初に知ったとき「結局自分でif文書くの…?」と思ったんですが、「Claudeがどのツールをいつ呼ぶかを判断してくれる」という部分がかなり便利で、使ってみると考え方が変わりました。

最小構成で動かしてみる

まずはシンプルな天気取得ツールを例に、基本の実装を確認します。

ツールの定義

ツールは辞書形式で定義します。一般的には namedescriptioninput_schema といった情報を持たせます。descriptionの質がClaudeの判断精度に直結するので、ここは丁寧に書いた方がいいです。

import os
import json
import anthropic
from dotenv import load_dotenv

load_dotenv()
client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "指定した都市の現在の天気と気温を返します。天気に関する質問が来た場合はこのツールを使ってください。",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "都市名(例: Tokyo, Osaka)"
                }
            },
            "required": ["city"]
        }
    }
]

description は日本語でも動くことが多いですが、英語の方が精度が上がることもあるようです(自分の体感)。チームやコードベースの事情に合わせて判断するのが無難かと。

ツールの実装と呼び出しループ

ツールの実体はただのPython関数です。今回はモックデータで返しますが、実際はここで外部APIやDBを叩きます。

def get_weather(city: str) -> str:
    # 実際は外部APIを呼ぶ
    mock_data = {
        "Tokyo": {"temp": 22, "condition": "晴れ"},
        "Osaka": {"temp": 24, "condition": "くもり"},
    }
    data = mock_data.get(city, {"temp": "不明", "condition": "不明"})
    return json.dumps({"city": city, **data}, ensure_ascii=False)


def run_tool(name: str, input_data: dict) -> str:
    if name == "get_weather":
        return get_weather(**input_data)
    return json.dumps({"error": f"unknown tool: {name}"})
def chat_with_tools(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]

    for _ in range(5):  # 無限ループ防止
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1024,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            # ツールを使わず直接回答した場合
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text

        if response.stop_reason == "tool_use":
            # assistantのレスポンスをそのままmessagesに追加
            messages.append({"role": "assistant", "content": response.content})

            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,  # ここ注意: block.id と一致させる
                        "content": result,
                    })

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

        break

    return "(応答を取得できませんでした)"


print(chat_with_tools("東京の天気を教えて"))

実行すると「東京の現在の天気は晴れ、気温は22℃です。」のような回答が返ってきます。

ハマりポイント3つ

実際に書いてみてつまずいた箇所をまとめておきます。

① tool_use_id はブロックの id と完全一致させる

tool_resultを返すとき、tool_use_idにはClaudeが発行した block.id を渡す必要があります。ここがズレると「どのツール呼び出しの結果かわからない」状態になってエラーになります。自前でIDを生成しようとして詰まりました。

② tool_result は role: “user” の content 配列に入れる

これが地味に落とし穴でした。ツール結果を返すとき、role: "user"content 配列の中に tool_result ブロックを並べる必要があります。さらに、その前に assistant 側の tool_use メッセージが先に messages に追加されている必要があります。順序を間違えるとエラーになることがあります。

③ ループの上限を設定する

Tool Use が連続して起きるケース(Claudeが複数のツールを順番に呼ぶ場合など)では、while True だと無限ループのリスクがあります。上のコードでは for _ in range(5) で上限を設けています。実運用では適切な回数を検討してください。

複数ツールを定義して使い分けてもらう

Tool Use の真価は複数ツールを並べたときに出てきます。Claudeがユーザーの質問内容を見て、どのツールを呼ぶか(あるいは呼ばないか)を判断してくれます。

tools = [
    {
        "name": "get_weather",
        "description": "指定した都市の現在の天気と気温を返します。",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "都市名"}
            },
            "required": ["city"]
        }
    },
    {
        "name": "get_stock_price",
        "description": "指定した銘柄コードの現在の株価を返します。",
        "input_schema": {
            "type": "object",
            "properties": {
                "symbol": {"type": "string", "description": "銘柄コード(例: AAPL, 7203)"}
            },
            "required": ["symbol"]
        }
    },
    {
        "name": "get_news",
        "description": "指定したトピックの最新ニュースを3件返します。",
        "input_schema": {
            "type": "object",
            "properties": {
                "topic": {"type": "string", "description": "検索するトピック"}
            },
            "required": ["topic"]
        }
    }
]

「Appleの株価と最近のニュースを教えて」と聞くと、状況によってはClaudeが get_stock_priceget_news を順番に(もしくは並列で)呼び出そうとします。天気には関係ないので get_weather は呼ばれません。ここが面白いところで、条件分岐を自分で書かなくていい。

余談ですが、description の書き方で Claudeのツール選択が結構変わります。「〜に関する質問が来た場合はこのツールを使ってください」みたいに具体的に書くと、意図した通りに動きやすくなる印象があります。

tool_choice で呼び出しを制御する

デフォルトでは Claudeが自律的にツールを使うかどうかを判断しますが、tool_choice パラメータで制御もできます。

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "any"},   # ツールを必ず使う
    # tool_choice={"type": "auto"}, # デフォルト(Claudeが判断)
    # tool_choice={"type": "tool", "name": "get_weather"}, # 特定ツールを強制
    messages=messages,
)

type: "any" は定義済みのツールの中から必ず何か使わせたいとき。type: "tool" で名前を指定すると、そのツールを強制的に呼ばせることもできます。テストや特定ユースケースで使うことがあります。

正直、これを使う場面はまだそこまで多くないですが、挙動を制御したいときには便利です。

実用例:DynamoDBのデータをClaudeに取ってこさせる

最初に書いた「LambdaからDynamoDBのデータをClaudeに判断させたい」という動機で作った例です。実際のコードに近い構成で書いています。

import boto3
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1")
table = dynamodb.Table("Orders")


def query_orders(date: str, status: str = None) -> str:
    kwargs = {
        "IndexName": "DateIndex",
        "KeyConditionExpression": Key("order_date").eq(date),
    }
    if status:
        kwargs["FilterExpression"] = "order_status = :s"
        kwargs["ExpressionAttributeValues"] = {":s": status}

    res = table.query(**kwargs)
    return json.dumps({
        "date": date,
        "count": res["Count"],
        "items": res["Items"][:5]  # 先頭5件だけ返す
    }, ensure_ascii=False, default=str)


tools = [
    {
        "name": "query_orders",
        "description": "指定した日付の注文データをDynamoDBから取得します。ステータスで絞り込みも可能です。",
        "input_schema": {
            "type": "object",
            "properties": {
                "date": {
                    "type": "string",
                    "description": "注文日(YYYY-MM-DD形式)"
                },
                "status": {
                    "type": "string",
                    "description": "注文ステータス(例: pending, shipped, delivered)。省略可。"
                }
            },
            "required": ["date"]
        }
    }
]

「今日の発送済み注文って何件ある?」という自然な質問に対して、Claudeが日付とステータスを解釈して query_orders を呼び出し、結果を人間が読みやすい形で回答してくれます。実際に動かしてみたときは「あ、これ便利だな」と素直に思いました。

ただ、IAMの権限周りとかLambdaでの実行環境の設定はそれなりに面倒なので、そこは別途きちんと整理する必要があります。

※この記事にはプロモーションが含まれます

ちなみに、Aiarty Image Enhancer(AI画像高画質化ツール。ノイズ除去・8倍拡大に対応)も気になっています。Aiarty Image Enhancer

まとめ

Tool Useは仕組みを理解してしまえばシンプルです。「Claudeが判断してこちらが実行する」という分業の構造さえ押さえれば、あとはツールの定義と関数の実装を積み上げるだけ。複数のツールを組み合わせることで、自然言語で複雑な処理を指示できる柔軟性が生まれます。

次のステップとしては、エラーハンドリングを強化したり、ツールの結果をもう一度Claudeに読ませて精査させるなど、さらに信頼性を高めた実装も考えられます。まずは基本の流れを押さえて、自分のユースケースに合わせて拡張していくのが良さそうです。

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

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

→ 次回の記事: 公開後にリンクが追加されます

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

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