LLM アプリ開発 #6 ツール呼び出しで外部機能を連携
第5回まで、Claude はテキストや構造化されたデータを「答え」として返してきました。ところが Claude は単独では今日の天気を知らず、私たちのデータベースをのぞくこともできません。学習した知識の中でしか答えられないのです。今回は、Claude に私たちが定義した関数を直接呼び出させて、外の世界とつなぐツール呼び出しを扱います。
ツール呼び出しとは #
流れはこうです。Claude に使えるツール(関数)の一覧と、各ツールの説明を伝えます。Claude は質問に答える途中でツールが必要だと判断すると、自分で関数を実行する代わりに「get_weather ツールを city=ソウル で呼んで」と要求します。その要求を受けて私たちのコードが実際に関数を実行し、結果を Claude に返します。Claude はその結果を見て最終的な答えを作ります。
肝心なのは、Claude が関数を自分で実行はしないという点です。Claude は「このツールをこの引数で呼びたい」という意思だけを表し、実行は私たちの役目です。
ツールを定義して呼び出す #
ツールは名前、説明、入力スキーマで定義します。入力スキーマは第5回で見た JSON スキーマと同じ形式です。
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_usedescription は単なる説明ではありません。Claude はこの説明を見て、いつこのツールを使うかを判断します。ですからツールが何をするかだけでなく、どんな場面で使うかを明確に書くことが重要です。
天気を尋ねたので、Claude は直接答えずにツールを呼ぼうとします。このとき stop_reason が tool_use で返り、応答に tool_use ブロックが入ります。
結果を返して仕上げる #
tool_use ブロックには、ツール名(name)と引数(input)、そして識別子(id)が入っています。私たちはその引数で関数を実行し、結果を tool_result で返します。このとき tool_use_id で、どの呼び出しに対する結果かを対応づけます。
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_reason が tool_use のあいだは回り続け、Claude がツールを呼ぶのをやめて自然に答えを終える(end_turn)まで繰り返します。
抜かしやすいのは、ツール呼び出しを含むアシスタント応答を必ず履歴に戻す(messages.append)という点です。そうしてこそ Claude は、自分がどのツールを呼んだかを覚えて結果と結びつけます。
ツールランナーで簡単に #
このループを毎回手で書く代わりに、SDK のツールランナーを使えば、呼び出しと実行、繰り返しを自動で処理してくれます。@beta_tool デコレータで関数を定義すれば、入力スキーマも関数シグネチャから自動で作られます。
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_resultのtool_use_idは、その結果が応答するtool_useブロックのidと正確に同じでなければなりません。ツールを複数呼ぶなら、それぞれ対応づけます。- アシスタント応答を履歴に入れない — ツール呼び出しを含む応答を
messagesに戻さないと、次の呼び出しで Claude が自分の呼び出しと結果を結びつけられず、エラーになります。 - 説明が不十分 —
descriptionがあいまいだと、Claude がツールを誤った場面で呼んだり、まったく呼ばなかったりします。何をするツールか、いつ使うべきかを明確に書きます。
まとめ #
今回は、Claude を外部機能とつなぐツール呼び出しを扱いました。
- 私たちがツールを定義すると、Claude はツールを呼ぶ意思を
tool_useで表し、実行は私たちが行います。 - 実行結果を
tool_resultでtool_use_idに合わせて返し、stop_reasonがtool_useのあいだはループを回します。 - ツールランナーを使えば、このループを自動で処理できます。
これで Claude が外部機能を使えるようになりました。次回の「LLM アプリ開発 #7 埋め込みとベクトル検索」では向きを変えて、私たちの文書を Claude が検索できる形に変える埋め込みを扱います。これは次回の RAG につながる準備の段階です。