拆解 Hermes 如何用触发器、边界算法和 handoff 摘要让长任务 Agent 在压缩后继续可靠执行。长任务 Agent最容易坏在一个很不起眼的地方它不是没能力继续推理而是已经看不完整自己刚才做过什么。一次真实的工具型会话里系统提示、历史消息、工具调用、终端输出、文件读取结果、截图、代码片段都会在每一轮重新塞回模型。窗口没满时模型已经开始丢约束、重复读文件、重新推导旧决策窗口真正溢出时请求会直接被 provider 拒掉。​Hermes 的上下文压缩机制​就是为了解这个问题。它处理的不是​长期记忆​。MEMORY.md、USER.md、memory provider 负责跨会话事实上下文压缩只管理当前会话的工作窗口。这个边界很重要因为压缩天然有损不能拿它替代真正的记忆系统。图上下文压缩只整理当前工作窗口长期事实仍应进入 memory 层ContextEngine把上下文管理做成可替换引擎Hermes 没把压缩逻辑写死在主循环里而是抽象成agent/context_engine.py里的ContextEngine。内置实现是ContextCompressor插件也可以接管整套上下文管理比如换成无损上下文管理 LCM。引擎选择由config.yaml的context.engine控制。插件不会自动启用必须显式配置没有匹配插件时系统回退到内置ContextCompressor。图上下文管理引擎的配置选择与回退路径这层设计让压缩从“一个功能”变成了“一个策略接口”。主循环只关心什么时候问引擎、什么时候拿回新的消息列表不必知道具体是 LLM 摘要、无损索引还是别的上下文管理方案。双层压缩Gateway 兜底Agent 主压缩Hermes 有两道压缩线位置和目的都不同。第一道在 gateway属于会话检查。它在 Agent 开始处理消息之前运行阈值固定在 85%。它不是日常压缩主力主要兜住隔夜会话、群聊积压、外部通道疯狂灌消息这类情况。第二道在 Agent 内部也就是ContextCompressor。它运行在工具循环里默认 50% 阈值优先使用 provider 返回的真实prompt_tokens。这是日常上下文管理的核心。图Gateway 兜底与 Agent 主压缩的分工维度Gateway 会话卫生Agent 压缩器触发阈值固定 85%默认 50%可通过compression.threshold配置运行位置Agent 处理前Agent 工具循环内部token 来源上轮真实 token缺失时用字符估算provider 返回的真实prompt_tokens优先主要目标防止超大会话直接打挂请求常规上下文管理额外保护hygiene_hard_message_limit默认 5000 条防抖、边界对齐、会话锁、摘要失败降级阈值错开不是随便定的。gateway 如果也按 50% 触发长会话会在很多轮里提前压缩成本高信息损耗也大。它只应该接管那些 Agent 压缩器没来得及处理的异常会话。三个触发器预检、响应后、错误恢复ContextCompressor不是等 API 报错才行动。一次 turn 内它有三个入口。图预检、响应后与错误恢复三类压缩入口Preflight请求发出前的廉价拦截预检压缩位于agent/turn_context.py的build_turn_context。它先跑一个便宜判断消息数量是否已经超过保护头尾的安全范围或者字符粗估是否已经很大。只有通过这个门控才会做更贵的 token 粗估。这里有两个细节容易被忽略。第一粗估必须把 tool schemas 算进去。工具一多schema 本身就可能占 20K 到 30K token只估 messages 会低估请求体。第二Hermes 会用should_defer_preflight_to_real_usage()抵抗 schema-heavy 请求的噪声。如果上一次压缩后的真实 token 已经证明请求能装下就不要被同一批 schema 的粗估反复吓到。Post-response用真实 prompt token 做日常决策响应后压缩位于agent/conversation_loop.py。它在模型响应回来、工具结果追加后执行是最常见的压缩路径。核心逻辑可以简化成这样_compressor agent.context_compressor if _compressor.last_prompt_tokens 0: real_tokens _compressor.last_prompt_tokens elif _compressor.last_prompt_tokens -1: real_tokens 0 else: real_tokens estimate_request_tokens_rough(messages, tools...) if agent.compression_enabled and _compressor.should_compress(real_tokens): messages, active_system_prompt agent._compress_context(...)它只看prompt_tokens不把completion_tokens算进触发条件。原因很实际推理模型的 reasoning token 可能很大如果 completion 也参与判断会让会话还没真正挤占输入窗口就过早压缩。last_prompt_tokens -1是一个哨兵值。压缩刚结束时系统还没拿到下一轮 provider 的真实 usage此时把 token 视为 0避免刚压完就被 schema 粗估拉回压缩循环。Error recovery窗口真的炸了再强制抢救provider 返回 413、context overflow或 Anthropic 长上下文层的 429 时Hermes 会进入错误恢复压缩。它会降级 context 设置并强制压缩重试最多max_compression_attempts3次。这条路径不是主流程而是保险丝。真正健康的会话应该靠预检和响应后压缩解决大部分时候不该走到 provider 报错这一步。阈值计算不是窗口乘百分比很多上下文压缩实现会直接用context_length × threshold。Hermes 没这么做。它先从窗口里扣掉max_tokens因为输出空间也占 provider 给的总窗口。输入预算应该是effective_window context_length - (max_tokens or 0)完整逻辑大致如下staticmethod def _compute_threshold_tokens( context_length: int, threshold_percent: float, max_tokens: int | None None, ) - int: effective_window context_length - (max_tokens or 0) if effective_window 0: effective_window context_length pct_value int(effective_window * threshold_percent) floored max(pct_value, MINIMUM_CONTEXT_LENGTH) if effective_window 0 and floored effective_window: return max( 1, min( int(effective_window * ContextCompressor._MIN_CTX_TRIGGER_RATIO), effective_window - 1, ), ) return floored这段代码同时解决了几个问题。第一给输出预留空间。自定义 provider 如果把max_tokens配到 65536输入预算会明显变小不扣掉它就容易撞。第二大窗口模型不应该太早压。MINIMUM_CONTEXT_LENGTH64K让大上下文模型不会因为 50% 阈值就频繁压缩。ContextCompressor先剪枝再摘要最后重组ContextCompressor.compress()的目标不是把历史消息简单截断而是把会话改造成三段保护头 结构化摘要 原样保留的尾部消息压缩过程分四步。图先剪枝、再摘要、最后重组消息窗口旧工具结果先做无损味道的降噪第一阶段不调用模型只做工程剪枝。保护尾之外的长工具输出会被压成信息化的一行而不是丢成空占位。[terminal] ran npm test - exit 0, 47 lines output [read_file] read config.py from line 1 (3,400 chars)这一步有三遍扫描Pass处理内容为什么需要Pass 1重复文件读取去重只保留最近全文同一个文件被反复 read 时旧副本没有必要继续占窗口Pass 2长工具结果缩成一行截图剥离 base64防止旧终端输出和旧截图永久拖累每轮请求Pass 3截断超大 tool_call 参数但保持 JSON 合法避免坏 JSON 毒化后续 provider 请求这个阶段看起来朴素却很值。很多上下文膨胀来自工具结果而不是用户真正说了多少话。先用确定性规则降噪可以减少后面 LLM 摘要的压力。图旧工具输出先被结构化压缩降低后续摘要负担边界算法压缩不能切坏消息结构压缩边界是这套机制里最有工程味的部分。它既要尽量多压又不能把 tool call 组切坏不能把最新用户任务卷进摘要也不能让早期头部无限增长。保护头只在第一次压缩时保护首轮任务框架protect_first_n默认保护最初几条非系统消息让首次任务设定活过第一次压缩。但这份保护会衰减if self.compression_count 1 or self._previous_summary: return 0 return self.protect_first_n原因很直接第一次压缩后早期任务框架已经进入 handoff 摘要。如果后续每次还保护前几条老消息它们会变成“不朽消息”在每个子会话里反复复制头部无界增长。系统提示不参与这个衰减。它由_protect_head_size()单独保护始终保留。尾部优先按 token 预算保护尾保护不是简单保留最后 N 条消息而是优先按 token 预算从尾往前切图按 token 预算保护活跃任务尾部这里的关键不是“保留最后 20 条”这种固定规则而是让最近活跃任务在 token 意义上尽量完整。尾部只会增大不会缩小最近一条 user 消息必须在尾里最近一条有文字内容的 assistant 回复也必须在尾里。tool_call 配对要么一起压要么一起留OpenAI 消息格式要求assistant tool_call后面紧跟匹配的tool结果。压缩切边界时如果切进工具组中间后续请求会报错。Hermes 用两道防线处理这个问题。第一道是边界对齐。边界落在 tool results 组里时_align_boundary_backward()会往前找到父 assistant 消息让assistant tool_results整组进入同一侧。第二道是重组后的清理。_sanitize_tool_pairs()会扫描还活着的 tool call id 和 tool result id图孤儿 tool result 删除与缺失结果补桩缺 result 的 tool call 会补一个类似[Result from earlier conversation...]的桩结果。它不假装还原历史内容只保证 provider 消息结构合法。结构化摘要给续作看的 handoff不是新指令摘要阶段由_generate_summary()执行使用辅助模型call_llm(taskcompression)。它不是让模型写一段“简短总结”而是强制填一份 handoff 模板。典型字段包括字段保存什么作用Historical Task Snapshot最近未完成用户输入的逐字原话防止旧任务在压缩后变形Goal总体目标保留会话主线Constraints Preferences约束、偏好、编码风格减少压缩后违反硬要求Completed Actions已完成动作、目标、结果、工具避免重复做已完成工作Active State工作目录、分支、改动文件、测试状态、进程支撑继续接手Blocked阻塞点和精确报错防止重新踩同一个坑Key Decisions技术决策和原因保留设计判断Resolved Questions已回答问题防止重复解释Relevant Files读过、改过、创建过的文件续作时定位上下文Critical Context精确配置值、报错、关键事实抢救容易丢的细节摘要前缀里有一条很重要的约束这是一段来自旧 context window 的参考材料不是当前用户指令。模型只应该响应摘要之后的新用户消息。摘要尾部也有明确边界--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---这不是形式主义。压缩摘要里经常包含用户旧请求的逐字引用弱模型可能把它当成新的待执行命令。Hermes 通过前缀、尾标记、时间锚定和过去时改写把“历史事实”和“当前指令”分开。图handoff 摘要提供历史参考但不应变成新的指令多次压缩靠 previous summary 迭代更新长会话可能压缩很多次。Hermes 不会每次只总结当前中段而是把上一版摘要作为PREVIOUS SUMMARY一起交给辅助模型让它更新而不是重写。图previous summary 在多次压缩中迭代更新这样旧项目状态会从 in progress 移到 completed新阻塞会被加入过时内容会被删掉。resume 场景下压缩器还会从当前消息里找最近的 handoff 摘要恢复_previous_summary。跨会话泄漏也做了防护。如果内存里有_previous_summary但当前消息列表找不到对应 handoff说明它可能来自已经结束的会话压缩器会丢弃这份摘要。摘要模型窗口压缩质量最常见的暗坑摘要模型的 context window 必须不小于主模型。原因很简单被压缩的中段会一次性发给辅助模型。如果辅助模型窗口比主模型小摘要调用可能直接 context-length 错误。更麻烦的是在默认降级路径下压缩器可能用 fallback 摘要继续推进中段细节就真的丢了。Hermes 用check_compression_model_feasibility在会话开始或首次压缩时探测辅助模型可行性。实际配置时最稳的方式还是显式选择窗口足够大的压缩模型auxiliary: compression: model: context window 不小于主模型的模型摘要预算也不是固定值。它按被压缩内容量动态缩放content_tokens estimate_messages_tokens_rough(turns_to_summarize) budget int(content_tokens * _SUMMARY_RATIO) # 0.20 return max(_MIN_SUMMARY_TOKENS, min(budget, self.max_summary_tokens))默认下限是 2000 token上限是min(context_length × 0.05, 12000)。这给摘要留出足够空间但不会让摘要本身膨胀成新的上下文负担。会话存储轮转与原地压缩的取舍压缩成功后Hermes 还要处理 SQLite 里的会话存储。这里有两种模式由compression.in_place控制。图原地压缩与会话轮转的落库差异模式行为优点风险原地压缩同一个session_id下软归档旧 turn插入压缩后的 active turn会话身份稳定少很多轮转边界问题需要存储层正确支持归档和检索会话轮转结束旧 session创建 parent 指向旧会话的新 session旧 transcript 保留便于搜索fork、回滚、goal 迁移、日志上下文同步都更复杂配置注释里已经倾向推荐in_place: true。长任务场景下同一个 session id 持续存在心智负担和边界 bug 都少一些。并发安全靠 state.db 的 session 级压缩锁。两个 Agent 实例共享同一 session 时只有一个能真正执行压缩另一个拿不到锁就原样返回让赢家完成。锁子系统异常时 fail-open避免会话卡死。Prompt caching压缩会打断一部分缓存但不该打断全部压缩和 prompt caching 是两套机制但它们会互相影响。正常多轮里system prompt 稳定前缀缓存命中率高。压缩发生后中段被重写压缩区后面的缓存自然失效但 system prompt 缓存仍然应该活着尾部三条消息也会在一两轮内重新形成滚动缓存。图压缩后 system 缓存保留滚动缓存重建失败处理宁可中止也不要静默丢上下文压缩失败不能只当普通异常处理。Hermes 把摘要失败分成几类失败类型处理方式原因没配置 provider长 cooldown 后返回失败继续重试只会浪费时间401 / 403标记认证失败并中止压缩凭证问题不会靠重试自愈网络中断标记网络失败并中止压缩保留原会话等网络恢复再压允许降级的普通失败插入确定性 fallback 摘要至少保留连续性锚点abort_on_summary_failuretrue直接中止用户显式选择保守策略中止时会设_last_compress_abortedTrue上层可以向用户提示“会话已冻结修复后再/compress或开新会话”。这比悄悄丢中段安全得多。防抖也很关键。如果连续两次压缩节省不到 10%should_compress()会直接返回 Falsedef should_compress(self, prompt_tokensNone) - bool: tokens prompt_tokens if prompt_tokens is not None else self.last_prompt_tokens if tokens self.threshold_tokens: return False if self._ineffective_compression_count 2: return False return True有些会话确实没有多少可压空间比如尾部全是近期大工具输出。继续压只会空转防抖能把这种循环停下来。图片也有专门恢复路径。provider 因图片太大拒绝时try_shrink_image_parts_in_messages()会把 base64 图片重新编码到限制以内再重试请求。旧图片在常规压缩中也会被_strip_historical_media()换成文字占位避免多 MB 媒体长期留在窗口里。Hermes 与 Claude Code同一个模式不同的控制面Hermes 和 Claude Code 都走“保护头、摘要中段、保留尾”的大方向但控制面差别很大。维度Hermes AgentClaude Code实现位置客户端ContextCompressor服务端 Compaction API 加客户端配合可配置性阈值、尾部比例、保护消息数、辅助模型、原地压缩都可配用户可调空间较少触发阈值Agent 默认 50%gateway 85%200K 模型上约 150K input tokens工具输出处理在上下文内剪枝、去重、截断microcompaction 将工具输出落盘窗口里保留引用摘要模型可独立配置辅助模型主要由服务端 compaction 处理失败防护防抖、并发锁、摘要失败分级、fallback更多由服务端策略接管续作恢复handoff 摘要 session_search memory provider摘要 重读最近文件 todo 恢复两者共同的弱点也很明确压缩擅长保留叙事连续性不擅长保证每个精确约束都活下来。第 2 轮说的偏好、第 8 轮确认的配置值、第 12 轮临时加的禁用项都可能在摘要时被模型认为“不够重要”。这就是为什么 memory 层不能省。跨压缩、跨会话必须存活的事实应该在压缩发生前进入 memory provider而不是寄希望于摘要模型每次都判断正确。配置建议先保证不丢再谈省钱一个保守但实用的配置可以从这里开始compression: enabled: true threshold: 0.50 target_ratio: 0.20 protect_last_n: 20 in_place: true abort_on_summary_failure: false auxiliary: compression: model: context window 不小于主模型的模型几条判断比参数本身更重要。第一辅助压缩模型窗口必须够大。这比换一个更便宜的摘要模型更重要。第二大上下文模型如果主要目标是省钱可以适当降低threshold小窗口模型不用强行调代码里已经有 85% 的退化保护。第三近期任务依赖工具输出和细粒度代码状态时可以提高target_ratio或protect_last_n。压得狠会省 token但更容易丢活跃任务细节。第四建议开启in_place。除非你明确依赖旧的会话轮转语义否则原地压缩更符合长会话的直觉。第五看到压缩告警不要忽略。Session compressed N times说明会话质量可能开始降Compression skipped说明窗口里缺少可压空间Context compression aborted往往是辅助模型、凭证或网络出了问题。源码索引文件关注点agent/context_engine.pyContextEngine抽象与生命周期agent/context_compressor.py压缩主算法、边界对齐、摘要生成、防抖agent/conversation_compression.py会话压缩落库、原地压缩、会话轮转、模型可行性探测agent/turn_context.py预检压缩入口agent/conversation_loop.py响应后压缩与错误恢复压缩agent/prompt_caching.pyAnthropicsystem_and_3缓存策略gateway/run.pyGateway 会话卫生安全网agent/model_metadata.pytoken 粗估包含 tool schemasagent/auxiliary_client.pyCodex gpt-5.5 窗口限制与阈值抬升推荐阅读Agent Tool Interface 架构拆解为什么好工具比强模型更决定成败Hermes Skill Runtime 架构拆解三层加载如何压住 Agent 上下文成本Agent 评测系统架构从指标分层到 GT/Judge 闭环的工程化落地团队落地 Agent 工程化 Loop 的一些必看小技巧Agent 工程化新底座用 CLI 契约层打通 HTTP 接口与业务能力