コンテキストウィンドウ管理 — トークン予算と配分戦略
LLMのコンテキストウィンドウをトークン予算として管理する手法を解説。入力・出力の配分戦略、Context Rot対策、コスト最適化の実践パターンを紹介します。
コンテキストウィンドウ=共有予算
LLMのコンテキストウィンドウは、システム指示、ツール定義、会話履歴、外部データ、モデル出力で共有する有限の予算です。APIやモデルによって出力上限の扱いは異なるため、利用中の公式仕様を確認した上で余白を確保します。
この「共有予算」の感覚を持たないまま開発を進めると、以下の問題が発生します。
- 出力が途中で切れる(トークン上限に到達)
- コストが予想の数倍に膨れる(長コンテキストの料金加算)
- 品質が低下する(Context Rot)
モデル一覧を固定しない
コンテキスト上限、最大出力、長文入力の料金、キャッシュ条件は頻繁に変わります。本記事では製品横断の固定表を正本にせず、利用時に各社のモデル仕様と料金ページを確認します。
公称上限が大きくても、関連情報を無制限に入れてよいわけではありません。ChromaのContext Rot研究が示すように、性能低下は入力長だけでなく、情報の類似性、配置、タスク、モデルによって変わります。固定の安全率ではなく、自分の評価セットで有効容量を測ります。
トークン予算の配分戦略
基本配分モデルの例
コンテキストウィンドウを4つのゾーンに分割して管理します。以下の比率は初期仮説であり、推奨固定値ではありません。
┌──────────────────────────────────────────┐
│ System Prompt (10%) │
│ モデルの役割・制約・出力フォーマット │
├──────────────────────────────────────────┤
│ Tool Definitions (15%) │
│ ツール名・説明・スキーマ │
├──────────────────────────────────────────┤
│ Context (50%) │
│ RAG結果 + 会話履歴 + ユーザー入力 │
├──────────────────────────────────────────┤
│ Response Budget (25%) │
│ モデルの出力用に確保 │
└──────────────────────────────────────────┘
予算管理の実装例
interface TokenBudget {
total: number;
systemPrompt: number;
tools: number;
context: number;
response: number;
}
function allocateTokenBudget(
totalTokens: number,
systemPromptTokens: number,
toolTokens: number,
maxResponseTokens: number,
): TokenBudget {
// 固定費(システムプロンプト + ツール定義)を先に確保
const fixedCost = systemPromptTokens + toolTokens;
// 出力予算を確保
const responseBudget = Math.min(
maxResponseTokens,
Math.floor(totalTokens * 0.25),
);
// 残りがコンテキスト予算
const contextBudget = totalTokens - fixedCost - responseBudget;
if (contextBudget < 0) {
throw new Error(
`トークン予算不足: 固定費(${fixedCost}) + 出力(${responseBudget}) > 合計(${totalTokens})`
);
}
return {
total: totalTokens,
systemPrompt: systemPromptTokens,
tools: toolTokens,
context: contextBudget,
response: responseBudget,
};
}
// 使用例: 128Kウィンドウのモデル
const budget = allocateTokenBudget(
128_000, // 合計
3_000, // システムプロンプト
5_000, // ツール定義(10ツール程度)
4_096, // 最大出力
);
// => context: 115,904トークン使用可能
コンテキストゾーン内の優先順位
コンテキスト予算(50%)の中でも、情報の優先順位を設定します。
# コンテキスト内の優先順位付け
CONTEXT_PRIORITIES = [
# 優先度1: 絶対に必要な情報
{"type": "user_latest_message", "priority": 1, "compressible": False},
{"type": "active_task_state", "priority": 1, "compressible": False},
# 優先度2: 重要だが圧縮可能
{"type": "rag_results", "priority": 2, "compressible": True},
{"type": "recent_conversation", "priority": 2, "compressible": True},
# 優先度3: あれば有用
{"type": "older_conversation", "priority": 3, "compressible": True},
{"type": "background_knowledge", "priority": 3, "compressible": True},
]
def build_context(
items: list[ContextItem],
budget_tokens: int,
) -> list[ContextItem]:
"""優先順位に基づいてコンテキストを構築する"""
result: list[ContextItem] = []
remaining = budget_tokens
# 優先度順にソート
sorted_items = sorted(items, key=lambda x: x.priority)
for item in sorted_items:
token_count = count_tokens(item.content)
if token_count <= remaining:
result.append(item)
remaining -= token_count
elif item.compressible and remaining > 100:
# 予算に収まるように圧縮
compressed = compress_to_budget(item.content, remaining)
result.append(ContextItem(content=compressed, **item.metadata))
remaining -= count_tokens(compressed)
# 予算が尽きたら残りは切り捨て
return result
Context Rot への対策
スライディングウィンドウ
最も基本的な対策は、古い会話を順次削除するスライディングウィンドウです。
def sliding_window(
messages: list[dict],
max_tokens: int,
) -> list[dict]:
"""最新のメッセージを優先して保持する"""
# システムメッセージは常に保持
system_messages = [m for m in messages if m["role"] == "system"]
other_messages = [m for m in messages if m["role"] != "system"]
system_tokens = sum(count_tokens(m["content"]) for m in system_messages)
remaining = max_tokens - system_tokens
# 最新のメッセージから逆順に追加
selected: list[dict] = []
for msg in reversed(other_messages):
msg_tokens = count_tokens(msg["content"])
if msg_tokens <= remaining:
selected.insert(0, msg)
remaining -= msg_tokens
else:
break
return system_messages + selected
サマリー+最新ウィンドウ
スライディングウィンドウの改良版として、古い会話をサマリーに圧縮し、最新の会話はそのまま保持するパターンがあります。
┌─────────────────────────────────────┐
│ System Prompt │
├─────────────────────────────────────┤
│ [Summary] これまでの会話の要約: │
│ ユーザーはECサイトの検索改善を │
│ 依頼。pgvectorの採用が決定済み... │
├─────────────────────────────────────┤
│ [Recent] 直近5ターンの会話(原文) │
├─────────────────────────────────────┤
│ Response Budget │
└─────────────────────────────────────┘
コスト最適化
プロンプトキャッシュの活用
同じシステムプロンプトやツール定義を繰り返し送信する場合、対応APIのプロンプトキャッシュでレイテンシや入力コストを下げられることがあります。割引率、最小入力長、保持時間はモデルと提供元の最新仕様を確認します。
# Anthropic APIのプロンプトキャッシュ
response = client.messages.create(
model="claude-opus-4-6-20260410",
max_tokens=4096,
system=[
{
"type": "text",
"text": long_system_prompt, # 変更頻度の低い部分
"cache_control": {"type": "ephemeral"}, # キャッシュ対象
}
],
messages=conversation_history,
)
キャッシュのTTLは通常5分です。5分以内に次のリクエストを送れば、キャッシュされた部分のトークンは大幅に割引されます。
200Kトークンの壁
AnthropicとGoogleは200Kトークンを超えるリクエストに追加料金を課しています。この閾値を超えると、超過分だけでなくリクエスト全体に加算される場合があるため、200K以内に収める工夫が重要です。
実践ポイント
トークン予算の監視
本番環境では、各リクエストのトークン使用量を継続的に監視します。
// トークン使用量の監視
interface TokenUsageMetrics {
inputTokens: number;
outputTokens: number;
cacheHitTokens: number;
utilizationRate: number; // context使用率
}
function logTokenUsage(response: APIResponse): void {
const metrics: TokenUsageMetrics = {
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
cacheHitTokens: response.usage.cache_read_input_tokens ?? 0,
utilizationRate:
response.usage.input_tokens / CONTEXT_WINDOW_SIZE,
};
// 使用率が80%を超えたら警告
if (metrics.utilizationRate > 0.8) {
logger.warn("コンテキスト使用率が80%を超過", metrics);
}
metricsCollector.record(metrics);
}
配分比率の調整指針
| シナリオ | System | Tools | Context | Response |
|---|---|---|---|---|
| チャットボット | 5% | 5% | 60% | 30% |
| コード生成 | 10% | 10% | 40% | 40% |
| RAG Q&A | 5% | 5% | 70% | 20% |
| エージェント | 15% | 20% | 45% | 20% |
まとめ
コンテキストウィンドウ管理は、LLMアプリケーションの品質・コスト・安定性を左右する基盤技術です。トークン予算を明示的に配分し、優先順位に基づいてコンテキストを構築し、キャッシュやスライディングウィンドウでコストを最適化する。この一連のプロセスを自動化することが、プロダクション品質のLLMアプリケーション開発の鍵です。