Skip to content

コンテキスト圧縮と MoA

源码版本v2026.7.20

職務

二つの役:① 履歴メッセージが provider のコンテキスト上限に近づいたとき、古いメッセージを要約/刈り込んで予算 (budget) を空ける(context_compressor)。② 任意で Mixture-of-Agents パスにより複数モデルの出力を集約する(moa_loop)。どちらも主ループ内から呼ばれ、「長い対話を走らせ続ける」「回答を安定させる」ための支えである。

主要ファイル

データフロー

  1. 主ループは毎ラウンド圧縮要否を評価(_should_run_preflight_estimate:213 参照)。
  2. 閾値到達後、context_compressor を呼ぶ:
  3. 圧縮が生んだ「新空間」は iteration_budget.refund()(agent/iteration_budget.py) で一部還付され、主ループが数ラウンド多く走れる。
  4. MoA が有効なら、moa_loop がこのラウンドで複数 provider を並行呼び出して集約し、主回答の増強または仲裁とする。

予算推定は圧縮の意思決定の基礎だ。以下は agent/context_compressor.py:419-437 の核心。

python
def _estimate_msg_budget_tokens(msg: dict) -> int:
    """Counts the full ``tool_call`` envelope, not just ``function.arguments``
    — counting only the arguments undercounted parallel-tool turns by 2-15x
    (#28053). Also counts provider replay fields (``codex_reasoning_items``)."""
    content_len = _content_length_for_budget(msg.get("content") or "")
    tokens = content_len // _CHARS_PER_TOKEN + 10  # +10 for role/key overhead
    for tc in msg.get("tool_calls") or []:
        if isinstance(tc, dict):
            tokens += len(str(tc)) // _CHARS_PER_TOKEN
    for key in _REPLAY_BUDGET_KEYS:
        tokens += _serialized_length_for_budget(msg.get(key)) // _CHARS_PER_TOKEN
    return tokens

かつて踏んだ二つの罠がコメントに書かれている。① function.arguments 文字列だけ数えると並行ツール呼び出しのケースで 2-15 倍少なく数える。② codex_reasoning_items のような replay フィールドを数えないと、「小さく見えて実は大きい」assistant メッセージが誤って保護され、圧縮後も上限に張り付き、その後も繰り返し圧縮される(#55572)。

浄化と永続化マーカー剥離は agent/context_compressor.py:136-160 にある。

python
def _fresh_compaction_message_copy(msg: Dict[str, Any]) -> Dict[str, Any]:
    """Shallow .copy() propagates ``_db_persisted``; new child session then
    skips every row (#57491). Strip at every copy site."""
    fresh = msg.copy()
    fresh.pop(_DB_PERSISTED_MARKER, None)
    return fresh


def _strip_persistence_markers(messages: List[Dict[str, Any]]) -> None:
    """Terminal sweep — invariant holds regardless of intermediate copy sites."""
    for msg in messages:
        if isinstance(msg, dict):
            msg.pop(_DB_PERSISTED_MARKER, None)

「二重の防線」だ。_fresh_compaction_message_copy が各 copy サイトでその場で剥離し、_strip_persistence_markerscompress() 末尾でもう一度完全な sweep を行う。前者は意図が明確、後者は構造的な安全網。画像の刈り込み(agent/context_compressor.py:497-515_strip_image_parts_from_parts)もロジックは似ている——image/image_url/input_image パートをテキストプレースホルダーに置き換え、画像がないときは None を返して呼び出し側にスキップさせ、各メッセージで無意味な新リストを生成しない。

設計動機

なぜ正確な token 計数ではなく「推定」を使うのか? provider の tokenizer は非公開(reasoning モデルの隠しフィールドは特にそう)で、tiktoken は遅い上に不正確だ。モジュールは char-based 推定 + 10 文字のオーバーヘッドという固定式を採用する。「圧縮するか」「tail をどう保護するか」の意思決定において、一貫して保守側に倒れていれば十分だ。正確である必要はなく、preflight の推定と同じ物差しを使うことだけが求められる(turn_context_should_run_preflight_estimate を参照)。さもないと「preflight は圧縮不要、圧縮内部は圧縮できない」というデッドロックが起きる。

なぜツール呼び出し名とパス言与を保持するのか? LLM が古いメッセージを要約するとき、「write_file(/tmp/foo.py) を呼んだ」という关键操作を飲み込みがちだ。次ラウンドでモデルが「前にファイルについて議論した」と読んでも、どこに書いたか分からなければ、重複作成や誤読を起こす。_extract_tool_call_name_and_args / _collect_path_mentions が hard な情報を summary の横に貼り付ける。代償は数行の余分な token、見返りは後続ツール呼び出しが記憶喪失しないことだ。

refund を iteration_budget に返す理由——予算を直接増やさないのは、max_iterations を有名無実化しないためだ。圧縮一回ごとに数ラウンド増やしていたら、長対話は無限に走れてしまう。refund は圧縮自身が節約した一回二回分のツール呼び出しイテレーション (tool dispatch iteration) だけを返す。「上限はあくまで上限」だ。

境界と失敗

  • 圧縮が前に進まない:_compression_made_progress は token 減少が 5% を超えたときだけ進展とみなす。あるラウンドが 2% しか節約しなければ「進展なし」となり、多段空転を防ぐ。このとき主ループは auto-reset で新会話に切り替える。
  • 永続化マーカーの残留:圧縮産物が _db_persisted を持っていると、新しい child session の state.db がそれをスキップし、compacted transcript が DB 上で完全に失われる(#57491)。terminal sweep が compress() 末尾で強制的にもう一度掃して安全網とする。
  • summary が新指示と誤読される:弱いモデルは summary 内の「## Active Task」を新規ユーザーリクエストとして扱ってしまう(#11475#14521)。_SUMMARY_END_MARKER で明示的な区切り線を入れ、alternation 境界シナリオでは _MERGED_PRIOR_CONTEXT_HEADER / _MERGED_SUMMARY_DELIMITER で二重に包んで曖昧さを排除する。
  • 履歴 prefix の復活:_HISTORICAL_SUMMARY_PREFIXES が旧バージョンの summary prefix を保存する。resume で新 lineage に入るとき、旧 prefix も一緒に剥離し、stale directive が body に埋まって返答を乗っ取るのを防ぐ。

まとめ

圧縮器は長対話の生命線。核心のトレードオフは「何を捨て、何を残すか」——ツール呼び出しとパス言及を優先保持する。失うと後続ラウンドが「記憶喪失」するからだ。MoA は直交する拡張で、オンオフ可能。どちらも主ループ構造を変えず、適切なタイミングで切入するだけ。

非公式コミュニティ学習サイト。MIT ライセンスの NousResearch/hermes-agent ソースに基づく。