Hermes Agent Wiki 非公式・日本語wiki

マイクロ圧縮

目次

圧縮のコストを分割払いにする方法です。

長い会話はいずれモデルのコンテキストウィンドウに収まらなくなり、何かを捨てるか要約するしかなくなります。 Hermes はこれまで、この処理を一括で行ってきました。 やり取りの記録がしきい値を超えるとセッションが止まり、真ん中の大きな部分を1回の呼び出しで要約してから、会話を再開します。 これでも動きますが、支払いは一度にまとめてやってきます。 目に見える停止が1回、大きな要約リクエストが1回、しかもしきい値をたまたま超えた瞬間に起きます。

マイクロ圧縮は、同じ支払いを分割で済ませます。 ターンが1つ終わるたびに、Hermes はまだ取り込んでいない最も古いやり取りを1つだけ、積み上げ式の要約に畳み込みます。 やっている作業は同じです。 セッションの途中でまとめて行うのではなく、少しずつ継続的に行うだけです。

無料ではありませんし、万能薬でもありません。また、既定では無効です。 compression.micro_compact: true で有効になります。 1回ごとの処理は圧縮用モデルへの実際の呼び出しで、ターンの終わりに実行されます。 回答はすでにストリーミングし終わっていますが、処理が終わるまでターンは閉じません。 さらに、1回ごとの処理は送信済みの履歴を書き換えるため、プロバイダー側のプロンプトキャッシュのプレフィックスが毎ターン壊れます。 有効にする前に プロンプトキャッシュ を読んでください。 環境によっては、このコストが得られる効果を上回ります。

この機能で得られるのは 調整の選択肢 です。 圧縮のコストをどう配分するか、どのモデルに払わせるかを自分で選べます。 圧縮用モデルの選び方 を参照してください。 ここで最も効くのはその選択です。

トレードオフとして、会話の中身がこれまでより少し早く要約に変わります。 圧縮が常に動いているので、会話の古い部分は一括圧縮のときより早く要約になります。 一括圧縮なら、ウィンドウが実際に埋まるまですべて原文のまま残ります。 セッションの前半の細かい内容が、より早く又聞きの情報になります。 その忠実さを少し手放す代わりに、長い停止を一度も経験せずに済み、コンテキストウィンドウはしきい値まで膨らんでは戻るのこぎり波ではなく、一定の小ささに保たれます。


何をするのか

正常に終わったターンのあとに毎回、finalize_turn がコンテキスト圧縮器に、やり取りを 1つ 取り込むよう依頼します。

  1. まだ要約されていない最も古いやり取りを見つけます。
  2. そのやり取りだけを、現在の積み上げ式の要約と一緒に、補助の要約用モデルへ送ります。
  3. 記録上のそれらのメッセージを、更新後の要約を持つ要約マーカー1つに置き換えます。

1ターンにつき1つのやり取りです。 会話がどれだけ長くなっても、1ターンあたりのコストには上限があります。

やり取り とは、エージェントの1ターン全体のことです。 アシスタントのメッセージと、そのツール結果、続くアシスタントの繰り返しを含み、次のユーザーメッセージの手前までを指します。 ツールを多く使う作業では、トークンの大半がここにあります。 ファイルの読み取りやコマンドの出力は、周りの文章よりはるかに大きいからです。 やり取りを1つずつ取り込む価値があるのは、このためです。 アシスタントとツールの1組だけでなくターン全体を取り込むことで、記録上の役割の交互の並びも厳密に正しく保てます。 要約マーカーはアシスタント役のメッセージで、ターン全体は必ず両側をユーザーメッセージに挟まれているからです。

ユーザーのメッセージは圧縮されません

やり取りは、意図して *アシスタント* のメッセージから始まるようにしています。 マイクロ圧縮はそこへたどり着くまでユーザーメッセージを素通りするので、ユーザーが入力した内容は一度も要約されません。 セッションがどれだけ長く続いても、圧縮が何回発動しても、プロンプトはセッション全体を通して原文のまま残ります。

これは設計全体の中で最も役に立つ性質なので、理由をはっきり書いておきます。 アシスタントが出力するものの多くは、自分が何をしたかの記録です。 このファイルを読んだ、あのコマンドを実行した、この結果が得られた、といった内容です。 こうした経過の説明は、要約してもほとんど失われません。 「こうやった」は、圧縮しても元とほぼ同じだけの情報を持っています。 ユーザーの指示は性質が違います。 ほかのすべてが導き出される元の意図であり、その後の作業から復元することはできません。 「既存の retry ヘルパーを使って、新しく作らないで」を要約の中で言い換えてしまうと、6ターン後にエージェントが、やるなと言われたことを自信満々にやる結果になります。

