コンテンツにスキップ

トークンとコンテキストウィンドウ

前章 で「1 回の LLM 呼び出しでは messages に全部書き下す必要がある」と言ったが、いくらでも書き放題ではない。LLM には「1 回に受け取れる入力の最大量」という硬い制限があり、この単位がトークンで、枠のサイズがコンテキストウィンドウ

この章ではトークンとは何か、なぜ大事か、どう付き合うかを整理する。

トークンとは (ざっくり)

LLM は文字列をそのまま読むのではなく、サブワード単位に区切った「トークン列」として扱う。英単語 1 個 = 1 トークン ... とは限らず、よく出る語は 1 トークン、珍しい単語は複数トークンに分解される。

代表的なトークン化方式は BPE (Byte-Pair Encoding) 系。以下は OpenAI の cl100k_base トークナイザで "Hello, world!" を分解したときの例:

"Hello, world!"
  ↓ トークナイザ
["Hello", ",", " world", "!"]   ← 4 トークン
  • Hello は 1 トークン (よく出るので)
  • , (カンマ) は 1 トークン
  • world (先頭スペース込み) は 1 トークン
  • ! は 1 トークン

スペースが単語の先頭に付くのが BPE 系の特徴。"hello world" は ["hello", " world"] の 2 トークンだが、先頭で " hello" のようにスペースから始まると別のトークンになることもある。

日本語はもっと細かく割れる

日本語は Unicode 1 文字あたり 3 バイトを超えることが多く、BPE の語彙に含まれていない漢字や組み合わせは1 文字で複数トークンになる。雑な経験則として:

言語 文字数 → トークン数の目安
英語 1 トークン ≈ 3〜4 文字 (英単語 1〜1.5 語分)
日本語 1 トークン ≈ 1〜2 文字 (漢字混じり)
コード 1 トークン ≈ 3〜5 文字 (記号が多い)

同じ情報量でも日本語は英語より 2〜3 倍トークンを食うので、同じコンテキストウィンドウに入る文章量も少なくなるし、課金も重くなる。この差はモデルによっても違い、Gemini 系は日本語トークナイザが比較的効率的、OpenAI 系はやや大食い、Anthropic は中間、というのが一般的な体感。

自分で確認するには

いちばん手軽なのは LiteLLM のレスポンスに入ってくる usage:

curl -s http://litellm.home.arpa/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"Hello, world!"}]}' \
  | jq '{prompt: .usage.prompt_tokens, completion: .usage.completion_tokens}'

同じ文字列を日本語と英語で投げて prompt_tokens を比べてみると、日本語の方がトークン数が多いのがはっきり分かる (本章の末尾でもう少し遊ぶ)。

コンテキストウィンドウ

LLM が一度に覚えていられるトークンの上限。リクエストの messages (system + user + assistant + tool) + tools スキーマ + LLM の生成応答、全部合わせてこのウィンドウに収まらないといけない

┌────────────────── コンテキストウィンドウ (例: 200,000 トークン) ──────────────────┐
│                                                                                    │
│  [system prompt]  [tools スキーマ]  [過去の user / assistant / tool のやり取り]     │
│                                                                                    │
│  ↑ ここまでが prompt_tokens ──────────────────────────────────────┐               │
│                                                                     │               │
│  [新しい user メッセージ]                                           │               │
│                                                     ──────────────┘               │
│                                                                                    │
│                                                       [LLM 生成応答]               │
│                                                        ↑ completion_tokens         │
│                                                                                    │
└────────────────────────────────────────────────────────────────────────────────────┘

prompt_tokens + completion_tokens ≤ context window という制約。超えたら:

  • リクエスト時点で prompt が既にオーバー → API が 400 エラーで即拒否
  • prompt は収まるが max_tokens (生成側の上限) が無いまま走って途中で打ち切り → finish_reason: "length" で応答が途中まで
  • エージェントがループで messages を増やしていった結果、N ターン後にオーバー → N-1 ターン目までは動いていたのに突然失敗

主要モデルの上限感 (目安)

数字は頻繁に更新される (新モデル / 拡張版 / ベータ等) ので、大雑把な世代感として見る。

モデル だいたいの context window
GPT-5.4 系 (gpt-5.4 / mini / nano) 1M トークン級
Claude Opus 4.6 / Sonnet 4.6 / Haiku 4.5 200K (extended thinking モードで 1M になるものもあり)
Gemini 2.5 Pro 1M 〜 2M
Gemini 2.5 Flash 1M 級
Llama 3.1 / 3.3 128K
Qwen3.5 128K 前後 (モデルサイズ依存)

ウィンドウは「長ければ長いほどよい」わけではない。以下の副作用がある:

  • 料金: 入力トークン数 × 単価で線形に増える
  • レイテンシ: 長いほど生成開始も終了も遅い
  • 品質: 長大な context ではモデルの集中力が落ち、途中の情報を見落とす「lost in the middle」問題が起きやすい
  • トークン価格は固定ではない: 一部モデルは長コンテキストだと単価が上がる ("> 128K tokens → $$/M が倍" のような階層課金)

だから実務では「ウィンドウは大きいけど実際に送る量は必要最小限にする」のが基本方針になる。

何がトークンを食うか

messages 配列の中身だけでなく、LLM にメタデータとして渡しているもの全部がトークンを消費する。

項目 該当する agent-demo のコード
system prompt setup.tsSYSTEM_PROMPT (数百トークン)
user メッセージ 人の入力
過去の assistant メッセージ (再送) 対話モードの messages 配列
過去の tool_call と tool result (再送) 同上
ツール定義 (JSON schema) tools.tsname / description / schema 全部 (1 ツール 50〜200 トークン程度)
LLM の生成応答 出力側の completion_tokens
(推論モデルの場合) 内部思考トークン completion_tokens_details.reasoning_tokens として別集計されるモデルもある

意外と大きいのがツール定義。本リポジトリの agent-demo は 7 ツール (search/fetch_url/wikipedia/now/calc/random_int/end_chat) を全部渡しているので、それだけで毎リクエストに数百〜千トークン強の固定オーバヘッドが乗る。

これが「ツールは増やせば増やすほど LLM 判断力が上がる」わけではない理由の 1 つで、多すぎるツール = (1) プロンプトが膨らむ、(2) LLM が選択に迷って誤選択が増える。agent-demo で AGENT_TOOLS=search,now のように絞れるようにしたのはこのため (ただし学習目的なのでデフォルトは全部入り)。

上限との付き合い方

長い会話や大量の検索結果を扱うエージェントは、以下のどれかで「入る量」を制御する:

1. スライディングウィンドウ (古いメッセージから捨てる)

シンプル。messages の先頭から (system は残して user/assistant の古いものから) 捨てる。

  • 長所: 実装が簡単、計算コストゼロ
  • 短所: 古い文脈が消えるので「さっき話した私の名前」が突然忘れられる

2. 要約圧縮

古い会話を別 LLM 呼び出しで「これまでの要約」に変換し、先頭に入れる。LangChain / LangGraph には ConversationSummaryMemory のような実装がある。

  • 長所: 情報の骨格が残る
  • 短所: 要約もトークンを食う (余計な呼び出し)、要約段階での情報損失

3. 選択的保持

過去メッセージのうち「重要」と判定したものだけ残す。重要度スコアリング + 閾値で足切り。

  • 長所: 情報密度が高く保てる
  • 短所: 重要度判定のロジックが必要

4. 外部化 (RAG)

過去の会話 / 知識を Vector DB 等に貯めておき、必要になったら検索して必要な部分だけ context に戻す。第 7 章 記憶の多層モデル の L4 と同じ話。

  • 長所: 容量が実質無制限
  • 短所: retrieval の精度が品質を左右する / 別途インフラが必要

プロンプトキャッシュで課金トークンを減らす

「送るトークン数」は減らせないが、プロバイダ側のキャッシュが効けば課金される実効トークン数は減る。Anthropic の cache_control や OpenAI の prompt caching を使うと:

  • 2 回目以降の同一プレフィックス (system + 長い固定 context) は実質無料 〜 割引価格
  • レイテンシも短くなる
  • TTL (数分〜1 時間) を超えると消える

詳細は 第 7 章 記憶の多層モデル の L2 "プロンプトキャッシュ (プロバイダ側 TTL)" を参照。

実機で遊んでみる

同じ意味の質問を英語と日本語で投げて、prompt_tokens の差を見る:

# 英語版
curl -s http://litellm.home.arpa/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"What is the height of Mount Fuji?"}]}' \
  | jq '.usage'

# 日本語版
curl -s http://litellm.home.arpa/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"富士山の高さは何メートルですか?"}]}' \
  | jq '.usage'

同じ情報を聞いているのに prompt_tokens が異なる。通常は日本語の方が大きい。これが「日本語は英語より context を食う」ことの直接確認。

さらに claude-sonnet-4-6gemini-2.5-flash で同じプロンプトを投げ比べると、トークン数がずれる。トークナイザがモデルごとに違うので、「何トークンか」はモデル依存だと分かる。

概念のおさらい

  • トークン = LLM が文字列を分解する最小単位。サブワードレベル
  • コンテキストウィンドウ = 入力 + 出力の合計トークンの上限
  • 日本語は英語より 2〜3 倍トークンを食う
  • ツール定義も毎リクエストに乗るので、ツールを増やすほど固定オーバヘッドが増える
  • ウィンドウは広ければよいものではない。料金 / レイテンシ / 品質 (lost in the middle) の三重苦
  • 上限との付き合い方は truncate / summarize / 選択 / RAG の 4 パターン
  • キャッシュ (第 7 章 L2) で課金トークンを減らせる場合がある

agent-demo との対応

  • setup.tsSYSTEM_PROMPT = 毎リクエストに乗る固定オーバヘッド
  • tools.ts で定義したツールの description / schema = これも毎リクエストに乗る
  • agent-chat.tsmessages = result.messages で履歴を蓄積する部分 = ターンを重ねるほど context が膨らむ
  • 現状の agent-demo にはコンテキスト超過対策が入っていない。長時間の対話では lost in the middle や context window エラーが起き得る。ここを鍛えるには第 7 章の L3/L4 の拡張が必要