<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Context-Compression | 有志者事竟成</title><link>https://www.liwenshen.com/tags/context-compression/</link><atom:link href="https://www.liwenshen.com/tags/context-compression/index.xml" rel="self" type="application/rss+xml"/><description>Context-Compression</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>zh</language><lastBuildDate>Thu, 02 Jul 2026 09:00:00 +0800</lastBuildDate><image><url>https://www.liwenshen.com/media/icon_hu_dd5d76fef920c49e.png</url><title>Context-Compression</title><link>https://www.liwenshen.com/tags/context-compression/</link></image><item><title>上下文压缩：让 Agent 在有限窗口里「记得住」</title><link>https://www.liwenshen.com/note/hermes-agent-engineering/04-context-compression/</link><pubDate>Thu, 02 Jul 2026 09:00:00 +0800</pubDate><guid>https://www.liwenshen.com/note/hermes-agent-engineering/04-context-compression/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;系列：通过 Hermes 探秘 Agent 工程 | 第 4 篇&lt;/strong&gt;
上一篇：&lt;a href="https://www.liwenshen.com/note/hermes-agent-engineering/03-system-prompt/"&gt;System Prompt：身份、上下文与策略的三层架构&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="问题上下文是有价商品"&gt;问题：上下文是&amp;quot;有价商品&amp;quot;&lt;/h2&gt;
&lt;p&gt;第二个系列文章里聊过，系统提示词是 Agent 的&amp;quot;宪法&amp;quot;——它告诉模型你是谁、能做什么、处于什么环境。但&lt;strong&gt;宪法只占上下文窗口的一部分&lt;/strong&gt;，更大的部分是对话历史本身。&lt;/p&gt;
&lt;p&gt;一个典型的 Agent 任务：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户说：帮我写一个 Python 脚本来解析日志文件&lt;/li&gt;
&lt;li&gt;Agent 运行 &lt;code&gt;write_file&lt;/code&gt; 生成脚本&lt;/li&gt;
&lt;li&gt;Agent 运行 &lt;code&gt;terminal&lt;/code&gt; 执行脚本&lt;/li&gt;
&lt;li&gt;脚本报错了&lt;/li&gt;
&lt;li&gt;Agent 运行 &lt;code&gt;read_file&lt;/code&gt; 查看错误行&lt;/li&gt;
&lt;li&gt;Agent 运行 &lt;code&gt;patch&lt;/code&gt; 修改脚本&lt;/li&gt;
&lt;li&gt;Agent 再次执行……&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到第 7 步时，上下文里已经有十几条消息，每条消息都带着工具的输入和输出。如果日志文件有几 MB，一条 &lt;code&gt;read_file&lt;/code&gt; 的 tool result 就能吃掉几千 token。&lt;/p&gt;
&lt;p&gt;对于一个 200K token 的窗口，如果不做干预，大约 30-50 轮工具调用就会把窗口撑满。之后要么被 Provider 拒绝（400 错误），要么被迫截断历史——而截断意味着丢失上下文，Agent 的表现会急剧下降。&lt;/p&gt;
&lt;p&gt;Hermes 的解决方案是&lt;strong&gt;上下文压缩&lt;/strong&gt;——不是简单丢弃旧消息，而是用一段结构化的摘要替换掉中间部分，保留关键信息、释放空间。&lt;/p&gt;
&lt;h2 id="压缩的三个核心问题"&gt;压缩的三个核心问题&lt;/h2&gt;
&lt;p&gt;在动手之前，先想清楚三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;什么时候压缩？&lt;/strong&gt; 对话进行到什么程度才需要压缩？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩哪部分？&lt;/strong&gt; 全部压缩还是只压缩一部分？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;怎么压缩？&lt;/strong&gt; 摘要应该包含什么内容？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hermes 的实现逻辑清晰，每一层都有明确的工程判断。&lt;/p&gt;
&lt;h2 id="phase-1要不要压缩"&gt;Phase 1：要不要压缩&lt;/h2&gt;
&lt;p&gt;Hermes 不会每轮对话都检查是否需要压缩（太浪费 token），而是在&lt;strong&gt;每次收到 API 响应后&lt;/strong&gt;，通过 Provider 返回的 &lt;code&gt;usage.prompt_tokens&lt;/code&gt; 来判断当前对话占用了多少 token。&lt;/p&gt;
&lt;p&gt;如果这个数字低于阈值（默认 50% 的上下文窗口），什么都不做。一旦超过阈值，就触发压缩。&lt;/p&gt;
&lt;p&gt;——这里有个精妙的地方：Hermes 用 &lt;strong&gt;Provider 实际报告的数字&lt;/strong&gt;，而不是自己估算。这意味着即使你的估算模型不准（比如图片 token 计算有偏差），最终也是按 Provider 实际计费来判断的，不会出现&amp;quot;预估没超但实际超了&amp;quot;的情况。&lt;/p&gt;
&lt;h2 id="phase-2确定切割边界"&gt;Phase 2：确定「切割边界」&lt;/h2&gt;
&lt;p&gt;压缩的核心原则是：&lt;strong&gt;保护头尾，只压中间&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;头部&lt;/strong&gt;：系统提示词 + 前几条用户消息。这部分包含任务最初的上下文，不能压。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尾部&lt;/strong&gt;：最近 ~20K token 的对话。这部分是刚刚发生的事，Agent 需要它来继续工作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中间&lt;/strong&gt;：最早的历史对话，已经被尾部&amp;quot;盖过去&amp;quot;了——这是可以压缩的部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但切边界不是简单的&amp;quot;从头数 N 条消息&amp;quot;。Hermes 有几个特殊处理：&lt;/p&gt;
&lt;h3 id="工具调用组不能切断"&gt;工具调用组不能切断&lt;/h3&gt;
&lt;p&gt;Agent 的对话里，一条 assistant tool_calls 消息后面一定跟着几条 tool result 消息（每个工具调用一条结果）。如果在中间切断——tool_calls 被压掉了，tool result 还留着——Provider 会收到一个&amp;quot;没有对应 tool_call 的 tool_result&amp;quot;，返回 400 错误。&lt;/p&gt;
&lt;p&gt;所以压缩切边界的时候，必须保证每一对 &lt;code&gt;tool_calls ↔ tool_results&lt;/code&gt; 要么整个保留、要么整个压缩。Hermes 把这个叫做 &lt;strong&gt;boundary alignment&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="最近一条用户消息不能被压进去"&gt;最近一条用户消息不能被压进去&lt;/h3&gt;
&lt;p&gt;如果压缩算法不小心把用户最新的一条消息切到了&amp;quot;中间区域&amp;quot;，这段对话就会被写进摘要。摘要的开头有一行明确指令：&amp;quot;&lt;strong&gt;只响应摘要之后的消息，不要响应摘要里的请求&lt;/strong&gt;&amp;quot;——但用户最新的一条消息和摘要里的消息可能混在一起，模型可能误以为这条新消息也需要&amp;quot;响应&amp;quot;，导致 Agent 卡在历史任务上出不来。&lt;/p&gt;
&lt;p&gt;所以 Hermes 有一个强制回路：无论 token 预算怎么算，&lt;strong&gt;最近一条用户消息必须在尾部区域&lt;/strong&gt;。如果算出来的切割点会把它压在中间，就把切割点往前拉，直到这条消息落在尾部。&lt;/p&gt;
&lt;p&gt;同理，最近一条 assistant 可见回复（用户最后看到的那个回复）也不能被压进去——否则用户在 WebUI 里会突然看到一条&amp;quot;Context Compact&amp;quot;的占位符，而看不到刚刚的回复。&lt;/p&gt;
&lt;h2 id="phase-3生成结构化摘要"&gt;Phase 3：生成结构化摘要&lt;/h2&gt;
&lt;p&gt;切割点确定后，中间区域会被提取出来，送进一个&lt;strong&gt;辅助 LLM 模型&lt;/strong&gt;生成摘要。&lt;/p&gt;
&lt;p&gt;但在送进摘要模型之前，Hermes 对中间区域的工具输出做了&lt;strong&gt;三层瘦身&lt;/strong&gt;。这三层发生在摘要模型调用之前，是免费的（不需要额外 LLM 调用）。&lt;/p&gt;
&lt;h3 id="第一层工具结果替换为一行描述"&gt;第一层：工具结果替换为一行描述&lt;/h3&gt;
&lt;p&gt;对中间区域里每一条 tool result（tool role 的消息），如果内容超过 200 字符，就用一段自动生成的描述替代：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[terminal] ran `git log --oneline -20` -&amp;gt; exit 0, 20 lines output
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[read_file] read /project/src/main.py from line 1 (15,200 chars)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[search_files] content search for &amp;#39;compress&amp;#39; in agent/ -&amp;gt; 12 matches
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[web_extract] https://example.com/docs (8,400 chars)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不是手动模板——Hermes 根据每种工具的类型自动选择合适的折痕格式。每种工具都有一个摘要模板：terminal 显示 exit code + 行数，read_file 显示路径 + 长度，show_command 显示匹配数量……&lt;/p&gt;
&lt;h3 id="第二层内容截断每消息上限-6000-字符"&gt;第二层：内容截断（每消息上限 6000 字符）&lt;/h3&gt;
&lt;p&gt;经过第一层替换后，剩下的长文本（比如用户消息、assistant 的文本内容）在送到摘要模型时会被截断到 6000 字符。具体是头 4000 + 尾 1500，中间用 &lt;code&gt;...[truncated]...&lt;/code&gt; 替代。&lt;/p&gt;
&lt;p&gt;这意味着：一条 50,000 字符的日志文件，先被折痕替代（如果它是 tool result），之后作为中间区域的内容又被截断到 6000 字符——只有两端留下来了。&lt;/p&gt;
&lt;h3 id="第三层图片清理"&gt;第三层：图片清理&lt;/h3&gt;
&lt;p&gt;中间区域里的 base64 图片消息会被替换成文字占位符（如 &lt;code&gt;[Attached image — stripped after compression]&lt;/code&gt;），只保留最新的一张 user 消息里的图片。&lt;/p&gt;
&lt;p&gt;这一层专门处理 agent 使用 computer_use 时产生的大体积截图——不卸载就会每次请求都携带几 MB 的数据。&lt;/p&gt;
&lt;h3 id="三层瘦身让我能得到什么"&gt;三层瘦身让我能得到什么？&lt;/h3&gt;
&lt;p&gt;一个典型的例子：假设中间区域有 20 条工具调用结果，平均每次 5000 字符，总计 100,000 字符。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一层后：每条工具结果被 1-2 行描述替代，总计 ~80 字符 → 缩减 99.9%&lt;/li&gt;
&lt;li&gt;第二层后：如果有超长 assistant 思考内容（超过 6000 字符），被截断&lt;/li&gt;
&lt;li&gt;第三层后：如果有 base64 图片，被替换&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以摘要模型实际看到的可能是 2000-3000 字符的&amp;quot;精炼版历史&amp;quot;，而不是 100K+ 的原始对话。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="hermes-摘要里丢失了什么thinking-不设防"&gt;Hermes 摘要里丢失了什么？thinking 不设防&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;_serialize_for_summary&lt;/code&gt; 函数只读取两类字段：&lt;code&gt;content&lt;/code&gt; 和 &lt;code&gt;tool_calls&lt;/code&gt;。这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 用户消息（完整度视截断情况）&lt;/li&gt;
&lt;li&gt;✅ 工具调用名称和参数&lt;/li&gt;
&lt;li&gt;✅ 工具结果（折痕替代后的短描述）&lt;/li&gt;
&lt;li&gt;❌ thinking / reasonig_content —— 直接不在摘要范围内&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说：你之前对话中模型的思考链（reasoning content）不会被写入摘要，它直接就&amp;quot;消失&amp;quot;了。&lt;/p&gt;
&lt;p&gt;这是一个取舍：思考链可能很长（几千甚至上万 token），但对后续对话的价值相对有限。Hermes 选择不携带 thinking 内容跨摘要——如果模型在下次对话中需要这些信息，它可以通过工具重新读取文件来获取。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="工具对完整性的强制保护"&gt;工具对完整性的强制保护&lt;/h2&gt;
&lt;p&gt;摘要生成完成后，压缩的消息列表里可能出现一种&amp;quot;非法状态&amp;quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一条 &lt;code&gt;assistant&lt;/code&gt; 消息里有 &lt;code&gt;tool_call&lt;/code&gt;，但对应的 &lt;code&gt;tool_result&lt;/code&gt; 在摘要中被截断了&lt;/li&gt;
&lt;li&gt;一条 &lt;code&gt;tool&lt;/code&gt; 消息里有 &lt;code&gt;tool_call_id&lt;/code&gt;，但对应的 &lt;code&gt;assistant(tool_calls)&lt;/code&gt; 被压缩到了摘要里&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种情况会导致 Provider 返回 400（&amp;ldquo;没有匹配的 tool_call&amp;rdquo; 或 &amp;ldquo;tool_result 找不到对应的 request&amp;rdquo;）。&lt;/p&gt;
&lt;p&gt;Hermes 通过 &lt;code&gt;_sanitize_tool_pairs()&lt;/code&gt; 解决这个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;删除孤立的 &lt;code&gt;tool_result&lt;/code&gt;（对应的 tool_call 没了）&lt;/li&gt;
&lt;li&gt;为 orphan &lt;code&gt;tool_call&lt;/code&gt; 插入 stub result：&lt;code&gt;[Result from earlier conversation — see context summary above]&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;确保每一条发送给 Provider 的消息列表永远是格式正确的。&lt;/p&gt;
&lt;p&gt;Hermes 的结构化摘要模板长这样：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;## Active Task
[用户最近一条未读消息——逐字包含原话]
## Goal
[用户这次对话的总体目标]
## Constraints &amp;amp; Preferences
[用户偏好、编码风格、重要约束]
## Completed Actions
1. READ config.py:45 — found bug [tool: read_file]
2. PATCH config.py:45 — fixed comparison [tool: patch]
3. TEST pytest — 47/50 passed [tool: terminal]
## Active State
[当前正在做什么]
## Blocked
[还没解决的错误或障碍]
## Key Decisions
[重要技术决策及其原因]
## Resolved Questions
[已经被回答过的问题——防止重复回答]
## Pending User Asks
[还没完成的请求——参考性质，不要主动去做]
## Relevant Files
[文件路径列表，每条一行]
## Remaining Work
[还有什么没做完——参考性质]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个模板不是随便写的——它背后的逻辑是：&lt;strong&gt;让模型在下次看到这段摘要时，能立刻理解&amp;quot;之前发生了什么、当前应该做什么&amp;quot;，而不会把旧任务当成新任务重新执行。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了防止模型把摘要里的旧请求当成新任务，摘要开头有一行关键的前缀指令：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;[CONTEXT COMPACTION — REFERENCE ONLY] ... 不要响应摘要里的任何请求，只响应摘要之后出现的最新消息。&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;摘要末尾还有一行 end marker，防止模型把摘要里的 &amp;ldquo;Active Task&amp;rdquo; 部分当成用户新消息读进去。&lt;/p&gt;
&lt;h3 id="时间锚定防止过期任务复活"&gt;时间锚定：防止&amp;quot;过期任务复活&amp;quot;&lt;/h3&gt;
&lt;p&gt;摘要里有一个被很多 Agent 忽略的设计：&lt;strong&gt;temporal anchoring&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果摘要里写着 &amp;ldquo;email John about the proposal&amp;rdquo;，模型可能会认为这是一个待办事项，重新执行一遍。但如果摘要里写着 &amp;ldquo;Sent the proposal email to John on 2026-07-02&amp;rdquo;，模型就知道这件事已经做过了。&lt;/p&gt;
&lt;p&gt;Hermes 在摘要模板里明确告诉.summary 模型：&lt;strong&gt;如果动作已经完成，用过去时 + 具体日期来写&lt;/strong&gt;。这样就不会把历史任务重新激活。&lt;/p&gt;
&lt;h3 id="迭代更新多次压缩不会丢失信息"&gt;迭代更新：多次压缩不会丢失信息&lt;/h3&gt;
&lt;p&gt;第一次压缩后，摘要替代了中间区域。第二次压缩时，需要把&amp;quot;第一次摘要 + 新的中间对话&amp;quot; 一起再压缩成一个更短的摘要。&lt;/p&gt;
&lt;p&gt;Hermes 的 &lt;code&gt;ContextCompressor&lt;/code&gt; 类保存了 &lt;code&gt;_previous_summary&lt;/code&gt; 字段——每次生成的摘要被&amp;quot;记住&amp;quot;，下次压缩时作为迭代更新的基础。摘要模型的 prompt 会变成：&amp;ldquo;这是上次压缩的摘要，这是新发生的对话，请更新摘要&amp;rdquo;。&lt;/p&gt;
&lt;p&gt;这确保了在 50 轮甚至 100 轮对话后，早期的关键信息（用户最初的目标、重要决策、文件路径）不会随着一次又一次的压缩而消失。&lt;/p&gt;
&lt;h2 id="phase-4组装与清理"&gt;Phase 4：组装与清理&lt;/h2&gt;
&lt;p&gt;压缩完成后，Hermes 把三部分拼成一个新的消息列表：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[头部消息（原样）]
[摘要消息]
[尾部消息（原样）]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后做几件清理工作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具对清理&lt;/strong&gt;：检查有没有&amp;quot;有 tool_call 但没有对应 tool_result&amp;quot;或反之的情况。如果有，要么插入一个占位 result，要么删除孤立的 result，确保消息列表永远合法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片清理&lt;/strong&gt;：老消息里的 base64 图片（可能几 MB 一条）会被替换成文本占位符。只保留最近一张 user 消息里的图片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据注入&lt;/strong&gt;：第一条系统提示词里会追加一个压缩说明，让模型知道&amp;quot;早期的对话已经被压缩成摘要了&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;session 分离&lt;/strong&gt;：压缩完成后，旧的对话历史会被归档（或结束），新的压缩后历史会启用一个新的 session_id（旋转模式）或在原 session 下更新（原地归档模式）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="两种压缩模式旋转-vs-原地归档"&gt;两种压缩模式：旋转 vs 原地归档&lt;/h2&gt;
&lt;p&gt;Hermes 实现了两种压缩后处理模式：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;旋转模式（legacy）&lt;/th&gt;
&lt;th&gt;原地归档模式（in-place）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;session_id&lt;/td&gt;
&lt;td&gt;新的子 session_id&lt;/td&gt;
&lt;td&gt;保持原来的 session_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;旧对话&lt;/td&gt;
&lt;td&gt;结束原 session，创建新 session&lt;/td&gt;
&lt;td&gt;原 session 内归档（active=0），替换为新消息列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索&lt;/td&gt;
&lt;td&gt;原对话仍在数据库里，可搜索恢复&lt;/td&gt;
&lt;td&gt;仍在数据库里，可搜索恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;优点&lt;/td&gt;
&lt;td&gt;干净、每段独立&lt;/td&gt;
&lt;td&gt;只有一个 session_id，不产生孤儿对话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缺点&lt;/td&gt;
&lt;td&gt;session_id 频繁变化&lt;/td&gt;
&lt;td&gt;实现更复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;默认使用旋转模式，但在配置 &lt;code&gt;compression.in_place: true&lt;/code&gt; 时切换到原地归档模式。原地归档模式下，整个对话保持同一个 session_id，不会因为压缩而产生&amp;quot;父 session / 子 session / 孙 session&amp;quot;的层级关系。&lt;/p&gt;
&lt;h2 id="并发压缩锁"&gt;并发压缩锁&lt;/h2&gt;
&lt;p&gt;一个 session 可能同时有多个 agent 实例在运行（比如主对话线程和 background_review 线程）。如果两者同时检测到 token 超过阈值并各自触发压缩，两个线程都会生成摘要、各自旋转 session_id——最终会产生两个并行的&amp;quot;子 session&amp;quot;，其中一个会成为孤儿。&lt;/p&gt;
&lt;p&gt;Hermes 使用一个基于 SQLite state.db 的压缩锁来解决这个问题：压缩前先获取锁，获取失败则放弃本次压缩，等另一边的压缩完成后再试。&lt;/p&gt;
&lt;h2 id="摘要失败怎么办"&gt;摘要失败怎么办？&lt;/h2&gt;
&lt;p&gt;辅助模型可能失败（网络超时、限流、模型不存在）。Hermes 的处理方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;主模型回退&lt;/strong&gt;：如果辅助模型不可用，会退回到用主模型来生成摘要——毕竟主模型是经过验证的，虽然贵一点但至少能用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定性摘要&lt;/strong&gt;：如果所有 LLM 都失败了（aux 和 main 都失败了），Hermes 会生成一个&lt;strong&gt;确定性摘要&lt;/strong&gt;——不依赖 LLM，直接从对话里提取：用户说了什么、调用了哪些工具有哪些文件、最后几条消息是什么。虽然不如 LLM 写的丰富，但至少能防止信息完全丢失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中止并保留&lt;/strong&gt;：如果配置了 &lt;code&gt;abort_on_summary_failure: true&lt;/code&gt;，或者错误类型是认证失败（401/403），Hermes 会&lt;strong&gt;放弃压缩，保持对话原样&lt;/strong&gt;——因为在这种情况下创建子 session 会让对话进入一个无法继续的状态，不如不生。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="什么时候触发压缩"&gt;什么时候触发压缩？&lt;/h2&gt;
&lt;p&gt;手动触发：用户发送 &lt;code&gt;/compress&lt;/code&gt; 命令时。&lt;/p&gt;
&lt;p&gt;自动触发：API 响应里的 &lt;code&gt;prompt_tokens&lt;/code&gt; 超过 &lt;code&gt;threshold_tokens&lt;/code&gt;（默认上下文窗口的 50%，最小不低于 64K token）。&lt;/p&gt;
&lt;p&gt;有一个&amp;quot;反复无效压缩&amp;quot;的保护：如果最近两次压缩各自只减少了不到 10% 的消息量，会自动跳过压缩——这说明对话里有一条巨大的消息（比如一次 terminal 输出几十万字符），压缩算法压不动它，再压缩也是浪费。&lt;/p&gt;
&lt;h2 id="工程启示"&gt;工程启示&lt;/h2&gt;
&lt;h3 id="1-分层保护是-agent-的标准配置"&gt;1. 分层保护是 Agent 的标准配置&lt;/h3&gt;
&lt;p&gt;重要的不是&amp;quot;压缩率&amp;quot;，是&amp;quot;保护什么&amp;quot;。头部（初始上下文）、尾部（最近对话）、工具对（消息合法性）——这三个边界必须守住了，再谈压缩。&lt;/p&gt;
&lt;h3 id="2-结构化摘要比自由摘要效果好得多"&gt;2. 结构化摘要比自由摘要效果好得多&lt;/h3&gt;
&lt;p&gt;给摘要模型一个模板（Active Task、Goal、Completed Actions…），比让它自由发挥写一段总结要稳定得多。模板确保每次压缩后关键信息都在固定的位置，下游的模型不需要&amp;quot;理解&amp;quot;摘要的格式——它只需要定位 &amp;ldquo;## Active Task&amp;rdquo; 这一行从哪开始。&lt;/p&gt;
&lt;h3 id="3-抽象边边界对齐能力"&gt;3. 抽象&amp;quot;边边界对齐&amp;quot;能力&lt;/h3&gt;
&lt;p&gt;在 Agent 工程中，很少有&amp;quot;单纯的数据处理&amp;quot;——你要考虑 tool_call / tool_result 的配对规则、system message 的特殊性、图片消息的大小代价。这些边界情况不是 bug，是 Agent 特有的业务逻辑。&lt;/p&gt;
&lt;h3 id="4-并发安全是底线"&gt;4. 并发安全是底线&lt;/h3&gt;
&lt;p&gt;多 Agent 共享 session 时，&amp;ldquo;两个线程同时做同一件事&amp;quot;是常见的故障模式。压缩锁不只是&amp;quot;防止出错&amp;rdquo;——它保证了即使有 background_review 在跑，主对话线程的压缩也能正确工作。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;上下文压缩不是简单的&amp;quot;旧消息塞进摘要&amp;quot;。它是一整套工程方案：&lt;strong&gt;分层保护 + 边界对齐 + 结构化摘要 + 迭代更新 + 图片清理 + 并发锁 + 降级策略&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Hermes 的设计让一个 200K token 的窗口可以支撑数十甚至上百轮对话。关键在于：压缩是有损的，所以必须让这个&amp;quot;损&amp;quot;发生在对话中已经&amp;quot;过期&amp;quot;的部分，而不能损到最近的消息和最初的系统提示。&lt;/p&gt;
&lt;p&gt;下一篇，我们将进入 Hermes 的记忆系统——Agent 如何跨会话持久化信息、如何让用户在下次打开 Hermes 时，&amp;ldquo;上次我们都聊了什么&amp;rdquo;。&lt;/p&gt;</description></item></channel></rss>