ですから、この非対称は意図したものです。 導き出された材料は圧縮し、正の情報源は残します。 代わりに、真ん中をどこまで小さくできるかには下限ができます。 ユーザーのターンは積み重なり、取り込まれることがないからです。 実際にはこの下限は低く、プロンプトはふつうツール結果1つ分のごく一部にすぎません。 それでも下限であることは確かです。 1万〜2万トークンのプロンプトを日常的に貼り付けているなら、その重さは設計上コンテキストに残り続けます。

触れない範囲

さらに2つの範囲が保護され、原文のまま残ります。

  • 先頭: システムプロンプトと冒頭のメッセージです。

セッションの土台になる指示が言い換えられることはありません。

  • 末尾: 直近のメッセージのうち、トークン数の予算で決まる範囲です。

すぐに関係する内容はすべて全文のまま残ります。

マイクロ圧縮が手を入れるのは、この2つに挟まれた真ん中だけです。

しくみ

カーソル

圧縮器はカーソルを持っています。 まだ取り込んでいない最初のメッセージの位置です。 処理が成功するたびに、要約したばかりのやり取りの先へ進みます。

メモリ上のカーソルが無い、または範囲外のとき(新しいプロセスや再開したセッションなど)は、記録から最後の要約マーカーを探し、その直後から再開して復元します。 記録そのものが正の情報源なので、セッションを再開しても済んだ作業を要約し直すことはありません。

積み上げ式の要約

やり取りごとの要約を山のように溜めるのではなく、要約はちょうど1つだけで、新しいやり取りをそこへ統合していきます。 要約用モデルには、新しい材料に含まれる決定事項・要件・ファイルパス・未解決の問いを取り込み、もう関係のない細部は捨て、既存の構成を保つよう指示します。 見つけた認証情報を [REDACTED] に置き換えることも明示的に指示しています。

要約は累積していくので、記録には最新のマーカーだけを残します。 それ以前のマーカーは完全に冗長です。 現在の要約に、それらが持っていた内容はすべて含まれています。 そのため、新しいマーカーに置き換わった時点で削除します。 これは見た目以上に重要です。 残しておくと、ほぼ同じ文章の写しが、それぞれ見出しと終了マーカーの枠付きで積み重なり、記録は縮むどころか毎ターン大きくなります。

マーカーを取り除くと、その前後にあったユーザーのターンが隣り合うので、モデルに渡すときはひとつのユーザーメッセージにまとめます。 まとめた行には display_metadata.model_only の印が付きます。元のメッセージは圧縮済みの行として表示用の履歴に残り、 表示用の投影(再開、ページ単位の履歴、プロンプトのタイムライン)はどれもこのまとめた行を飛ばします。そのため、再開したセッションでもそれぞれの入力は1回だけ表示されます。

デフラグ

要約への統合を繰り返すと、要約は間延びしてきます。 繰り返しが増え、中身に見合わない大きさになります。 積み上げ式の要約があるトークン数のしきい値(既定は2000)を超えると、次の処理は デフラグ を行います。 補助の呼び出し1回で、積み上げ式の要約 *そのもの* を要約し直して新しい簡潔な版にし、記録上の要約マーカーをその場で書き換えます。

デフラグは記録の構造には触れません。 メッセージの取り込みや差し込みはせず、カーソルも動かさず、ユーザーのターンもそのままです。 処理するのは蓄積された要約の文章だけで、会話のメッセージは扱わないので、「ユーザーのメッセージは圧縮されません」という保証はデフラグ中も守られます。

セッションデータベースとの同期

メモリ上で差し替えるだけでは足りません。 Hermes の通常のセッション書き出しは追記のみなので、元の行は有効なまま残り、再開すると要約と、それが置き換えたメッセージの *両方* を読み込んでしまいます。 そうなると、セッションはいきなりコンテキストの上限を超えます。

そこで、各処理では archive_and_compact も呼びます。 有効な行をアトミックにソフトアーカイブし、圧縮後の行を挿入します。 そのうえでメッセージに書き出し済みの印を付け、続く追記のみの書き出しがそれらを飛ばすようにします。 このデータベース処理が失敗した場合はログに記録し、セッションはそのまま続きます。 その場合、次の一括圧縮で整理されるまで、再開時に二重に読み込まれます。

