ByteNoteByteNote

字节笔记本

2026年6月21日

hermes教程-上下文压缩与缓存

API中转
¥120

可插拔的上下文引擎

上下文管理基于 ContextEngine 抽象基类(agent/context_engine.py)构建。内置的 ContextCompressor 是默认实现,但插件可以替换为其他引擎(例如无损上下文管理)。

yaml
context:
  engine: "compressor"    # 默认 — 内置的有损摘要
  engine: "lcm"           # 示例 — 提供无损上下文的插件

引擎负责:

  • 决定何时触发压缩(should_compress()
  • 执行压缩(compress()
  • 可选地暴露代理可调用的工具(例如 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

有关构建上下文引擎插件,请参阅上下文引擎插件

双重压缩系统

Hermes 有两个独立的压缩层,它们独立运行:

text
                     ┌──────────────────────────┐
  传入消息           │   网关会话卫生            │ 在上下文的 85% 触发
  ─────────────────► │   (代理前,粗略估计)      │ 大型会话的安全网
                     └─────────────┬────────────┘
                                   │
                                   ▼
                     ┌──────────────────────────┐
                     │   代理 ContextCompressor │ 在上下文的 50% 触发(默认)
                     │   (循环内,真实 token)    │ 正常的上下文管理
                     └──────────────────────────┘

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.yamlcompression 键下读取:

yaml
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  # gpt-5.5 在 Codex OAuth 上:将触发阈值提升至 85%(默认:true)
## 摘要模型/提供者在 auxiliary 下配置:
auxiliary:
  compression:
    model: null              # 覆盖摘要模型(默认:自动检测)
    provider: auto           # 提供者:"auto", "openrouter", "nous", "main" 等
    base_url: null           # 自定义 OpenAI 兼容端点

参数详情

参数默认值范围描述
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(硬编码)始终保留的系统提示 + 首次交换
codex_gpt55_autoraisetruebool将 ChatGPT Codex OAuth 路由上的 gpt-5.5 触发阈值提升至 85%(见下文)。设置为 false 以保持全局 threshold

Codex gpt-5.5 阈值自动提升

ChatGPT Codex OAuth 后端将 gpt-5.5 的上下文窗口硬限制为 272K(同一 slug 在 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。要回退到全局值:

bash
hermes config set compression.codex_gpt55_autoraise false

计算值(以 200K 上下文模型和默认值为例)

text
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主代理模型的上下文窗口——绝不是辅助/摘要模型的。在 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:确定边界

text
┌─────────────────────────────────────────────────────────────┐
│  消息列表                                                   │
│                                                             │
│  [0..2]  ← protect_first_n(系统 + 首次交换)                │
│  [3..N]  ← 中间轮次 → 被摘要                                │
│  [N..end] ← 尾部(按 token 预算或 protect_last_n)           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

尾部保护基于 token 预算:从末尾向前遍历,累积 token 直到预算耗尽。如果预算保护的消息少于固定数量,则回退到 protect_last_n 计数。

边界对齐以避免拆分 tool_call/tool_result 组。_align_boundary_backward() 方法向后遍历连续的 tool 结果,找到父 assistant 消息,保持组完整。

阶段 3:生成结构化摘要

警告 — 摘要模型上下文长度

摘要模型的上下文窗口必须至少与主代理模型一样大。整个中间部分通过一次 call_llm(task="compression") 调用发送给摘要模型。如果摘要模型的上下文较小,API 将返回上下文长度错误——_generate_summary() 捕获它,记录警告,并返回 None。然后压缩器在没有摘要的情况下丢弃中间轮次,静默丢失对话上下文。这是导致压缩质量下降的最常见原因。

中间轮次使用辅助 LLM 通过结构化模板进行摘要:

text
## Goal
[用户试图完成的目标]

## Constraints & Preferences
[用户偏好、编码风格、约束、重要决策]

## Progress
### Done
[已完成的工作 — 具体文件路径、运行的命令、结果]
### In Progress
[正在进行的工作]
### Blocked
[遇到的任何阻塞或问题]

## Key Decisions
[重要的技术决策及其原因]

## Relevant Files
[读取、修改或创建的文件 — 每个文件附带简要说明]

## Next Steps
[接下来需要做什么]

## Critical Context
[特定值、错误消息、配置细节]

摘要预算随被压缩内容的量而缩放:

  • 公式: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() 清理:

  • 引用已移除调用的 tool 结果 → 移除
  • 结果已被移除的 tool 调用 → 注入存根结果

迭代重新压缩

在后续压缩中,之前的摘要会传递给 LLM,并指示更新它,而不是从头开始摘要。这可以在多次压缩中保留信息——项目从“进行中”移动到“已完成”,添加新进度,并移除过时信息。

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

压缩前后示例

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

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

text
[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”策略:

text
断点 1:系统提示           (所有轮次稳定)
断点 2:倒数第 3 条非系统消息  ─┐
断点 3:倒数第 2 条非系统消息   ├─ 滚动窗口
断点 4:最后一条非系统消息      ─┘

工作原理

apply_anthropic_cache_control() 深度复制消息并注入 cache_control 标记:

python
## 缓存标记格式
marker = {"type": "ephemeral"}
## 或 1 小时 TTL:
marker = {"type": "ephemeral", "ttl": "1h"}

标记根据内容类型不同而应用:

内容类型标记位置
字符串内容转换为 [{"type": "text", "text": ..., "cache_control": ...}]
列表内容添加到最后一个元素的字典中
无/空添加为 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)
yaml
## 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% 处触发。


分享: