Claude に並列で複数のツールを呼ばせたら、1回のターンで3〜4個のツールが一気に実行されるようになって、レスポンスが体感で半分以下になる — ここまでは多くの記事が触れています。ですが本番に乗せた瞬間、「3つ中1つだけタイムアウトしたとき、残りの結果をどう扱うか」「DB 書き込みが同一レコードに2つ走ったらどうするか」といった現場の事故に直面することになります。
Parallel Tool Use は Claude の応答体験を大きく変える強力な機能ですが、並列度が上がった分、失敗パターンの組み合わせも増えるのが本番運用の実情です。公式ドキュメントは「使える」ところまでしか踏み込まないので、今回は私が実際にプロダクトで並列ツールを運用して得た知見をまとめ、制御・エラー設計・観測性の3つを軸に解説します。
なぜ Parallel Tool Use が効くのか — 数字で見るレイテンシ
並列ツール呼び出しがあるかどうかで、エージェントの応答時間は劇的に変わります。例えば「在庫確認」「価格取得」「配送料計算」を3つの API で並列実行するエージェントの場合、直列だと 200ms + 180ms + 150ms = 530ms ですが、並列なら max(200, 180, 150) = 200ms で終わります。Claude のレスポンス生成自体のオーバーヘッドを加えても、体感レイテンシは半分以下になります。
Anthropic の公式ブログでも、Claude 3.5 Sonnet 以降のモデルは「独立したツールは自動的に並列実行する方向に強化されている」と明記されています。ただし、「独立している」とモデルが判断するかはプロンプト設計に依存しますし、判断を誤らせると直列実行に戻って体感速度が落ちます。
並列が効く典型ケース
- 複数の外部 API を同時に叩ける検索・比較系(商品検索、フライト比較、ニュース取得)
- 同じ処理を異なる入力で一括実行するバッチ系(複数 URL のスクレイピング、複数ファイルの要約)
- 独立した計算を行う評価系(複数の評価軸でのスコアリング、複数モデルの並列問い合わせ)
並列が効きにくい・危険なケース
- 直前のツール結果を元に次のツールを決める連鎖処理(Search → Read → Analyze)
- DB 書き込みやファイル操作で同一リソースを触る可能性があるもの
- 外部サービスのレート制限が厳しく、並列度を絞りたいもの
Claude の並列実行を制御する2つの軸
Parallel Tool Use の制御軸は、実は「Claude 側の判断を制御する軸」と「アプリケーション側の実行を制御する軸」の2つに分かれています。この区別が曖昧だと、思ったように並列が効きません。
軸1: Claude の判断を制御する — tool_choice と disable_parallel_tool_use
Claude がそもそも並列で tool_use ブロックを複数出すかどうかは、tool_choice パラメータで制御できます。
# 並列を明示的に禁止する例
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=[
{"name": "get_inventory", "description": "在庫を取得", "input_schema": {"type": "object", "properties": {"sku": {"type": "string"}}}},
{"name": "get_price", "description": "価格を取得", "input_schema": {"type": "object", "properties": {"sku": {"type": "string"}}}},
],
tool_choice={
"type": "auto",
"disable_parallel_tool_use": True # 1ターンに1ツールだけ呼ぶ
},
messages=[{"role": "user", "content": "SKU-A123 の在庫と価格を教えて"}]
)disable_parallel_tool_use: True を指定すると、Claude は1ターンにつき1つの tool_use ブロックしか出さなくなります。DB 書き込みの順序を保証したい場合や、副作用のあるツールを直列化したい場合に使います。
逆にデフォルト(False)では、Claude は「独立している」と判断したツールを同じターンで複数呼び出します。プロンプトで「可能な限り並列に情報を取得してから回答してください」と明示すると、並列率が目に見えて上がります。
軸2: アプリケーション側の実行を制御する — asyncio.gather と Semaphore
Claude が複数の tool_use ブロックを返しても、それをどう実行するかはあなた次第です。Claude の応答に複数の tool_use ブロックがあっても、あなたのコードが直列で処理していたら並列にはなりません。ここで詰まる人が非常に多いので、具体的なコードで示します。
import anthropic
import asyncio
import httpx
from typing import Any
client = anthropic.AsyncAnthropic()
# 各ツールの実装(async で書くのが必須)
async def get_inventory(sku: str) -> dict:
async with httpx.AsyncClient(timeout=5.0) as http:
r = await http.get(f"https://api.example.com/inventory/{sku}")
r.raise_for_status()
return r.json()
async def get_price(sku: str) -> dict:
async with httpx.AsyncClient(timeout=5.0) as http:
r = await http.get(f"https://api.example.com/price/{sku}")
r.raise_for_status()
return r.json()
async def get_shipping(sku: str, zip_code: str) -> dict:
async with httpx.AsyncClient(timeout=5.0) as http:
r = await http.get(f"https://api.example.com/shipping", params={"sku": sku, "zip": zip_code})
r.raise_for_status()
return r.json()
TOOL_MAP = {
"get_inventory": get_inventory,
"get_price": get_price,
"get_shipping": get_shipping,
}
# 並列度の上限(外部APIへの同時接続を制限)
SEMAPHORE = asyncio.Semaphore(5)
async def run_tool(block: Any) -> dict:
"""1つの tool_use ブロックを実行して tool_result を返す"""
async with SEMAPHORE:
try:
fn = TOOL_MAP[block.name]
result = await fn(**block.input)
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
"is_error": False,
}
except Exception as e:
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {type(e).__name__}: {e}",
"is_error": True,
}
async def run_parallel_tools(response) -> list[dict]:
"""Claude の応答内のすべての tool_use ブロックを並列実行"""
tool_blocks = [b for b in response.content if b.type == "tool_use"]
# asyncio.gather で並列実行(Semaphore で同時実行数を制限)
results = await asyncio.gather(
*[run_tool(b) for b in tool_blocks],
return_exceptions=False # 例外は run_tool 内でキャッチ済み
)
return resultsポイントは3つあります。
第一に、asyncio.gather を使う。ここで for ループで回してしまうと並列になりません。非同期関数を集めて gather で束ねるのが王道です。
第二に、Semaphore で同時実行数を制限する。外部 API には必ずレート制限があるので、asyncio.Semaphore(5) のように上限を設けておくのが本番のマナーです。特に Claude が10個以上のツールを並列で呼んできた場合、制限なしだと外部 API が落ちる原因になります。
第三に、各ツールの例外を run_tool 内で必ずキャッチする。gather(return_exceptions=False) でツールの1つが例外を出すと、他のツールの結果も失われます。return_exceptions=True にする手もありますが、「例外を tool_result の is_error: True に変換する」のが最も綺麗な方法です。この設計なら、3つ中1つが失敗しても、残り2つの結果を Claude に渡して部分的な回答を継続できます。
部分失敗(Partial Failure)の設計 — 本番で最もハマるポイント
Parallel Tool Use を運用していて最も事故が多いのが、部分失敗の扱いです。5つのツールを並列で呼んで、3つ成功・2つ失敗した場合、あなたのエージェントは何をすべきでしょうか?
パターン A: 全て成功したときだけ回答する(厳格)
金融取引や医療情報のように、不完全なデータで回答するのが危険なケースで使います。1つでも失敗したら全体を失敗扱いにしてリトライします。
async def strict_multi_tool(messages, tools):
"""全ツール成功を必須とする厳格モード"""
response = await client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
tools=tools,
messages=messages,
)
if response.stop_reason != "tool_use":
return response
results = await run_parallel_tools(response)
# 1つでも失敗があればリトライへ
if any(r["is_error"] for r in results):
raise PartialFailureError(
failed_tools=[r["tool_use_id"] for r in results if r["is_error"]]
)
# 全て成功 → 次のターンへ
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": results})
return await strict_multi_tool(messages, tools)パターン B: 失敗したツールの結果だけ Claude に返す(柔軟)
検索・情報収集のように、部分結果でも回答できるケースで使います。失敗したツールの tool_result に is_error: True とエラー内容を入れて Claude に返すと、Claude 自身が「この情報は取得できなかったので、残りの情報で回答します」と判断してくれます。
async def lenient_multi_tool(messages, tools):
"""部分失敗を許容するモード — Claude に判断を委ねる"""
response = await client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
tools=tools,
messages=messages,
)
if response.stop_reason != "tool_use":
return response
results = await run_parallel_tools(response)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": results})
# エラーがあってもそのまま続行 → Claude が部分情報で回答
return await lenient_multi_tool(messages, tools)私の経験では、プロダクトによってこの2つを明確に使い分けるのが重要です。Claude はエラー結果を受け取ると、「取得できた情報で回答できるか」「ユーザーに追加情報を求めるべきか」を自然に判断してくれるので、パターン B は意外なほど使い勝手が良いのです。ただし「決済金額」のように1つでも欠けると危険なデータはパターン A 一択です。
パターン C: 失敗したツールだけリトライする(ハイブリッド)
一時的なネットワークエラーなど、リトライで復旧する可能性が高いエラーで使います。
async def run_tool_with_retry(block, max_retries=3):
"""指数バックオフで個別リトライ"""
for attempt in range(max_retries):
async with SEMAPHORE:
try:
fn = TOOL_MAP[block.name]
result = await fn(**block.input)
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
"is_error": False,
}
except (httpx.TimeoutException, httpx.ConnectError) as e:
# 一時的なエラーはリトライ
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
continue
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error after {max_retries} retries: {e}",
"is_error": True,
}
except Exception as e:
# リトライ不可能なエラーは即失敗
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Non-retryable error: {e}",
"is_error": True,
}リトライする例外の種類をしっかり分けておくのがコツです。401 Unauthorized のような認証エラーはリトライしても無駄ですし、400 Bad Request はパラメータのバグなので修正が先です。
冪等性(Idempotency)— 同じツールが2回走っても安全か?
並列ツール呼び出しで最も怖いのは、Claude が同じツールを同じ引数で2回呼んでしまうケースです。例えば「SKU-A123 の在庫確認」を並列で2回発行したり、再送やリトライで同じ書き込みツールが2回実行されたりします。読み取りだけなら無害ですが、書き込み系ツールは破滅的な事故になります。
書き込みツールに冪等性キーを必ず持たせる
async def create_order(user_id: str, sku: str, quantity: int, idempotency_key: str) -> dict:
"""注文作成 — 冪等性キーで重複防止"""
async with httpx.AsyncClient(timeout=10.0) as http:
r = await http.post(
"https://api.example.com/orders",
json={"user_id": user_id, "sku": sku, "quantity": quantity},
headers={"Idempotency-Key": idempotency_key}
)
r.raise_for_status()
return r.json()idempotency_key はツールの引数として Claude に渡してもらうか、アプリケーション側で tool_use_id から自動生成します。tool_use_id は Claude が各 tool_use ブロックに付与する一意の ID なので、これをそのまま冪等性キーに使うのが最も簡単で安全です。
async def run_tool_idempotent(block) -> dict:
"""tool_use_id を冪等性キーに自動注入"""
if block.name == "create_order":
# ツールの引数に idempotency_key を自動付与
input_with_key = {**block.input, "idempotency_key": block.id}
result = await create_order(**input_with_key)
else:
fn = TOOL_MAP[block.name]
result = await fn(**block.input)
return {"type": "tool_result", "tool_use_id": block.id, "content": str(result)}冪等性の設計全般については、別記事のClaude Agent SDK で冪等性を設計するも参考になります。
ストリーミング環境での並列ツール処理
ストリーミングレスポンス(SSE)で Parallel Tool Use を使う場合、tool_use ブロックはストリーム途中で完成する順序がバラバラです。ここで詰まる人が多いので、正しい実装を示します。
import anthropic
import asyncio
client = anthropic.AsyncAnthropic()
async def stream_and_execute_parallel(messages, tools):
tool_blocks = []
assistant_content = []
async with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=2048,
tools=tools,
messages=messages,
) as stream:
async for event in stream:
# 各イベントを順次処理(テキストブロック・tool_use ブロック両方)
pass
# ストリーム完了時点で final_message を取得
final = await stream.get_final_message()
tool_blocks = [b for b in final.content if b.type == "tool_use"]
assistant_content = final.content
# ストリーム完了後にまとめて並列実行
if tool_blocks:
results = await asyncio.gather(*[run_tool(b) for b in tool_blocks])
messages.append({"role": "assistant", "content": assistant_content})
messages.append({"role": "user", "content": results})
# 次のターンへ
return await stream_and_execute_parallel(messages, tools)
return finalストリーミング中に tool_use が完成するたびに実行を始めたい気持ちはわかりますが、Anthropic SDK は tool_use ブロックの完成を stream.get_final_message() で確定させる設計なので、ストリーム完了を待ってから並列実行するのが最もバグが少ない実装です。
UI 側では、テキストブロックの出力を先にユーザーに見せつつ、tool_use の実行は裏側で並列に走らせるのが良い体験になります。
並列ツール実行の観測性(Observability)
本番で並列ツールを運用するなら、個別ツールのレイテンシ・成功率・並列度を必ずメトリクス化してください。ボトルネックがどこにあるか見えないと、最適化の判断ができません。
import time
from prometheus_client import Histogram, Counter
TOOL_LATENCY = Histogram(
"claude_tool_duration_seconds",
"Tool execution duration",
["tool_name", "status"]
)
TOOL_CALLS = Counter(
"claude_tool_calls_total",
"Total tool calls",
["tool_name", "status"]
)
PARALLEL_DEGREE = Histogram(
"claude_parallel_tool_degree",
"Number of tools called in parallel per turn",
buckets=[1, 2, 3, 5, 8, 13, 21]
)
async def run_tool_observed(block) -> dict:
start = time.monotonic()
status = "success"
try:
async with SEMAPHORE:
fn = TOOL_MAP[block.name]
result = await fn(**block.input)
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
"is_error": False,
}
except Exception as e:
status = "error"
return {
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {e}",
"is_error": True,
}
finally:
duration = time.monotonic() - start
TOOL_LATENCY.labels(tool_name=block.name, status=status).observe(duration)
TOOL_CALLS.labels(tool_name=block.name, status=status).inc()
async def run_parallel_tools_observed(response):
tool_blocks = [b for b in response.content if b.type == "tool_use"]
PARALLEL_DEGREE.observe(len(tool_blocks)) # 並列度を記録
return await asyncio.gather(*[run_tool_observed(b) for b in tool_blocks])並列度(PARALLEL_DEGREE)を分布で記録しておくと、「Claude が実際にどれくらい並列化しているか」が見えます。プロンプトを変更したとき、並列度の分布が変わるのを見ながらチューニングするのが効果的です。
OpenTelemetry での観測については、Claude API での OpenTelemetry 導入ガイドで詳しく解説しています。
よくある落とし穴 — 私が本番でハマった5つのパターン
落とし穴 1: 並列実行したいのに直列化している
for block in response.content: result = await run_tool(block) と書くと、見た目は await を使っていても直列になります。必ず asyncio.gather(*[run_tool(b) for b in blocks]) で束ねてください。
落とし穴 2: tool_result の順序が tool_use と違う
asyncio.gather は渡した順序で結果を返すので通常は問題ありませんが、ログや UI 表示で順序を気にする場合は、各結果に tool_use_id を必ず含めて照合する設計にしてください。Claude API の仕様上、tool_result の順序は tool_use の順序と一致する必要はありませんが、同じ tool_use_id がペアになっている必要があります。
落とし穴 3: セマフォなしで並列度を制限していない
私が初期に起こした事故は、Claude が8つのツールを並列で呼び出し、外部 API のレート制限に引っかかってエージェントごと落ちたことでした。Semaphore での制限は必ず入れてください。並列度の上限は、外部 API のレート制限に応じて慎重に決めます。
落とし穴 4: 例外を gather(return_exceptions=False) で飛ばしている
1つのツールが例外を出すと、他のツールの結果もすべて失われます。各ツール関数の内部で例外をキャッチし、is_error: True の tool_result を返すのが鉄則です。
落とし穴 5: 冪等性キーをクライアント側で生成している
uuid.uuid4() で毎回新しいキーを作ると、Claude がリトライした瞬間に同じツールが2回実行されます。冪等性キーは tool_use_id を使うか、(user_id, sku, timestamp) のようにリクエストの論理的な一意性から決定的に生成してください。
並列度をプロンプトで誘導する
Claude が実際にどれだけ並列化するかは、プロンプト設計にも強く依存します。私の運用経験から効果があった書き方を共有します。
効果があった書き方の例:
ユーザーの質問に答えるために必要な情報は、可能な限り 同時に 取得してください。相互に依存しないツールは並列で呼び出し、レスポンスを高速化してください。
あまり効果がなかった書き方の例:
効率的にツールを使ってください。
「同時に」「並列で」というキーワードを明示的に含めるだけで、実測で並列度が平均 1.3 → 2.8 に上がりました。Claude はプロンプトに忠実なので、期待する挙動は言語化しておくのが大事です。
本番チェックリスト
Parallel Tool Use を本番投入する前に、以下を確認してください。
asyncio.gatherで実際に並列実行されているか(プロファイラで確認)- Semaphore で並列度が外部 API の上限内に収まっているか
- 各ツール関数内で例外をキャッチして
is_error: Trueに変換しているか - 書き込み系ツールに冪等性キーが入っているか
- 部分失敗時のポリシー(厳格 / 柔軟 / リトライ)がプロダクトに合っているか
- 並列度・レイテンシ・成功率のメトリクスが取れているか
- プロンプトに「並列で取得してほしい」旨が明記されているか
Parallel Tool Use の設計思想をさらに深めるなら、Claude API 高度なツール使用完全ガイドやPython asyncio での Claude API 並列リクエスト実装も合わせて読むと理解が立体的になります。
次の一歩
まず手元のエージェントで、1回の Claude レスポンスあたりの tool_use ブロック数を計測してみてください。平均が 1.0 に近ければ、並列化の余地が大きく残っています。プロンプトに「可能な限り並列に取得してください」を1行加えるだけで、体感レイテンシが一段速くなるのを実感できるはずです。そこから Semaphore・冪等性・観測性を順に足していけば、本番レベルの並列エージェントが組み上がります。