要約に失敗したとき

要約の呼び出しは失敗することがあります。 補助モデルに接続できない、利用枠を使い切った、あるいはそのやり取り自体がなぜか要約できない、といった場合です。 このときは記録に手を加えず、失敗を数えます。

*同じ* やり取りで3回続けて失敗した場合は、それでもカーソルをその先へ進めます。 そうしないと、1つの不良なやり取りを毎ターン永遠に再試行することになります。 飛ばしたメッセージは記録に残り、次の一括圧縮で拾われます。

一括圧縮との関係

マイクロ圧縮は一括圧縮を置き換えるものではなく、先送りするものです。 しきい値による圧縮は引き続き存在し、それでもウィンドウが埋まれば発動します。 要約マーカーの形式も同じなので、2つは相互に動きます。 実際には、マイクロ圧縮が記録をしきい値より十分下に保つので、一括圧縮が発動する回数はずっと少なくなります。

設定

マイクロ圧縮は 既定では無効 です。明示的に有効にしてください。

compression:
  micro_compact: true             # default: false
  micro_compact_every_n_turns: 1  # cadence — how often a pass runs
  micro_compact_defrag_threshold_tokens: 2000

micro_compact が未設定か false なら、Hermes はこれまでとまったく同じ動きをします。 つまり一括圧縮だけです。 圧縮に関するほかの部分は何も変わりません。

micro_compact_every_n_turns は、有効・無効の切り替えに次いで重要な調整項目です。 後述するキャッシュの破損を、どれくらいの頻度で払うかを決めるからです。 1 にすると、ターンが終わるたびに処理が走ります。 回収は最も積極的になり、プレフィックスも毎ターン1回壊れます。 5 にすると、破損の回数も回収の速さも5分の1になります。 セッションが長く続き、プロバイダーのキャッシュ割引が大きいなら、こちらの方向が適しています。 1 未満の値は、機能を黙って無効にするのではなく 1 に切り上げます。 カウンターは処理が確定した回数ではなくターンごとに進むので、取り込むものが無いターンでも周期は進み、止まってしまうことはありません。

micro_compact_defrag_threshold_tokens は、積み上げ式の要約が際限なく大きくなる代わりに要約し直されるタイミングです。 デフラグ を参照してください。

既定で有効にせず、自分で有効にする形で提供しているのは、次の節で説明するプロンプトキャッシュのコストがあるためです。 このコストは実在し、どんな場合でも払う価値があるわけではありません。 引き継いだ設定に任せるのではなく、自分で決めるべきことです。

プロンプトキャッシュ: 自分で引き受けるコスト

この機能を有効にする前に読んでください。 有効にしない理由として最も強い論点です。

長く続く会話は、キャッシュされたプロンプトのプレフィックスを毎ターン再利用します。 キャッシュされた入力トークンは、キャッシュされていないトークンの何分の一かの料金で課金されます。 この割引が効くのは、プレフィックスが変わらない間だけです。 マイクロ圧縮の処理は送信済みの履歴を書き換えます。 書き換えた位置から先のプレフィックスは無効になるので、マイクロ圧縮を有効にすると、一括圧縮のたびに1回ではなく *毎ターン* キャッシュが壊れます。

これは、先回りの刈り込み(proactive prune)が意図して避けているのと同じコストです。 刈り込みは compression.proactive_prune_min_reclaim_tokens(既定は4096)で自らに条件をかけています。 設定コメントの言葉を借りれば、書き換えを「ツールの繰り返しごとの小さな破損ではなく、ときどき起きる1回の大きな破損」にとどめるためです。

マイクロ圧縮には、これに相当する *回収量* の条件がありません。 処理は、取り込んだ1つのやり取りでたまたま節約できた分を、大きくても小さくても確定させます。 その代わりにあるのが *頻度* のつまみ、micro_compact_every_n_turns です。 値を上げると破損は減り、ときどき起きる形になります。 刈り込みの条件と同じ目的を別の道筋で果たすものですが、より大きな回収を待つのではなく、取り込む量を減らすことでそこへ近づきます。 刈り込みとまったく同じ意味合いがほしいなら、マイクロ圧縮に回収量のしきい値を付けるのが自然な次の一手ですが、まだ存在しません。

ですから、正直に言えば、これは節約ではなく、あるコストを別のコストと交換するものです。

