每条消息都发给 API没有服务端状态。当聊天历史膨胀到十几万 token 时Hermes 不是简单地丢掉旧消息——它用一个廉价模型把中间轮次总结成摘要在保护头尾的同时让上下文可持续地收敛。一、问题全量发送是唯一的游戏规则Hermes 和 Claude Code、Cursor、GitHub Copilot Chat 一样采用客户端维护完整会话历史的模式。每次 API 调用不是只发当前用户消息——而是把 system prompt 全部历史消息打包发送。agent/conversation_loop.py在每一轮循环中约 line 800把agent.messages完整会话历史拷贝为api_messages然后走一整条处理链路agent.messages → api_messages → 孤儿对清理 → 空白规范化 → thinking-only 轮删除 → surrogate 移除 → build_api_kwargs() → OpenAI client.chat.completions.create()agent/chat_completion_helpers.py的build_api_kwargs()第 606 行拿到api_messages后按 provider 分支——Chat Completions、Anthropic Messages、Bedrock Converse、Codex Responses——各自构建请求体。消息列表始终是全量的。这和 ChatGPT 网页版有本质区别。网页版是服务端维护状态——服务器记住对话历史客户端只发最新消息。而 Hermes 选择了无状态协议上的有状态客户端——没有服务端存储上下文必须每次全量携带。这种设计带来了一个直接问题对话越长请求越大。超过模型的上下文窗口上限时必须做点什么。二、System Prompt一次构建全 Session 复用在讨论压缩之前先看 System Prompt 是怎么组织的——因为它决定了压缩时哪些内容被保护。agent/system_prompt.py的build_system_prompt_parts()第 113 行把 system prompt 拆成三个 tier用\n\n拼接stable — SOUL.md 身份 工具使用指导 skills prompt 环境提示 平台提示 context — 用户 system_message 上下文文件AGENTS.md / .cursorrules volatile — memory 快照 USER.md 时间戳/模型/提供商信息行关键设计决策整个 session 只构建一次缓存在agent._cached_system_prompt上。源码第 126-129 行明确写道Hermes never re-renders parts of this string mid-session — thats the only way to keep upstream prompt caches warm across turns.这个设计服务于两个目标。第一Cloudflare / Anthropic 等服务商的 prompt cache 按前缀匹配——system prompt 不变意味着每个后续请求的前缀都是相同的缓存命中率接近 100%。第二memory 快照的变化不会主动触发 system prompt 重建——只有 context compression 发生时才会 rebuild因为 session 可能被旋转到子 session。三、上下文窗口解析10 步链不必手动配置在压缩被触发之前Hermes 需要知道模型有多少上下文可用。用户不需要在config.yaml里写context_length: 128000——agent/model_metadata.py的get_model_context_length()有一个 10 级优先级解析链0. config.yaml 显式覆盖 —— 用户知道的最好 1. 持久缓存 context_length_cache.yaml —— 之前探测过的 2. API 端点 /v1/models 实时查询 —— 权威来源 3. 本地服务器查询Ollama 等 4. Anthropic /v1/models API 5. Provider 专用查询Copilot / Nous / Codex / GMI / Ollama 6. OpenRouter 实时 API 元数据 7. 硬编码默认表最长 key 优先子串匹配 8. 本地服务器查询最后手段 9. 默认回退 256K对于deepseek-v4-pro命中第 7 步——DEFAULT_CONTEXT_LENGTHS字典第 244 行的deepseek-v4-pro: 1_000_000。最长 key 优先子串匹配确保deepseek-v4-pro17 字符不会被deepseek8 字符误匹配。首次命中后缓存到context_length_cache.yaml按modelbase_url做 key——同一模型在不同 provider 下可以有不同的上下文长度。四、Context Compression三明治策略agent/context_compressor.py是上下文管理的核心3045 行是整个代码库中复杂度最高的单文件之一。4.1 触发条件ContextCompressor.__init__()第 953 行接受一个关键参数threshold_percent: float 0.50 # 上下文窗口的 50%对于 128K 窗口 → 压缩阈值约 64K token。对于 1M 窗口如 deepseek-v4-pro→ 约 500K。实际计算会扣除max_tokens输出预算由_compute_threshold_tokens()第 912 行完成。should_compress()第 1136 行在 token 数超过阈值时返回 True——但在返回之前有三层防护失败冷却summary LLM 调用失败后600 秒内不重试。防止 API 429 / 持续失败循环烧钱。防抖动连续 2 次压缩各节省不到 10% → 跳过提示用户/new或/compress topic。防止每轮只压缩 1-2 条消息的无意义循环。无压缩窗口整个对话都在尾部预算内 →compress_start compress_end记录为无效压缩。下一轮should_compress()仍然返回 True 时会触发第 2 条防护。4.2 三阶段算法compress()方法第 2670 行的核心逻辑Phase 1剪枝旧工具结果便宜无 LLM 调用_prune_old_tool_results()第 1179 行遍历消息列表把不在保护范围内的toolrole 消息内容替换为占位符_PRUNED_TOOL_PLACEHOLDER [Old tool output cleared to save context space]保护范围由protect_last_n默认 20 条和tail_token_budget默认约 20% * threshold双重决定。文件读取、shell 执行等大输出占了绝大部分 token 消耗这一步能清除大量冗余数据——而且不花任何 LLM 调用成本。Phase 2确定压缩边界三条边界把消息列表分成三个区[0 ... compress_start) ← head受保护system prompt 前 N 个交换 [compress_start ... compress_end) ← middle送去总结 [compress_end ... end) ← tail受保护最后 ~20K token 的上下文_protect_head_size()第 2272 行保护 system prompt 前protect_first_n默认 3个用户-助手交换。_align_boundary_forward()第 2245 行确保边界落在完整的 tool turn 对之间——不在 tool_call 和 tool_result 中间切。_find_tail_cut_by_tokens()第 2546 行从末尾向前扫描累计 token 估算直到达到tail_token_budget。保护的是最近的用户指令和工具执行上下文——这些对当前任务最相关。Phase 3LLM 总结中间轮次_generate_summary()第 1641 行用辅助模型配置在auxiliary.compression下生成结构化摘要。如果辅助模型不可用会回退到主模型如果两者都失败则使用静态回退——提取最近 N 轮的路径引用和关键工具调用不依赖 LLM。迭代更新是这里的关键设计。第一次压缩时middle 的消息被总结为一个结构化的摘要。第二次压缩时_previous_summary非空——新要被总结的内容会附加上旧摘要LLM 被指示在旧基础上更新而非从零开始。这防止了信息在多轮压缩中逐渐丢失。4.3 摘要注入格式压缩后摘要以特殊前缀注入消息中第 44-70 行[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. ... Respond ONLY to the latest user message that appears AFTER this summary — that message is the single source of truth for what to do right now.这个前缀经过了至少三次迭代第 153-190 行的_HISTORICAL_SUMMARY_PREFIXES记录了每次变化的完整文本。早期版本包含resume exactly from Active Task指令——导致模型在新话题上仍然执着完成旧任务。#11475 和 #14521 的 bug 报告显示弱模型会把 summary 中的## Active Task引用当成新的用户输入。当前版本明确声明summary 只是背景参考不是活跃指令最新用户消息是唯一的真相来源反向信号stop / undo / never mind必须立即终止 summary 中描述的任何在途工作。摘要末尾还有一个边界标记第 133-136 行--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---这防止弱模型把 assistant-role 的 summary 当作自己的输出重复#33256。4.4 Token 估算Hermes 不使用模型特定的 tokenizer——那需要加载模型的 tokenizer 文件对本地部署不友好。替代方案是字符估算_CHARS_PER_TOKEN 4 # context_compressor.py 第 202 行 _IMAGE_TOKEN_ESTIMATE 1600 # 匹配 Claude Code 的常量_estimate_msg_budget_tokens()第 301 行不仅算 content 长度还计算tool_call 的完整 JSON 结构id type function.name 完整的 JSON envelope。早期版本只算function.arguments字符串长度——导致一个 4 工具调用的 turn 被估算为 ~73 token实际 ~1090保护尾部超标压缩失效#28053。五、三条触发路径上下文的压缩不在一个固定位置——它在三个时机被检查确保不会在工具调用循环中越过上下文上限。┌─ Turn Prologueagent/turn_context.py:build_turn_context │ 每轮开始前。用廉价字符估算判断是否可能超标。 │ 如果消息数少但单条消息极大如一次读入了 50K 行日志 │ 也触发——这是 #27405 修复的边界情况。 │ ├─ Pre-APIagent/conversation_loop.py 约 line 990 │ 每次 API 调用前。工具调用循环中一次大文件读取可能 │ 让 token 数暴涨——这个检查捕获了 turn prologue 之后 │ 新增的内容。每轮最多压缩 3 次compression_attempts 3。 │ └─ Post-Responseagent/conversation_loop.py 约 line 2101 API 响应后通过 update_from_response() 更新实际 token 计数。 为下轮 should_compress() 提供精确数据。三条路径都是防守性的——宁可多检查一次也不让 API 返回 context length exceeded 错误。六、API 请求的最后一道关口即使压缩充分发往 API 的消息列表还需要经过一系列清理——agent/conversation_loop.py约 line 890-950和agent/chat_completion_helpers.py的build_api_kwargs()孤儿 tool 对清理_sanitize_api_messages()—— 去掉没有对应 tool_result 的 tool_call补上缺失的 tool_call_id stub。不依赖context_compressor是否运行——session 加载和手动消息操作都可能产生孤儿对。thinking-only 轮次删除_drop_thinking_only_and_merge_users()—— Anthropic 的 thinking block 不能作为消息的最后一部分。删掉纯 thinking 的 assistant turn 并合并相邻的 user 消息。空白规范化每一条 message content 做strip()—— 跨轮次确保位对位一致的前缀提高 KV cache 命中率llama.cpp / vLLM / Ollama 的 prefix matching 依赖严格相同的字节前缀。tool_call JSON 规范化sort_keysTrue重整 arguments JSON —— 同样的工具调用参数不管模型以什么顺序序列化 key都变成一致的字符串。surrogate 移除_sanitize_messages_surrogates()—— Ollama 上运行的 Kimi K2.5 / GLM-5 / Qwen 可能返回 UD800-UDFFF 范围的 lone surrogate会导致 OpenAI SDK 内部的json.dumps()崩溃。Anthropic prompt cachingapply_anthropic_cache_control()—— 在 system 和最后 3 条消息上注入cache_control断点将输入 token 成本降低约 75%。七、Session 管理压缩如何影响会话持久化压缩不只是修改消息列表——它还涉及 session 在 SQLitestate.db中的组织方式。agent/conversation_compression.py处理这部分逻辑。传统模式compress_context()第 435 行压缩后创建新的 child session。旧 session 结束时设置parent_session_id指向新 session。/session命令和 TUI 的 session 列表中父子关系形成一条链。代价是 session ID 变化——任何持有旧 session_id 的外部引用都会失效。In-place 模式compression.in_place: true#38763压缩在同一 session_id 下原地重写消息。archive_and_compact()把旧活跃行软归档设置active0, compacted1然后插入压缩后的消息作为新的活跃行。session_id 保持稳定消除了 session 旋转带来的整类 bug——日志引用、gateway 路由、外部监控都不需要感知压缩事件。压缩过程有分布式锁保护。_CompressionLockLeaseRefresher第 75 行在state.db中获取一个带 TTL 的 lease后台线程定时续约。防止两个并发进程如 gateway 和 cron同时对同一 session 执行压缩。八、与 Claude Code 的设计对比Claude Code 的上下文管理采取了更激进的策略。Claude Code 的/compact命令由用户手动触发——没有自动压缩。它的系统提示词包含additional_data区段把项目文件注入在每次请求的末尾。当上下文逼近窗口上限时Claude Code 选择的策略是——由用户决定是否压缩。Hermes 选择了一条不同的路自动压缩。这不是说 Hermes 更智能。这是两种设计哲学的差异。Claude Code 的假设是用户知道自己想要什么——压缩什么、保留什么——并且愿意手动控制。Hermes 的假设是用户不应该被上下文管理打断——当窗口快满时Agent 应该自己处理而用户看到一个️ Compacting context状态提示就够了。具体到代码层面的差异维度HermesClaude Code压缩触发自动50% 阈值手动/compact压缩模型独立辅助模型可配置推测为同一模型工具结果剪枝有免费 pre-pass无独立阶段迭代更新旧摘要作为输入迭代更新不明确防抖/冷却三层防护用户手动控制节奏Session 旋转支持传统 in-place推测无 session 概念两者都采用全量消息发送模式——这是客户端 AI Agent 的共性。差异在于谁来决定何时以及如何压缩上下文。九、核心哲学Hermes 上下文管理体现了一个贯穿始终的设计原则上下文是稀缺资源管理它的成本不应该由用户承担。三层 system prompt 的缓存设计——让 token 开销只花一次。10 步上下文解析链——让用户不需要手动配置模型参数。50% 自动压缩阈值——让用户不需要数 token 和纠结何时该/compact。防抖和冷却机制——让系统不会在边缘情况下陷入无限压缩循环。失败后的静态回退——让摘要不可用时仍然不丢上下文。这些设计不是独立的 feature——它们是一个统一策略的不同侧面让 Agent 自己管理自己的内存。这和 Claude Code 的用户控制哲学形成了一对有趣的对话——两者各有合理性但 Hermes 明确地选择了自治这条路。这也解释了为什么context_compressor.py是 3045 行而不是 300 行。自动压缩看起来像是一个简单的把旧消息发给小模型总结的功能——但要做好它需要处理边界对齐、迭代更新、失败冷却、防抖动、占位符替换、摘要前缀演进、图片 token 估算、session 旋转锁……每一个边缘情况都对应一个 issue#11475、#14521、#28053、#29559、#33256、#40803 等等在源码中留下了详尽的 fix 注释。这不是过度工程。这是边界驱动设计——每个检查点背后都有一个真实用户踩过的坑。Hermes Agent 由 Nous Research 开发。本文基于 Hermes v0.18.0 源码分析。全文完。
Hermes Agent 自进化架构深度解析(八):上下文管理——全量发送下的压缩与边界
发布时间:2026/7/12 11:27:10
每条消息都发给 API没有服务端状态。当聊天历史膨胀到十几万 token 时Hermes 不是简单地丢掉旧消息——它用一个廉价模型把中间轮次总结成摘要在保护头尾的同时让上下文可持续地收敛。一、问题全量发送是唯一的游戏规则Hermes 和 Claude Code、Cursor、GitHub Copilot Chat 一样采用客户端维护完整会话历史的模式。每次 API 调用不是只发当前用户消息——而是把 system prompt 全部历史消息打包发送。agent/conversation_loop.py在每一轮循环中约 line 800把agent.messages完整会话历史拷贝为api_messages然后走一整条处理链路agent.messages → api_messages → 孤儿对清理 → 空白规范化 → thinking-only 轮删除 → surrogate 移除 → build_api_kwargs() → OpenAI client.chat.completions.create()agent/chat_completion_helpers.py的build_api_kwargs()第 606 行拿到api_messages后按 provider 分支——Chat Completions、Anthropic Messages、Bedrock Converse、Codex Responses——各自构建请求体。消息列表始终是全量的。这和 ChatGPT 网页版有本质区别。网页版是服务端维护状态——服务器记住对话历史客户端只发最新消息。而 Hermes 选择了无状态协议上的有状态客户端——没有服务端存储上下文必须每次全量携带。这种设计带来了一个直接问题对话越长请求越大。超过模型的上下文窗口上限时必须做点什么。二、System Prompt一次构建全 Session 复用在讨论压缩之前先看 System Prompt 是怎么组织的——因为它决定了压缩时哪些内容被保护。agent/system_prompt.py的build_system_prompt_parts()第 113 行把 system prompt 拆成三个 tier用\n\n拼接stable — SOUL.md 身份 工具使用指导 skills prompt 环境提示 平台提示 context — 用户 system_message 上下文文件AGENTS.md / .cursorrules volatile — memory 快照 USER.md 时间戳/模型/提供商信息行关键设计决策整个 session 只构建一次缓存在agent._cached_system_prompt上。源码第 126-129 行明确写道Hermes never re-renders parts of this string mid-session — thats the only way to keep upstream prompt caches warm across turns.这个设计服务于两个目标。第一Cloudflare / Anthropic 等服务商的 prompt cache 按前缀匹配——system prompt 不变意味着每个后续请求的前缀都是相同的缓存命中率接近 100%。第二memory 快照的变化不会主动触发 system prompt 重建——只有 context compression 发生时才会 rebuild因为 session 可能被旋转到子 session。三、上下文窗口解析10 步链不必手动配置在压缩被触发之前Hermes 需要知道模型有多少上下文可用。用户不需要在config.yaml里写context_length: 128000——agent/model_metadata.py的get_model_context_length()有一个 10 级优先级解析链0. config.yaml 显式覆盖 —— 用户知道的最好 1. 持久缓存 context_length_cache.yaml —— 之前探测过的 2. API 端点 /v1/models 实时查询 —— 权威来源 3. 本地服务器查询Ollama 等 4. Anthropic /v1/models API 5. Provider 专用查询Copilot / Nous / Codex / GMI / Ollama 6. OpenRouter 实时 API 元数据 7. 硬编码默认表最长 key 优先子串匹配 8. 本地服务器查询最后手段 9. 默认回退 256K对于deepseek-v4-pro命中第 7 步——DEFAULT_CONTEXT_LENGTHS字典第 244 行的deepseek-v4-pro: 1_000_000。最长 key 优先子串匹配确保deepseek-v4-pro17 字符不会被deepseek8 字符误匹配。首次命中后缓存到context_length_cache.yaml按modelbase_url做 key——同一模型在不同 provider 下可以有不同的上下文长度。四、Context Compression三明治策略agent/context_compressor.py是上下文管理的核心3045 行是整个代码库中复杂度最高的单文件之一。4.1 触发条件ContextCompressor.__init__()第 953 行接受一个关键参数threshold_percent: float 0.50 # 上下文窗口的 50%对于 128K 窗口 → 压缩阈值约 64K token。对于 1M 窗口如 deepseek-v4-pro→ 约 500K。实际计算会扣除max_tokens输出预算由_compute_threshold_tokens()第 912 行完成。should_compress()第 1136 行在 token 数超过阈值时返回 True——但在返回之前有三层防护失败冷却summary LLM 调用失败后600 秒内不重试。防止 API 429 / 持续失败循环烧钱。防抖动连续 2 次压缩各节省不到 10% → 跳过提示用户/new或/compress topic。防止每轮只压缩 1-2 条消息的无意义循环。无压缩窗口整个对话都在尾部预算内 →compress_start compress_end记录为无效压缩。下一轮should_compress()仍然返回 True 时会触发第 2 条防护。4.2 三阶段算法compress()方法第 2670 行的核心逻辑Phase 1剪枝旧工具结果便宜无 LLM 调用_prune_old_tool_results()第 1179 行遍历消息列表把不在保护范围内的toolrole 消息内容替换为占位符_PRUNED_TOOL_PLACEHOLDER [Old tool output cleared to save context space]保护范围由protect_last_n默认 20 条和tail_token_budget默认约 20% * threshold双重决定。文件读取、shell 执行等大输出占了绝大部分 token 消耗这一步能清除大量冗余数据——而且不花任何 LLM 调用成本。Phase 2确定压缩边界三条边界把消息列表分成三个区[0 ... compress_start) ← head受保护system prompt 前 N 个交换 [compress_start ... compress_end) ← middle送去总结 [compress_end ... end) ← tail受保护最后 ~20K token 的上下文_protect_head_size()第 2272 行保护 system prompt 前protect_first_n默认 3个用户-助手交换。_align_boundary_forward()第 2245 行确保边界落在完整的 tool turn 对之间——不在 tool_call 和 tool_result 中间切。_find_tail_cut_by_tokens()第 2546 行从末尾向前扫描累计 token 估算直到达到tail_token_budget。保护的是最近的用户指令和工具执行上下文——这些对当前任务最相关。Phase 3LLM 总结中间轮次_generate_summary()第 1641 行用辅助模型配置在auxiliary.compression下生成结构化摘要。如果辅助模型不可用会回退到主模型如果两者都失败则使用静态回退——提取最近 N 轮的路径引用和关键工具调用不依赖 LLM。迭代更新是这里的关键设计。第一次压缩时middle 的消息被总结为一个结构化的摘要。第二次压缩时_previous_summary非空——新要被总结的内容会附加上旧摘要LLM 被指示在旧基础上更新而非从零开始。这防止了信息在多轮压缩中逐渐丢失。4.3 摘要注入格式压缩后摘要以特殊前缀注入消息中第 44-70 行[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. ... Respond ONLY to the latest user message that appears AFTER this summary — that message is the single source of truth for what to do right now.这个前缀经过了至少三次迭代第 153-190 行的_HISTORICAL_SUMMARY_PREFIXES记录了每次变化的完整文本。早期版本包含resume exactly from Active Task指令——导致模型在新话题上仍然执着完成旧任务。#11475 和 #14521 的 bug 报告显示弱模型会把 summary 中的## Active Task引用当成新的用户输入。当前版本明确声明summary 只是背景参考不是活跃指令最新用户消息是唯一的真相来源反向信号stop / undo / never mind必须立即终止 summary 中描述的任何在途工作。摘要末尾还有一个边界标记第 133-136 行--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---这防止弱模型把 assistant-role 的 summary 当作自己的输出重复#33256。4.4 Token 估算Hermes 不使用模型特定的 tokenizer——那需要加载模型的 tokenizer 文件对本地部署不友好。替代方案是字符估算_CHARS_PER_TOKEN 4 # context_compressor.py 第 202 行 _IMAGE_TOKEN_ESTIMATE 1600 # 匹配 Claude Code 的常量_estimate_msg_budget_tokens()第 301 行不仅算 content 长度还计算tool_call 的完整 JSON 结构id type function.name 完整的 JSON envelope。早期版本只算function.arguments字符串长度——导致一个 4 工具调用的 turn 被估算为 ~73 token实际 ~1090保护尾部超标压缩失效#28053。五、三条触发路径上下文的压缩不在一个固定位置——它在三个时机被检查确保不会在工具调用循环中越过上下文上限。┌─ Turn Prologueagent/turn_context.py:build_turn_context │ 每轮开始前。用廉价字符估算判断是否可能超标。 │ 如果消息数少但单条消息极大如一次读入了 50K 行日志 │ 也触发——这是 #27405 修复的边界情况。 │ ├─ Pre-APIagent/conversation_loop.py 约 line 990 │ 每次 API 调用前。工具调用循环中一次大文件读取可能 │ 让 token 数暴涨——这个检查捕获了 turn prologue 之后 │ 新增的内容。每轮最多压缩 3 次compression_attempts 3。 │ └─ Post-Responseagent/conversation_loop.py 约 line 2101 API 响应后通过 update_from_response() 更新实际 token 计数。 为下轮 should_compress() 提供精确数据。三条路径都是防守性的——宁可多检查一次也不让 API 返回 context length exceeded 错误。六、API 请求的最后一道关口即使压缩充分发往 API 的消息列表还需要经过一系列清理——agent/conversation_loop.py约 line 890-950和agent/chat_completion_helpers.py的build_api_kwargs()孤儿 tool 对清理_sanitize_api_messages()—— 去掉没有对应 tool_result 的 tool_call补上缺失的 tool_call_id stub。不依赖context_compressor是否运行——session 加载和手动消息操作都可能产生孤儿对。thinking-only 轮次删除_drop_thinking_only_and_merge_users()—— Anthropic 的 thinking block 不能作为消息的最后一部分。删掉纯 thinking 的 assistant turn 并合并相邻的 user 消息。空白规范化每一条 message content 做strip()—— 跨轮次确保位对位一致的前缀提高 KV cache 命中率llama.cpp / vLLM / Ollama 的 prefix matching 依赖严格相同的字节前缀。tool_call JSON 规范化sort_keysTrue重整 arguments JSON —— 同样的工具调用参数不管模型以什么顺序序列化 key都变成一致的字符串。surrogate 移除_sanitize_messages_surrogates()—— Ollama 上运行的 Kimi K2.5 / GLM-5 / Qwen 可能返回 UD800-UDFFF 范围的 lone surrogate会导致 OpenAI SDK 内部的json.dumps()崩溃。Anthropic prompt cachingapply_anthropic_cache_control()—— 在 system 和最后 3 条消息上注入cache_control断点将输入 token 成本降低约 75%。七、Session 管理压缩如何影响会话持久化压缩不只是修改消息列表——它还涉及 session 在 SQLitestate.db中的组织方式。agent/conversation_compression.py处理这部分逻辑。传统模式compress_context()第 435 行压缩后创建新的 child session。旧 session 结束时设置parent_session_id指向新 session。/session命令和 TUI 的 session 列表中父子关系形成一条链。代价是 session ID 变化——任何持有旧 session_id 的外部引用都会失效。In-place 模式compression.in_place: true#38763压缩在同一 session_id 下原地重写消息。archive_and_compact()把旧活跃行软归档设置active0, compacted1然后插入压缩后的消息作为新的活跃行。session_id 保持稳定消除了 session 旋转带来的整类 bug——日志引用、gateway 路由、外部监控都不需要感知压缩事件。压缩过程有分布式锁保护。_CompressionLockLeaseRefresher第 75 行在state.db中获取一个带 TTL 的 lease后台线程定时续约。防止两个并发进程如 gateway 和 cron同时对同一 session 执行压缩。八、与 Claude Code 的设计对比Claude Code 的上下文管理采取了更激进的策略。Claude Code 的/compact命令由用户手动触发——没有自动压缩。它的系统提示词包含additional_data区段把项目文件注入在每次请求的末尾。当上下文逼近窗口上限时Claude Code 选择的策略是——由用户决定是否压缩。Hermes 选择了一条不同的路自动压缩。这不是说 Hermes 更智能。这是两种设计哲学的差异。Claude Code 的假设是用户知道自己想要什么——压缩什么、保留什么——并且愿意手动控制。Hermes 的假设是用户不应该被上下文管理打断——当窗口快满时Agent 应该自己处理而用户看到一个️ Compacting context状态提示就够了。具体到代码层面的差异维度HermesClaude Code压缩触发自动50% 阈值手动/compact压缩模型独立辅助模型可配置推测为同一模型工具结果剪枝有免费 pre-pass无独立阶段迭代更新旧摘要作为输入迭代更新不明确防抖/冷却三层防护用户手动控制节奏Session 旋转支持传统 in-place推测无 session 概念两者都采用全量消息发送模式——这是客户端 AI Agent 的共性。差异在于谁来决定何时以及如何压缩上下文。九、核心哲学Hermes 上下文管理体现了一个贯穿始终的设计原则上下文是稀缺资源管理它的成本不应该由用户承担。三层 system prompt 的缓存设计——让 token 开销只花一次。10 步上下文解析链——让用户不需要手动配置模型参数。50% 自动压缩阈值——让用户不需要数 token 和纠结何时该/compact。防抖和冷却机制——让系统不会在边缘情况下陷入无限压缩循环。失败后的静态回退——让摘要不可用时仍然不丢上下文。这些设计不是独立的 feature——它们是一个统一策略的不同侧面让 Agent 自己管理自己的内存。这和 Claude Code 的用户控制哲学形成了一对有趣的对话——两者各有合理性但 Hermes 明确地选择了自治这条路。这也解释了为什么context_compressor.py是 3045 行而不是 300 行。自动压缩看起来像是一个简单的把旧消息发给小模型总结的功能——但要做好它需要处理边界对齐、迭代更新、失败冷却、防抖动、占位符替换、摘要前缀演进、图片 token 估算、session 旋转锁……每一个边缘情况都对应一个 issue#11475、#14521、#28053、#29559、#33256、#40803 等等在源码中留下了详尽的 fix 注释。这不是过度工程。这是边界驱动设计——每个检查点背后都有一个真实用户踩过的坑。Hermes Agent 由 Nous Research 开发。本文基于 Hermes v0.18.0 源码分析。全文完。