はじめに
RAG の context を少し足しただけのつもりが、急にリクエストごと落ちることがあります。私も最初は「検索結果が多すぎたのかな」くらいで見ていたのですが、ログに残っていたのはステータスコードと request_id だけでした。
これだと、どのユーザー入力が長いのか、RAG の chunk が多いのか、会話履歴が膨らんだのか、max_tokens や max_completion_tokens を取りすぎたのかが分かりません。
この記事では、context_length_exceeded や prompt is too long 系のエラーを見たときに、prompt を作る直前で token budget をログに出す形に直します。ポイントは、prompt 本文を保存することではなく、削る判断に必要な数字だけを残すことです。
再現環境
今回の手元確認は次の条件です。
- Python 3.12
- OpenAI 互換の Chat Completions endpoint
-
tiktokenのo200k_baseで入力 token を概算 - 実測エラーの model は
claude-haiku-4-5 -
max_tokensは 16
タイトルには context_length_exceeded と書いていますが、今回そのものの OpenAI エラー本文は取れていません。GPT 系モデルへの live request は router 側で channel unavailable になったため、ここでは同じ予算超過の症状として実際に取れた prompt is too long を使います。OpenAI の公式ドキュメント上も、context window は input token と output token、モデルによっては reasoning token の合計で考える必要があります。
エラー全文
実際に 300,000 個の hello を user message に入れて投げたときのレスポンスです。request body や API key はログに残していません。
{
"error": {
"message": "prompt is too long: 300028 tokens > 200000 maximum",
"type": "invalid_request_error",
"param": "",
"code": null
}
}
ここで見たいのは、300028 tokens > 200000 maximum の部分です。アプリ側で事前に近い数字を出せていれば、少なくとも「RAG chunk を何件削ればいいか」は判断できます。
何が起きたか
context window は、入力だけの箱ではありません。OpenAI のドキュメントでは、入力 token、出力 token、reasoning model では reasoning token も同じ window の中で考えると説明されています。
たとえば公式 docs 上の gpt-4o は context window が 128,000 tokens、max output が 16,384 tokens です。gpt-5.4-mini は context window が 400,000 tokens で、max input が 272,000 tokens、max output が 128,000 tokens とされています。
つまり、アプリ側で max_output_tokens を大きく予約すると、そのぶん入力に使える予算は減ります。RAG で「検索結果を 20 chunk 入れたら落ちた」と見えていても、実際には会話履歴、system prompt、few-shot example、tool schema、予約した output token が足し算されて超えていることがあります。
再現手順
まず token 推定用のライブラリを入れます。
python -m venv .venv
source .venv/bin/activate
python -m pip install tiktoken
最小の再現はこんな感じです。--call を付けない限り API には投げず、token budget log だけを出すようにしています。
import json
import tiktoken
MODEL_CONTEXT_WINDOWS = {
"claude-haiku-4-5": 200_000,
"gpt-4o": 128_000,
"gpt-5.4-mini": 400_000,
}
def estimate_messages_tokens(messages):
encoding = tiktoken.get_encoding("o200k_base")
return sum(len(encoding.encode(m["content"])) for m in messages)
model = "claude-haiku-4-5"
max_output_tokens = 16
messages = [
{"role": "system", "content": "Answer briefly."},
{"role": "user", "content": "hello " * 300_000},
]
input_estimate = estimate_messages_tokens(messages)
reserved_total = input_estimate + max_output_tokens
print(json.dumps({
"event": "llm.token_budget",
"model": model,
"input_estimate_tokens": input_estimate,
"max_output_tokens": max_output_tokens,
"reserved_total_tokens": reserved_total,
"context_window_tokens": MODEL_CONTEXT_WINDOWS[model],
"over_budget_tokens": max(0, reserved_total - MODEL_CONTEXT_WINDOWS[model]),
"truncation_policy": "reject_over_budget",
}, ensure_ascii=False, indent=2))
私の手元では、このログになりました。
{
"event": "llm.token_budget",
"model": "claude-haiku-4-5",
"input_estimate_tokens": 300004,
"max_output_tokens": 16,
"reserved_total_tokens": 300020,
"context_window_tokens": 200000,
"over_budget_tokens": 100020,
"truncation_policy": "reject_over_budget"
}
API が返した token 数は 300028、手元推定は 300004 でした。24 token ずれています。ここを完全一致させようとすると provider ごとの差や message wrapper の差でつらくなるので、私は「estimate」として扱い、余白を持たせる方に寄せています。
先に出したかったログ
本番で残したいのは、だいたいこのくらいです。
{
"event": "llm.token_budget",
"request_id": "app_01hxx...",
"model": "gpt-4o",
"input_estimate_tokens": 118900,
"max_output_tokens": 12000,
"reserved_total_tokens": 130900,
"context_window_tokens": 128000,
"over_budget_tokens": 2900,
"truncation_policy": "drop_low_score_rag_chunks",
"rag_chunk_count": 18,
"conversation_turns": 12
}
逆に、私はここに prompt 本文、RAG の原文、ユーザーの入力全文、API key、Authorization header は入れません。エラー解決のためのログで、別の事故を作るのは普通に嫌だからです。
修正: prompt 作成直前に予算を見てから削る
RAG の場合、いきなり全文を詰めて API に投げるのではなく、prompt を組む直前に「入れる候補」をスコア順に積みます。
import logging
import tiktoken
log = logging.getLogger(__name__)
def count_tokens(text):
encoding = tiktoken.get_encoding("o200k_base")
return len(encoding.encode(text))
def estimate_messages_tokens(messages):
return sum(count_tokens(m["content"]) for m in messages)
def build_messages(system_prompt, question, chunk_texts):
context = "\n\n".join(chunk_texts)
return [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"質問:\n{question}\n\n参考資料:\n{context}"},
]
def fit_rag_context(system_prompt, question, chunks, model, context_window, max_output_tokens):
limit = int(context_window * 0.95)
kept = []
for chunk in sorted(chunks, key=lambda x: x["score"], reverse=True):
trial = kept + [chunk["text"]]
messages = build_messages(system_prompt, question, trial)
reserved_total = estimate_messages_tokens(messages) + max_output_tokens
if reserved_total <= limit:
kept = trial
final_messages = build_messages(system_prompt, question, kept)
input_estimate = estimate_messages_tokens(final_messages)
reserved_total = input_estimate + max_output_tokens
log.info("llm.token_budget", extra={
"model": model,
"input_estimate_tokens": input_estimate,
"max_output_tokens": max_output_tokens,
"reserved_total_tokens": reserved_total,
"context_window_tokens": context_window,
"rag_chunk_count": len(kept),
"dropped_chunk_count": len(chunks) - len(kept),
"truncation_policy": "drop_low_score_rag_chunks",
})
return final_messages
削る順番も先に決めておくと、障害時に議論しやすいです。私なら次の順で削ります。
- 生の全文資料を削り、検索済み chunk だけにする
- 類似度の低い RAG chunk を削る
- 古い会話履歴を summary に寄せる
- few-shot example を減らす
- 最後に
max_output_tokensを下げる
max_output_tokens を先に下げると、回答が途中で切れて別の問い合わせになります。まず入力側の無駄を削る方が、原因の説明もしやすいと思います。
Responses API の truncation について
Responses API には truncation の設定があります。公式リファレンスでは、auto は context window に収まるように会話の先頭側 item を落とし、disabled は input が window を超える場合に 400 error にする、という説明です。
個人的には、RAG では auto に全部任せるより、アプリ側で削った事実をログに出したいです。どの chunk が消えたか分からないまま回答だけ返ると、後で「なぜこの根拠が使われなかったのか」を追えなくなります。
Flatkey AI で確認したかったこと
Flatkey AI は OpenAI 互換の base URL で複数モデルを呼べて、key、usage、routing を同じ dashboard で見られる前提のサービスです。こういう router を挟む場合でも、アプリ側の token budget log は残した方がいいです。
dashboard 側の usage は請求や routing の確認に向いています。一方で、アプリ側の input_estimate_tokens、max_output_tokens、rag_chunk_count は「どの機能の prompt 設計が悪かったか」を見るためのログです。役割が違うので、どちらか片方だけだと少し足りません。
まとめ
context_length_exceeded 系のエラーで最初に見るべきなのは、model 名、input token estimate、reserved output token、context window、truncation policy です。
私の今回の反省は、request を送った後の error log だけでは遅かったことです。prompt を作る直前に budget を出しておけば、RAG chunk を削るのか、会話履歴を summary にするのか、出力上限を下げるのかをその場で判断できました。
参考にしたもの:
- OpenAI docs: Managing context for text generation
- OpenAI model docs:
gpt-4o,gpt-5.4-mini - OpenAI API reference: Chat Completions
max_completion_tokens, Responsestruncation - Flatkey AI product knowledge: one key, OpenAI-compatible endpoint, usage and routing dashboard
間違いあったらコメントください。よろしくお願いします。