Messages APIにリクエストを送信すると、Claudeの応答には、モデルが応答の生成を停止した理由を示すstop_reasonフィールドが含まれます。これらの値を理解することは、異なる応答タイプを適切に処理する堅牢なアプリケーションを構築するために重要です。 API応答におけるstop_reasonの詳細については、Messages APIリファレンスを参照してください。

stop_reasonとは?

stop_reasonフィールドは、すべての成功したMessages API応答の一部です。リクエストの処理における失敗を示すエラーとは異なり、stop_reasonは、Claudeが応答生成を正常に完了した理由を教えてくれます。
応答例
{
  "id": "msg_01234",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Here's the answer to your question..."
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 100,
    "output_tokens": 50
  }
}

停止理由の値

end_turn

最も一般的な停止理由です。Claudeが応答を自然に完了したことを示します。
if response.stop_reason == "end_turn":
    # 完全な応答を処理
    print(response.content[0].text)

end_turnでの空の応答

時々、Claudeはstop_reason: "end_turn"で空の応答(コンテンツなしで正確に2-3トークン)を返すことがあります。これは通常、特にツール結果の後で、Claudeがアシスタントターンが完了したと解釈した場合に発生します。 一般的な原因:
  • ツール結果の直後にテキストブロックを追加する(Claudeはユーザーが常にツール結果の後にテキストを挿入することを期待するように学習するため、パターンに従うためにターンを終了します)
  • Claudeの完了した応答を何も追加せずに送り返す(Claudeはすでに完了したと判断しているため、完了したままになります)
空の応答を防ぐ方法:
# 間違い:tool_resultの直後にテキストを追加
messages = [
    {"role": "user", "content": "Calculate the sum of 1234 and 5678"},
    {"role": "assistant", "content": [
        {
            "type": "tool_use",
            "id": "toolu_123",
            "name": "calculator",
            "input": {"operation": "add", "a": 1234, "b": 5678}
        }
    ]},
    {"role": "user", "content": [
        {
            "type": "tool_result",
            "tool_use_id": "toolu_123",
            "content": "6912"
        },
        {
            "type": "text",
            "text": "Here's the result"  # tool_resultの後にテキストを追加しない
        }
    ]}
]

# 正しい:追加のテキストなしでツール結果を直接送信
messages = [
    {"role": "user", "content": "Calculate the sum of 1234 and 5678"},
    {"role": "assistant", "content": [
        {
            "type": "tool_use",
            "id": "toolu_123",
            "name": "calculator",
            "input": {"operation": "add", "a": 1234, "b": 5678}
        }
    ]},
    {"role": "user", "content": [
        {
            "type": "tool_result",
            "tool_use_id": "toolu_123",
            "content": "6912"
        }
    ]}  # tool_resultのみ、追加のテキストなし
]

# 上記を修正した後でも空の応答が返される場合:
def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=messages
    )

    # 応答が空かどうかをチェック
    if (response.stop_reason == "end_turn" and
        not response.content):

        # 間違い:空の応答で単純に再試行しない
        # Claudeはすでに完了したと判断しているため、これは機能しません

        # 正しい:新しいユーザーメッセージで継続プロンプトを追加
        messages.append({"role": "user", "content": "Please continue"})

        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages
        )

    return response
ベストプラクティス:
  1. ツール結果の直後にテキストブロックを追加しない - これはClaude にすべてのツール使用後にユーザー入力を期待するように教えます
  2. 修正なしで空の応答を再試行しない - 空の応答を単純に送り返しても役に立ちません
  3. 継続プロンプトは最後の手段として使用 - 上記の修正で問題が解決しない場合のみ

max_tokens

Claudeがリクエストで指定されたmax_tokens制限に達したため停止しました。
# 制限されたトークンでのリクエスト
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=10,
    messages=[{"role": "user", "content": "Explain quantum physics"}]
)

if response.stop_reason == "max_tokens":
    # 応答が切り詰められました
    print("Response was cut off at token limit")
    # 継続するために別のリクエストを検討

stop_sequence

Claudeがカスタム停止シーケンスの1つに遭遇しました。
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    stop_sequences=["END", "STOP"],
    messages=[{"role": "user", "content": "Generate text until you say END"}]
)

