Skip to main content

上下文压缩与缓存

Hermes Agent 采用双层压缩系统和 Anthropic 的提示词缓存机制,以高效管理长时间对话中的上下文窗口使用。

源文件:agent/context_engine.py (ABC)、agent/context_compressor.py(默认引擎)、agent/prompt_caching.pygateway/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 响应的 token 使用量

配置驱动选择通过 context.engineconfig.yaml 中进行。解析顺序如下:

  1. 检查 plugins/context_engine/<name>/ 目录
  2. 检查通用插件系统(register_context_engine()
  3. 回退至内置的 ContextCompressor

插件引擎不会自动激活——用户必须显式将 context.engine 设置为插件名称。默认的 "compressor" 始终使用内置引擎。

可通过 hermes plugins → 提供商插件 → 上下文引擎 配置,或直接编辑 config.yaml

有关开发上下文引擎插件,请参阅 上下文引擎插件

双重压缩系统

Hermes 具有两个独立运行的压缩层:

                     ┌──────────────────────────┐
Incoming message │ Gateway 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%
  • token 来源:优先使用上一轮 API 报告的实际 token 数;若不可用,则回退到粗略的字符估算(estimate_messages_tokens_rough
  • 触发条件:仅当 len(history) >= 4 且压缩功能启用时
  • 目的:捕获逃逸出代理自身压缩器的会话

网关净化阈值有意高于代理压缩器的阈值。若设置为 50%(与代理相同),会导致长会话中每轮都提前压缩。

2. 代理 ContextCompressor(50% 阈值,可配置)

位于 agent/context_compressor.py。这是主要压缩系统,在代理的工具循环内部运行,并可访问准确的 API 报告 token 数。

配置

所有压缩设置均从 config.yaml 中的 compression 键读取:

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

参数详情

参数默认值范围描述
threshold0.500.0–1.0当提示词 token 数 ≥ threshold × context_length 时触发压缩
target_ratio0.200.10–0.80控制尾部保护 token 预算:threshold_tokens × target_ratio
protect_last_n20≥1始终保留的最近消息最小数量
protect_first_n3(硬编码)系统提示 + 第一次交互始终保留

计算值(以 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() 方法遵循四阶段算法:

阶段 1:清除旧工具结果(低成本,无需 LLM 调用)

超过 200 字符的旧工具结果(不在受保护尾部内)被替换为:

[Old tool output cleared to save context space]

这是一次低成本的预处理,可显著节省冗长工具输出(文件内容、终端输出、搜索结果)所占用的 token。

阶段 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) │
│ │
└─────────────────────────────────────────────────────────────┘

尾部保护基于token 预算:从末尾开始反向遍历,累计 token 直至预算耗尽。若预算保护的消息数少于固定值,则回退至固定的 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 token
  • 最大值:min(context_length × 0.05, 12,000) token

阶段 4:组装压缩后消息

压缩后的消息列表包含:

  1. 头部消息(首次压缩时,在系统提示中附加说明)
  2. 摘要消息(角色选择避免连续同角色违规)
  3. 尾部消息(保持不变)

孤立的 tool_call/tool_result 对由 _sanitize_tool_pairs() 清理:

  • 引用已删除调用的工具结果 → 删除
  • 其结果已被删除的工具调用 → 注入占位结果

迭代式再压缩

在后续压缩中,上一次的摘要会被传递给 LLM,指令为更新而非从头总结。这能跨多次压缩保留信息——项目从“进行中”变为“已完成”,新增进展被加入,过时信息被移除。

压缩器实例上的 _previous_summary 字段存储上一次摘要文本,用于此目的。

压缩前后示例

压缩前(45 条消息,约 95K token)

[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 token)

[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           (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": ...}]
列表内容添加到最后一个元素的字典中
None/空值作为 msg["cache_control"] 添加
工具消息作为 msg["cache_control"] 添加(仅原生 Anthropic 支持)

缓存感知设计模式

  1. 稳定的系统提示:系统提示是断点 1,跨所有轮次缓存。避免在对话过程中修改它(压缩仅在首次压缩时追加说明)。

  2. 消息顺序至关重要:缓存命中要求前缀完全匹配。在中间插入或删除消息会使之后所有内容的缓存失效。

  3. 压缩与缓存的交互:压缩后,压缩区域的缓存被无效化,但系统提示缓存仍保留。滚动的 3 条消息窗口可在 1–2 轮内重新建立缓存。

  4. 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% 以下,则警告状态将被清除。
如果压缩未能将使用量降低至警告水平(对话内容过于密集),警告将持续存在,但压缩不会再次触发,直到使用量重新超过阈值为止。