CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-04-23上級

Claude API の Parallel Tool Use を本番で使いこなす — 並列実行の制御・エラー設計・レイテンシ最適化

Claude API の Parallel Tool Use は並列実行でレイテンシを半分以下にできますが、部分失敗やステート衝突の事故も増えます。制御方法・エラー設計・観測性を本番運用の視点で解説します。

claude-api81tool-use21parallelproduction87latency2

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_choicedisable_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_resultis_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_resultis_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: Truetool_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・冪等性・観測性を順に足していけば、本番レベルの並列エージェントが組み上がります。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-05-26
Claude API の構造化レスポンスを本番で安定させる — tool_use と JSON Schema、多層検証の実装メモ
Claude API の応答を JSON で受け取る実装は数行で書けますが、本番運用では「形は正しいが意味が壊れている」レスポンスにどう備えるかが分かれ目になります。tool_use・JSON Schema・ドメイン検証を組み合わせた多層パイプラインを、壁紙アプリの分類処理での実体験を交えて整理しました。
API & SDK2026-05-22
Claude API の tool_result could not be submitted が再発しないための回復ハンドラ設計
Claude API でエージェントを長時間動かしていると、ある日突然 'tool_result could not be submitted' が連発し始めることがあります。再試行しても直らない、ストリーミング途中で死ぬ、そして次のセッションでもまた出る — 個人開発で運営している6サイトの自動投稿パイプラインで実際に踏み抜いた4種類の原因と、TypeScript で書いた回復ハンドラの実装を整理しました。
API & SDK2026-04-28
Claude API のレイテンシを下げる4つのインフラ施策 — モデルを変える前に試したい打ち手
Claude API の応答が遅いと感じたとき、モデル変更の前に効くインフラ施策があります。リージョン選定・HTTP接続再利用・プロンプトキャッシュ・ストリーミングの4点を、実装と計測の観点で整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →