「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 になります。