这篇文档讲 Hermes 主 Agent 如何处理长对话、工具输出膨胀、模型上下文窗口不足,以及 token 使用统计。

先给结论:

Hermes 的上下文压缩不是模型主动调用某个 memory/skill tool。

默认实现是 agent 层的自动机制:
1. 运行前估算当前请求 token。
2. 运行后读取 API 返回的真实 prompt_tokens。
3. 当接近模型上下文阈值,agent 自动调用 ContextCompressor。
4. ContextCompressor 保留系统提示词、早期关键上下文、最新上下文,把中间历史压缩成 handoff summary。
5. 压缩后结束旧 session,创建一个 compression continuation session,继续对话。

所以它和 memory 系统不同。memory 是模型根据提示词和工具描述主动决定是否持久化事实;上下文压缩主要是 runtime 根据 token 压力触发,模型只会在下一轮看到压缩后的摘要。

场景一:长对话继续写代码,Agent 还没调用模型就先压缩

假设用户已经和 Hermes 讨论了很久:

用户:继续,把刚才 auth 模块的重构收尾,先跑测试。

此时历史里可能已经有:

几十轮代码讨论
多次 read_file / search_files 输出
terminal 测试日志
模型解释和用户修正

如果直接把完整历史、系统提示词、工具 schema 全部发给模型,可能超过上下文窗口。Hermes 会在进入主 tool loop 前做 preflight compression。

1.1 触发点不是模型判断,而是 agent 层估算

实现入口在 run_agent.py 的 preflight 段落。逻辑是:

如果 compression.enabled=true,
并且 messages 数量超过保护头部 + 保护尾部的最低要求,
就估算当前请求 token。

估算内容包括:
1. messages
2. system_prompt
3. tools schema

如果估算值 >= context_compressor.threshold_tokens,
就调用 _compress_context()。

这里有一个容易误解的关键点:Hermes 代码里的 messages 不总是指“最终发给模型的 API messages”。

run_agent.py 里,概念大致分成三层:

messages
  Hermes 内部的会话历史。
  主要包含 user / assistant / tool 消息。
  会写入 session DB,也会被上下文压缩处理。

active_system_prompt / self._cached_system_prompt
  Hermes 单独缓存的完整 system prompt。
  最终发 API 时会作为第一个 system message 放进 api_messages。
  但它不长期混在 messages 历史里。

api_messages
  每次真正调用模型前临时构造出来的请求消息。
  结构近似是:[system message] + messages + 本轮临时注入内容。

最终调用模型前,Hermes 会做类似这样的事情:

effective_system = active_system_prompt or ""
if effective_system:
    api_messages = [{"role": "system", "content": effective_system}] + api_messages

所以,从 API 视角看,system prompt 仍然是第一个 message;只是从 Hermes 内部持久化和压缩视角看,system prompt 被单独缓存,不直接放进长期 messages 历史。

这样设计的含义是:

1. 会话历史可以单独压缩,不会误伤系统规则。
2. system prompt 可以在一个 session 内保持稳定,利于 provider prefix cache。
3. memory、skills、context files 等系统层内容可以作为 system prompt 快照保存,而不是每轮重复追加到 transcript。
4. 本轮临时召回的外部上下文可以只注入 api_messages,不污染持久化历史。

另一个独立概念是 tools schema。Hermes 默认使用 provider 的原生 tool calling,所以工具定义通常不塞进 system prompt,也不是第二个 message,而是 API 请求里的独立 tools 参数:

return _ct.build_kwargs(
    model=self.model,
    messages=api_messages,
    tools=self.tools,
    ...
)

因此,Hermes 估算 preflight token 时要估算的是完整 API 请求压力:

system prompt
+ conversation messages
+ tools schema

如果只按内部 messages 历史估算,就会漏掉 system prompt 和 tools schema;如果只按最终 api_messages 估算,又要注意 tools 仍然是单独字段,不在 message content 里。工具很多时,tools schema 可能单独占 20K-30K tokens,这就是文档说“只看 messages 会低估”的准确含义。

相关实现说明 / Preflight Compression

英文原文:

Preflight context compression
Before entering the main loop, check if the loaded conversation history already exceeds the model's context threshold. This handles cases where a user switches to a model with a smaller context window while having a large existing session — compress proactively rather than waiting for an API error.

中文对照:

预检上下文压缩。
在进入主循环之前,检查加载出来的会话历史是否已经超过模型上下文阈值。
这用于处理一种情况:用户在已有大历史会话中切换到更小上下文窗口的模型。
系统会主动压缩,而不是等 API 报错后再处理。

这段话的作用是:说明 Hermes 的压缩不是完全被动等报错,而是在调用模型前就尝试把请求控制在安全范围内。

1.2 压缩阈值如何来

Hermes 启动 AIAgent 时初始化 ContextCompressor。默认配置来自 config.yamlcompression 区域:

compression.enabled: 默认 true
compression.threshold: 默认 0.50
compression.target_ratio: 默认 0.20
compression.protect_last_n: 默认 20
model.context_length: 可显式覆盖模型上下文长度
auxiliary.compression.context_length: 可显式覆盖摘要模型上下文长度

默认含义:

如果模型上下文窗口是 200,000 tokens,
threshold=0.50,
那么大约到 100,000 tokens 时就会触发压缩。

为什么不是快满了才压缩?因为下一轮还会加入:

当前用户输入
工具 schema
模型输出
tool call 结果
可能的多轮修正

50% 阈值是为了给工具调用和后续输出留出余量。

场景二:模型调用很多工具,成功后根据真实 prompt_tokens 决定压缩

假设模型执行了一轮工具调用:

模型:调用 search_files 搜索 auth。
工具:返回很多匹配。
模型:调用 read_file 读取多个文件。
工具:返回大量代码。
模型:调用 terminal 跑测试。
工具:返回长日志。

这一轮结束后,Hermes 会从模型 API response 的 usage 里读取 token 统计。

2.1 模型返回的 response.usage 是什么

这里的 usage 不是 assistant 输出的一部分,也不是下一轮会被注入模型的消息内容。它是模型服务端随 API response 返回的元数据,用来告诉调用方:

这次请求读入了多少 token
这次请求生成了多少 token
其中有多少命中了 prompt cache
其中有多少写入了 prompt cache
是否有单独统计的 reasoning token

Hermes 在 run_agent.py 里拿到 response.usage 后,会做三件事:

1. 调用 normalize_usage(),把不同 provider 的字段统一成 CanonicalUsage。
2. 把 prompt_tokens 写入 ContextCompressor,作为后续压缩判断依据。
3. 把 input/output/cache/reasoning/total 等统计累加到当前 session,并持久化到 session DB。

所以 usage 的意义分成两层:

provider 原始 usage
  由 OpenAI / Anthropic / Codex Responses / OpenAI-compatible provider 返回。
  字段名和是否拆分 cache token 取决于 provider。

Hermes CanonicalUsage
  Hermes 自己归一化后的内部口径。
  后续压缩判断、/usage 展示、/insights、成本估算都尽量使用这个统一口径。

相关实现说明 / Usage Normalization

英文原文:

Normalize raw API response usage into canonical token buckets.

Handles three API shapes:
- Anthropic: input_tokens/output_tokens/cache_read_input_tokens/cache_creation_input_tokens
- Codex Responses: input_tokens includes cache tokens; input_tokens_details.cached_tokens separates them
- OpenAI Chat Completions: prompt_tokens includes cache tokens; prompt_tokens_details.cached_tokens separates them

In both Codex and OpenAI modes, input_tokens is derived by subtracting cache
tokens from the total — the API contract is that input/prompt totals include
cached tokens and the details object breaks them out.

中文对照:

把原始 API response usage 归一化成标准 token 桶。

处理三类 API 形态:
- Anthropic:input_tokens / output_tokens / cache_read_input_tokens / cache_creation_input_tokens
- Codex Responses:input_tokens 包含 cache tokens;input_tokens_details.cached_tokens 会把它们拆出来
- OpenAI Chat Completions:prompt_tokens 包含 cache tokens;prompt_tokens_details.cached_tokens 会把它们拆出来

在 Codex 和 OpenAI 模式里,input_tokens 会通过从总数中减去 cache tokens 得到。
这是因为这些 API 的 input/prompt 总数包含缓存 token,而 details 对象负责拆分细项。

这段话的作用是:说明 Hermes 不直接相信某一个 provider 的字段名,而是先把原始 usage 变成自己的标准结构。

2.2 Hermes 统一后的 token 字段有哪些

不同 provider 返回的 usage 字段名不完全一样。Hermes 会先用 normalize_usage() 把它们统一成 CanonicalUsage

CanonicalUsage(
    input_tokens,
    output_tokens,
    cache_read_tokens,
    cache_write_tokens,
    reasoning_tokens,
)

然后 Hermes 再派生两个常用值:

prompt_tokens = input_tokens + cache_read_tokens + cache_write_tokens
total_tokens = prompt_tokens + output_tokens

各字段含义如下:

字段 含义 和上下文窗口的关系 和计费的关系
input_tokens 本次请求中新输入的 prompt tokens,不含缓存命中/写入部分 占本次输入窗口 通常按输入价计费
cache_read_tokens 本次 prompt 中命中 provider prompt cache 的 tokens 仍占本次输入窗口 通常低价或免费,取决于 provider
cache_write_tokens 本次写入 provider prompt cache 的 tokens 仍占本次输入窗口 通常有单独写入价格
prompt_tokens Hermes 统一后的本次完整输入 tokens 压缩判断主要看它 输入总消耗口径
output_tokens 模型这次生成出来的 tokens 不作为下一轮 prompt 压缩触发依据 通常按输出价计费
reasoning_tokens thinking/reasoning 消耗,有些 provider 单独报告 不作为压缩触发依据 可能包含在输出计费或单独计费
total_tokens prompt_tokens + output_tokens 不是单纯上下文占用,更多用于总消耗统计 总消耗口径

再换成更接近 API response 的说法:

原始 usage 常见字段 Hermes 归一化后字段 说明
input_tokens input_tokensprompt_tokens 的一部分 Anthropic 里通常就是非缓存输入;Codex Responses 里可能包含 cache,需要再拆
output_tokens output_tokens 模型实际生成的输出 token
prompt_tokens prompt_tokens 的原始总数,再拆成 input/cache OpenAI Chat Completions 常见字段,通常已经包含 cache token
completion_tokens output_tokens OpenAI Chat Completions 的输出 token
cache_read_input_tokens cache_read_tokens Anthropic 风格的缓存命中 token
cache_creation_input_tokens cache_write_tokens Anthropic 风格的缓存写入 token
input_tokens_details.cached_tokens cache_read_tokens Codex Responses 风格的缓存命中细项
input_tokens_details.cache_creation_tokens cache_write_tokens Codex Responses 风格的缓存写入细项
prompt_tokens_details.cached_tokens cache_read_tokens OpenAI-compatible 风格的缓存命中细项
prompt_tokens_details.cache_write_tokens cache_write_tokens OpenAI-compatible 风格的缓存写入细项
output_tokens_details.reasoning_tokens reasoning_tokens reasoning/thinking token,只有部分 provider 返回

举一个普通对话例子:

system prompt: 10K
历史 messages: 50K
tools schema: 20K
当前用户消息: 1K
模型输出: 3K

即使用户这轮只说了“继续”,本次请求也不是只消耗“继续”的 token。API 请求需要带上当前可见上下文:

prompt_tokens ≈ 10K + 50K + 20K + 1K = 81K
output_tokens ≈ 3K
total_tokens ≈ 84K

如果 provider 开了 prompt cache,可能返回:

input_tokens = 21K
cache_read_tokens = 60K
cache_write_tokens = 0
output_tokens = 3K

Hermes 会统一成:

prompt_tokens = 21K + 60K + 0 = 81K
total_tokens = 81K + 3K = 84K

这里最容易误解的是 cache_read_tokens。它可能便宜,但它仍然是本次 prompt 的一部分,仍然参与上下文窗口压力判断。

这些 token 值会累加到当前 session,也会写入 SQLite 的 sessions 表,供 /usage/insights、退出摘要等功能使用。

2.3 Hermes 如何兼容不同 provider 的 usage 字段

Hermes 兼容三类主要 API 形态:

Anthropic:
  input_tokens
  output_tokens
  cache_read_input_tokens
  cache_creation_input_tokens

Codex Responses:
  input_tokens
  output_tokens
  input_tokens_details.cached_tokens
  input_tokens_details.cache_creation_tokens

OpenAI Chat Completions / OpenAI-compatible:
  prompt_tokens
  completion_tokens
  prompt_tokens_details.cached_tokens
  prompt_tokens_details.cache_write_tokens

统一规则是:

Anthropic:
  input_tokens 直接使用 input_tokens
  output_tokens 直接使用 output_tokens
  cache_read/write 从 cache_read_input_tokens / cache_creation_input_tokens 读取

Codex Responses:
  input_tokens 原始值通常包含 cache tokens
  所以 Hermes 会减去 cached_tokens 和 cache_creation_tokens,得到非缓存 input_tokens

OpenAI Chat Completions:
  prompt_tokens 原始值通常包含 cache tokens
  所以 Hermes 会减去 cached_tokens 和 cache_write_tokens,得到非缓存 input_tokens

这样 Hermes 内部永远可以用同一组字段理解不同 provider 的 usage。

相关实现说明 / Token Count Persistence

英文原文:

Persist token counts to session DB for /insights.
Do this for every platform with a session_id so non-CLI sessions (gateway, cron, delegated runs) cannot lose token/accounting data if a higher-level persistence path is skipped or fails.

中文对照:

为了 /insights,把 token 计数持久化到 session DB。
只要平台有 session_id,就执行这件事,这样非 CLI 会话(gateway、cron、delegated runs)即使更高层持久化路径被跳过或失败,也不会丢失 token/计费数据。

这段话的作用是:token 管理不只是“压缩触发条件”,也是观测和成本统计的一部分。

2.4 为什么压缩触发看 prompt_tokens,而不是 total_tokens

Hermes 在一轮工具调用结束后会判断是否需要压缩。这里它优先使用真实的 prompt_tokens

如果 last_prompt_tokens > 0:
  使用 prompt_tokens
否则:
  用 rough estimate 估算 messages + tools

它故意不把 completion/reasoning tokens 当成上下文占用的主判断依据。

原因是:

context window 压力主要来自下一次请求要携带的输入历史。
completion_tokens / reasoning_tokens 是这一次模型输出消耗,
并不等价于下一次 prompt 必然要携带的上下文长度。

这对 thinking models 很重要。某些模型会输出大量 reasoning tokens,如果把 reasoning 也算进压缩触发,可能过早压缩。

相关实现说明 / Prompt Tokens Only

英文原文:

Only use prompt_tokens — completion/reasoning tokens don't consume context window space.
Thinking models (GLM-5.1, QwQ, DeepSeek R1) inflate completion_tokens with reasoning, causing premature compression.

中文对照:

只使用 prompt_tokens,因为 completion/reasoning tokens 不消耗下一次请求的上下文窗口空间。
Thinking 模型(GLM-5.1、QwQ、DeepSeek R1)会用 reasoning 让 completion_tokens 膨胀,导致过早压缩。

这段话的作用是:避免把“本次生成很长”误判成“下次输入上下文太长”。

场景三:API 报 context overflow,Agent 压缩后重试

再看一个更被动的场景:

用户:继续。

Hermes 发请求给模型。
模型服务返回错误:
context_length_exceeded
或者 413 Request payload too large
或者某些 provider 返回泛化的 400,但错误内容显示 prompt 太长。

这时 Hermes 不会直接失败,而是进入错误分类和恢复流程。

3.1 先判断是输入太长,还是输出上限太大

Hermes 会区分两类错误:

1. Prompt too long
   输入历史超过上下文窗口。
   解决:压缩历史。

2. max_tokens too large
   输入本身没超,但请求的输出上限太大,导致 input + max_tokens 超过窗口。
   解决:降低本次 max_tokens,不压缩历史。

这个区分很重要。否则一个简单的输出上限配置错误,会错误触发压缩,导致历史丢失细节。

相关实现说明 / Two Different Errors

英文原文:

Distinguish two very different errors:
1. "Prompt too long": the INPUT exceeds the context window.
   Fix: reduce context_length + compress history.
2. "max_tokens too large": input is fine, but input_tokens + requested max_tokens > context_window.
   Fix: reduce max_tokens (the OUTPUT cap) for this call.
   Do NOT shrink context_length — the window hasn't shrunk.

中文对照:

区分两种完全不同的错误:
1. “Prompt too long”:输入超过上下文窗口。
   修复方式:降低 context_length,并压缩历史。
2. “max_tokens too large”:输入没问题,但 input_tokens + 请求的 max_tokens 超过上下文窗口。
   修复方式:降低本次 max_tokens,也就是输出上限。
   不要缩小 context_length,因为模型窗口并没有变小。

这段话的作用是:把 token 管理拆成“输入上下文管理”和“输出预算管理”,避免用压缩解决所有问题。

3.2 如果 provider 暴露了真实 context limit,Hermes 会更新模型窗口

有些 provider 的错误信息会带真实限制,例如:

This model's maximum context length is 131072 tokens.

Hermes 会解析这个数值。如果它比当前记录的 context_length 小,就把 compressor 的 context_length 更新到新的限制,然后压缩并重试。

如果这个限制是 provider 明确返回的,Hermes 还可能缓存起来;如果只是自己猜的 probe tier,则只保留在内存里,不写入长期缓存。

3.3 最多压缩重试几次

错误恢复里会限制 compression attempts,避免无限循环:

max_compression_attempts = 3

如果压缩后仍然过大,用户会看到类似建议:

Try /new to start a fresh conversation, or /compress to retry compression.

这说明 Hermes 的压缩是降级手段,不保证无限续命。长时间高强度会话多次压缩后,摘要质量会下降。

场景四:用户手动 /compress API endpoints

自动压缩之外,用户可以手动触发:

用户:/compress API endpoints

这里的 API endpoints 是 focus topic。意思是:

压缩历史时,请优先保留和 API endpoints 相关的信息;
其他内容可以更激进地摘要或丢弃。

4.1 CLI/Gateway 命令不是发给模型闲聊

/compress 是命令层处理,不是普通用户消息。Gateway 实现会:

1. 找到当前 session。
2. 读取 transcript。
3. 创建一个临时 AIAgent。
4. 估算当前 token。
5. 调用 tmp_agent._compress_context(..., focus_topic=focus_topic)。
6. 把压缩后的 transcript 写入新的 continuation session。
7. 返回压缩结果说明。

相关命令说明 / Manual Compress Command

英文原文:

Handle /compress command -- manually compress conversation context.

Accepts an optional focus topic: `/compress <focus>` guides the summariser to preserve information related to *focus* while being more aggressive about discarding everything else.

中文对照:

处理 /compress 命令:手动压缩对话上下文。

接受可选的 focus topic:`/compress <focus>` 会引导摘要器保留和 focus 相关的信息,同时更激进地丢弃其他内容。

这段话的作用是:手动压缩给用户一个主动控制摘要重点的入口。

4.2 focus topic 如何进入摘要 prompt

当用户输入:

/compress database schema

Hermes 会把 database schema 拼进摘要模型的 prompt:

相关提示词 / Focus Topic Guidance

英文原文:

FOCUS TOPIC: "{focus_topic}"
The user has requested that this compaction PRIORITISE preserving all information related to the focus topic above. For content related to "{focus_topic}", include full detail — exact values, file paths, command outputs, error messages, and decisions. For content NOT related to the focus topic, summarise more aggressively (brief one-liners or omit if truly irrelevant). The focus topic sections should receive roughly 60-70% of the summary token budget. Even for the focus topic, NEVER preserve API keys, tokens, passwords, or credentials — use [REDACTED].

中文对照:

FOCUS TOPIC: "{focus_topic}"
用户要求本次压缩优先保留和上述 focus topic 相关的所有信息。
对于和 "{focus_topic}" 相关的内容,要包含完整细节:精确值、文件路径、命令输出、错误信息和决策。
对于不相关内容,要更激进地摘要,可以用简短一句话,真正无关则省略。
focus topic 相关部分应该占据大约 60-70% 的摘要 token 预算。
即使是 focus topic,也绝不要保留 API key、token、密码或凭据,要用 [REDACTED]。

这段话的作用是:让手动压缩不是“平均压缩”,而是按用户指定主题做带权摘要。

场景五:大工具输出不会直接塞满上下文,而是先落盘

上下文爆炸最常见来源不是用户聊天,而是工具输出:

用户:帮我搜索所有引用。
模型调用 search_files。
工具返回 500 个匹配和大量上下文。

如果完整输出直接作为 tool message 放进对话,下一轮模型请求很容易爆炸。

Hermes 在工具结果返回后有三层防线。

5.1 第一层:工具自己限制输出

工具作者可以在工具内部做截断或分页。例如搜索工具、网页提取工具、文件读取工具通常会提供 limit、offset、preview 之类能力。

这是最早、最便宜的防线。

5.2 第二层:单个结果超过阈值就持久化到 sandbox 文件

如果一个 tool result 太大,Hermes 不会直接把完整结果塞进上下文,而是写到 sandbox:

/tmp/hermes-results/{tool_use_id}.txt

然后在上下文里放一个 <persisted-output> 块:

<persisted-output>
This tool result was too large (xxx characters, xxx KB).
Full output saved to: /tmp/hermes-results/xxx.txt
Use the read_file tool with offset and limit to access specific sections of this output.

Preview:
...
</persisted-output>

模型如果需要完整结果,可以再调用 read_file 读取特定片段。

相关提示词 / Tool Result Persistence Message

英文原文:

This tool result was too large ({original_size:,} characters, {size_str}).
Full output saved to: {file_path}
Use the read_file tool with offset and limit to access specific sections of this output.

Preview (first {len(preview)} chars):
{preview}

中文对照:

这个工具结果太大({original_size:,} 个字符,{size_str})。
完整输出已保存到:{file_path}
请使用 read_file 工具,并通过 offset 和 limit 访问这个输出的特定片段。

预览(前 {len(preview)} 个字符):
{preview}

这段话的作用是:模型不是丢失了工具结果,而是拿到一个“索引 + 预览 + 读取方法”。

5.3 第三层:单轮多个中等工具结果合计超预算,也会落盘

有些场景不是单个工具结果很大,而是一轮里多个结果加起来很大:

read_file A:50K 字符
read_file B:60K 字符
terminal output:80K 字符
web_extract:70K 字符

单个可能没超过阈值,但合计超过 turn budget。Hermes 会在一轮工具调用结束后执行 aggregate budget enforcement,把最大的未持久化结果优先落盘,直到整轮结果低于预算。

默认预算来自 tools/budget_config.py

default_result_size: 100,000 chars
turn_budget: 200,000 chars
preview_size: 1,500 chars

read_file 有特殊处理:

read_file: infinity

代码注释说明这是为了避免 persist -> read -> persist 的循环。也就是说,读文件本身不按单结果阈值强制落盘,但仍可能在其他机制中被摘要或压缩。

相关实现说明 / Three-Layer Tool Result Defense

英文原文:

Defense against context-window overflow operates at three levels:

1. Per-tool output cap: Tools like search_files pre-truncate their own output before returning.
2. Per-result persistence: After a tool returns, if its output exceeds the tool's registered threshold, the full output is written into the sandbox temp dir. The in-context content is replaced with a preview + file path reference.
3. Per-turn aggregate budget: After all tool results in a single assistant turn are collected, if the total exceeds MAX_TURN_BUDGET_CHARS (200K), the largest non-persisted results are spilled to disk until the aggregate is under budget.

