
MAXプランの週間クォータが3日で溶けた。
×20のクォータがあるはずなのに、水曜には残量が怪しくなってる。それ自体は「まあそんなもんか」で済ませてたんだけど、ふと気になった。コンテキストウィンドウの中身って、実際どうなってるんだろう。
前の記事ではAI秘書のトークン節約について書いた。CLAUDE.mdを削ったり、MCPツール定義を削ったり。でも今回はAI秘書じゃなくて、Claude Code自体の話。道具のほうが大食いだったとは。
きっかけ
4月14日、スペイン語圏のあるツイートが目に留まった。
「Claude Codeのトークン浪費の大半はユーザー側に原因がある」
言いたいことはわかる。CLAUDE.mdが肥大化してるとか、プロンプトが冗長だとか。でも「大半がユーザー側」って、ちゃんと測って言ってるのか?
じゃあ俺が測ってやる、と思った。
実測してみた
Claude Codeの内部transcript(セッションを記録してるJSONL)を解析した。
1ターンあたり188,000トークン。うち164,000トークン(87%)が会話履歴。
CLAUDE.mdは12,700トークン。MCPツール定義は3,900トークン。合わせても全体の9%。これを半分にしても5%弱しか浮かない。
本丸は会話履歴の肥大化だった。CLAUDE.mdを必死に削ってた自分がちょっと恥ずかしい。
犯人はツールI/O
じゃあ履歴の中身は何か。
開いて驚いた。履歴の約80%がツールの入出力だった。 ファイルの読み取り結果、Bashコマンドの出力、grepの結果。AIがその場で使って、判断して、次に進んだ時点で役目を終えてるデータ。
なのに、それがコンテキストウィンドウにずっと居座って、毎ターントークンを食い続けている。
50ターンのセッションだと、最初のほうにgrepした結果がまだコンテキストにいる。もう二度と見ることはないのに。
/compactの矛盾
「/compactすればいいじゃん」って思うかもしれない。自分もそう思ってた。
でも/compactの仕組み、AIに全履歴を読ませて要約させるんだよね。
トークンを節約するために、トークンを大量に消費する。しかも要約の過程でニュアンスが消える。「あの時なぜこの設計にしたか」みたいな文脈が、丸められて消えることがある。
要約後にまた作業を続ければ、また肥大化して、またcompactして…の繰り返し。根本解決じゃない。
時間じゃなく、種類で分ける
ここで発想を変えた。
MemGPTやLangChainのSummaryBufferMemoryは、古いものから要約する。時間ベースの圧縮。でも問題は「古さ」じゃない。
10ターン前の「この設計にした理由」は今でも価値がある。さっきのgrepの結果は、1ターン前でも用済み。
時間じゃなく、種類で分ければいい。
- 会話本文(人間が書いたこと、AIが答えたこと)→ 残す
- ツール入出力(ファイル内容、コマンド結果)→ 退避する
この発想で作ったのがThroughlineだ。
3層モデル
Throughlineは会話を3つのレイヤーに分解してSQLiteに保存する。
L1(Skeleton) — 古いターンの一行要約。軽量モデルが生成する。1ターン約10トークン。
L2(Body) — 直近20ターンの会話本文。ユーザーの発言とAIの応答がそのまま残る。圧縮なし、ロスレス。
L3(Detail) — ツール入出力、システムメッセージ。SQLiteに退避してコンテキストには一切残さない。必要になったらAIが自分でSQLiteから引っ張ってくる。
/clearを打っても大丈夫。SQLiteは消えないから、次のセッション開始時にトランザクション一発で前セッションの記憶を引き継ぐ。PIDを追いかけたり、時間窓で判定する必要はない。決定的に動く。
数字で言うとこう。
Throughlineなし(50ターン、/clearなし):
コンテキスト ≈ 125,000トークン(80%が用済みのツールI/O)
Throughlineあり(50ターン → /clear → 復帰):
コンテキスト ≈ 13,000トークン
(直近20ターンのL2 + 古い30ターンのL1要約)
約90%の削減。
失敗した設計の話もしておく
最初からこの形だったわけじゃない。
初期の設計では、L2を「重要な判断の構造化抽出」にしようとした。[DECISION] WebSocketを採用、[CONSTRAINT] ポート8080は使えない みたいなタグ付きで、会話から重要な情報だけ引き抜くイメージ。
理論上は美しいんだけど、実装して気づいた。AIが将来何を必要とするか、予測できない。
分類器が「これは重要じゃない」と判断した情報が、10ターン後に必要になることがある。しかもそれが消えてることに気づけない。80%の精度は、残り20%が見えなくなることを意味する。
結局、L2は会話全文をそのまま残す方式に落ち着いた。引き算だけの設計。元のClaude Codeのコンテキストから、ツールI/Oを抜くだけ。これなら「Throughlineのせいで品質が落ちた」が原理的に起きない。
セッション間の引き継ぎも最初はファイルベースで、10秒の時間窓で/clearを検出しようとした。並行セッションで壊れた。結局SQLiteのUPDATE一発に落ち着いた。シンプルなほうが壊れない。
要約コストがほぼゼロな理由
L1の要約はHaiku 4.5で生成するんだけど、ここにも工夫がある。
自分の過去86セッションを分析したところ、ターン数の中央値は13だった。半数以上が20ターン以内で終わる。
ThroughlineはL2を20ターン分保持するから、短いセッションでは要約モデルが一度も動かない。要約が必要になるのは21ターン目以降。しかもそこから1ターンずつ遅延的に処理する。
つまり、要約処理自体のトークン消費がほぼゼロ。/compactの「節約のために大量消費する」矛盾がそもそも発生しない。
おまけ:トークンモニター
開発の副産物として、マルチセッション対応のトークンモニターもできた。
▶ Throughline 2ed5039c ████░░░░░░░░░░░░░░░░ 205.1k / 21% 残 794.9k claude-opus-4-6
transcriptのJSONLからAPIの実測値(message.usage)を読み取るので、文字数÷4みたいな雑な推定じゃなくて正確な値が出る。1Mコンテキストの検出も自動。
複数セッションを走らせてる時に、どれがどれだけ食ってるかリアルタイムでわかる。地味に便利。
まとめ
クォータが3日で溶ける問題の正体は、コンテキストウィンドウの87%を占める会話履歴だった。その大部分はツールI/Oの残骸。
CLAUDE.mdの最適化やプロンプト短縮は全体の9%に効く対策で、やらないよりはいい。でも本丸はそこじゃなかった。
本来こういう問題はプラットフォーム側が解決すべきなのかもしれない。でも今すぐ困ってたから自分で作った。Node.js 22.5以上、依存ゼロ、MIT。MAX契約があれば動く。
同じ問題で困ってる人がいたら、気が向いたら覗いてみてください。
このブログは「Claude Code 始めました」— Claude Codeを本格利用中のユーザーが、実体験をベースに書いている技術ブログです。