一括圧縮のみ(既定) マイクロ圧縮を有効
圧縮による停止 しきい値で長い停止が1回 各ターンに分散
コンテキストの占有 しきい値まで膨らんでは戻る 低く一定に保たれる
キャッシュのプレフィックス 圧縮と圧縮の間は保たれる 毎ターン壊れる

どちらが得かは、利用者ごとの数字で決まります。 プロバイダーがキャッシュ済み入力をどれだけ割り引くか、プレフィックスがどれだけ大きいか、セッションがどれだけ長く続くか、セッション途中の停止が実際にどれだけ痛いか、です。 キャッシュ割引が大きく、プレフィックスも大きいプロバイダーでは、毎ターンの無効化のコストが、なくした停止のコストを上回ることも十分ありえます。 思い込みで決めず、自分のセッションを測ってください。 測り方 を参照してください。

圧縮用モデルの選び方

マイクロ圧縮は auxiliary.compression のモデルを使います。

auxiliary:
  compression:
    provider: openai-api
    model: <your choice>
    base_url: <endpoint>

これが最も重要な調整項目で、どこでも通用する正解はありません。 ハードウェアと、何を手放してよいかによって変わります。

各処理で送るのは積み上げ式の要約とやり取り1つなので、プロンプトは小さく(数千トークン)済みます。 ただし呼び出しは 毎ターン、ターンの終わりに発生します。 重要な性質は2つです。

  • 待ち時間が支配的です。

処理はターンごとに走るので、実時間のコストを繰り返し感じることになります。 30秒かかるモデルを使うと、どのターンも「ターン+30秒」になります。

  • 推論(reasoning)モデルは向いていません。

やり取りを1つ要約に統合するのは機械的な作業です。 思考するモデルはそこに推論トークンを使い、同じくらいの大きさの通常の instruct モデルよりかなり遅くなりますが、出力は何も良くなりません。

ある特定の環境での実測値をいくつか載せます。 推奨ではなく、傾向を示す例として見てください。

モデル 観測結果
7B 4-bit instruct、ローカル(MLX、Apple Silicon) 1回あたり約31秒。同じマシンでほかの処理も動いていた
大きな MoE 推論モデル、リモート GPU さらに明らかに遅い。要約の作業に思考トークンを使っていた

傾向としては、小さく速い、推論をしない instruct モデルがたいてい適しており、大きいモデルや「賢い」モデルはここではむしろ悪くなることが多い、ということです。 実際にどこに落ち着くかは、動かせる環境によって変わります。

処理が遅すぎると感じたら、効果の大きい順におおよそ次の選択肢があります。 より速いか小さい圧縮用モデルを選ぶ。 ほかの処理と取り合いにならないホストで動かす。 あるいはマイクロ圧縮を無効にして一括圧縮に戻す。

測り方

マイクロ圧縮は、主にトークンや時間を節約するための最適化ではありません。 節約できたトークン数で評価すると、実力を低く見積もることになります。 実際に得られるものは次の2つです。

  1. 長い停止が分割払いになります。

要約の作業量は同じですが、セッションの途中で1回止まるのではなく、ターンのあとに少しずつ行われます。

  1. コンテキストが長持ちします。

真ん中が継続的に回収されるので、占有率はしきい値まで膨らむのこぎり波にならず、低く保たれます。 強制的な圧縮が必要になるまで、セッションはずっと長く続きます。 多くの場合、いつまでも続けられます。

したがって重要な数字は 占有率、つまりウィンドウがどれだけ埋まった状態に保たれているかを、圧縮のしきい値に対する割合で表したものです。 40%前後で安定しているセッションには、続けるための余裕があります。 90%を超えて上がり続けているセッションは、まもなく止まります。 2つ目の数字は、一括圧縮が実際に何回発動したか です。 理想は0回です。

帳面上は何も節約していないのに、この2点ではっきり得をしているセッションもありえます。

処理のたびに、内容を含まない JSON を1行出力します。 形式は一括圧縮のテレメトリーと同じです。

micro compaction telemetry: {"event":"micro_compaction","outcome":"absorbed",
"tokens_before":12739,"tokens_after":12060,"tokens_delta":-679,
"occupancy_pct":38.4,"threshold_tokens":34816,"context_limit":40960,
"exchange_tokens":868,"rolling_summary_tokens":31,"passes_total":1,
"tokens_saved_total":679,"duration_ms":14,...}