if response.stop_reason == "stop_sequence":
    print(f"Stopped at sequence: {response.stop_sequence}")

tool_use

Claudeがツールを呼び出しており、実行を期待しています。
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=[weather_tool],
    messages=[{"role": "user", "content": "What's the weather?"}]
)

if response.stop_reason == "tool_use":
    # ツールを抽出して実行
    for content in response.content:
        if content.type == "tool_use":
            result = execute_tool(content.name, content.input)
            # 最終応答のためにClaudeに結果を返す

pause_turn

Claudeが長時間実行される操作を一時停止する必要がある場合に、ウェブ検索などのサーバーツールで使用されます。
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=[{"type": "web_search_20250305", "name": "web_search"}],
    messages=[{"role": "user", "content": "Search for latest AI news"}]
)

if response.stop_reason == "pause_turn":
    # 会話を継続
    messages = [
        {"role": "user", "content": original_query},
        {"role": "assistant", "content": response.content}
    ]
    continuation = client.messages.create(
        model="claude-sonnet-4-5",
        messages=messages,
        tools=[{"type": "web_search_20250305", "name": "web_search"}]
    )

refusal

Claudeが安全上の懸念により応答の生成を拒否しました。
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "[Unsafe request]"}]
)

if response.stop_reason == "refusal":
    # Claudeは応答を拒否しました
    print("Claude was unable to process this request")
    # リクエストの言い換えや修正を検討
Claude Sonnet 4.5またはOpus 4.1を使用中にrefusal停止理由に頻繁に遭遇する場合は、異なる使用制限を持つSonnet 4(claude-sonnet-4-20250514)を使用するようにAPI呼び出しを更新することを試してみてください。Sonnet 4.5のAPI安全フィルターの理解について詳しく学んでください。
Claude Sonnet 4.5のAPI安全フィルターによってトリガーされる拒否について詳しく学ぶには、Sonnet 4.5のAPI安全フィルターの理解を参照してください。

model_context_window_exceeded

Claudeがモデルのコンテキストウィンドウ制限に達したため停止しました。これにより、正確な入力サイズを知らなくても、可能な限り最大のトークンをリクエストできます。
# 可能な限り多くを取得するために最大トークンでリクエスト
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=64000,  # モデルの最大出力トークン
    messages=[{"role": "user", "content": "Large input that uses most of context window..."}]
)

if response.stop_reason == "model_context_window_exceeded":
    # 応答はmax_tokensの前にコンテキストウィンドウ制限に達しました
    print("Response reached model's context window limit")
    # 応答は依然として有効ですが、コンテキストウィンドウによって制限されました
この停止理由は、Sonnet 4.5以降のモデルでデフォルトで利用可能です。以前のモデルの場合は、この動作を有効にするためにベータヘッダーmodel-context-window-exceeded-2025-08-26を使用してください。

停止理由を処理するためのベストプラクティス

1. 常にstop_reasonをチェック

応答処理ロジックでstop_reasonをチェックすることを習慣にしてください:
def handle_response(response):
    if response.stop_reason == "tool_use":
        return handle_tool_use(response)
    elif response.stop_reason == "max_tokens":
        return handle_truncation(response)
    elif response.stop_reason == "model_context_window_exceeded":
        return handle_context_limit(response)
    elif response.stop_reason == "pause_turn":
        return handle_pause(response)
    elif response.stop_reason == "refusal":
        return handle_refusal(response)
    else:
        # end_turnやその他のケースを処理
        return response.content[0].text

2. 切り詰められた応答を適切に処理

トークン制限やコンテキストウィンドウにより応答が切り詰められた場合:
def handle_truncated_response(response):
    if response.stop_reason in ["max_tokens", "model_context_window_exceeded"]:
        # オプション1:特定の制限についてユーザーに警告
        if response.stop_reason == "max_tokens":
            message = "[Response truncated due to max_tokens limit]"
        else:
            message = "[Response truncated due to context window limit]"
        return f"{response.content[0].text}\n\n{message}"

        # オプション2:生成を継続
        messages = [
            {"role": "user", "content": original_prompt},
            {"role": "assistant", "content": response.content[0].text}
        ]
        continuation = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            messages=messages + [{"role": "user", "content": "Please continue"}]
        )
        return response.content[0].text + continuation.content[0].text

3. pause_turnのための再試行ロジックを実装

一時停止する可能性のあるサーバーツールの場合:
def handle_paused_conversation(initial_response, max_retries=3):
    response = initial_response
    messages = [{"role": "user", "content": original_query}]
    
    for attempt in range(max_retries):
        if response.stop_reason != "pause_turn":
            break
            
        messages.append({"role": "assistant", "content": response.content})
        response = client.messages.create(
            model="claude-sonnet-4-5",
            messages=messages,
            tools=original_tools
        )
    
    return response

停止理由 vs. エラー

stop_reason値と実際のエラーを区別することが重要です:

停止理由(成功した応答)

  • 応答本文の一部
  • 生成が正常に停止した理由を示す
  • 応答には有効なコンテンツが含まれる

エラー(失敗したリクエスト)

  • HTTPステータスコード4xxまたは5xx
  • リクエスト処理の失敗を示す
  • 応答にはエラーの詳細が含まれる
try:
    response = client.messages.create(...)
    
    # stop_reasonで成功した応答を処理
    if response.stop_reason == "max_tokens":
        print("Response was truncated")
    
except anthropic.APIError as e:
    # 実際のエラーを処理
    if e.status_code == 429:
        print("Rate limit exceeded")
    elif e.status_code == 500:
        print("Server error")

ストリーミングの考慮事項

ストリーミングを使用する場合、stop_reasonは:
  • 初期のmessage_startイベントではnull
  • message_deltaイベントで提供される
  • その他のイベントでは提供されない
with client.messages.stream(...) as stream:
    for event in stream:
        if event.type == "message_delta":
            stop_reason = event.delta.stop_reason
            if stop_reason:
                print(f"Stream ended with: {stop_reason}")

一般的なパターン

ツール使用ワークフローの処理

def complete_tool_workflow(client, user_query, tools):
    messages = [{"role": "user", "content": user_query}]
    
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-5",
            messages=messages,
            tools=tools
        )
        
        if response.stop_reason == "tool_use":
            # ツールを実行して継続
            tool_results = execute_tools(response.content)
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})
        else:
            # 最終応答
            return response

完全な応答の確保

def get_complete_response(client, prompt, max_attempts=3):
    messages = [{"role": "user", "content": prompt}]
    full_response = ""

    for _ in range(max_attempts):
        response = client.messages.create(
            model="claude-sonnet-4-5",
            messages=messages,
            max_tokens=4096
        )

        full_response += response.content[0].text

        if response.stop_reason != "max_tokens":
            break

        # 中断したところから継続
        messages = [
            {"role": "user", "content": prompt},
            {"role": "assistant", "content": full_response},
            {"role": "user", "content": "Please continue from where you left off."}
        ]

    return full_response

入力サイズを知らずに最大トークンを取得

model_context_window_exceeded停止理由により、入力サイズを計算せずに可能な限り最大のトークンをリクエストできます:
def get_max_possible_tokens(client, prompt):
    """
    入力トークン数を計算する必要なく、
    モデルのコンテキストウィンドウ内で可能な限り多くのトークンを取得
    """
    response = client.messages.create(
        model="claude-sonnet-4-5",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=64000  # モデルの最大出力トークンに設定
    )

    if response.stop_reason == "model_context_window_exceeded":
        # 入力サイズを考慮して可能な限り最大のトークンを取得
        print(f"Generated {response.usage.output_tokens} tokens (context limit reached)")
    elif response.stop_reason == "max_tokens":
        # 要求されたトークンを正確に取得
        print(f"Generated {response.usage.output_tokens} tokens (max_tokens reached)")
    else:
        # 自然な完了
        print(f"Generated {response.usage.output_tokens} tokens (natural completion)")

    return response.content[0].text
stop_reason値を適切に処理することで、異なる応答シナリオを適切に処理し、より良いユーザーエクスペリエンスを提供する、より堅牢なアプリケーションを構築できます。