中文对照:

防止上下文窗口溢出的机制分三层:

1. 工具自身输出上限:例如 search_files 在返回前先截断自己的输出。
2. 单结果持久化:工具返回后,如果输出超过该工具注册的阈值,完整输出会写入 sandbox 临时目录;上下文中的内容替换为预览 + 文件路径引用。
3. 单轮总预算:一个 assistant turn 中所有 tool results 收集完后,如果总量超过 MAX_TURN_BUDGET_CHARS(200K),就把最大的未持久化结果写入磁盘,直到总量低于预算。

这段话的作用是:把“工具输出管理”和“会话历史压缩”区分开。前者发生在工具结果进入上下文前后,后者发生在整个会话历史过大时。

场景六:真正压缩时,哪些消息被保留,哪些被摘要

假设当前消息序列是:

0 system: 主系统提示词
1 user: 初始需求
2 assistant: 初始方案
3 user: 约束 A
4 assistant: 调用 read_file
5 tool: 很长文件内容
6 assistant: 修改文件
7 tool: patch 结果
...
60 user: 继续修复测试失败
61 assistant: 调用 terminal
62 tool: 最新测试输出

压缩器不会把所有旧消息直接删除。默认策略是:

1. 保护头部 protect_first_n,默认 3 条。
   这里保护的是内部 messages 的开头,例如最早的用户需求和早期 assistant 回复。
   system prompt 不在这组 messages 里,它由 self._cached_system_prompt 单独缓存,最终发 API 时再作为第一个 system message 注入。

2. 保护尾部。
   不是只看最后 N 条,而是按 token budget 从后往前累积。
   同时至少保留少量消息。

3. 中间区域进入摘要。

4. 摘要插入到头部和尾部之间。

5. 清理 tool_call / tool_result 的孤儿配对,保证发给 API 的消息合法。

6.1 先做便宜裁剪:旧工具结果摘要成一行

在调用摘要模型前,Hermes 会先裁剪老旧 tool results。

比如原始 tool result 是:

pytest 输出 3000 行

会被替换成类似:

[terminal] ran `pytest tests/` -> exit 1, 3000 lines output

这一步不调用 LLM,成本低,能先砍掉大量重复日志。

它还会做:

重复 tool result 去重
旧 assistant tool_call arguments 截断

特别是 write_file 或 patch 参数可能很大,如果不截断,即使 tool result 被裁剪,assistant message 里的 arguments 仍会撑爆上下文。

6.2 尾部保护按 token,不按固定条数

旧实现如果只保留最后 N 条消息,会有问题:

最后 20 条都很短 -> 浪费可用上下文,丢太多历史。
最后 20 条都很长 -> 仍然超上下文。

所以 Hermes 现在用 token budget 从尾部往前累积。默认 tail budget 来自:

threshold_tokens * summary_target_ratio

例如:

context_length = 200,000
threshold = 50% = 100,000
target_ratio = 20%
tail_token_budget = 20,000

这样最新约 20K tokens 的上下文被保护,不进入摘要。

6.3 最新用户消息必须留在尾部

Hermes 特别保护最后一个 user message。原因是摘要前缀会告诉模型:

Respond ONLY to the latest user message that appears AFTER this summary.

如果最后一个用户请求被压进 summary 里,模型可能认为它只是历史摘要,不是当前任务,从而停住或重复旧工作。

所以压缩器会确保最近的 user message 一定在 summary 之后。

场景七:摘要模型看到的完整核心提示词

压缩中间历史时,Hermes 会调用辅助 LLM。这个 LLM 的任务不是继续对话,而是生成给“下一个 assistant”看的交接摘要。

7.1 摘要前置指令

相关提示词 / Summarizer Preamble

英文原文:

You are a summarization agent creating a context checkpoint. Your output will be injected as reference material for a DIFFERENT assistant that continues the conversation. Do NOT respond to any questions or requests in the conversation — only output the structured summary. Do NOT include any preamble, greeting, or prefix. Write the summary in the same language the user was using in the conversation — do not translate or switch to English. NEVER include API keys, tokens, passwords, secrets, credentials, or connection strings in the summary — replace any that appear with [REDACTED]. Note that the user had credentials present, but do not preserve their values.

中文对照:

你是一个摘要代理,正在创建上下文检查点。
你的输出会作为参考材料注入给另一个继续对话的 assistant。
不要回答对话里的任何问题或请求,只输出结构化摘要。
不要包含任何开场白、问候语或前缀。
使用用户在对话中使用的同一种语言写摘要,不要翻译或切换到英文。
绝不要在摘要中包含 API key、token、密码、secret、凭据或连接字符串;如果出现这些内容,用 [REDACTED] 替换。
可以说明用户曾经提供过凭据,但不要保留具体值。

这段话的作用是:把摘要模型和执行模型隔离开,避免摘要模型误把历史问题当作当前问题来回答,也避免凭据泄露进长期摘要。

7.2 摘要结构模板:源码变量 _template_sections

这一节展示的是代码里的 _template_sections 变量。它不是一个运行时工具,也不是模型输出字段,而是 ContextCompressor._generate_summary() 里拼接 prompt 时使用的“摘要格式模板”。

在代码里,它会被插入到两类 prompt 中:

第一次压缩:
  Use this exact structure:
  {_template_sections}

再次压缩:
  Update the summary using this exact structure.
  {_template_sections}

也就是说,_template_sections 决定了摘要模型必须按哪些标题组织 handoff summary。

这里要特别标注清楚:

_template_sections
  是 Hermes 源码里的 Python 局部变量名。
  不是 prompt 里要求模型输出的字段名。
  不是 API response usage 里的字段。
  不是用户能调用的工具。

{_template_sections}
  是 f-string 模板里的插入点。
  实际调用摘要模型时,不会把字符串 "{_template_sections}" 原样发给模型。
  它会被替换成下面这整段 Summary Template。

文档里保留 {summary_budget}{content_to_summarize}{_template_sections} 这些占位符,是为了对应代码里的 f-string 模板;实际运行时它们会被替换为具体 token 预算、待摘要内容和模板正文。

相关提示词 / Summary Template

英文原文:

## Active Task
[THE SINGLE MOST IMPORTANT FIELD. Copy the user's most recent request or
task assignment verbatim — the exact words they used. If multiple tasks
were requested and only some are done, list only the ones NOT yet completed.
The next assistant must pick up exactly here. Example:
"User asked: 'Now refactor the auth module to use JWT instead of sessions'"
If no outstanding task exists, write "None."]

## Goal
[What the user is trying to accomplish overall]

## Constraints & Preferences
[User preferences, coding style, constraints, important decisions]

## Completed Actions
[Numbered list of concrete actions taken — include tool used, target, and outcome.
Format each as: N. ACTION target — outcome [tool: name]
Example:
1. READ config.py:45 — found `==` should be `!=` [tool: read_file]
2. PATCH config.py:45 — changed `==` to `!=` [tool: patch]
3. TEST `pytest tests/` — 3/50 failed: test_parse, test_validate, test_edge [tool: terminal]
Be specific with file paths, commands, line numbers, and results.]

## Active State
[Current working state — include:
- Working directory and branch (if applicable)
- Modified/created files with brief note on each
- Test status (X/Y passing)
- Any running processes or servers
- Environment details that matter]

## In Progress
[Work currently underway — what was being done when compaction fired]

## Blocked
[Any blockers, errors, or issues not yet resolved. Include exact error messages.]

## Key Decisions
[Important technical decisions and WHY they were made]

## Resolved Questions
[Questions the user asked that were ALREADY answered — include the answer so the next assistant does not re-answer them]

## Pending User Asks
[Questions or requests from the user that have NOT yet been answered or fulfilled. If none, write "None."]

## Relevant Files
[Files read, modified, or created — with brief note on each]

## Remaining Work
[What remains to be done — framed as context, not instructions]

## Critical Context
[Any specific values, error messages, configuration details, or data that would be lost without explicit preservation. NEVER include API keys, tokens, passwords, or credentials — write [REDACTED] instead.]

Target ~{summary_budget} tokens. Be CONCRETE — include file paths, command outputs, error messages, line numbers, and specific values. Avoid vague descriptions like "made some changes" — say exactly what changed.

Write only the summary body. Do not include any preamble or prefix.

中文对照:

## Active Task
[这是最重要的字段。逐字复制用户最近的请求或任务分配,也就是用户使用的原话。
如果用户请求了多个任务,并且只有一部分完成了,只列出尚未完成的任务。
下一个 assistant 必须从这里准确接手。例如:
"User asked: 'Now refactor the auth module to use JWT instead of sessions'"
如果没有未完成任务,写 "None"。]

## Goal
[用户整体想完成什么]

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

## Completed Actions
[具体已完成动作的编号列表,包括使用的工具、目标和结果。
格式:N. ACTION target — outcome [tool: name]
示例:
1. READ config.py:45 — found `==` should be `!=` [tool: read_file]
2. PATCH config.py:45 — changed `==` to `!=` [tool: patch]
3. TEST `pytest tests/` — 3/50 failed: test_parse, test_validate, test_edge [tool: terminal]
要具体写出文件路径、命令、行号和结果。]

## Active State
[当前工作状态,包括:
- 工作目录和分支(如果相关)
- 修改/创建的文件,以及每个文件的简要说明
- 测试状态(X/Y 通过)
- 正在运行的进程或服务
- 重要环境细节]

## In Progress
[当前正在进行的工作,也就是压缩触发时正在做什么]

## Blocked
[未解决的阻塞、错误或问题。包含准确错误信息。]

## Key Decisions
[重要技术决策以及为什么这样决策]

## Resolved Questions
[用户已经问过且已经回答的问题;包含答案,这样下一个 assistant 不会重复回答]

## Pending User Asks
[用户尚未被回答或完成的问题/请求。如果没有,写 "None"。]

## Relevant Files
[读过、修改过或创建过的文件,以及每个文件的简要说明]

## Remaining Work
[剩余工作,以上下文形式描述,不要写成直接指令]

## Critical Context
[如果不显式保留就会丢失的具体值、错误信息、配置细节或数据。
绝不要包含 API key、token、密码或凭据;写 [REDACTED]。]

目标约 {summary_budget} tokens。要具体,包含文件路径、命令输出、错误信息、行号和具体值。
避免 “做了一些修改” 这种模糊描述,要写清楚具体改了什么。

只写摘要正文。不要包含任何开场白或前缀。

这段模板决定了 Hermes 压缩摘要的质量重点:它不是普通聊天摘要,而是任务交接摘要。最重要的是 Active TaskActive StateCompleted ActionsRemaining Work

7.3 第一次压缩和重复压缩不同

第一次压缩时,prompt 是:

注意:下面展示的是源码里的 f-string prompt 模板。{content_to_summarize} 会被替换成被压缩的历史消息正文,{_template_sections} 会被替换成上一节完整的 Summary Template。

相关提示词 / First Compaction Prompt

英文原文:

Create a structured handoff summary for a different assistant that will continue this conversation after earlier turns are compacted. The next assistant should be able to understand what happened without re-reading the original turns.

TURNS TO SUMMARIZE:
{content_to_summarize}

Use this exact structure:

{_template_sections}

中文对照:

为另一个 assistant 创建结构化交接摘要。这个 assistant 会在早期轮次被压缩后继续对话。
下一个 assistant 应该能够在不重新读取原始轮次的情况下理解已经发生了什么。

需要摘要的轮次:
{content_to_summarize}

使用下面这个精确结构:

{_template_sections}

这段话的作用是:第一次压缩从原始中间消息生成摘要。

再次压缩时,Hermes 不会从零开始,而是把上一次 summary 和新产生的消息一起给摘要模型:

这里同样是源码模板。运行时 {self._previous_summary} 是上一次压缩得到的摘要,{content_to_summarize} 是新增的待合并轮次,{_template_sections} 仍然会展开成上一节的完整结构模板。

相关提示词 / Iterative Update Prompt

英文原文:

You are updating a context compaction summary. A previous compaction produced the summary below. New conversation turns have occurred since then and need to be incorporated.

PREVIOUS SUMMARY:
{self._previous_summary}

NEW TURNS TO INCORPORATE:
{content_to_summarize}

Update the summary using this exact structure. PRESERVE all existing information that is still relevant. ADD new completed actions to the numbered list (continue numbering). Move items from "In Progress" to "Completed Actions" when done. Move answered questions to "Resolved Questions". Update "Active State" to reflect current state. Remove information only if it is clearly obsolete. CRITICAL: Update "## Active Task" to reflect the user's most recent unfulfilled request — this is the most important field for task continuity.

{_template_sections}

中文对照:

你正在更新上下文压缩摘要。之前一次压缩已经生成了下面的摘要。此后又发生了新的对话轮次,需要合并进去。

之前的摘要:
{self._previous_summary}

需要合并的新轮次:
{content_to_summarize}

使用这个精确结构更新摘要。保留所有仍然相关的既有信息。
把新的已完成动作加入编号列表(继续编号)。
当事项完成后,把它们从 "In Progress" 移到 "Completed Actions"。
把已经回答的问题移到 "Resolved Questions"。
更新 "Active State" 以反映当前状态。
只有在信息明显过时时才移除。
关键:更新 "## Active Task",反映用户最近尚未完成的请求。这是任务连续性最重要的字段。

{_template_sections}

这段话的作用是:支持多次压缩,不让第二次压缩把第一次摘要中的关键信息冲掉。

场景八:压缩后的摘要如何注入给下一轮模型

压缩完成后,Hermes 会生成一个特殊前缀:

相关提示词 / Summary Prefix

英文原文:

