CLAUDE LABEN
SUNSET — 旧 Workbench と実験的プロンプトツール API の廃止は明日8月17日です。残り1日となりましたLATEST — 最新は8月15日の v2.1.233 です。GitLab のマージリクエスト URL が --worktree と claude agents で扱えるようになり、MR は !N 形式で表示されますSECURITY — Windows で NT の \??\ デバイスプレフィックスを含むパスが UNC 検証をすり抜ける問題が直りました。NTLM 資格情報が漏れる経路が塞がれていますTODO — Opus 4.8・Sonnet 5・Fable 5・Mythos 5 以降では TodoWrite と Task 系ツールが既定で無効になりました。CLAUDE_CODE_ENABLE_TODO_TOOLS=1 で戻せますFORK — 8月14日の v2.1.232 で subagent_type: 'fork' が既定になり、会話全体とプロンプトキャッシュをそのまま引き継げるようになっていますMENTION — プロンプトで @ を打つと別のセッションを名前で呼び出せます。SendMessage がそのセッションへ直接届きますSUNSET — 旧 Workbench と実験的プロンプトツール API の廃止は明日8月17日です。残り1日となりましたLATEST — 最新は8月15日の v2.1.233 です。GitLab のマージリクエスト URL が --worktree と claude agents で扱えるようになり、MR は !N 形式で表示されますSECURITY — Windows で NT の \??\ デバイスプレフィックスを含むパスが UNC 検証をすり抜ける問題が直りました。NTLM 資格情報が漏れる経路が塞がれていますTODO — Opus 4.8・Sonnet 5・Fable 5・Mythos 5 以降では TodoWrite と Task 系ツールが既定で無効になりました。CLAUDE_CODE_ENABLE_TODO_TOOLS=1 で戻せますFORK — 8月14日の v2.1.232 で subagent_type: 'fork' が既定になり、会話全体とプロンプトキャッシュをそのまま引き継げるようになっていますMENTION — プロンプトで @ を打つと別のセッションを名前で呼び出せます。SendMessage がそのセッションへ直接届きます
記事一覧/API & SDK
API & SDK/2026-04-26中級

Claude API の stop_reason を読み解く — 応答の途切れを「終了」と誤認しないための設計

Claude API のレスポンスに含まれる stop_reason を正確に分岐するだけで、未完了出力の取りこぼしや無駄なリトライが大きく減ります。end_turn・max_tokens・pause_turn・refusal の見分け方と実装パターンをまとめました。

Claude46API27stop_reason2error-handling10production87

「Claude の応答が途中で切れている気がする」— 本番に出した直後、サポートにこの報告が届いて慌てた経験がある方は少なくないはずです。私自身、初めて Messages API を本番投入したとき、stop_reason を真面目に見ていなかったために、max_tokens で打ち切られた応答をそのままユーザーに表示してしまったことがありました。

stop_reason はレスポンスの「終わり方」を教えてくれる小さなフィールドですが、ここを正しく分岐するだけで、未完了出力の取りこぼし・不要なリトライ・ツール呼び出し後の継続忘れといった、本番でよく起きる事故をまとめて防げます。ここでは6種類の stop_reason を「どう見分け」「どう扱うか」を、私が現場で踏んだ落とし穴とともに整理します。

stop_reason は応答の「終わり方」を教える唯一の信号

Messages API は、応答が完了したときに必ず stop_reason を返します。代表的な値は次の6つです。

  • end_turn: モデルが自然にターンを終えた(最も健全な終了)
  • max_tokens: max_tokens の上限に達して打ち切られた(応答は未完了の可能性が高い)
  • stop_sequence: リクエストで指定した stop_sequences のいずれかが出現した
  • tool_use: ツール呼び出しブロックが含まれており、ツール結果を返して継続が必要
  • pause_turn: 拡張思考や長時間ツール実行の途中で「いったん区切られた」状態。継続呼び出しが必要
  • refusal: モデルが安全性の判断で応答を拒否した

ここで誤解されがちなのは、「stop_reason が返ってきた = 完了」と読むことです。実際には、max_tokenstool_usepause_turn の3つは「途中で止まった」状態であり、後続の処理を呼び出さないとユーザーに不完全な出力が届きます。

end_turn と max_tokens を取り違えると本番で詰まる

最も多い事故が、max_tokens の応答を end_turn と同じように画面に出してしまうケースです。たとえば次のようなシンプルな実装を考えてみてください。