occupancy_pct は、tokens_after を圧縮のしきい値に対する割合で表したもので、余裕の大きさを示す数字です。 モデルのウィンドウサイズがまだ確定していないときは null になります。 確定させるには同期の /models 問い合わせが発生することがあり、テレメトリーがターンを止める原因になってはいけないので、キャッシュ済みの値だけを読むからです。

tokens_delta は、処理によって記録が縮んだときに負の値になります。 tokens_saved_total と passes_total はセッション全体で累積するので、1回の実行全体を最後の行から要約できます。 ペイロードに記録の内容は含まれず、数値だけです。

ログから答えを出すには、次のコマンドを使います。

python scripts/micro_compaction_report.py [--per-session] [LOGFILE ...]

既定では $HERMES_HOME/logs/agent.log を読みます。 処理回数、結果の内訳、正味の節約トークン数、取り込んだやり取りの平均サイズ、処理時間を表示します。

うまく動いているときの様子

実際のセッションの例です。 3.5時間かけたプロジェクト全体のコードレビューで、記録は約7.5万トークン、ウィンドウは40万、圧縮のしきい値は32万でした。

処理 メッセージ数 トークン数 増減 占有率 所要時間
1 40 -> 39 27,479 -> 27,778 +299 8.7% 2.2s
2 61 -> 59 48,676 -> 48,128 -548 15.0% 4.5s
3 70 -> 67 58,309 -> 55,915 -2,394 17.5% 9.1s
4 84 -> 80 75,251 -> 69,818 -5,433 21.8% 36.2s
5 84 -> 80 74,659 -> 70,264 -4,395 22.0% 31.2s

ここから読み取れることは3つです。

占有率が横ばいになりました。 約22%まで上がって止まっています。 最後の2回の処理は同じ(84 -> 80 メッセージ)で、その間に会話は4,841トークン増え、マイクロ圧縮は4,395トークンを回収しました。 これが釣り合いの状態です。 ウィンドウはしきい値へ向かって進み続けず、安定しています。

一括圧縮は発動しませんでした。 セッション全体を通して、長い停止は一度も起きていません。

回収が本格化するのは、末尾の予算を超えてからです。 最初の数回はほとんど何も回収できていません。 末尾の予算(ここでは64,000トークン、ウィンドウの16%)を下回っている間は、記録のほぼ全体が保護された末尾で、手を入れてよい部分がごくわずかしかないからです。 セッションの序盤で処理が1回も走らないのは正常です。

コストも率直に書きます。 処理には2〜37秒、中央値で約31秒かかりました。 ほかの処理も動いている小さなローカルモデルでの数字です。 3時間半のうちに合計約2分の要約が分散して入った計算になります。 7.5万トークンの真ん中を一括圧縮するのと比べれば、それでもこちらのほうが得な取引ですが、37秒の上乗せは誤差とは言えません。 圧縮用モデルの選び方 を参照してください。

数字を正直に読む

セッション最初の処理は、たいていトークンを節約せず、むしろ増やします。 要約マーカーを挿入すると、約400トークンの決まった枠が付きます。 圧縮の前置き、履歴の見出し、終了マーカーです。 1回目の処理では、この分を取り込んだやり取り1つで賄うことになります。 最初の処理で tokens_delta: +330 と出ても、故障ではありません。

2回目以降はマーカーを追加するのではなく *置き換える* ので、枠の分はすでに払い終わっており、取り込んだやり取りはほぼそのまま節約になります。 損益分岐はふつう2回目か3回目です。 1行ごとの数字よりセッション単位の見方が重要なのはこのためです。 1ターンではなく、セッションの推移で機能を評価してください。

人が読みやすい、より簡素なログ行も引き続き出力されます。

Micro-compaction: 37 -> 36 messages
Micro-compaction defrag: rolling summary re-summarized (1843 chars)
Micro-compaction: skipping exchange at cursor 12 after 3 consecutive failures

メッセージ数が少ししか変わらないのは想定どおりです。 効果が表れるのはトークン数です。 ツールを多く使ったやり取りを1つ取り込むと、メッセージ数は1つか2つしか変わらなくても、数百トークン減ることがあります。

失敗時の動き

マイクロ圧縮は、全体を通してベストエフォートです。 finalize_turn での呼び出しは、どんな例外もログに記録して握りつぶすように包まれています。 失敗しても会話は変更されずに戻り、ターンは通常どおり完了します。 性能が落ちることはあっても、セッションを壊すことはできないようになっています。