[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Your current task is identified in the '## Active Task' section of the summary — resume exactly from there. IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system prompt is ALWAYS authoritative and active — never ignore or deprioritize memory content due to this compaction note. Respond ONLY to the latest user message that appears AFTER this summary. The current session state (files, config, etc.) may reflect work described here — avoid repeating it:

中文对照:

[上下文压缩,仅供参考] 早期轮次已经被压缩成下面的摘要。
这是来自之前上下文窗口的交接材料,请把它视为背景参考,而不是当前活动指令。
不要回答或执行摘要中提到的问题或请求;它们已经被处理过。
你当前的任务在摘要的 "## Active Task" 部分标明,请从那里准确继续。
重要:system prompt 中的持久记忆(MEMORY.md、USER.md)始终权威且有效,不要因为这个压缩说明而忽略或降低 memory 内容的优先级。
只响应这个摘要之后出现的最新用户消息。
当前 session 状态(文件、配置等)可能已经反映了摘要中描述的工作,避免重复执行:

这段话的作用是:防止模型把摘要里的旧用户请求当成新请求来执行,同时告诉模型 memory 仍然比压缩摘要更权威。

Hermes 还会给 system prompt 追加一段 note:

相关提示词 / System Compression Note

英文原文:

[Note: Some earlier conversation turns have been compacted into a handoff summary to preserve context space. The current session state may still reflect earlier work, so build on that summary and state rather than re-doing work. Your persistent memory (MEMORY.md, USER.md) remains fully authoritative regardless of compaction.]

中文对照:

[注意:为了节省上下文空间,一些早期对话轮次已经被压缩成交接摘要。
当前 session 状态可能仍然反映早期工作,所以应该基于摘要和当前状态继续,而不是重新做一遍。
无论是否发生压缩,你的持久记忆(MEMORY.md、USER.md)仍然完全权威。]

这段话的作用是:强化“压缩摘要是历史背景,不是替代 memory 的新系统规则”。

场景九:压缩不是原地覆盖旧会话,而是创建 continuation session

这点很重要。Hermes 不是简单把旧 session 历史改短。

_compress_context() 成功后,它会:

1. 在旧 session 上 commit memory。
2. 把旧 session 标记为 end_reason='compression'。
3. 创建一个新的 session_id。
4. 新 session 的 parent_session_id 指向旧 session。
5. 把压缩后的 messages 写入新 session。
6. 更新 system prompt。
7. 通知 context engine 和 memory manager:这是 compression 触发的 session switch。

这样做的好处:

原始旧会话仍可搜索、可追溯。
新会话携带压缩摘要继续工作。
resume 时可以沿 compression chain 找到最新 continuation。

hermes_state.py 里有 get_compression_tip(),用于沿着 compression continuation chain 找到最新 session。

相关实现说明 / Compression Continuation

英文原文:

A compression continuation is a child session where:
1. The parent's end_reason = 'compression'
2. The child was created AFTER the parent was ended (started_at >= ended_at)

The second condition distinguishes compression continuations from delegate subagents or branch children, which can also have a parent_session_id but were created while the parent was still live.

中文对照:

compression continuation 是这样一种子 session:
1. 父 session 的 end_reason = 'compression'
2. 子 session 在父 session 结束之后创建(started_at >= ended_at)

第二个条件用于区分 compression continuation 和 delegate subagent 或 branch child。
它们也可能有 parent_session_id,但通常是在父 session 仍然活跃时创建的。

这段话的作用是:解释为什么压缩续接和 subagent/branch 不是一回事。

场景十:摘要失败怎么办

如果辅助摘要模型不可用、超时、报错,Hermes 不会静默删除中间历史。

它会插入一个 fallback marker:

相关提示词 / Summary Failure Fallback

英文原文:

Summary generation was unavailable. {n_dropped} message(s) were removed to free context space but could not be summarized. The removed messages contained earlier work in this session. Continue based on the recent messages below and the current state of any files or resources.

中文对照:

摘要生成不可用。为了释放上下文空间,{n_dropped} 条消息被移除,但无法被摘要。
被移除的消息包含这个 session 中较早的工作。
请基于下面的最近消息以及当前文件或资源状态继续。

这段话的作用是:即使摘要失败,也让模型知道“这里发生过上下文丢失”,避免它误以为历史本来就不存在。

同时 Hermes 有几种恢复策略:

如果配置的 summary model 不可用,尝试回退到 main model 摘要。
如果是 transient error,进入短 cooldown,避免不断重试。
如果没有 auxiliary provider,进入较长 cooldown。
手动 /compress 会把摘要失败作为 warning 告诉用户。

场景十一:为什么压缩多次会降低质量

多次压缩本质上是:

原始细节 -> 摘要 1
摘要 1 + 新历史 -> 摘要 2
摘要 2 + 新历史 -> 摘要 3

每次都会有信息损失。Hermes 用结构化模板降低损失,但不能消除损失。

所以 _compress_context() 中如果发现 compression_count >= 2,会提醒:

Session compressed N times — accuracy may degrade. Consider /new to start fresh.

这就是它的管理策略:

短期:靠摘要续命。
长期:建议 /new 开新会话,或者用 memory/skills 把真正长期稳定的信息外置。

场景十二:Context Engine 是可插拔的

Hermes 默认使用内置 ContextCompressor,但上下文管理是一个可插拔接口。

agent/context_engine.py 定义了抽象接口:

update_from_response()
should_compress()
compress()
has_content_to_compress()
on_session_start()
on_session_end()
get_tool_schemas()
handle_tool_call()
update_model()

配置入口:

context:
  engine: compressor

如果换成其他 engine,例如 LCM,它可以:

用 DAG 管理历史
提供 lcm_grep / lcm_describe / lcm_expand 工具
让模型按需检索历史片段

但默认 compressor 不提供模型可调用的压缩工具。它主要是 agent 层自动机制。

相关实现说明 / Context Engine Lifecycle

英文原文:

A context engine controls how conversation context is managed when approaching the model's token limit. The built-in ContextCompressor is the default implementation.

The engine is responsible for:
- Deciding when compaction should fire
- Performing compaction (summarization, DAG construction, etc.)
- Optionally exposing tools the agent can call (e.g. lcm_grep)
- Tracking token usage from API responses

Lifecycle:
1. Engine is instantiated and registered
2. on_session_start() called when a conversation begins
3. update_from_response() called after each API response with usage data
4. should_compress() checked after each turn
5. compress() called when should_compress() returns True
6. on_session_end() called at real session boundaries

中文对照:

context engine 控制当接近模型 token 限制时如何管理对话上下文。内置 ContextCompressor 是默认实现。

engine 负责:
- 决定什么时候触发 compaction
- 执行 compaction(摘要、DAG 构建等)
- 可选地暴露模型可调用的工具(例如 lcm_grep)
- 跟踪 API response 中的 token 使用情况

生命周期:
1. engine 被实例化和注册
2. 会话开始时调用 on_session_start()
3. 每次 API response 后调用 update_from_response(),传入 usage 数据
4. 每轮后检查 should_compress()
5. should_compress() 返回 true 时调用 compress()
6. 真正的 session 边界调用 on_session_end()

这段话的作用是:说明 Hermes 把“上下文管理策略”抽象成插件点,默认摘要压缩只是其中一种实现。

整体流程总结

把所有场景串起来,Hermes 的上下文和 token 管理是这样的:

用户发消息
  -> 加载历史 messages
  -> 构建/复用 system prompt
  -> 注入 memory、skills、context files 等 ephemeral context
  -> 估算 messages + system prompt + tools schema token
  -> 如果超过 threshold,preflight compression
  -> 调用模型
  -> 读取 API usage,记录 prompt/output/cache/reasoning tokens
  -> 如果模型返回 tool_calls,执行工具
  -> 大工具结果先按 per-result / per-turn budget 落盘或截断
  -> 工具结果加入 messages
  -> 根据真实 prompt_tokens 判断是否需要压缩
  -> 如果需要,调用 ContextCompressor
  -> 压缩器保留 head + tail,摘要 middle
  -> 旧 session end_reason=compression,新建 continuation session
  -> 用压缩后的 messages 继续下一轮

当前机制的边界和风险

Hermes 已经做了很多防爆设计:

1. 模型上下文长度探测和配置覆盖。
2. preflight token 估算,包含 tools schema。
3. API usage 真实 token 记录。
4. context overflow / 413 错误恢复。
5. 大工具结果落盘。
6. per-turn aggregate budget。
7. 压缩摘要使用结构化 handoff 模板。
8. 保护最新用户消息。
9. 修复 tool_call/tool_result 孤儿配对。
10. 多次压缩提醒质量下降。
11. 压缩 session 链保留旧历史可追溯。

但它不是完美的无限上下文:

1. 摘要是有损的。
2. 多次压缩会逐步丢细节。
3. 如果辅助模型不可用,fallback marker 只能提醒上下文丢失,不能恢复细节。
4. 如果工具 schema 太多,哪怕 messages 很短也会有很高 token 压力。
5. 如果模型频繁读取超大文件,落盘能降低上下文压力,但模型仍需要学会按 offset/limit 局部读取。
6. 压缩摘要质量依赖 summary model 能力。

更强的设计可以继续加:

1. 基于重要性的消息级评分,而不是只按 head/middle/tail。
2. 按任务实体建立结构化 state store。
3. 对工具输出建立可检索索引,而不仅是文件路径。
4. 对 summary 做事实一致性校验。
5. 对重复压缩摘要做去重、分层、归档。
6. 根据工具 schema 使用频率做动态 tool pruning。
7. 把旧 session 的关键片段做向量/FTS 检索,在需要时召回。

一句话总结:

Hermes 的 token 管理不是单一“压缩一下历史”。

它是一套分层系统:
工具输出先预算化,
请求前做 token 预检,
请求后记录真实 usage,
溢出错误触发恢复,
长历史通过结构化 handoff summary 续接,
旧 session 通过 compression chain 保留可追溯性。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