# 最低限の Messages API 呼び出し(このままでは本番に出せません)
import anthropic
 
client = anthropic.Anthropic()
 
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "長文の議事録を要約してください..."}],
)
 
# ⚠️ stop_reason を見ずに本文だけ返してしまう実装
print(resp.content[0].text)

このコードは、応答が max_tokens で打ち切られた場合、文の途中でぷつりと切れたテキストをそのまま表示します。私が実際に踏んだ事例では、議事録要約の最終段落の途中で切れており、ユーザーから「重要な決定事項が抜けている」とクレームを受けました。

最低限、次のように分岐させておくだけで救われます。

# stop_reason を分岐する最小実装
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "..."}],
)
 
text = "".join(b.text for b in resp.content if b.type == "text")
 
if resp.stop_reason == "end_turn":
    return {"status": "ok", "text": text}
elif resp.stop_reason == "max_tokens":
    # 続きを要求するか、max_tokens を上げて再試行する
    return {"status": "truncated", "text": text, "hint": "max_tokens を上げてください"}
elif resp.stop_reason == "refusal":
    return {"status": "refused", "text": text}
else:
    return {"status": resp.stop_reason, "text": text}
# 期待出力例(max_tokens で打ち切られた場合):
# {"status": "truncated", "text": "...議事録の途中まで...", "hint": "max_tokens を上げてください"}

このとき大切なのは、max_tokens を機械的に倍にして再試行するのではなく、「そもそも max_tokens がユースケースに対して小さすぎないか」を疑うことです。要約タスクで 1,024 トークンに収まることはまれで、初期値を 4,096〜8,192 に引き上げるだけで max_tokens 検知はほぼゼロになります。

tool_use と pause_turn は「継続」を要求するシグナル

tool_use が返ったときは、応答に含まれる tool_use ブロックを実行し、tool_result を含む新しいユーザーメッセージとして同じ会話を継続する必要があります。ここで継続を忘れると、ユーザーから見ると「ツールを呼び出すと毎回応答が止まる」状態になります。

# tool_use を受けて継続する典型パターン
def run_with_tools(messages, tools):
    resp = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        tools=tools,
        messages=messages,
    )
 
    while resp.stop_reason == "tool_use":
        tool_blocks = [b for b in resp.content if b.type == "tool_use"]
        tool_results = []
        for block in tool_blocks:
            output = run_tool(block.name, block.input)  # 実装はアプリ側
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": str(output),
            })
 
        messages.append({"role": "assistant", "content": resp.content})
        messages.append({"role": "user", "content": tool_results})
 
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )
 
    return resp

pause_turn は、拡張思考や長時間のサーバーサイドツール(コード実行など)を使ったときに、モデル側が「いったん区切るのでこのまま続きを呼んでほしい」と返してくる値です。実装パターンは tool_use と似ており、レスポンスをそのまま messages に積んで、追加メッセージなしで messages.create をもう一度呼ぶだけで継続できます。

詳しい実装はClaude API のストリーミングとツール利用を組み合わせるで扱っているので、ストリーミング併用時の細かなイベント順序を知りたい方はあわせて参照してください。

refusal は再試行ではなく「設計」で受け止める

refusal は、モデルが安全性の観点で応答を拒否した場合に返ります。ここで多くのチームが最初にやってしまうのが、自動リトライです。私もやらかしました。同じプロンプトをそのまま投げ直しても結果は変わらず、ただレートリミットを消費するだけになります。

refusal を受け取ったときの実装方針は、シンプルに次の3点に絞ると安定します。

  • 同じプロンプトで自動リトライしない(無意味であり、コストが膨らむ)
  • ユーザーには「この内容には応答できない」旨を表示する(拒否理由をそのまま見せない)
  • 入力をログに残し、本当に拒否すべき入力なのか・プロンプトに改善余地があるかを後で人がレビューする

Claude API のエラーハンドリング設計で取り上げているリトライ方針と組み合わせると、429 や 5xx と refusal を取り違えずに扱えるようになります。

ストリーミングでの stop_reason は最後に届く

ストリーミングを使うと、message_delta イベントの中で stop_reason が後から確定します。最初の message_start には含まれません。これを知らずに「ストリーム開始時点で stop_reason を読みたい」と書いてしまうと、必ず null になります。

