ブログに戻る
コラム

コンテキストウィンドウとは?LLMの上限と超えたときの対処法

FastMetal

コンテキストウィンドウとは、LLMが一度のリクエストで扱えるトークン数の上限です。 システムプロンプト、会話履歴、ツール定義、そしてモデルが生成する出力まで、すべてがこの一つの枠を共有します。OpenAI互換APIでチャットを組んでいて「途中から急にエラーになる」「長い会話で返答が途切れる」と感じたら、原因はほぼこの上限です。

この記事では、コンテキストウィンドウが何を数えているのか、なぜ会話が進むほど枠が減っていくのか、そして超えたときにどう対処すればよいかを整理します。

コンテキストウィンドウとは

コンテキストウィンドウ(コンテキスト長とも呼ばれます)は、モデルが「一度に見渡せる」情報量の上限です。単位は文字数ではなくトークンで、日本語は英語より多くのトークンを消費する傾向があります(詳しくはトークンとはを参照してください)。

重要なのは、次のものがすべて同じ枠に含まれるという点です。

枠を消費するもの内容
システムプロンプト役割や制約の指示(システムプロンプトとは
会話履歴これまでのユーザー発言とアシスタント応答
ツール定義Function calling で渡す関数のスキーマ
添付データ貼り付けた文書、検索結果、コードなど
モデルの出力これから生成される応答(max_tokens 分)

「入力の上限」だと思われがちですが、出力の分も同じ枠から引かれます。入力で枠を使い切ると、モデルには返答を書く余地が残りません。

なぜ会話が進むほど枠が減るのか

Chat Completions APIはステートレスです。サーバーは前回のやり取りを覚えていないので、クライアントは毎回、会話履歴を丸ごと送り直します。その結果、送信トークン数はターンごとに積み上がっていきます。

ターン送信する内容送信トークン数(例)
1システムプロンプト + 質問1約500
2上記 + 回答1 + 質問2約1,200
3上記 + 回答2 + 質問3約2,000
10上記 + 回答9 + 質問10約8,000〜

※ あくまで例です。実際の数値は文章量やモデルのトークナイザによって変わります。

会話が長くなるほど、1ターンあたりの送信量は増え続けます。ここが「最初は動いていたのに、しばらく使うと壊れる」典型的な理由です。

超えるとどうなるのか

上限を超えたときの挙動は、大きく二つに分かれます。

  • APIがエラーを返す — リクエスト全体がモデルの上限を超えている場合、APIはリクエストを受け付けず、コンテキスト長の超過を示すエラーを返します。
  • 出力が途切れる — 入力は収まっているが、出力のための余地が足りない場合、応答が途中で切れます。レスポンスの finish_reasonlength になっていれば、このケースです。

前者はクライアント側での例外処理が必要で、後者は気づきにくいぶん厄介です。finish_reason を確認する習慣をつけておくと、原因の切り分けが速くなります。

対処法:枠の中に収める5つの方法

  1. 古いターンを削る — 最もシンプルで効果的です。直近N往復だけを残し、それより前は捨てます(後述のコード例)。
  2. 履歴を要約して置き換える — 古い部分を一度モデルに要約させ、その要約を1メッセージとして残します。文脈を保ちつつトークンを大幅に減らせます。
  3. システムプロンプトを軽くする — システムプロンプトは毎ターン必ず送られるため、ここの冗長さは全ターンに効きます。指示は要点に絞ります。
  4. max_tokens で出力の余地を確保する — 出力に必要な分をあらかじめ見積もり、入力側がそこまで食い込まないように管理します。
  5. ウィンドウの広いモデルを選ぶ — 長文の要約や大量のコードを扱う用途なら、そもそも上限の広いモデルが向いています。各モデルのコンテキスト長はモデルカタログで確認できます。

広いウィンドウは「無料」ではない

上限の広いモデルを選べば超過は避けられますが、コストの面では注意が必要です。ステートレスなAPIでは毎ターン、履歴のすべてが入力トークンとして課金されます。上限が広いほど「入りきってしまう」ため、気づかないうちに1リクエストあたりの入力が肥大化しがちです。

対策の考え方はLLM APIのコストを下げる7つの方法で整理していますが、ここで一つだけ挙げるならキャッシュです。多くのモデルには、同じ前置き(システムプロンプトや固定の文書)を繰り返し送るときに、その部分を通常より安く読み取るキャッシュの仕組みがあります。会話の先頭を固定しておくほど、この恩恵を受けやすくなります。

実際に使ってみる:直近N往復だけ送る

履歴を直近の数往復に絞ってから送る、最小限の実装例です。base_url を FastMetal に向けるだけで、OpenAI SDK がそのまま使えます。

from openai import OpenAI

client = OpenAI(
    api_key="<FASTMETAL_API_KEY>",
    base_url="https://api.fastmetal.ai/v1",
)

SYSTEM = {"role": "system", "content": "簡潔に日本語で答えてください。"}
KEEP_TURNS = 5  # 直近5往復(ユーザー+アシスタント=10メッセージ)だけ残す

history = []  # {"role": "user"|"assistant", "content": "..."} を追加していく

def ask(question: str) -> str:
    history.append({"role": "user", "content": question})
    trimmed = history[-KEEP_TURNS * 2:]
    resp = client.chat.completions.create(
        model="anthropic-claude-haiku-4-5",
        messages=[SYSTEM] + trimmed,
        max_tokens=512,
    )
    choice = resp.choices[0]
    if choice.finish_reason == "length":
        print("注意: 出力が途中で切れました。max_tokens か履歴の量を見直してください。")
    answer = choice.message.content
    history.append({"role": "assistant", "content": answer})
    return answer

システムプロンプトは常に先頭に置き、削るのは会話履歴だけにするのがポイントです。resp.usage.prompt_tokens をログに出しておけば、ターンごとの送信量の推移も把握できます。

よくある質問

Q. コンテキストウィンドウと max_tokens は何が違いますか? コンテキストウィンドウは入力と出力を合わせた全体の上限で、max_tokens はそのうち出力に使ってよい上限です。max_tokens を大きくしすぎると、入力が少ししか入らなくなります。

Q. 上限を超えたかどうかはどうやって分かりますか? リクエスト全体が超過している場合はAPIがエラーを返します。出力だけが足りない場合は、レスポンスの finish_reasonlength になります。どちらもクライアント側で検知して、履歴の削減や max_tokens の調整につなげてください。

Q. 日本語だと上限に達しやすいですか? はい。日本語は英語より1文字あたりのトークンが重いため、同じ文字数でも枠を早く消費します。履歴の圧縮やシステムプロンプトの簡潔化は、日本語のアプリでは特に効果が大きくなります。

まとめ

コンテキストウィンドウは、システムプロンプトから出力までを一つに束ねるトークンの枠です。ステートレスなAPIでは会話が進むほど枠が減るため、履歴を削る・要約する・出力の余地を確保する、という管理が欠かせません。用途に合ったコンテキスト長のモデルはモデルカタログで比較でき、単価は料金ページで確認できます。OpenAI互換APIなら base_url を変えるだけで、同じコードのまま複数のモデルを試せます。

最新のAIモデルを今すぐ試す

最新のAIモデルはFastMetalのAPIキー1つで利用できます。ブラウザですぐに試す、またはOpenAI SDKからそのまま呼び出せます。