# Claude stop_reason別の分岐設計とループ制御

> Claude APIのstop_reasonは7種類あり、end_turn以外を無視するとエージェントが止まる。中小企業向けに、各値の意味と分岐・再試行・上限設計を整理する。

- Canonical: https://kuucorp.com/blog/claude-stop-reason-handling-agent-loop-design-smb/
- Date: 2026-10-20
- Last modified: 2026-10-20
- Publisher: Kuu株式会社 (https://kuucorp.com)

---
社内の問い合わせ対応エージェントが、ある日から回答の途中で文が切れる。別の日は、検索ツールを呼んだまま何も返さず無言になる。エラーログは空です。原因の多くは、APIの応答に含まれる `stop_reason` を見ずに、常に「正常終了」とみなしているコードにあります。

## stop_reasonとは何で、なぜ分岐が必要なのか

> stop_reasonはClaudeが生成を止めた理由を示す値で、7種類あります。HTTP 200で返るため、エラー監視だけでは異常を拾えません。

`stop_reason` はMessages APIの成功応答に必ず入る項目です。4xx/5xxのエラーとは別の仕組みで、通信が成功していても「途中で切れた」「ツールの結果待ち」「回答を拒否した」といった状態を示します。公式ドキュメントが定める値は次のとおりです。

| 値 | 意味 | 基本の扱い |
|---|---|---|
| `end_turn` | 自然に回答を終えた | 応答を使う。空応答に注意 |
| `max_tokens` | `max_tokens` の上限に達した | 上限を上げるか続きを生成 |
| `stop_sequence` | 指定した停止文字列が出た | `response.stop_sequence` で種類を確認 |
| `tool_use` | ツールを呼び出したい | 実行して `tool_result` を返す |
| `pause_turn` | サーバーツールのループが反復上限（既定10）に達した | assistantの内容をそのまま返して継続 |
| `refusal` | 回答を拒否した | `stop_details` を読み、フォールバックを検討 |
| `model_context_window_exceeded` | コンテキストウィンドウを使い切った | 切り詰められた応答として扱う |

設計の出発点は、`end_turn` を前提にせず、値ごとに明示的に分岐することです。エージェントの全体構造は[エージェントハーネスのアーキテクチャ](/blog/agent-harness-architecture/)と合わせて読むと位置づけが分かります。

## ループはどの値で回し、どの値で止めるのか

> 続行するのは tool_use と pause_turn の2値だけです。それ以外は終了か異常として扱い、ループの無限化を防ぎます。

エージェントループの骨格は単純です。`tool_use` と `pause_turn` は「続きがある」ので次のリクエストを送り、他の値は終了処理に回します。

```python
MAX_STEPS = 20  # ループ上限は自社で決める
for step in range(MAX_STEPS):
    resp = client.messages.create(model=MODEL, max_tokens=4096,
                                  messages=messages, tools=tools)
    match resp.stop_reason:
        case "tool_use":
            messages.append({"role": "assistant", "content": resp.content})
            messages.append({"role": "user", "content": run_tools(resp)})
        case "pause_turn":
            messages.append({"role": "assistant", "content": resp.content})
        case "end_turn" | "stop_sequence":
            return resp
        case "max_tokens" | "model_context_window_exceeded":
            return mark_truncated(resp)
        case "refusal":
            return handle_refusal(resp)
raise LoopLimitExceeded()
```

`pause_turn` は公式ドキュメントでも継続回数に上限を設けるよう書かれています。ステップ数の上限は、課金の暴走を止める最後の砦です。上限と予算の設計は[エージェントのループ実行制御](/blog/agent-loop-execution-control-budget-timeout/)で詳しく扱っています。

## tool_useとmax_tokensでは何に気をつけるのか

> tool_resultは1通のuserメッセージにまとめ、先頭に置きます。max_tokensでtool_useが切れた場合は実行せず、上限を上げて再試行します。

`tool_use` では、次の規則を守らないと400エラーか空の `end_turn` になります。

1. `tool_use` ブロックすべてに対応する `tool_result` を返す（`tool_use_id` を一致させる）
2. `tool_result` は1つのuserメッセージにまとめ、contentの先頭に置く。テキストはその後ろ
3. サーバーツールの未完了呼び出しが残る場合は、`tool_result` だけを送る
4. ツール実行が失敗したら `is_error: true` を付け、「何が起き、次に何を試すか」を書く

`max_tokens` は特に注意が必要です。最後のブロックが `tool_use` なら、引数のJSONが途中で切れていて実行できません。公式の手順どおり、より大きい `max_tokens` で同じリクエストを再送します。テキストだけなら、続きを生成させるか、切り詰めた旨を利用者に表示します。出力形式を固定している場合の失敗パターンは[構造化出力の失敗設計](/blog/claude-structured-outputs-failure-handling-smb/)にまとめています。

## refusalと空のend_turnはどう扱うのか

> refusalは正常応答として届くため、独立した指標で監視します。空のend_turnは、tool_result直後のテキスト追加が主因です。

**refusal**: 拒否は4xxではなくHTTP 200で返ります。エラー率だけの監視では見えないため、`refusal` の件数を別指標にします。`stop_details` には `category` と `explanation` が入りますが、どちらも `null` になり得ます。`stop_reason` で分岐し、利用者向け文言は自社で用意してください。公式の指針では、拒否後は会話コンテキストをリセットするか、別のClaudeモデルで再試行します。同じモデルに同じ内容を再送しても拒否が続くためです。モデルごとのフォールバック対応は異なり、Haiku 5.5にはサーバー側フォールバックがないので、クライアント側で再試行を実装します。バッチ処理でも、拒否は成功結果として `stop_reason: "refusal"` で返ります。

**空の end_turn**: 2〜3トークンで内容のない `end_turn` が返ることがあります。公式が挙げる原因は、`tool_result` の直後にテキストブロックを足したことと、完了済みの応答をそのまま再送したことです。対処は、まず `tool_result` だけを送ること。それでも空なら、最後の手段として「続けてください」のuserメッセージを足して再呼び出しします。

**ストリーミング**: `stop_reason` は `message_start` では `null` で、`message_delta` イベントで届きます。ストリームを受ける実装は、最後まで読んでから判定してください。

### 規模別の留意点（SMB）

従業員数十名規模では、まず `max_tokens`・`refusal`・ループ上限の3つを実装するだけで事故の大半を防げます。停止理由別の件数をログに出しておくと、モデル変更後の回帰にも気づけます。

## 参考

- [Stop reasons and fallback（Claude Platform Docs）](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)
- [Handle tool calls（Claude Platform Docs）](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls)
- [Handle streaming refusals（Claude Platform Docs）](https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals)

## まとめ

`stop_reason` の分岐は、エージェントを止まらず、暴走せず、静かに壊れないようにする最小の設計です。続行は `tool_use` と `pause_turn`、終了は `end_turn` 系、異常は `max_tokens`・`refusal`・コンテキスト超過、と役割を分けてください。分岐設計の見直しや運用の定着は、Kuu株式会社の[AI Ops（導入〜定着の伴走）](https://kuucorp.com/services/ai-ops/)にご相談ください。
