上下文壓縮與快取
Hermes Agent 使用雙重壓縮系統和 Anthropic 提示快取來在長對話中有效管理上下文視窗使用量。
原始碼檔案:agent/context_engine.py(ABC)、agent/context_compressor.py(預設引擎)、
agent/prompt_caching.py、gateway/run.py(會話衛生)、run_agent.py(搜尋 _compress_context)
可插拔的上下文引擎
上下文管理建立在 ContextEngine ABC(agent/context_engine.py)之上。內建的 ContextCompressor 是預設實作,但外掛可以用替代引擎(例如無損上下文管理)替換它。
context:
engine: "compressor" # 預設——內建有損摘要
engine: "lcm" # 範例——提供無損上下文的外掛
引擎負責:
- 決定何時應觸發壓縮(
should_compress()) - 執行壓縮(
compress()) - 可選擇暴露 Agent 可呼叫的工具(例如
lcm_grep) - 追蹤 API 回應中的 token 使用量
選擇透過 config.yaml 中的 context.engine 進行設定驅動。解析順序:
- 檢查
plugins/context_engine/<name>/目錄 - 檢查通用外掛系統(
register_context_engine()) - 回退到內建的
ContextCompressor
外掛引擎永不自動啟用——使用者必須明確設定 context.engine 為外掛的名稱。預設的 "compressor" 始終使用內建引擎。
透過 hermes plugins → Provider Plugins → Context Engine 配置,或直接編輯 config.yaml。
如需建構上下文引擎外掛,參見 Context Engine 外掛。
雙重壓縮系統
Hermes 有兩個獨立運作的壓縮層:
┌──────────────────────────┐
Incoming message │ Gateway Session Hygiene │ 在 85% 上下文時觸發
─────────────────► │ (前處理,粗略估計) │ 大型會話的安全網
└─────────────┬────────────┘
│
▼
┌──────────────────────────┐
│ Agent ContextCompressor │ 在 50% 上下文時觸發(預設)
│ (迴圈內,真實 token) │ 正常上下文管理
└──────────────────────────┘
1. Gateway 會話衛生(85% 閾值)
位於 gateway/run.py(搜尋 Session hygiene: auto-compress)。這是一個安全網,在 Agent 處理訊息之前執行。它防止當會話在輪次之間過大增長時(例如 Telegram/Discord 中的隔夜累積)API 失敗。
- 閾值:固定為模型上下文長度的 85%
- Token 來源:優先使用上一輪的實際 API 回報 token;回退到粗略的基於字元估算(
estimate_messages_tokens_rough) - 觸發條件:僅當
len(history) >= 4且壓縮已啟用時 - 目的:捕捉逃過 Agent 自身壓縮器的會話
閘道器衛生閾值刻意高於 Agent 的壓縮器。將其設為 50%(與 Agent 相同)會在長閘道器會話中導致每輪都過早壓縮。
2. Agent ContextCompressor(50% 閾值,可設定)
位於 agent/context_compressor.py。這是主要壓縮系統,在 Agent 的工具迴圈內運行,可存取準確的、API 回報的 token 數量。
設定
所有壓縮設定從 config.yaml 的 compression 鍵讀取:
compression:
enabled: true # 啟用/停用壓縮(預設:true)
threshold: 0.50 # 上下文視窗比例(預設:0.50 = 50%)
target_ratio: 0.20 # 保留為尾部的閾值比例(預設:0.20)
protect_last_n: 20 # 最小保護的尾部訊息數(預設:20)
codex_gpt55_autoraise: true # Codex OAuth 上的 gpt-5.5:將觸發點提高到 85%(預設:true)
# 摘要模型/供應商在 auxiliary 下設定:
auxiliary:
compression:
model: null # 摘要的覆寫模型(預設:自動偵測)
provider: auto # 供應商:"auto"、"openrouter"、"nous"、"main" 等
base_url: null # 自訂 OpenAI 相容端點
參數詳解
| 參數 | 預設值 | 範圍 | 描述 |
|---|---|---|---|
threshold | 0.50 | 0.0-1.0 | 當提示 token ≥ threshold × context_length 時觸發壓縮 |
target_ratio | 0.20 | 0.10-0.80 | 控制尾部保護 token 預算:threshold_tokens × target_ratio |
protect_last_n | 20 | ≥1 | 永遠保留的最近訊息最小數量 |
protect_first_n | 3 | (硬編碼) | 系統提示 + 第一次交談永遠保留 |
codex_gpt55_autoraise | true | bool | 在 ChatGPT Codex OAuth 路由上將 gpt-5.5 的觸發點提高到 85%(見下文)。設為 false 以保持全域 threshold |
Codex gpt-5.5 閾值自動提升
ChatGPT Codex OAuth 後端將 gpt-5.5 硬性限制在 272K 上下文視窗(同一模型在 OpenAI 直接 API 和 OpenRouter 上暴露 1.05M,在 GitHub Copilot 上為 400K)。在預設的 50% 觸發點下,壓縮會在約 136K 時觸發——僅為模型實際可使用視窗的一半。當活躍路由為 Codex OAuth(provider: openai-codex)且模型為 gpt-5.5 時,Hermes 將觸發點提高到 85%(約 231K)並印出一次性的通知與退出命令。只有此特定路由受影響;其他供應商上的 gpt-5.5 保持你的全域 threshold。要退回全域值:
hermes config set compression.codex_gpt55_autoraise false
計算值(200K 上下文模型的預設值)
context_length = 200,000
threshold_tokens = 200,000 × 0.50 = 100,000
tail_token_budget = 100,000 × 0.20 = 20,000
max_summary_tokens = min(200,000 × 0.05, 12,000) = 10,000
注意——閾值源自主模型的上下文視窗
threshold_tokens始終為threshold × context_length,其中context_length是主 Agent 模型的上下文視窗——永遠不是輔助/摘要模型的。在 262,144 token 的模型上使用預設的0.50,閾值為262,144 × 0.50 = 131,072。該數字接近常見的「128K 上下文」是百分比的巧合,而非輔助模型視窗是觸發點的標誌。輔助模型的上下文視窗是獨立的問題——參見下方的「摘要模型上下文長度」警告,了解它如何影響摘要是否能產生,而非壓縮何時觸發。
壓縮演算法
ContextCompressor.compress() 方法遵循 4 階段演算法:
階段 1:修剪舊工具結果(低成本,無 LLM 呼叫)
受保護尾部之外的舊工具結果(>200 字元)被替換為:
[Old tool output cleared to save context space]
這是一個低成本的預掃描,從冗長的工具輸出(檔案內容、終端機輸出、搜尋結果)中節省大量 token。
階段 2:確定邊界
┌─────────────────────────────────────────────────────────────┐
│ Message list │
│ │
│ [0..2] ← protect_first_n(system + 第一次交談) │
│ [3..N] ← 中間輪次 → 已摘要 │
│ [N..end] ← 尾部(按 token 預算或 protect_last_n) │
│ │
└─────────────────────────────────────────────────────────────┘
尾部保護基於 token 預算:從結尾向前遍歷,累積 token 直到預算用盡。如果預算保護的訊息數更少,則回退到固定的 protect_last_n 數量。
邊界對齊以避免拆分 tool_call/tool_result 群組。_align_boundary_backward() 方法遍歷連續的工具結果以找到父代 assistant 訊息,保持群組完整。
階段 3:產生結構化摘要
警告——摘要模型上下文長度
摘要模型的上下文視窗至少需要與主 Agent 模型一樣大。整個中間部分在單一
call_llm(task="compression")呼叫中發送給摘要模型。如果摘要模型的上下文較小,API 會回傳上下文長度錯誤——_generate_summary()捕捉它、記錄警告並回傳None。壓縮器然後沒有摘要地丟棄中間輪次,靜默地丟失對話上下文。這是壓縮品質降級最常見的原因。
中間輪次使用輔助 LLM 以結構化範本進行摘要:
## Goal
[使用者想要完成什麼]
## Constraints & Preferences
[使用者偏好、編碼風格、限制、重要決策]
## Progress
### Done
[已完成的工作——具體檔案路徑、執行的命令、結果]
### In Progress
[目前進行中的工作]
### Blocked
[遇到的任何阻礙或問題]
## Key Decisions
[重要的技術決策及原因]
## Relevant Files
[讀取、修改或建立的檔案——附簡要說明]
## Next Steps
[接下來需要做什麼]
## Critical Context
[具體值、錯誤訊息、設定細節]
摘要預算隨被壓縮的內容量成比例:
- 公式:
content_tokens × 0.20(_SUMMARY_RATIO常數) - 最小值:2,000 tokens
- 最大值:
min(context_length × 0.05, 12,000)tokens
階段 4:組裝壓縮後的訊息
壓縮後的訊息列表為:
- 頭部訊息(第一次壓縮時在系統提示上附加註記)
- 摘要訊息(選擇角色以避免連續相同角色違反)
- 尾部訊息(未修改)
孤立的 tool_call/tool_result 對由 _sanitize_tool_pairs() 清理:
- 引用已移除呼叫的工具結果 → 移除
- 結果已被移除的工具呼叫 → 注入存根結果
迭代式重新壓縮
在後續壓縮中,先前的摘要被傳遞給 LLM 並附帶更新它的指示,而非從頭摘要。這在多次壓縮之間保留了資訊——項目從「In Progress」移動到「Done」,新增了新進度,過時的資訊被移除。
壓縮器實例上的 _previous_summary 欄位儲存了上次的摘要文字以供此用途。
前/後範例
壓縮前(45 則訊息,約 95K tokens)
[0] system: "You are a helpful assistant..."(系統提示)
[1] user: "Help me set up a FastAPI project"
[2] assistant: <tool_call> terminal: mkdir project </tool_call>
[3] tool: "directory created"
[4] assistant: <tool_call> write_file: main.py </tool_call>
[5] tool: "file written (2.3KB)"
... 再 30 輪的檔案編輯、測試、除錯 ...
[38] assistant: <tool_call> terminal: pytest </tool_call>
[39] tool: "8 passed, 2 failed\n..." (5KB 輸出)
[40] user: "Fix the failing tests"
[41] assistant: <tool_call> read_file: tests/test_api.py </tool_call>
[42] tool: "import pytest\n..." (3KB)
[43] assistant: "I see the issue with the test fixtures..."
[44] user: "Great, also add error handling"
壓縮後(25 則訊息,約 45K tokens)
[0] system: "You are a helpful assistant...
[Note: Some earlier conversation turns have been compacted...]"
[1] user: "Help me set up a FastAPI project"
[2] assistant: "[CONTEXT COMPACTION] Earlier turns were compacted...
## Goal
Set up a FastAPI project with tests and error handling
## Progress
### Done
- Created project structure: main.py, tests/, requirements.txt
- Implemented 5 API endpoints in main.py
- Wrote 10 test cases in tests/test_api.py
- 8/10 tests passing
### In Progress
- Fixing 2 failing tests (test_create_user, test_delete_user)
## Relevant Files
- main.py — FastAPI app with 5 endpoints
- tests/test_api.py — 10 test cases
- requirements.txt — fastapi, pytest, httpx
## Next Steps
- Fix failing test fixtures
- Add error handling"
[3] user: "Fix the failing tests"
[4] assistant: <tool_call> read_file: tests/test_api.py </tool_call>
[5] tool: "import pytest\n..."
[6] assistant: "I see the issue with the test fixtures..."
[7] user: "Great, also add error handling"
提示快取(Anthropic)
來源:agent/prompt_caching.py
透過快取對話前綴,在多輪對話中將輸入 token 成本降低約 75%。使用 Anthropic 的 cache_control 斷點。
策略:system_and_3
Anthropic 允許每個請求最多 4 個 cache_control 斷點。Hermes 使用「system_and_3」策略:
Breakpoint 1: System prompt (跨所有輪次穩定)
Breakpoint 2: 倒數第三個非系統訊息 ─┐
Breakpoint 3: 倒數第二個非系統訊息 ├─ 滾動視窗
Breakpoint 4: 最後一個非系統訊息 ─┘
運作方式
apply_anthropic_cache_control() 深複製訊息並注入 cache_control 標記:
# 快取標記格式
marker = {"type": "ephemeral"}
# 或 1 小時 TTL:
marker = {"type": "ephemeral", "ttl": "1h"}
標記根據內容類型以不同方式套用:
| 內容類型 | 標記放置位置 |
|---|---|
| 字串內容 | 轉換為 [{"type": "text", "text": ..., "cache_control": ...}] |
| 列表內容 | 新增至最後一個元素的字典 |
| None/空 | 新增為 msg["cache_control"] |
| 工具訊息 | 新增為 msg["cache_control"](僅原生 Anthropic) |
快取感知設計模式
-
穩定的系統提示:系統提示是斷點 1,跨所有輪次快取。避免在對話中途修改它(壓縮僅在第一次壓縮時附加註記)。
-
訊息順序很重要:快取命中需要前綴匹配。在中間新增或移除訊息會使之後的所有快取失效。
-
壓縮快取互動:壓縮後,壓縮區域的快取被無效化,但系統提示快取存活。滾動的 3 則訊息視窗在 1-2 輪內重新建立快取。
-
TTL 選擇:預設為
5m(5 分鐘)。對於使用者在輪次之間休息的長時間會話,使用1h。
啟用提示快取
當以下條件滿足時,提示快取自動啟用:
- 模型是 Anthropic Claude 模型(透過模型名稱偵測)
- 供應商支援
cache_control(原生 Anthropic API 或 OpenRouter)
# config.yaml——TTL 可設定(必須為 "5m" 或 "1h")
prompt_caching:
cache_ttl: "5m"
CLI 在啟動時顯示快取狀態:
💾 Prompt caching: ENABLED (Claude via OpenRouter, 5m TTL)
上下文壓力警告
中間上下文壓力警告已被移除(參見 run_agent.py 中的迭代預算區塊,其中註記:「無中間壓力警告——它們導致模型在複雜任務上過早『放棄』」)。當提示 token 達到設定的 compression.threshold(預設 50%)時觸發壓縮,無之前的警告步驟;閘道器會話衛生作為次要安全網在模型上下文視窗的 85% 時觸發。