Claude にツールを 1 個渡して動かすのは簡単です。難しいのは、その先です。ツールが複数同時に呼ばれる、引数が欠けている、バックエンドが例外を投げる、トークン課金が読めない——こうした条件が重なる本番環境で、クライアント側ツール実行のループは静かに崩れます。本稿(2026-10-09 検証)は、Claude API のクライアントツールを「ゼロから完成まで通る 1 本の実装」として組み立て、ラウンドトリップの正しい形、strict と tool_choice、並列ツール使用、本番で詰まる箇所、そして運用設計までを一通り示します。
クライアントツールとサーバーツールの境界
まず「なぜ自分で実行コードを書く必要があるのか」を押さえます。ツールには実行場所の違う 2 種類があります。クライアントツール(ユーザー定義ツール、および bash や text_editor のような Anthropic 定義スキーマを持つツール)は、あなたのアプリケーション内で実行されます。Claude は stop_reason: "tool_use" と 1 つ以上の tool_use ブロックを返し、あなたのコードが操作を実行して tool_result を送り返します。
一方、web_search・web_fetch・code_execution・tool_search などのサーバーツールは Anthropic のインフラ上で実行され、結果が直接返るため、アプリケーション側に実行ハンドラーは不要です。つまり「実装の責務が発生するのはクライアントツール側だけ」です。本稿が対象とするのはこちらです。
ラウンドトリップの正しい実装
クライアントツールの基本は 1 往復です。レスポンスが tool_use ブロックを運び、あなたのコードがルックアップを実行し、二度目のリクエストで tool_result ブロックに結果を乗せて送り返すと、Claude が答えを返します。この「二度目のリクエスト」を正しく組むことが本番品質の分かれ目です。
空ディレクトリから作ります。
mkdir order-agent && cd order-agent
python -m venv .venv && source .venv/bin/activate
echo "anthropic>=0.40" > requirements.txt
pip install -r requirements.txt
export ANTHROPIC_API_KEY=sk-ant-...
tools.py(ツール定義とディスパッチ層):
# tools.py
TOOLS = [
{
"name": "get_order_status",
"description": (
"Look up the current status of a customer order by its "
"order ID. Use when the user asks about shipping or delivery."
),
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string",
"description": "The order ID, e.g. 'A-10293'."}
},
"required": ["order_id"],
"additionalProperties": False,
},
"strict": True,
}
]
_ORDERS = {
"A-10293": {"status": "shipped", "carrier": "Yamato", "eta": "2026-10-11"},
"A-55012": {"status": "processing", "carrier": None, "eta": None},
}
def get_order_status(order_id: str) -> dict:
order = _ORDERS.get(order_id)
if order is None:
return {"error": "not_found", "order_id": order_id}
return {"order_id": order_id, **order}
HANDLERS = {"get_order_status": get_order_status}
def run_tool(name: str, args: dict) -> dict:
handler = HANDLERS.get(name)
if handler is None:
return {"error": "unknown_tool", "name": name}
try:
return handler(**args)
except Exception as e:
return {"error": "tool_exception", "detail": str(e)}
agent.py(ラウンドトリップ・ループ):
# agent.py
import json
from anthropic import Anthropic
from tools import TOOLS, run_tool
client = Anthropic()
MODEL = "claude-opus-5-5"
MAX_TURNS = 8 # 無限ループ防止の上限
def chat(user_text: str) -> str:
messages = [{"role": "user", "content": user_text}]
for _ in range(MAX_TURNS):
resp = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=TOOLS,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
if resp.stop_reason != "tool_use":
return "".join(b.text for b in resp.content if b.type == "text")
# 1) アシスタントのターン(tool_use を含む)をそのまま追加する
messages.append({"role": "assistant", "content": resp.content})
# 2) 各 tool_use を実行し、tool_use_id ごとに tool_result を作る
tool_results = []
for block in resp.content:
if block.type != "tool_use":
continue
print(f"[tool] {block.name}({block.input})")
result = run_tool(block.name, block.input)
print(f"[result] {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False),
"is_error": "error" in result,
})
# 3) 二度目のリクエストで結果を返す(次ループで送信)
messages.append({"role": "user", "content": tool_results})
return "max turns reached"
if __name__ == "__main__":
print(chat("注文 A-10293 はいまどこですか?"))
動作確認と期待される挙動
python agent.py を実行します。終了コードは 0、標準出力にはまずディスパッチ層のログが出ます。[tool] / [result] の行は自分の print 由来なので内容は決まっています(order_id の値は Claude のツールコールに依存するため、通常は入力した注文 ID になります)。成功時の出力はおおよそ次の形です。
[tool] get_order_status({'order_id': 'A-10293'})
[result] {'order_id': 'A-10293', 'status': 'shipped', 'carrier': 'Yamato', 'eta': '2026-10-11'}
(最後に Claude の自然文の回答が 1 行。配送済み・Yamato・2026-10-11 を踏まえた文面が返る)
成功の見分け方は 3 つです。(1) [result] に error キーが無い、(2) ループが 2 周目で stop_reason != "tool_use" に落ちて自然文が返る、(3) 存在しない ID(例: A-99999)を渡すと [result] が {'error': 'not_found', ...} になり、is_error: True の tool_result を受けて Claude が「見つからない」旨を返す。最後の自然文は毎回同一とは限らないため、断定はできません。
引数欠落の挙動はモデルで差が出ます。「私の注文はどこ?」のように ID 無しで尋ねた場合、Claude Opus は不足パラメータを認識して問い返す可能性が高く、stop_reason は tool_use ではなく end_turn(ID を尋ねる自然文)になりやすいです。Claude Sonnet は尋ねることもあれば、妥当な値を推測して埋めることもあります。この差は本番でのバリデーション設計に直結します。
スキーマを厳密化する: strict と tool_choice
ツールコールがスキーマ定義に正確に一致することを求めるには、ツール定義に strict: true を追加します。本稿では additionalProperties: false と併用していますが、strict 下で使えるスキーマ型・機能の範囲には制約があり得るため、適用前に公式ドキュメントの制約事項を確認してください。
ツール呼び出しを強制したい場合、プロンプトでお願いするのではなく tool_choice を設定するのが推奨です。既定の {"type": "auto"} は、リクエストがそのツールの説明された機能に対応し、かつ答えがまだコンテキスト内に無いときに呼び出します。分類器のように「必ず 1 回ツールを通す」設計なら auto ではなく明示指定が確実です。
並列ツール使用とステートフルなツール
既定では Claude は 1 レスポンスで複数の tool_use ブロックを返し得ます。これを無効化するには tool_choice に disable_parallel_tool_use: True を設定します。本稿の agent.py では読み取り専用の 1 ツールですが、あえて無効化しています。理由は設計判断で、ツールが状態を変更する(在庫を引き当てる、課金する)、または結果が次の呼び出しの入力になる依存関係がある場合、並列実行は競合や二重実行の温床になるからです。読み取りのみ・独立・冪等なツール群なら、並列を許可してレイテンシを稼ぐほうが合理的です。
本番で詰まる 6 つの箇所
-
アシスタントのターンを戻し忘れる:
tool_resultを送る前に、tool_useを含むアシスタントのcontentを会話履歴へそのまま追加する必要があります。これを省くと API エラーになります。 -
tool_use_idの取りこぼし: 並列で複数ブロックが来たとき、すべてのtool_use_idに対応するtool_resultを返さないとエラーになります。ループは「全tool_useを走査して漏れなく返す」構造にします。 -
ループの停止条件:
stop_reasonを見て終了判定しつつ、MAX_TURNSの上限を必ず置きます。モデルがツールを呼び続けるケースで無限ループを防ぎます。 -
例外を投げてループを殺す: ハンドラ内の失敗は raise せず、構造化した error を返し
is_error: Trueのtool_resultにします。Claude 側で回復(別 ID の確認、ユーザーへの質問)が可能になります。 -
contentを文字列化し忘れる:tool_resultのcontentは文字列です。dict はそのまま入れず JSON 文字列化します。 - コスト計上を見落とす: 後述。
設計:ディスパッチ層・タイムアウト・冪等性(私ならこう作る)
ここが公式ドキュメントの引き写しには無い部分です。あくまで設計上の意見として、事実と区別して述べます。
第一に、ツール実行は必ずディスパッチ registry(名前→関数の辞書)に通し、ループ本体から直接関数を呼ばない設計にします。ツールが増えても agent.py の制御フローは不変で、ハンドラの追加・テスト・権限チェックが 1 箇所に集約されます。本稿の HANDLERS と run_tool がその最小形です。
第二に、各ハンドラにタイムアウトを課します。外部 API を叩くツールが応答しないと、ループ全体が固まり、ユーザー体感と課金の両方が悪化します。run_tool 内で concurrent.futures などにより上限時間を設け、超過時は {"error": "timeout"} を返して Claude に判断を委ねます。
第三に、状態変更ツールは冪等キーを持たせます。並列を無効化しても、ネットワーク再送でハンドラが二度走ることはあり得ます。注文確定のような操作は idempotency_key を引数スキーマに含め、バックエンド側で重複を吸収する前提で組むのが安全です。
Tool Runner を使うか自前ループを書くかも設計判断です。Tool Runner を使うと SDK がツールを自動実行して結果を送り返すため、ラウンドトリップ実装を自前で書かずに済みます。独立・冪等なツール中心なら Tool Runner が速い。逆に、権限分岐・監査ログ・細かいタイムアウト・部分失敗時の挙動を自分で握りたいなら、本稿のような明示ループが向きます。利用可能な SDK とバージョン要件は公式ドキュメントで確認してください。
コストの見積もり
クライアント側ツールは通常の Claude API リクエストと同一料金で、サーバーツールのような使用量ベースの追加課金は発生しません。追加トークンが乗るのは 3 箇所です。tools パラメータ(名前・説明・スキーマ)、リクエスト/レスポンス双方の tool_use ブロック、そしてリクエストの tool_result ブロック。説明文を冗長にすると毎リクエストで課金されるため、ツール説明は必要十分に保ちます。
加えて、ツール使用のためのシステムプロンプトにも固定費があります。2026-10-09 時点の公式トークン数テーブルでは、tool_choice が auto または none のとき、Claude Opus 5.5 と Claude Sonnet 5.5 はいずれも 286 トークンです。ツールを一切渡さないケースで tool_choice を none にすれば、この追加分は 0 になります。
References
[1] https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview