ツール呼び出し(Tool Use)とストリーミング、どちらか片方は動いたのに、両方を組み合わせようとした途端に止まってしまった。そんな経験はないでしょうか。
Claude API を触り始めて最初にぶつかる壁の1つが、この「Tool Use × Streaming」の組み合わせです。どちらも理解しているつもりでも、ストリームのイベント処理が複雑で、思わぬ挙動をすることがあります。
Tool Use × Streaming の仕組みを整理する
なぜ組み合わせが難しいのかを理解するところから始めましょう。
通常のストリーミングでは、Claude が生成するテキストをチャンク単位で受け取ります。しかしツール呼び出しが発生すると、Claude は「テキスト生成を一時中断してツールを要求する」という特殊なレスポンスを返します。
ストリームで届くイベントの種別は主に以下の通りです。
content_block_start— テキストまたはツール呼び出しブロックの開始content_block_delta— テキストの差分、またはツール入力 JSON の断片content_block_stop— ブロック終了message_delta— stop_reason が含まれ、ツール呼び出し要求ならtool_use
この flow を正確に処理できるかどうかが、実装の成否を分けます。
ツール呼び出しの基本については Claude API Tool Use ガイド、ストリーミングと Tool Use の詳細な挙動については Streaming × Tool Use の実装詳解 も参考になります。
Python 環境のセットアップ
Python 3.10 以上と Anthropic SDK があれば始められます。
pip install anthropic>=0.40.0今回は2つのシンプルなツールを定義します。
get_current_time— 現在時刻を ISO 8601 形式で返す(引数なし)calculate— 数式を評価して数値を返す(expression引数あり)
シンプルなツールにすることで、ツール処理のロジックそのものに集中できます。
基本実装:ツール定義とストリーミング処理
まずはツールの定義と実行関数から見ていきます。
import anthropic
import json
import math
from datetime import datetime
from typing import Any
client = anthropic.Anthropic()
# ツール定義(JSON スキーマ形式)
TOOLS = [
{
"name": "get_current_time",
"description": "現在の日時を ISO 8601 形式で返します",
"input_schema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "calculate",
"description": "数式を評価して結果を返します。四則演算・冪乗・sqrt などに対応しています",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "評価する数式(例: '2 + 3 * 4', 'sqrt(16)')"
}
},
"required": ["expression"]
}
}
]
def execute_tool(name: str, inputs: dict[str, Any]) -> str:
"""ツールを実際に実行して結果文字列を返す"""
if name == "get_current_time":
return datetime.now().isoformat()
elif name == "calculate":
expr = inputs.get("expression", "")
try:
# 安全のため、使用できる関数を明示的に制限する
allowed_names = {
"sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
"tan": math.tan, "log": math.log, "abs": abs, "pi": math.pi
}
result = eval(expr, {"__builtins__": {}}, allowed_names)
return str(result)
except Exception as e:
return f"計算エラー: {str(e)}"
return f"未知のツール: {name}"次がメインのストリーミング処理です。ここが実装の核心です。
def chat_with_tools(messages: list, model: str = "claude-sonnet-4-6", depth: int = 0) -> str:
"""
ストリーミング + Tool Use でチャットを実行する。
ツール呼び出しが発生した場合は深さを増やして再帰的に処理する。
"""
if depth > 5:
return "(ツール呼び出しの上限に達しました)"
collected_text = []
tool_calls = []
current_tool = None
current_tool_input_raw = ""
with client.messages.stream(
model=model,
max_tokens=2048,
tools=TOOLS,
messages=messages
) as stream:
for event in stream:
event_type = event.type
if event_type == "content_block_start":
block = event.content_block
if block.type == "tool_use":
# ツール呼び出しブロックが始まった
current_tool = {"id": block.id, "name": block.name}
current_tool_input_raw = ""
elif event_type == "content_block_delta":
delta = event.delta
if delta.type == "text_delta":
# テキストをリアルタイムで表示しながら蓄積
print(delta.text, end="", flush=True)
collected_text.append(delta.text)
elif delta.type == "input_json_delta":
# ツール入力 JSON の断片を蓄積(完成してから parse する)
current_tool_input_raw += delta.partial_json
elif event_type == "content_block_stop":
if current_tool is not None:
# JSON が完成したのでパースしてツール呼び出しリストに追加
tool_input = json.loads(current_tool_input_raw) if current_tool_input_raw else {}
tool_calls.append({**current_tool, "input": tool_input})
current_tool = None
current_tool_input_raw = ""
# ツール呼び出しがなければテキストをそのまま返す
if not tool_calls:
return "".join(collected_text)
# ツール実行 → 結果を次のリクエストに渡す
print("\n[ツールを実行中...]")
# アシスタントの発言(テキスト + ツール呼び出し)を履歴に追加
assistant_content = []
if collected_text:
assistant_content.append({"type": "text", "text": "".join(collected_text)})
for tc in tool_calls:
assistant_content.append({
"type": "tool_use",
"id": tc["id"],
"name": tc["name"],
"input": tc["input"]
})
messages.append({"role": "assistant", "content": assistant_content})
# 各ツールを実行し、結果を tool_result として追加
tool_results = []
for tc in tool_calls:
result = execute_tool(tc["name"], tc["input"])
print(f" {tc['name']}({tc['input']}) → {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": tc["id"],
"content": result
})
messages.append({"role": "user", "content": tool_results})
# ツール結果を踏まえた最終回答を再帰的に取得
return chat_with_tools(messages, model, depth + 1)最後に、会話ループを作ります。
def main():
print("Claude AIアシスタント(ツール対応・ストリーミング版)")
print("終了: 'quit' または 'exit'\n")
messages = []
while True:
user_input = input("You: ").strip()
if not user_input or user_input.lower() in ("quit", "exit"):
break
messages.append({"role": "user", "content": user_input})
print("Assistant: ", end="", flush=True)
response = chat_with_tools(messages)
print()
# 最終応答を履歴に追加
messages.append({"role": "assistant", "content": response})
if __name__ == "__main__":
main()これを実行して「今何時ですか?」「3の15乗はいくつ?」などと聞いてみてください。Claude がツールを呼び出して正確に答えてくれます。
実装で詰まりやすいポイント
実際に書いてみると、いくつかの落とし穴があります。私が最初に詰まった箇所をまとめます。
ツール入力 JSON は断片で届く
input_json_delta イベントでは JSON が細切れで届きます。{"expression": "2** のような途中の状態です。必ず content_block_stop を待ってから json.loads() してください。
上記の実装では current_tool_input_raw に文字列として蓄積し、ブロック終了時にまとめてパースしています。これが最もシンプルなアプローチです。
テキストとツール呼び出しが混在することがある
Claude が「計算してみますね。」とテキストを少し出力してからツールを呼び出すことがあります。collected_text と tool_calls を別々に管理しているのはこのためです。アシスタントの発言として両方を履歴に含める必要があります。
eval() のセキュリティに注意
今回の calculate ツールでは eval() を使っています。allowed_names で利用可能な関数を明示的に制限しているので、単純な数式の評価であれば安全ですが、本番環境で外部ユーザーが入力する場合はさらに厳格な入力バリデーションを追加してください。
実用的なツールの追加例
基本の仕組みが理解できたら、ツールを増やしてみましょう。実際のアプリでよく組み込むのは以下のようなものです。
# 例:ファイル内容を読み込むツール
def read_file_tool(file_path: str) -> str:
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# 長すぎる場合はトリミング
return content[:5000] if len(content) > 5000 else content
except FileNotFoundError:
return f"ファイルが見つかりません: {file_path}"
except Exception as e:
return f"読み込みエラー: {str(e)}"このパターンで、データベース検索・外部 API 呼び出し・コード実行など、どんな処理もツールとして組み込めます。ツール定義の description を丁寧に書くことで、Claude が適切な場面でそのツールを呼び出すようになります。
Python SDK のより詳しい使い方については Python SDK 基本チュートリアル も参照してみてください。
まずはこのコードをそのまま動かしてみてください
仕組みを理解するには、手を動かすのが一番の近道です。まずは今回のコードをそのままコピーして動かしてみてください。
ツール定義の description を変えると Claude の挙動が変わること、ツール結果を受け取ってから Claude が最終回答を組み立てていること、その flow がストリームで可視化されていること。実際に動かすと、ドキュメントを読むだけでは掴みきれない感覚が身についてきます。
自分のユースケースに合ったツールが決まったら、それを追加するだけで一気に実用的なアシスタントに育ちます。