第 35 章:コンテキスト圧縮
「いっぱいになったら要約すればいい」——ところが要約自体もモデル呼び出しで、たった今あふれさせたウィンドウに収まらないといけない。
5 分で要点掴む
- 関門は 1 つでなく 3 つ:プロアクティブ 0.90、プリフライト 0.95、Provider エラー後のリアクティブ
- 閾値はトークン余裕から逆算する。丸いパーセントで決めない
- プライマ + 要約 + 硬い下限つきの直近末尾を残す。末尾を切るならツール対の境界を修復する
- 要約器自身の入力に上限を置く。リアクティブのリトライは 1 回だけ、フラグはリセットしない
- 書き換えはその地点以降のキャッシュを無効化する。境界は安定した入力から導く
10 分パス:35.1-35.3 → 35.6 → Kocoro OSS Lab
公開ソースのスナップショット:実装例は 2026-07-27 に確認した Kocoro 公開
origin/mainのコミット4ec6772に基づきます。学ぶべき対象は不変条件であり、定数は最新ソースで再確認してください。
35.1 91 分目に死んだ Turn の話
あなたの Agent はもう 90 分リファクタリングしています。30 ファイルを読み、テストを 4 回走らせ、diff の計画がまるごとコンテキストに乗っています。34 反復目、もう 1 つファイルを読みました。
[iter 34] file_read path=build/generated/schema.pb.go
result: 412 KB
[iter 34] → リクエスト組み立て中... 203,417 tokens
[iter 34] ✗ provider: context_length_exceeded
じゃあ圧縮してリトライすればいい。自然な発想です。
でも今どこに立っているか見てください。すでに上限を超えた状態で圧縮しようとしていて、その圧縮自体がモデル呼び出し——たった今あふれさせたウィンドウに収まらなければなりません。要約も失敗したら、次の手はありません。この Turn を捨てる。90 分ぶんの状態ごと。
この実行を殺したのは 412 KB のファイルではありません。防御線が 1 本しかなかったことです。
35.2 3 つの関門、壊れ方はそれぞれ違う
答えは閾値の精度を上げることではなく、関門を増やすことです。そして効く理由は、壊れる原因が互いに独立していることにあります。
プロアクティブはウィンドウの 0.90 で、通常経路です。学習を永続化し、要約し、履歴を再構成して続ける。これはトークンの推定で動きます。推定は近似なので、412 KB の結果はチェックの合間を一気に突き抜けられます。
プリフライトは 0.95 で、まさにそれを受け止めます。リクエストが出ていく直前に再チェックする——圧縮がまだ惨めでなく安く済む、最後の瞬間です。
リアクティブは Provider のコンテキスト超過エラーで発火します。これが要るのは、推定が一番まずい方向に外れうるからであり、そして「結局収まるのか」を決めるのは Provider だけだからです。
1 つ目だけなら 91 分目が来ます。3 つ目だけなら、長いセッションはすべて不要なエラー往復を払います。3 つは冗長ではありません。それぞれが、上の関門があなたにつく特定の嘘を受け止めています。
35.3 なぜ 0.85 ではなく 0.90 なのか
閾値はパーセントで書くとどれも似て見えます。トークンに直すと差が出ます。
200K のウィンドウなら、0.85 は崖が 170K、0.90 は崖が 180K。その 10K には誰が住んでいるか。ピークが 170K〜180K にちょうど収まるセッション群——およそ 5%——が、もう一度も圧縮せずに済みます。節約できたのは要約呼び出し 1 回だけではありません。不可逆な情報損失を 1 回避けています。
では 0.95 まで押し上げてもっと節約しては。圧縮が残すべきなのは「次のリクエストが収まる」空間ではなく、「次のリクエストとその応答が収まる」空間だからです。高すぎれば、圧縮してもその Turn は収まらない。要約代を払って、何も手に入りません。
0.90 を使える閾値にしているのは、その上に 0.95 の関門があることです。バックストップを外した瞬間、0.90 は攻めではなく無謀になります。 ここからどんなシステムにも持って行ける規則が出ます。閾値は本当に必要なトークン余裕から逆算し、残りを誰が受け止めるか名指しする。名指しできないなら、その閾値は高すぎます。
これは第 32 章にある、より古い単一関門の Harness の説明にもなります。低いトリガーに関門 1 つという設計は誤りではなく、同じ曲線の別の点でした。高いトリガーを安全にする 2 つ目の関門が、まだ存在しなかった時期の点です。
35.4 何を失うかを選ぶ
圧縮は定義上、有損です。だから本当の設計問題は 1 つだけ。どの損失を選ぶか。
生き残るのは 3 領域です。プライマ——冒頭のメッセージ群で、当初の要求とシステム設定が入っています。失えば Agent は自信たっぷりに別の問題を解きます。要約——モデル呼び出しで作る、圧縮済みの中間部。直近——末尾をそのまま残す。「さっきのあの方針」が指す先を持つために要ります。
公開 Runtime は既定で直近 20 ターン対を保持し、予算圧迫下でも 3 の下限を残します。下限のほうが既定値よりずっと重要です。圧迫されると末尾を削り続けたくなりますが、数ターンを切ると Agent は筋を完全に見失い、20 分前に終えた作業を再導出し始めます。節約したトークンより高くつきます。
本番で刺さる細部がもう 1 つ。その直近スライスは末尾から位置ベースで取られます。不用意に切ると tool_use と対応する tool_result の間で切断し、孤立した片割れが残って Provider に即座に拒否されます。末尾を残す実装には境界修復が要り、しかもこの手のバグは長いセッションでしか姿を見せません。
35.5 要約器も失敗する
多くの設計が飛ばす部分であり、91 分目が本当に取り返しのつかないものになる場所です。GenerateSummary は他と同じモデル呼び出しです。失敗するし、空を返すし、タイムアウトするし、小さいティアなら自分の入力でオーバーフローします。
だからまず入力に上限を置きます。要約器へ渡すトランスクリプトは 540,000 文字に制限し、頭尾を残して中間を省略、明示的なマーカーを入れます。これを飛ばすと、巨大セッションの緊急圧縮は小ティアのモデルに処理不能な入力を渡すことになり、復旧経路は元のリクエストとまったく同じ理由で死にます。
次にリトライに線を引きます。リアクティブ経路は Run の生涯を通じてリセットされないフラグを立てます。
reactiveCompacted = true // never reset — prevents infinite reactive loops
緊急リトライは 1 回きり。それも失敗したら、きれいに失敗する。圧縮・リトライ・超過・再圧縮を続ける Agent は、実費を燃やして元の地点に戻っているだけです。
最後に、どの関門が壊れたかを記録します。Runtime はプロアクティブ・プリフライト・リアクティブ・緊急・強制停止の各経路に別の Phase Tag を出し、要約失敗と学習永続化の失敗を分けています。このスナップショットでは 10 種類。深夜 3 時に compaction_failed では何も判断できません。reactive_summary_no_prior なら、どの段が折れたか、退避できる以前の要約があったかまで分かります。
35.6 圧縮とキャッシュは逆を向いている
Prompt Cache はバイト安定なプレフィックスに報い、圧縮は履歴を書き換えます。書き換えるたびその地点以降のキャッシュが無効になる——この 2 つは正面から衝突し、衝突は請求書に出ます。
間違えたときの代償が公開ソースに残っています。巨大なユーザーメッセージが、その時点のメッセージスライスから導いた予算で切り詰められていました。履歴が伸びるにつれ、切断点は毎ターン動きます。毎ターン、切り詰めた位置でキャッシュのプレフィックスが切れ、メッセージ全体が新規 cache creation として再課金される。追加ターンあたりおよそ 0.67 ドル。メッセージ 1 本のために、しかも見返りはゼロです。
修正は、境界を「変わるもの」から導くのをやめること。セッション単位で安定した目標値の固定比率——0.80——を使い、「いま履歴がどれだけあるか」から計算した値をやめました。切り詰めは同じ、境界は安定、プレフィックスは無傷です。
これは壁に貼る価値のある一文に一般化できます。境界が現在の履歴長に依存する切り詰めは、必ずキャッシュを壊す。 境界は動かない入力から導くこと。第 39 章がこの規律の全体像です。
スケーリングの選択にも注目を。比率は固定の文字数上限ではなくコンテキストウィンドウに掛かります。だから 200K 時代のサイズ設定が、1M コンテキストのモデル族へ移った瞬間に黙って過剰切り詰めを始めることがありません。
35.7 スナップショットの根拠
| 観測 | 4ec6772 のソース位置 |
|---|---|
| プロアクティブ 0.90 と cliff の計算 | window.go L23 |
| プリフライト関門 0.95 | loop.go L38 |
ShouldCompact の推定チェック | window.go L76 |
ShapeHistory のプライマ/要約/直近整形 | window.go L91 |
| 直近 20 ターン対を保持、下限 3 | window.go L50 |
| 要約器の入力上限 540,000 文字 | summarize.go L25 |
| リアクティブ圧縮は繰り返さない | loop.go L3847 |
| 段階別の失敗テレメトリ | loop.go L185 |
| キャッシュ安全のための安定した切断境界 | window.go L47 |
これらは特定時点の実装例であり、普遍的な契約ではありません。
35.8 圧縮が間違った道具になるとき
コンテキストの問題がすべて圧縮の問題というわけではありません。反射的に手を伸ばすと、払わなくてよかった品質を払うことになります。
セッションがウィンドウに近づくことがないなら、この機構は純粋なリスクです。最小メッセージ数で無効化してください。問題が巨大なツール結果 1 件なら、トランスクリプトへ要約せず、ポインタを残してディスクへ退避します。第 36 章がその経路で、これは 2 番目ではなく最初に手を伸ばすべき選択肢です。内容そのものが成果物なら——ユーザーが求めたトランスクリプトなら——圧縮は成果物の破壊です。圧縮が整形するのは作業コンテキストであって、出力ではありません。
監査が要る実行なら、モデルが見る側を圧縮し、完全な記録は別に永続化します。圧縮は Prompt 整形の操作であって保存ポリシーではなく、保存ポリシーとして使えば、残す義務のある証拠を失います。
35.9 よくある落とし穴
閾値 1 つ、バックストップなし。 現場で最も多い設計であり、91 分目の直接の原因です。
要約を再圧縮する。 要約済みのテキストを通すたび、失う信号は増え、節約できるトークンは減ります。何を圧縮済みか記録し、再処理をやめること。
あふれたのと同じティアで要約する。 手軽で、しかも最も必要なときにきっちり失敗します。要約器は安いティアへ回し、専用の入力上限を与えます。
空の要約を受け入れる。 "" を返した要約器を額面どおり受け取ると、会話の中間を黙って消します。空をチェックし、以前の要約を保持してください。
guardrail の指摘を要約入力に混ぜる。 前 Turn の「同じことを繰り返しているようです」は会話ではありません。ループが注入したメッセージを先に取り除かないと、要約は Agent が叱られた事実を忠実に記録します。
推定を信じる。 文字数ヒューリスティックのトークン計算は境界で外れます。これは直すべき欠陥ではなく、プリフライトが再チェックし、リアクティブが存在する理由そのものです。
Kocoro OSS Lab(10 分)
window.goを読み、180K の崖から 0.90 を導いているコメントを探します。数字だけでなく計算そのものがコメントに書かれている点に注目。loop.go内のcompressOldToolResultsの 3 か所の呼び出しを追い、それぞれがどの関門に属するか特定します。- 自分のシステムが
compaction_failedの代わりに何を出すべきか書き出します。失敗しうる 5 経路を列挙し、今のテレメトリでそれらを区別できるか確かめてください。
ここだけは押さえる
- 関門は 1 つでなく 3 つ。 プロアクティブは推定を、プリフライトは呼び出し直前を、リアクティブは Provider の事実を見ます。それぞれが上の関門の特定の嘘を受け止めます。
- 閾値は計算から出す。 トークン余裕から逆算し、残りを誰が受け止めるか名指しし、その推論をコメントに書く。そこなら腐りません。
- 要約器は失敗しうるモデル呼び出し。 入力に上限を置き、リトライを厳密に 1 回に縛り、フラグはリセットしない。
- 圧縮とキャッシュは逆を向く。 現在の履歴長から導いた境界はプレフィックスを壊します。境界は安定入力から。
- まず退避を考える。 巨大な結果 1 件が属するのはディスクとポインタであって、あなたのトランスクリプトではありません。
次章:第 36 章は大きな結果を Prompt の外へ出す方法を、第 37 章は一度に全部を要約せず古い結果を年齢順に劣化させる方法を扱います。