Files
hermes-agent/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/context-compression-and-caching.md
T
2026-06-14 14:30:48 -04:00

326 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 上下文压缩与缓存
description: Hermes Agent 如何通过双重压缩系统和 Anthropic prompt 缓存高效管理上下文窗口。
---
# 上下文压缩与缓存
Hermes Agent 使用双重压缩系统和 Anthropic prompt(提示词)缓存,在长对话中高效管理上下文窗口用量。
源文件:`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` 是默认实现,但插件可以用其他引擎替换它(例如无损上下文管理)。
```yaml
context:
engine: "compressor" # default — built-in lossy summarization
engine: "lcm" # example — plugin providing lossless context
```
引擎负责:
- 决定何时触发压缩(`should_compress()`
- 执行压缩(`compress()`
- 可选地暴露 agent 可调用的工具(例如 `lcm_grep`
- 追踪 API 响应中的 token 用量
通过 `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 插件](/developer-guide/context-engine-plugin)。
## 双重压缩系统
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. 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 自身压缩器的会话
Gateway 清理阈值有意高于 agent 压缩器的阈值。将其设置为 50%(与 agent 相同)会导致长 gateway 会话在每一轮都过早触发压缩。
### 2. Agent ContextCompressor50% 阈值,可配置)
位于 `agent/context_compressor.py`。这是**主要压缩系统**,在 agent 的工具循环内运行,可访问准确的 API 报告 token 数。
## 配置
所有压缩设置从 `config.yaml``compression` 键读取:
```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` | `0.50` | 0.0-1.0 | 当 prompt 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` | (硬编码)| 系统提示词 + 首次交互始终保留 |
### 计算值(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]
```
这是一个廉价的预处理步骤,可从冗长的工具输出(文件内容、终端输出、搜索结果)中节省大量 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()` 方法会跳过连续的工具结果,找到父级 assistant 消息,保持组的完整性。
### 阶段 3:生成结构化摘要
:::warning 摘要模型上下文长度
摘要模型的上下文窗口必须**至少与主 agent 模型一样大**。整个中间部分通过单次 `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. 尾部消息(未修改)
`_sanitize_tool_pairs()` 清理孤立的 tool_call/tool_result 对:
- 引用已删除调用的工具结果 → 删除
- 结果已被删除的工具调用 → 注入存根结果
### 迭代重压缩
在后续压缩中,前一次摘要会连同指令一起传递给 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"
```
## Prompt 缓存(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` 标记:
```python
# 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`
### 启用 Prompt 缓存
满足以下条件时,prompt 缓存自动启用:
- 模型为 Anthropic Claude 模型(通过模型名称检测)
- 提供商支持 `cache_control`(原生 Anthropic API 或 OpenRouter
```yaml
# config.yaml — TTL is configurable (must be "5m" or "1h")
prompt_caching:
cache_ttl: "5m"
```
CLI 在启动时显示缓存状态:
```
💾 Prompt caching: ENABLED (Claude via OpenRouter, 5m TTL)
```
## 上下文压力警告
中间上下文压力警告已被移除(参见 `run_agent.py` 中的迭代预算块,其中注明:"No intermediate pressure warnings — they caused models to 'give up' prematurely on complex tasks")。压缩在 prompt token 达到配置的 `compression.threshold`(默认 50%)时触发,无需事先警告步骤;gateway 会话清理作为二级安全网在模型上下文窗口的 85% 处触发。