LLM アプリ開発 #6 ツール呼び出しで外部機能を連携

読了 5分

第5回まで、Claude はテキストや構造化されたデータを「答え」として返してきました。ところが Claude は単独では今日の天気を知らず、私たちのデータベースをのぞくこともできません。学習した知識の中でしか答えられないのです。今回は、Claude に私たちが定義した関数を直接呼び出させて、外の世界とつなぐツール呼び出しを扱います。

ツール呼び出しとは #

流れはこうです。Claude に使えるツール(関数)の一覧と、各ツールの説明を伝えます。Claude は質問に答える途中でツールが必要だと判断すると、自分で関数を実行する代わりに「get_weather ツールを city=ソウル で呼んで」と要求します。その要求を受けて私たちのコードが実際に関数を実行し、結果を Claude に返します。Claude はその結果を見て最終的な答えを作ります。

肝心なのは、Claude が関数を自分で実行はしないという点です。Claude は「このツールをこの引数で呼びたい」という意思だけを表し、実行は私たちの役目です。

ツールを定義して呼び出す #

ツールは名前、説明、入力スキーマで定義します。入力スキーマは第5回で見た JSON スキーマと同じ形式です。

tool_define.py
import anthropic

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 f"{city}の現在の天気は晴れ、気温22度です。"

messages = [{"role": "user", "content": "ソウルの天気はどう?"}]

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)

print(response.stop_reason)  # tool_use

description は単なる説明ではありません。Claude はこの説明を見て、いつこのツールを使うかを判断します。ですからツールが何をするかだけでなく、どんな場面で使うかを明確に書くことが重要です。

天気を尋ねたので、Claude は直接答えずにツールを呼ぼうとします。このとき stop_reasontool_use で返り、応答に tool_use ブロックが入ります。

結果を返して仕上げる #

tool_use ブロックには、ツール名(name)と引数(input)、そして識別子(id)が入っています。私たちはその引数で関数を実行し、結果を tool_result で返します。このとき tool_use_id で、どの呼び出しに対する結果かを対応づけます。

tool_loop.py
while response.stop_reason == "tool_use":
    # ツール呼び出しを含むアシスタント応答を先に履歴へ追加する
    messages.append({"role": "assistant", "content": response.content})

    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            if block.name == "get_weather":
                result = get_weather(**block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result,
            })

    # ツール結果は user ロールで返す
    messages.append({"role": "user", "content": tool_results})

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        messages=messages,
    )

print(next(b.text for b in response.content if b.type == "text"))

while 文で囲んだ理由があります。Claude はツールを一度だけ使って終わるとは限りません。天気を確認したあと、別のツールをさらに呼ぶこともあります。そこで stop_reasontool_use のあいだは回り続け、Claude がツールを呼ぶのをやめて自然に答えを終える(end_turn)まで繰り返します。

抜かしやすいのは、ツール呼び出しを含むアシスタント応答を必ず履歴に戻す(messages.append)という点です。そうしてこそ Claude は、自分がどのツールを呼んだかを覚えて結果と結びつけます。

ツールランナーで簡単に #

このループを毎回手で書く代わりに、SDK のツールランナーを使えば、呼び出しと実行、繰り返しを自動で処理してくれます。@beta_tool デコレータで関数を定義すれば、入力スキーマも関数シグネチャから自動で作られます。

tool_runner.py
from anthropic import beta_tool

@beta_tool
def get_weather(city: str) -> str:
    """指定した都市の現在の天気を取得する。

    Args:
        city: 都市名。
    """
    return f"{city}の現在の天気は晴れ、気温22度です。"

runner = client.beta.messages.tool_runner(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=[get_weather],
    messages=[{"role": "user", "content": "ソウルの天気はどう?"}],
)

for message in runner:
    print(message)
注記
ツールランナーは現在ベータ機能です。ループを自分で制御する必要があるとき、たとえばツールを実行する前にユーザーの確認を取ったり、呼び出しを記録したりするときは、前述の手動ループを使います。そうでない一般的な場合は、ツールランナーが便利です。

よくつまずくところ #

  • tool_use_id を合わせないtool_resulttool_use_id は、その結果が応答する tool_use ブロックの id と正確に同じでなければなりません。ツールを複数呼ぶなら、それぞれ対応づけます。
  • アシスタント応答を履歴に入れない — ツール呼び出しを含む応答を messages に戻さないと、次の呼び出しで Claude が自分の呼び出しと結果を結びつけられず、エラーになります。
  • 説明が不十分description があいまいだと、Claude がツールを誤った場面で呼んだり、まったく呼ばなかったりします。何をするツールか、いつ使うべきかを明確に書きます。

まとめ #

今回は、Claude を外部機能とつなぐツール呼び出しを扱いました。

  • 私たちがツールを定義すると、Claude はツールを呼ぶ意思を tool_use で表し、実行は私たちが行います。
  • 実行結果を tool_resulttool_use_id に合わせて返し、stop_reasontool_use のあいだはループを回します。
  • ツールランナーを使えば、このループを自動で処理できます。

これで Claude が外部機能を使えるようになりました。次回の「LLM アプリ開発 #7 埋め込みとベクトル検索」では向きを変えて、私たちの文書を Claude が検索できる形に変える埋め込みを扱います。これは次回の RAG につながる準備の段階です。

X