# ストリーミングで stop_reason を取得する
stop_reason = None
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=4096,
    messages=[{"role": "user", "content": "..."}],
) as stream:
    for event in stream:
        if event.type == "message_delta" and event.delta.stop_reason:
            stop_reason = event.delta.stop_reason
        # text_delta などの処理は省略
 
print("stop_reason:", stop_reason)
# 期待出力例: stop_reason: end_turn

ストリーミングでは、UI に「考え中」表示を出している間に pause_turn が来る可能性もあります。表示側の状態管理を「終わり = end_turn のみ」にしておくと、pause_turn を継続せずに UI が止まったように見える事故を防げます。

stop_sequence は便利だが、設定を忘れがち

stop_sequence は、リクエスト時に渡した stop_sequences のいずれかが出力に出現したときだけ返ります。出力フォーマットを強制したいときには強力で、たとえば </answer> という終端タグが出た瞬間に応答を打ち切り、後続処理を簡潔にできます。

# stop_sequences で出力を制約する
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    stop_sequences=["</answer>"],
    messages=[{"role": "user", "content": "回答を <answer>...</answer> で囲んでください"}],
)
 
if resp.stop_reason == "stop_sequence":
    # どの停止文字列にヒットしたかは resp.stop_sequence で確認できます
    print("matched:", resp.stop_sequence)

私がコードレビューでよく見かける落とし穴は、過去に stop_sequences=["\n\n"] を仕込んで応答を短く保っていたケースです。数ヶ月後に「複数段落の応答が出なくなった」と問い合わせが来て、原因を追うと忘れ去られた stop_sequences が残っているという展開です。「短い応答しか返らない」と感じたら、まず stop_sequences を全文検索して確認するのが早道です。

stop_reason ごとの最小実装チェックリスト

最後に、私が現場で各 stop_reason に対して入れている最小限の挙動を一覧にしておきます。

  • end_turn — そのまま応答を返す。完了扱い
  • max_tokens — ログとエラートラッカーに記録し、truncated 状態として返す。必要なら max_tokens を引き上げる
  • stop_sequence — 一致した停止文字列を必要に応じて除去して返す。設定が古くないか定期的に見直す
  • tool_use — ツールを実行し、assistantuser メッセージを追加して再呼び出し。ループ上限を必ず設ける
  • pause_turnassistant メッセージだけを追加して再呼び出し。追加のユーザー入力は不要
  • refusal — 中立的なメッセージを表示し、入力をレビュー用にログに残す。自動リトライしない

まずは max_tokens の取りこぼしから潰す

stop_reason の6種類を一度に完璧に扱おうとすると、実装が膨らみます。私のおすすめは、まず「max_tokens の取りこぼしを検知してログに残す」だけを今日のうちに入れることです。これだけで、本番で起きている「応答が途切れる」系の問題の半分以上は可視化できます。残りの tool_usepause_turnrefusal は、ユースケースが拡がってから順に対応しても十分間に合います。

後者は API クライアントの設計章で、外部サービス呼び出しの結果コードをどう分岐するかという考え方が整理されており、stop_reason の取り扱いにも応用できます。

シェア

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

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

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

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

関連記事

API & SDK2026-06-22
pause_turn の継続ループに上限を入れる — 無人で長時間サーバーツールを安全に回す
web_search や code execution など長時間サーバーツールを使うと返ってくる pause_turn を、無人運用で安全に継続する設計をまとめました。4種類の stop_reason を1ループで分岐し、継続回数と時間に上限を入れ、セグメントを跨ぐ usage を数える実装まで扱います。
API & SDK2026-04-23
Claude API プロンプトインジェクション防御の設計パターン — 検出・サニタイゼーション・多層防御
ユーザー入力や外部データが混入する本番LLMアプリに必要なプロンプトインジェクション防御を体系的に解説します。信頼境界と命令チャネルの分離という設計原則から、入力サニタイゼーション、デリミタ戦略、出力バリデーション、マルチターン対策、投入前のレッドチーミングまで実装コード付きです。
API & SDK2026-07-24
メモリストアの棚卸しが半分しか返らない — path_prefix セグメント一致への移行
agent-memory-2026-07-22 で memories.list が変わりました。順序固定・depth 厳格化・path_prefix のセグメント一致。件数が静かに半減した実例と、破壊的変更に耐える棚卸し層、depth=1 再帰の走査コストの実測をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →