Claude Media
入力サイズ不明でも最大出力を引き出す実装パターン — model_context_window_exceeded

入力サイズ不明でも最大出力を引き出す実装パターン — model_context_window_exceeded

入力トークン数を事前計算せずに、コンテキストウィンドウが許す最大量までClaudeに出力させる実装パターンをmodel_context_window_exceededで組む方法を解説します。

stop_reasonフィールドが教えてくれること

Messages APIの成功レスポンスには必ずstop_reasonフィールドが含まれます。これはエラーとは別物で、「なぜ生成が終わったか」を示すフィールドです。値ごとに次にすべきことが変わります。

stop_reason発生条件次にすべきこと
end_turn発生条件自然に応答が完結した次にすべきことそのまま使う
max_tokens発生条件指定したmax_tokensに到達した次にすべきことmax_tokensを上げるか、続きを生成させる
model_context_window_exceeded発生条件コンテキストウィンドウを使い切った次にすべきこと打ち切りとして扱う
tool_use発生条件Claudeがtoolを呼び出した次にすべきことtoolを実行し結果を返す

このうちmax_tokensmodel_context_window_exceededはどちらも「途中で打ち切られた」ことを示す点で共通していますが、原因は別物です。前者は呼び出し側が指定した値に達したのに対し、後者はモデル自体の物理的な容量に達しています。本記事のパターンは、この違いを逆手に取ってmax_tokensを意図的に大きく設定し、実際の上限をコンテキストウィンドウ側に決めさせるという発想に立っています。

入力サイズを計算せずに最大出力を引き出す

長い入力を渡したとき、コンテキストウィンドウの残量から実際に出力できるトークン数を毎回正確に計算するのは面倒です。model_context_window_exceededというstop_reasonを使うと、この計算を省略できます。max_tokensに大きめの値を指定してリクエストを送るだけで、コンテキストウィンドウが許す上限まで生成させ、そこで打ち切られたことをレスポンスから判定できます。

対応はSonnet 4.5以降のモデルが既定です。それ以前のモデルではmodel-context-window-exceeded-2025-08-26ベータヘッダーを付けてopt-inします。このstop_reasonの値はSDKのbeta名前空間にのみ型定義されているため、コードはclient.beta.messages経由で呼び出し、Beta接頭辞の型を使います。従来の実装では、入力に何トークン使っているかをトークンカウンティングAPIで事前に計算し、そこから逆算してmax_tokensの残り枠を決めるという手順を踏んでいました。ドキュメントや会話履歴の長さが呼び出しごとに変わる用途では、この事前計算のたびに1回分のAPI呼び出しが増えることになり、実装も一段階増えます。

実装パターン

次の関数は、入力サイズを事前計算せずに「取れるだけの出力」を得るための共通パターンです。ポイントはstop_reasonを3種類に分岐させることです。

def get_max_possible_tokens(client, prompt):
    """
    Get as many tokens as possible within the model's context window
    without needing to calculate input token count
    """
    response = client.beta.messages.create(
        model="claude-opus-5",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=20000,  # Python SDKではmax_tokensが約21kを超えるとstreaming必須
    )
 
    if response.stop_reason == "model_context_window_exceeded":
        # 入力サイズに応じた最大限のトークンを取得できた
        print(f"Generated {response.usage.output_tokens} tokens (context limit reached)")
    elif response.stop_reason == "max_tokens":
        # 要求どおりのトークン数を取得できた
        print(f"Generated {response.usage.output_tokens} tokens (max_tokens reached)")
    else:
        # 自然な完了
        print(f"Generated {response.usage.output_tokens} tokens (natural completion)")
 
    return next((block.text for block in response.content if block.type == "text"), "")
async function getMaxPossibleTokens(client: Anthropic, prompt: string): Promise<string> {
  const response = await client.beta.messages.create({
    model: "claude-opus-5",
    max_tokens: 20000,
    messages: [{ role: "user", content: prompt }]
  });
 
  const tokens = response.usage.output_tokens;
  if (response.stop_reason === "model_context_window_exceeded") {
    console.log(`Generated ${tokens} tokens (context limit reached)`);
  } else if (response.stop_reason === "max_tokens") {
    console.log(`Generated ${tokens} tokens (max_tokens reached)`);
  } else {
    console.log(`Generated ${tokens} tokens (natural completion)`);
  }
 
  const textBlock = response.content.find(
    (block): block is Anthropic.Beta.BetaTextBlock => block.type === "text"
  );
  return textBlock?.text ?? "";
}

3つの分岐が意味することをまとめます。

stop_reason何が起きたか実務上の解釈
model_context_window_exceeded何が起きたかコンテキストウィンドウの残量を使い切った実務上の解釈入力サイズに応じて得られる最大量まで生成できた。想定どおりの動作
max_tokens何が起きたか指定したmax_tokensに到達した実務上の解釈入力が短く、コンテキストウィンドウにまだ余裕がある状態で頭打ちになった
それ以外(end_turn等)何が起きたかClaudeが自然に応答を終えた実務上の解釈出力が短くて済むタスクだった。上限には無関係

この関数を呼ぶ側は、入力プロンプトの長さを気にせず一律でmax_tokensを大きめの値(この例では20000)に設定して投げるだけで済みます。実際にどこまで生成されるかはコンテキストウィンドウの残量が決め、レスポンスのstop_reasonを見れば「上限まで使い切ったのか」「要求量に届いただけなのか」を区別できます。呼び出し側のロジックを1本にまとめられる点が、事前計算方式に対する最大のメリットです。

curlで挙動を確認する

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",
    "max_tokens": 20000,
    "messages": [{"role": "user", "content": "Large input that uses most of context window..."}]
  }' | jq '.stop_reason'

長い入力を渡したときにmodel_context_window_exceededが返れば、このパターンが機能している証拠です。短い入力ではmax_tokens到達か自然完了になるはずなので、両方のケースを一度試して分岐の挙動を確認しておくと実装ミスに早く気づけます。実装を組み込む前に、このcurlコマンドで期待どおりのstop_reasonが返るかを確かめておくと安全です。

Python SDKの実装上の注意点

Python SDKにはmax_tokensが約21,000を超える場合はストリーミング(stream=True)を使う必要があるという制約があります。上のサンプルコードのmax_tokens: 20000という値は、この制約のすぐ手前に収まる水準として選ばれています。もっと大きいmax_tokensを指定して同じパターンを使いたい場合は、client.beta.messages.stream()でストリーミング実装に切り替える必要があります。

入力サイズが変わると何が起きるかを具体的に見る

このパターンの動作を具体的な数値で追うと理解しやすくなります。仮にモデルのコンテキストウィンドウが1Mトークンだとして、max_tokens: 20000を指定したリクエストを3パターン試すと、次のように分岐します。

  • 入力が10,000トークンの場合: 残りのウィンドウは十分にあるため、生成はmax_tokensの20,000に先に到達し、stop_reason: "max_tokens"で終わる
  • 入力が990,000トークンの場合: 残りのウィンドウは10,000トークン分しかなく、max_tokensの20,000に届く前にコンテキストウィンドウを使い切り、stop_reason: "model_context_window_exceeded"で終わる。実際に生成されたのは約10,000トークン分
  • 入力がごく短く、応答も短くて済むタスクの場合: どちらの上限にも達せずstop_reason: "end_turn"で自然に終わる

呼び出し側のコードは3パターンいずれでも同じで構いません。入力サイズが呼び出しごとにばらつくバッチ処理では、この分岐をそのまま活かして「入力が短ければmax_tokensいっぱいまで、長ければウィンドウが許す分だけ」出力させる、という実装が1つの関数で完結します。

このパターンが向く場面と向かない場面

向くのは、入力の長さが呼び出しごとにばらつき、事前にトークンカウンティングAPIを呼ぶコストや実装の手間を避けたい場面です。例えば大量のドキュメントを要約するバッチ処理で、ドキュメントごとに事前トークン数計算を挟まずに「取れるだけ要約させる」実装にしたい場合に向いています。

向かないのは、出力トークン数を厳密に制御したい場面です。model_context_window_exceededは「コンテキストウィンドウが尽きた」ことを示すだけで、出力を途中で切る動作そのものは変わりません。文の途中で切れたテキストが返る可能性があるため、切れ目のない完結した出力が必須の用途では、事前にトークンカウンティングAPIで入力サイズを確認し、余裕を持ったmax_tokensを明示的に計算するアプローチのほうが安全です。コンテキストウィンドウ全体の挙動(入力オーバーフローとの違いを含む)はコンテキストウィンドウがオーバーフローしたときのClaude APIの挙動にまとめています。

判断軸このパターンが向く事前計算が向く
入力サイズのばらつきこのパターンが向く呼び出しごとに大きく変動する事前計算が向くほぼ一定、または既知
出力の完結性このパターンが向く多少の打ち切りが許容できる(要約・下書き生成等)事前計算が向く途中で切れると成果物として成立しない
実装コストこのパターンが向くトークンカウンティングAPIの呼び出しを1回減らしたい事前計算が向くすでに事前見積もりの仕組みがある
エラーハンドリングこのパターンが向くstop_reason分岐だけで完結させたい事前計算が向く入力段階での400エラー回避を重視する

どちらの方針を選ぶ場合でも、入力自体がコンテキストウィンドウそのものを超えているケースは別扱いです。その場合は生成が始まる前に400のinvalid_request_errorが返るため、model_context_window_exceededのパターンでは救えません。事前のトークンカウンティングを完全に省略できるわけではなく、「入力が明らかに上限を超えていないことの確認」だけは依然として必要です。

出力が途中で切れた場合の後処理

model_context_window_exceededで打ち切られたレスポンスは、そのままでは文や段落の途中で終わっている可能性があります。最低限の対処は、切れていることを読者や後段の処理に明示することです。

def handle_truncated_response(response):
    text = next((block.text for block in response.content if block.type == "text"), "")
    if response.stop_reason in ["max_tokens", "model_context_window_exceeded"]:
        if response.stop_reason == "max_tokens":
            note = "[max_tokens到達により応答が途中で切れています]"
        else:
            note = "[コンテキストウィンドウの上限により応答が途中で切れています]"
        return f"{text}\n\n{note}"
    return text

max_tokens到達とmodel_context_window_exceededは原因が違うため、通知メッセージも分けておくと後からログを見たときに原因を切り分けやすくなります。

完結した出力がどうしても必要な場合は、通知を付けるのではなく続きを生成させる設計にします。公式ドキュメントはmax_tokens打ち切り向けにこの手法を示していますが、直前の応答をアシスタントメッセージとして会話に追加し「続きから書いてください」と指示する考え方自体は、model_context_window_exceededによる打ち切りにも同様に応用できます。

def get_complete_response(client, prompt, max_attempts=3):
    messages = [{"role": "user", "content": prompt}]
    full_response = ""
 
    for _ in range(max_attempts):
        response = client.beta.messages.create(
            model="claude-opus-5", messages=messages, max_tokens=20000
        )
        full_response += next(
            (block.text for block in response.content if block.type == "text"), ""
        )
 
        if response.stop_reason not in ("max_tokens", "model_context_window_exceeded"):
            break
 
        messages = [
            {"role": "user", "content": prompt},
            {"role": "assistant", "content": full_response},
            {"role": "user", "content": "続きから書いてください。"},
        ]
 
    return full_response

ここで注意が必要なのは、model_context_window_exceededで打ち切られたケースでは続きを生成させる次のリクエストも、同じ長い入力(元のプロンプト + これまでの生成分)を引きずったまま同じコンテキストウィンドウの制約に当たるという点です。max_tokens到達のケースと違い、入力自体がすでに大きいので、単純なループでは再びmodel_context_window_exceededに当たり続ける可能性があります。この状態が繰り返し起きる長時間のエージェントループでは、サーバーサイドCompactionを併用して古い部分を要約し、実効的なコンテキスト容量を延ばす設計に切り替えたほうが扱いやすくなります(Compactionとプロンプトキャッシュを両立させる設計はCompactionとプロンプトキャッシュのCache Control両立設計、発動しきい値の考え方はCompactionの発動しきい値の決め方を参照)。

thinkingやtool useと組み合わせるときの注意

Claude Opus 5のように拡張思考(thinking)が既定で有効なモデルでこのパターンを使う場合、thinkingトークンも出力トークンの一部としてカウントされ、max_tokensの枠を消費します。thinkingに多くのトークンを使うタスクでは、テキスト生成に回せる分が相対的に減るため、model_context_window_exceededに到達するタイミングも早まります。tool useを併用するエージェントループでは、1回のリクエストで複数のtool callとその結果が積み重なり、これも同じくコンテキストウィンドウの消費として扱われます。長いツール結果を何度も往復させるループでこのパターンを使うと、model_context_window_exceededが想定より早い段階で発生することがあるため、単発のリクエストで使う場合とエージェントループの中で使う場合とでは、実際に得られる出力量の目安を分けて考える必要があります。

よくある質問

model_context_window_exceededとmax_tokensの両方に到達することはあるか

同時には起きません。生成がコンテキストウィンドウの残量を先に使い切ればmodel_context_window_exceededmax_tokensの指定値に先に到達すればmax_tokensになります。どちらが先に発生するかは入力の長さとmax_tokensの値の組み合わせで決まります。

200kトークンモデルでもこのパターンは使えるか

使えます。ただしSonnet 4.5より前のモデルではmodel-context-window-exceeded-2025-08-26ベータヘッダーを明示的に付ける必要があります。付けなければ、コンテキストウィンドウを超えたときに旧来のバリデーションエラーが返り、このパターンは機能しません。

max_tokensをとにかく大きくすればよいのか

そうとは限りません。モデルごとにmax_tokensには上限があり(Claude Opus 5は128,000)、これを超える値を指定するとリクエスト自体が通りません。上限内であっても、実際にどこまで生成できるかは入力サイズとモデルのコンテキストウィンドウで決まるため、天井を上げるだけで出力量が増えるわけではありません。加えてPython SDKのようにストリーミングが必須になる境界値もあるため、実装の切り替えコストとのバランスで妥当な値(本記事の例では20000)を選びます。極端に小さいmax_tokensを指定すると、そもそもmodel_context_window_exceededに到達する前にmax_tokens到達で打ち切られてしまい、このパターンの意味がなくなる点にも注意します。

この記事を共有:XはてブLinkedIn