上下文压缩和缓存
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" # default — built-in lossy summarization
engine: "lcm" # example — plugin providing lossless context
引擎负责:
- 决定何时触发压缩 (
should_compress()) - 执行压缩 (
compress()) - 可选择公开代理可以调用的工具(例如
lcm_grep) - 跟踪 API 响应中的令牌使用情况
选择是通过 config.yaml 中的 context.engine 配置驱动的。决议顺序:
1.检查plugins/context_engine/<name>/目录
2.检查通用插件系统(register_context_engine())
3. 回退到内置 ContextCompressor
插件引擎永远不会自动激活 - 用户必须显式将 context.engine 设置为插件的名称。默认的 "compressor" 始终使用内置的。
通过 hermes plugins → Provider Plugins → Context Engine 配置,或直接编辑 config.yaml。
要构建上下文引擎插件,请参阅Context Engine Plugins。
双压缩系统
Hermes 有两个独立运行的独立压缩层:
┌──────────────────────────┐
Incoming message │ 网关 Session Hygiene │ Fires at 85% of context
─────────────────► │ (pre-agent, rough est.) │ Safety net for large sessions
└─────────────┬────────────┘
│
▼
┌──────────────────────────┐
│ Agent ContextCompressor │ Fires at 50% of context (default)
│ (in-loop, real tokens) │ Normal context management
└──────────────────────────┘
1. 网关会话卫生(85% 阈值)
位于 gateway/run.py(搜索 Session hygiene: auto-compress)。这是一个安全网
在代理处理消息之前运行。它可以防止会话时 API 失败
在回合之间变得太大(例如,Telegram/Discord 中的过夜积累)。
- 阈值:固定为模型上下文长度的 85%
- 令牌来源:更喜欢上一轮实际 API 报告的令牌;回落
基于字符的粗略估计 (
estimate_messages_tokens_rough) - 触发:仅当
len(history) >= 4且启用压缩时 - 目的:捕获从代理自己的压缩器中逃脱的会话
网关卫生阈值有意高于代理的压缩器。 将其设置为 50%(与代理相同)会导致每回合过早压缩 在长时间的网关会话中。
2. Agent ContextCompressor(50%阈值,可配置)
位于agent/context_compressor.py。这是初级压缩
系统在代理的工具循环内运行,可以访问准确的、
API 报告的令牌计数。
配置
所有压缩设置均从 compression 键下的 config.yaml 读取:
compression:
enabled: true # Enable/disable compression (default: true)
threshold: 0.50 # Fraction of context window (default: 0.50 = 50%)
target_ratio: 0.20 # How much of threshold to keep as tail (default: 0.20)
protect_last_n: 20 # Minimum protected tail messages (default: 20)
# Summarization model/provider configured under auxiliary:
auxiliary:
compression:
model: null # Override model for summaries (default: auto-detect)
provider: auto # Provider: "auto", "openrouter", "nous", "main", etc.
base_url: null # Custom OpenAI-compatible endpoint
参数详细信息
| 参数 | 默认 | 范围 | 描述 |
|---|---|---|---|
threshold | threshold 0.50 | 0.50 0.0-1.0 | 当提示标记 ≥ threshold × context_length 时触发压缩 |
target_ratio | target_ratio 0.20 | 0.20 0.10-0.80 | 控制尾部保护令牌预算:threshold_tokens × target_ratio |
protect_last_n | protect_last_n 20 | 20 ≥1 | 始终保留的最近消息的最小数量 |
protect_first_n | protect_first_n 3 | 3 (硬编码) | 系统提示+首次交流始终保留 |
计算值(默认情况下为 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
压缩算法
ContextCompressor.compress() 方法遵循 4 阶段算法:
第 1 阶段:修剪旧工具结果(便宜,无需LLM)
受保护尾部之外的旧工具结果(>200 个字符)替换为:
[Old tool output cleared to save context space]
这是一个便宜的预传递,可以从冗长的工具中节省大量令牌 输出(文件内容、终端输出、搜索结果)。
第 2 阶段:确定边界
┌─────────────────────────────────────────────────────────────┐
│ Message list │
│ │
│ [0..2] ← protect_first_n (system + first exchange) │
│ [3..N] ← middle turns → SUMMARIZED │
│ [N..end] ← tail (by token budget OR protect_last_n) │
│ │
└─────────────────────────────────────────────────────────────┘
尾部保护是基于代币预算:从末端向后走,
积累代币直到预算用完。回落至固定值
如果预算可以保护较少的消息,则 protect_last_n 计数。
边界对齐以避免分裂 tool_call/tool_result 组。
_align_boundary_backward() 方法遍历连续的工具结果
查找家长助理消息,保持群组完整。
第 3 阶段:生成结构化摘要
:::警告摘要模型上下文长度
摘要模型必须具有一个上下文窗口至少与主代理模型一样大。整个中间部分通过单个 call_llm(task="compression") 调用发送到摘要模型。如果摘要模型的上下文较小,API 将返回上下文长度错误 — _generate_summary() 捕获该错误,记录警告并返回 None。然后压缩器会丢弃中间的转弯没有摘要,默默地丢失对话上下文。这是压实质量下降的最常见原因。
:::
中间的转弯是使用辅助LLM和结构化的总结的 模板:
## Goal
[What the user is trying to accomplish]
## Constraints & Preferences
[User preferences, coding style, constraints, important decisions]
## Progress
### Done
[Completed work — specific file paths, commands run, results]
### In Progress
[Work currently underway]
### Blocked
[Any blockers or issues encountered]
## Key Decisions
[Important technical decisions and why]
## Relevant Files
[Files read, modified, or created — with brief note on each]
## Next Steps
[What needs to happen next]
## Critical Context
[Specific values, error messages, configuration details]
摘要预算随着压缩内容的数量而变化:
- 公式:
content_tokens × 0.20(_SUMMARY_RATIO常量) - 最低数量:2,000 个代币
- 最大:
min(context_length × 0.05, 12,000)令牌
第 4 阶段:组装压缩消息
压缩后的消息列表为:
- 标题消息(在第一次压缩时在系统提示中附加注释) 2.摘要消息(选择角色以避免连续的相同角色违规) 3.尾部消息(未修改)
孤立的 tool_call/tool_result 对由 _sanitize_tool_pairs() 清理:
- 工具结果引用已删除的调用 → 已删除
- 结果被删除的工具调用 → 注入存根结果
迭代重新压缩
在后续压缩中,先前的摘要将传递给 LLM 指示更新它而不是从头开始总结。这保留了 多个压缩中的信息——项目从“进行中”移动到“完成”, 添加新的进度,并删除过时的信息。
压缩机实例上的 _previous_summary 字段存储最后的摘要
用于此目的的文本。
之前/之后示例
压缩前(45 条消息,~95K 令牌)
[0] system: "You are a helpful assistant..." (system prompt)
[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 more turns of file editing, testing, debugging ...
[38] assistant: <tool_call> terminal: pytest </tool_call>
[39] tool: "8 passed, 2 failed\n..." (5KB output)
[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 令牌)
[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"
提示缓存(人为)
来源:agent/prompt_caching.py
通过缓存多轮对话,将输入令牌成本降低约 75%
对话前缀。使用 Anthropic 的 cache_control 断点。
策略:system_and_3
Anthropic 允许每个请求最多有 4 个 cache_control 断点。Hermes
使用“system_and_3”策略:
Breakpoint 1: System prompt (stable across all turns)
Breakpoint 2: 3rd-to-last non-system message ─┐
Breakpoint 3: 2nd-to-last non-system message ├─ Rolling window
Breakpoint 4: Last non-system message ─┘
它是如何运作的
apply_anthropic_cache_control() 深度复制消息并注入
cache_control 标记:
# Cache marker format
marker = {"type": "ephemeral"}
# Or for 1-hour TTL:
marker = {"type": "ephemeral", "ttl": "1h"}
根据内容类型,标记的应用方式有所不同:
| 内容类型 | 标记去哪里 |
|---|---|
| 字符串内容 | 转换为 [{"type": "text", "text": ..., "cache_control": ...}] |
| 列表内容 | 添加到最后一个元素的字典 |
| 无/空 | 添加为 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 is configurable
model:
cache_ttl: "5m" # "5m" or "1h"
CLI 在启动时显示缓存状态:
💾 Prompt caching: ENABLED (Claude via OpenRouter, 5m TTL)
上下文压力警告
代理在压缩阈值的 85% 时发出上下文压力警告 (不是上下文的 85%,而是阈值的 85%,阈值本身就是上下文的 50%):
⚠️ Context is 85% to compaction threshold (42,500/50,000 tokens)
压缩后,如果使用率下降到阈值的 85% 以下,则进入警告状态 已清除。如果压缩未能降低到警告级别以下( 对话太密集),警告仍然存在,但压缩不会 重新触发,直到再次超过阈值。