◉CLAUDE LABEN
●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/API & SDK
⬡ API & SDK/2026-05-04中級

Claude API の stop_sequences が効かないときに確認すべき5つのポイント

Claude API の stop_sequences パラメータが期待通りに止まらない問題の原因を、トークン境界・空白・Tool Use・ストリーミングの観点から実例コード付きで切り分ける手順をまとめました。

api39stop-sequencestroubleshooting61tokenizationstreaming16

「stop_sequences に "\n\n" を渡したのに、出力が止まる気配がない」— Claude API を使い込んでいると、一度はぶつかる現象ではないでしょうか。私も RAG 風のフォーマット制御を組んでいたときに、まったく同じ問題で半日を溶かしました。

結論から言えば、stop_sequences が効かないように見える原因のほとんどは「Claude が生成した文字列とあなたが指定した文字列が、トークンレベルで一致していない」ことに起因します。ここでは現場で実際に遭遇する 5 つの典型パターンを切り分け、それぞれに具体的な対処コードを示します。

まず把握しておくべき stop_sequences の仕様

Claude API の stop_sequences は、生成中のテキストにバイト単位で完全一致する文字列が出現したタイミングで生成を停止させるパラメータです。Anthropic の公式ドキュメントでは「最大 4 つまで指定可能」「stop_reason が "stop_sequence" になる」と書かれていますが、現場で詰まるのはこのあたりではありません。

実装上、押さえておくべきポイントは次の 3 つです。

  • 検査対象は「Claude が今まさに出力している文字列バッファ」であり、プロンプト側のテキストではありません
  • マッチした停止文字列は出力に含まれません(Tool Use の場合を除く)
  • stop_reason には "end_turn" / "max_tokens" / "stop_sequence" / "tool_use" のいずれかが入ります。停止理由を必ずログに残しておくと、原因特定が一気に楽になります
# stop_reason は必ずログに残す
import anthropic
 
client = anthropic.Anthropic()
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    stop_sequences=["\n\nHuman:"],
    messages=[{"role": "user", "content": "5から1までカウントダウンして"}],
)
 
print(f"stop_reason: {response.stop_reason}")
print(f"stop_sequence: {response.stop_sequence}")  # マッチした文字列(マッチしなかったら None)
print(f"content: {response.content[0].text}")

stop_reason が "end_turn" や "max_tokens" になっていたら、そもそも停止文字列が出力に現れていません。一方、"stop_sequence" なのに止まる位置が思った通りでない場合は、後述する「空白の取り扱い」や「トークン境界」の問題を疑います。

原因 1: 出力にそもそも停止文字列が出現していない

最も多い勘違いがこれです。stop_sequences は「Claude が出力しないでほしい文字列」を指定する機能ではなく、「Claude が出力したらそこで止める文字列」を指定する機能です。

たとえば「JSON だけ返してほしいから "```" で止めよう」と考えても、Claude がコードブロック記法を使わずに JSON を直接返してきたら、停止文字列は永遠に現れません。stop_reason が "end_turn" で返ってくるはずです。

対処は、プロンプト側で停止文字列を必ず出力するように指示することです。

# ❌ 望ましくない: 停止文字列が出るかどうかは Claude 任せ
prompt = "ユーザー情報を JSON で返してください"
stop_sequences = ["```"]
 
# ✅ 推奨: 停止文字列を必ず出力するフォーマットを指示する
prompt = """ユーザー情報を以下の形式で返してください。
=== JSON START ===
{...JSON...}
=== JSON END ===
JSON 出力後は必ず === JSON END === を出力してください。"""
stop_sequences = ["=== JSON END ==="]

「Claude が必ず出すマーカー」を自分で設計するのが、現場で安定する書き方です。

原因 2: 空白・改行の数が一致していない

「\n\n で止めたいのに止まらない」という相談をよく受けます。原因は、Claude が出力しているのが \n\n ではなく \n 1 つだけだったり、逆に \n\n\n だったりするケースです。

トークナイザの観点から言うと、Claude は連続する空白や改行を、状況に応じて異なるトークン列にエンコードします。stop_sequences はバイト単位の一致を見るので、\n の数が 1 つでも違うとマッチしません。

切り分け方は、まず stop_sequences を外して 1 回生成し、出力を repr() で確認することです。

# 切り分け用: まずは stop_sequences なしで生成して実際の出力を見る
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=512,
    messages=[{"role": "user", "content": "5から1までカウントダウンして"}],
)
print(repr(response.content[0].text))
# 出力例: '5\n4\n3\n2\n1' ← \n\n ではなく \n だった

期待していた \n\n ではなく \n だけなら、stop_sequences=["\n"] に変えるか、プロンプトで「各項目の後に空行を入れてください」と明示します。

原因 3: トークン境界をまたぐ停止文字列

これが私自身ハマった一番厄介なケースです。Claude のトークナイザは、たとえば "###" のような文字列を、状況に応じて 1 トークンにも 2〜3 トークンにも分割します。

stop_sequences のマッチング自体はバイト単位で行われるので、原理的にはトークン境界の影響は受けないはずです。しかし実装上、ストリーミング応答ではトークン単位で配信されるため、停止文字列が複数トークンに分割されてストリームを越えると、消費側のロジックが不安定になることがあります。

特に、SSE で受信したテキストを逐次表示しつつ手元でも stop_sequences 相当の判定をしているコードでは、トークン境界をまたいだ停止文字列を取りこぼします。

対処は次の 2 つです。

# ✅ サーバー側の stop_sequences に任せ、クライアント側で再判定しない
with client.messages.stream(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    stop_sequences=["=== END ==="],
    messages=[{"role": "user", "content": "..."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
 
    # サーバーが停止判定を終えた後の最終メッセージで stop_reason を確認
    final = stream.get_final_message()
    print(f"\nstop_reason: {final.stop_reason}")
# ✅ クライアント側でも判定したいなら、複数トークンを跨ぐバッファを持つ
buffer = ""
STOP = "=== END ==="
 
with client.messages.stream(...) as stream:
    for text in stream.text_stream:
        buffer += text
        if STOP in buffer:
            # 停止文字列の手前までを最終出力として扱う
            final_text = buffer.split(STOP)[0]
            break

クライアント側の二重判定はバグの温床になりやすいので、可能な限りサーバー側の stop_sequences に任せるのが安全です。

原因 4: Tool Use モードでは挙動が変わる

tools パラメータを併用したリクエストでは、stop_sequences の優先順位が変わります。具体的には、Claude がツール呼び出しを開始した時点で stop_reason が "tool_use" になり、stop_sequences の判定はそれ以降の出力に対してのみ働きます。

# Tool Use と stop_sequences を併用するときの挙動
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=2048,
    tools=[{"name": "get_weather", "input_schema": {...}, "description": "天気を取得"}],
    stop_sequences=["=== END ==="],
    messages=[{"role": "user", "content": "東京の天気を教えて、最後に === END === と書いてください"}],
)
 
# Claude がツールを呼び出すと stop_reason = "tool_use" で一時停止する
# stop_sequences の判定はこのターンでは行われない
print(response.stop_reason)  # "tool_use"

Tool Use 経由で複数ターンの会話を構築している場合、最終ターンの平文応答で初めて stop_sequences が機能します。「ツール呼び出しを stop_sequences で止めたい」という設計は成立しないので、その用途には tool_choice パラメータを使ってください。

私の経験では、エージェント実装で「途中で止まらない」という相談の半分以上は、Tool Use と stop_sequences の役割分担を取り違えているケースでした。

原因 5: extended thinking が有効になっている

Claude の Extended Thinking(拡張思考)モードが有効な場合、stop_sequences は思考ブロックには適用されません。停止判定は最終的な回答テキストにのみ働きます。

これは公式ドキュメントにも明記されていますが、見落とされがちなポイントです。Extended Thinking を有効にして「JSON だけ返してほしいから } で止めよう」とした場合、思考ブロック内に } がいくら現れても止まりません。

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=4096,
    thinking={"type": "enabled", "budget_tokens": 2000},
    stop_sequences=["}"],  # ← 思考ブロック内の } では止まらない
    messages=[{"role": "user", "content": "計算結果を JSON で返して"}],
)
 
# response.content は [thinking_block, text_block] のリスト
# stop_sequences の判定は text_block にのみ適用される
for block in response.content:
    print(block.type, "->", block.text[:50] if hasattr(block, "text") else block.thinking[:50])

Extended Thinking を併用する場合は、回答セクションのフォーマットを明示し、その中に専用の停止マーカーを設計するのが確実です。

切り分けチェックリスト

ここまでの内容を、現場で詰まったときに上から順に試せるチェックリストにまとめました。

  • stop_reason の値を確認する(end_turn なら原因 1、stop_sequence ならパターン特定が必要)
  • stop_sequences を外して repr() で実際の出力を確認する
  • 改行・空白の数が想定と一致しているか確認する
  • ストリーミング使用時はクライアント側で二重判定していないか確認する
  • tools パラメータを使っているかどうか確認する
  • Extended Thinking が有効になっていないか確認する

このチェックリストで原因が絞れない場合は、停止文字列の設計そのものを「Claude が確実に出力するマーカー方式」に切り替えるのが、最も投資対効果の高い解決策です。

設計を変えるという選択肢

最後に、トラブルシューティングを超えた話を 1 つだけ。stop_sequences でフォーマットを制御するのは、本質的には「文字列マッチングによる制御」であり、Claude の自然言語生成の柔軟性とは相性が悪い面があります。

私はここ半年ほど、フォーマット制御が必要な用途では stop_sequences よりも次の 2 つを優先するようになりました。

  • Tool Use(function calling): 構造化出力が必要なら、JSON Schema 付きのツール定義を渡してツール呼び出し形式で受け取る
  • Constrained sampling 系の機能: Anthropic SDK の response_format 系オプションが提供される場合は積極的に使う

トラブルシューティングで時間を溶かす前に、「そもそも stop_sequences が最適解か?」を一度立ち止まって考える価値はあります。

個人開発で複数のアプリと Dolice Labs のサイト群を並行して回している身としては、停止文字列の不一致のような細部に半日を溶かすより、最初から壊れにくい設計に寄せておくほうが結局は早い、と何度も痛感してきました。私自身、この記事のチェックリストは実際に手元のコードで詰まるたびに書き足してきたものです。

API の挙動を深く理解するうえでは、Claude API トークンカウントとコスト最適化ガイド や Claude API 高度な Tool Use 完全ガイド も参考になります。あわせて Claude API ストリーミング Tool Use 実装 を読んでおくと、ストリーミング系の落とし穴に強くなれます。

次にこの記事を活かす一歩として、今手元にある stop_sequences を使ったコードを 1 つ取り出し、stop_reason をログに出すコードを 3 行追加してみてください。それだけで、次にこの問題に遭遇したときの切り分け時間は 10 分の 1 になります。

シェア

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

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

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

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

関連記事

⬡ API & SDK2026-04-26
Claude API × WebSocket × Redis Pub/Sub でスケーラブルなリアルタイム AI チャットサーバーを構築する — Node.js 本番設計・マルチユーザー対応・コスト制御の実装
Claude API × WebSocket × Redis Pub/Sub でリアルタイム AI チャットサーバーを本番運用する設計と実装。SSE との判断基準、マルチインスタンス対応、停止遅延がユーザーごとの予算精度をどう崩すかまで、計測コードつきで扱います。
⬡ API & SDK2026-05-22
Claude の『このモデルは現在利用できません』が出る時に効くモデル選択フォールバックの設計
Claude API で『このモデルは現在利用できません(this model is currently unavailable)』と返ってきたとき、529 Overloaded やレート制限と同じ扱いにすると遠回りになります。エラーの種類を SDK の例外クラスで見分け、モデル ID をリクエスト単位で切り替えるフォールバックの実装と、思い込みで書きやすい落とし穴をまとめました。
⬡ API & SDK2026-05-16
Claude API で多言語レビュー返信を自動化した実装ログ — App Store 非公式制限との付き合い方
壁紙・ヒーリング系アプリを複数運営する個人開発者が、Claude APIで多言語レビュー返信を自動化した実装ログ。App Storeの8秒待機ルールや1セッション30件制限など、公式ドキュメントには書かれていない落とし穴と解決策を詳解します。