Claude APIのstop_sequencesで出力を途中で止める — 区切り文字とmax_tokensの見分け方
stop_sequencesに渡した文字列で生成を止め、stop_reasonとstop_sequenceで発火元を読み取る方法を、区切り文字での分割とテンプレ穴埋めの実装例つきで解説します。
stop_sequences は、Messages APIのリクエストに渡す文字列の配列です。モデルがその文字列のどれかを生成した時点で、生成が止まります。止まったかどうかは、レスポンスの stop_reason が "stop_sequence" になることで分かります。どの文字列で止まったかは、同じレスポンスの stop_sequence フィールドに入ります。
用途は、出力の末尾に付く前置きや解説を切り落とす、区切り文字ごとに出力を分割する、決まった形式の穴埋めで必要な部分だけを受け取る、の3つが中心です。この記事では指定方法から、max_tokens による打ち切りとの判別、ストリーミング時の挙動までを順に扱います。
stop_sequencesの指定とレスポンスの読み方
stop_sequences はリクエストボディの任意パラメータで、文字列の配列を渡します。公式のリファレンスでは「モデルに停止させたいカスタムの文字列」と説明されています。モデルが自然に終わったときの stop_reason は "end_turn" です。渡した文字列のどれかに当たると、stop_reason が "stop_sequence" になり、stop_sequence フィールドに一致した文字列が入ります。
最小の呼び出しは次のとおりです。
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"stop_sequences": ["END", "STOP"],
"messages": [{"role": "user", "content": "Generate text until you say END"}]
}'Python SDKなら、同じ内容を次のように書けます。
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-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}")止まらなかったとき、つまり end_turn や max_tokens で終わったときの stop_sequence は null です。スキーマ上は「文字列またはnull」で、カスタムの停止文字列が生成されたときだけ非nullになります。
3つの終わり方を分岐で見分ける
同じ「応答が終わった」でも、end_turn、max_tokens、stop_sequence では意味が違います。stop_sequence は自分で決めた合図に当たった正常な停止で、max_tokens はトークン上限に達した強制的な打ち切りです。後者は文の途中で切れている可能性があるので、そのまま利用側に渡せません。
| stop_reason | 何が起きたか | 出力の扱い |
|---|---|---|
end_turn | 何が起きたかモデルが自然に終えた | 出力の扱いそのまま使える |
stop_sequence | 何が起きたか渡した文字列が生成された | 出力の扱い合図の手前までを使う |
max_tokens | 何が起きたかmax_tokens かモデルの上限に達した | 出力の扱い途中で切れている前提で扱う |
この3つを1つの関数で振り分けておくと、呼び出し側が毎回 stop_reason を気にしなくて済みます。
def read_text(response):
text = "".join(b.text for b in response.content if b.type == "text")
reason = response.stop_reason
if reason == "end_turn":
return text, "complete"
if reason == "stop_sequence":
return text, f"stopped_by:{response.stop_sequence}"
if reason == "max_tokens":
return text, "truncated"
raise RuntimeError(f"unexpected stop_reason: {reason}")stop_reason はこの3つ以外にも tool_use、pause_turn、refusal、model_context_window_exceeded があります。全体の設計はstop_reasonとエラーの違いにまとめています。上の関数は、停止文字列を使うリクエストで起きる3値だけを扱う前提です。
停止文字列は出力に含まれるのか
実装でまず気になるのは、END のような合図そのものが content のテキストに残るかどうかです。ここは create のリファレンスにも、停止理由のガイドにも記載がありません。「stop_sequence フィールドに一致した文字列が入る」という説明があるだけです。
記載がないものを推測で決め打ちすると、後段のパーサが合図ごと受け取って壊れます。次の確認コードを、自分が使うモデルと停止文字列の組み合わせで一度走らせておくと安全です。
resp = client.messages.create(
model="claude-opus-5-5",
max_tokens=200,
stop_sequences=["###"],
messages=[{"role": "user", "content": "短い挨拶を書き、最後に ### と書いてください"}],
)
text = resp.content[0].text
print(repr(text), resp.stop_reason, repr(resp.stop_sequence))
print("合図が本文に含まれる:", "###" in text)結果は出力の末尾を見れば分かります。加えて、合図が含まれる場合に備えた保険を入れておけば、どちらに転んでも動きます。
if resp.stop_reason == "stop_sequence" and text.endswith(resp.stop_sequence):
text = text[: -len(resp.stop_sequence)]どちらの挙動でも壊れないように書いておくのが、記載のない仕様に対する現実的な作法です。
区切り文字で出力を分割する
1つ目の使い道は、複数のパートを順番に作らせて、区切り文字の位置で毎回止める方法です。たとえば商品説明を「概要」「特徴」「注意」の3パートで作りたいとき、パートごとに終端の合図を決めておきます。
PARTS = [("概要", "<<END_SUMMARY>>"), ("特徴", "<<END_FEATURES>>")]
results = {}
for name, marker in PARTS:
r = client.messages.create(
model="claude-opus-5-5",
max_tokens=600,
stop_sequences=[marker],
messages=[{
"role": "user",
"content": f"次の商品の「{name}」だけを書き、書き終えたら {marker} と出力してください。\n商品: 折りたたみ式の電動自転車",
}],
)
text, status = read_text(r)
if status == "truncated":
raise RuntimeError(f"{name} が max_tokens で切れました")
results[name] = text.strip()ここでの要点は、max_tokens を各パートの想定より少し大きめに置くことです。合図が来る前に上限へ達すると truncated になり、パートが途中で切れたと分かります。合図の手前まででよいのか、続きを取り直すのかは、アプリ側のルールとして決めておきます。
停止文字列には、地の文に出にくい記号の並びを選びます。END のような一般的な英単語は、本文中に自然に現れるだけで止まってしまいます。上の例の <<END_SUMMARY>> のように、パートごとに別の文字列を割り当てると、どの合図で止まったかが stop_sequence からも読めます。
テンプレ穴埋めで必要な部分だけを受け取る
2つ目は、出力に付く前置きや解説を切り落とす使い方です。タグで囲んで答えさせ、閉じタグを停止文字列にします。
resp = client.messages.create(
model="claude-opus-5-5",
max_tokens=300,
stop_sequences=["</answer>"],
messages=[{
"role": "user",
"content": "次の文の感情を positive / negative / neutral のどれかで答えてください。"
"答えは <answer> と </answer> で囲んでください。\n\n文: 配送が早くて助かりました。",
}],
)
raw = resp.content[0].text
label = raw.split("<answer>", 1)[-1].strip()閉じタグに当たった時点で止まるので、その後ろに続くはずの解説を生成させずに済みます。split で開きタグの後ろを取る書き方にしているのは、停止文字列が本文に残るかどうかに依存しないためです。前節の保険と同じ考え方で、閉じタグが残っていても落ちません。
もう1つの使い方として、人間とAIの対話ログのようなプロンプトで、次の話者のラベルを停止文字列にする方法があります。ラベルが出た時点で止まるので、AIが1人2役で会話を続けてしまうのを防げます。
prompt = (
"以下は、サポート担当(AI)と利用者の会話です。\n\n"
"利用者: パスワードを忘れました。\n"
"担当:"
)
resp = client.messages.create(
model="claude-opus-5-5",
max_tokens=300,
stop_sequences=["\n利用者:"],
messages=[{"role": "user", "content": prompt}],
)担当の返答を書き終えて次の行に 利用者: が来た時点で止まるので、返答の1ターン分だけが手に入ります。改行を含めて "\n利用者:" とすると、返答の途中で「利用者:」という語が出ても誤って止まりにくくなります。ただし、停止文字列が本文に残るかどうかは、前節のとおり実測で確かめてください。
max_tokensと見分けて再試行する
stop_sequences を使う処理で最も多い不具合は、max_tokens で切れた出力を、合図で止まった出力と同じに扱ってしまうことです。上限が小さいと、合図に届く前に打ち切られ、中途半端な文字列が正常な結果として流れます。
出力が切れたときの切り分け
- Qstop_reason は stop_sequence か?答えがいいえのとき
max_tokensならmax_tokensの値を上げるか、プロンプトで出力を短くさせます。end_turnなら、合図の指示が通っていないので、プロンプトの書き方を見直します。stop_sequenceフィールドで、どの文字列が発火したかも確認できます。
max_tokens を上げる場合の考え方は、入力サイズが読めないときの上限設定を扱った最大出力を引き出す実装パターンが参考になります。出力を絞る側の設計はadvisorのmax_tokensで出力を抑える方法で扱っています。
ストリーミングで停止文字列を受け取る
stream: true で受け取る場合、stop_reason はイベントによって値が変わります。公式のスキーマでは、非ストリーミングでは常に非nullで、ストリーミングでは message_start イベントでnull、それ以降は非nullとされています。
イベントの形は、公式のストリーミングの例で message_delta の delta に stop_reason と stop_sequence が並んでいます。end_turn で終わる例では、次のように stop_sequence が null です。
data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null}, "usage": {"output_tokens": 15}}停止文字列で止まった場合は、同じ位置の stop_reason が "stop_sequence" になり、stop_sequence に一致した文字列が入る構造です。SDKの messages.stream() を使うなら、イベントを自分で読まず、最後に組み上がった Message から stop_reason と stop_sequence を見れば足ります。公式の例では、組み上げ用の get_final_message()(TypeScriptでは finalMessage())を使っています。
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=1024,
stop_sequences=["</answer>"],
messages=[{"role": "user", "content": PROMPT}],
) as stream:
for chunk in stream.text_stream:
print(chunk, end="", flush=True)
message = stream.get_final_message()
print()
print(message.stop_reason, repr(message.stop_sequence))テキストは届いた順に画面へ流しつつ、止まった理由は最後にまとめて判定できます。画面に流す途中の段階では、まだ停止文字列で止まるか分からない点に注意してください。画面表示に合図がそのまま混ざる可能性があるなら、表示側でも前節の保険と同じ除去が要ります。
TypeScript SDKでも考え方は同じです。stop_sequences を渡し、finalMessage() で組み上がった Message から stop_reason と stop_sequence を読みます。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-opus-5-5",
max_tokens: 1024,
stop_sequences: ["</answer>"],
messages: [{ role: "user", content: prompt }],
});
const message = await stream.finalMessage();
console.log(message.stop_reason, message.stop_sequence);公式に記載がない点と運用上の注意
create のリファレンスと停止理由のガイドには、次の点が書かれていません。
- 停止文字列の個数や1つあたりの長さの上限
- 停止文字列が出力テキストに含まれるかどうか
- 複数の文字列が同時に当たったときの優先順位
上限に関する記載がないため、停止文字列は数個の短い文字列にとどめるのが無難です。数十個を並べる設計にする前に、使うモデルで実際に通るかを試してください。上限に触れた場合の挙動も記載がないため、本番に載せる前に実測で確かめるのが確実です。
もう1点、停止文字列を指定しても、モデルがその文字列を出力してくれるとは限りません。プロンプトで「書き終えたら合図を書く」と指示しているときに、モデルがそれを守らず end_turn で終わることがあります。stop_reason が end_turn なら、合図なしで完結した出力として扱うのか、エラーにするのかを、分岐で決めておきます。
停止文字列の選び方とテスト
停止文字列の良し悪しは、本文に自然に現れるかどうかで決まります。本文に出る文字列を選ぶと、意図しない位置で止まり、stop_reason は stop_sequence なのに中身が半端、という分かりにくい不具合になります。
| 候補 | 向き不向き | 理由 |
|---|---|---|
END | 向き不向き不向き | 理由英文の本文や見出しに現れやすい |
\n\n | 向き不向き用途次第 | 理由段落の区切りで必ず止まるので、最初の1段落だけ欲しいときに限る |
</answer> | 向き不向き向く | 理由開きタグとセットで指示でき、本文には出にくい |
<<END_SUMMARY>> | 向き不向き向く | 理由パートごとに別の文字列を割り当てられる |
改行を停止文字列にすると、最初の1行や1段落だけを取り出せます。ただし、リストや箇条書きを出力させるプロンプトでは、1項目目の末尾で止まります。意図した使い方かどうかを先に決めてください。
誤発火の検知には、出力の長さを見る方法があります。たとえば、パートの本文は最低でも数十文字あるはずだと分かっているなら、それより短い結果を警告にします。
MIN_CHARS = 30 # パートごとに想定する最低字数(アプリ側の基準)
text, status = read_text(r)
if status.startswith("stopped_by:") and len(text.strip()) < MIN_CHARS:
print(f"短すぎます。本文中で停止文字列が出た可能性: {text!r}")stop_reason が stop_sequence でも、中身が空に近ければ、合図が本文の早い位置で出ています。その場合は停止文字列を別の記号列に変えます。
最低字数の基準は、停止文字列ごとに持たせると使いやすくなります。区切り文字で分割する構成なら、パートごとに概要は短め、特徴は長めのように想定が違うためです。基準を下回った結果は、そのまま後段に流さず、再生成の対象として記録しておくと原因をたどれます。ログには stop_sequence の値と出力の長さを一緒に残すと、どの合図が早く出やすいかも後から集計できます。
合図の指示が守られているかは、テストで先に潰せます。同じプロンプトを数回流し、stop_reason の内訳を数えるだけで、運用時の取りこぼし率の目安になります。
from collections import Counter
counts = Counter()
for _ in range(10):
r = client.messages.create(
model="claude-opus-5-5",
max_tokens=300,
stop_sequences=["</answer>"],
messages=[{"role": "user", "content": PROMPT}],
)
counts[r.stop_reason] += 1
print(counts) # 例: Counter({'stop_sequence': 10})stop_sequence 以外が混ざるなら、end_turn はプロンプトの指示が弱い、max_tokens は上限が足りない、と原因を切り分けられます。
まとめ
stop_sequences は、出力の末尾を切るための道具であると同時に、アプリ側のパーサへ「ここまでが1単位」と伝える合図にもなります。使うときは、3つの終わり方を read_text のような関数で一か所に集め、max_tokens による切断を正常終了に混ぜないことが肝です。停止文字列の本文への混入は、記載がないので、手元で一度実測して保険の処理まで入れておくと安全です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。