Hermes: Context Budget 与压缩策略

发布时间:2026/5/20 8:36:13

Hermes: Context Budget 与压缩策略 在 Agent 系统里“上下文”经常被误解成一个越大越好的容器模型窗口越大就把历史消息、文件内容、工具输出、项目规范全部塞进去。这个直觉只对了一半。真正生产可用的 Agent Harness 要解决的不是“能不能放下”而是“放进去以后模型还能不能稳定理解当前任务、还能不能遵守最新用户指令、还能不能保持工具调用序列合法、成本会不会失控”。Hermes 的源码给了一个很好的工程答案上下文管理不是单点功能而是一条链路。静态项目上下文由agent/prompt_builder.py发现、扫描、截断后放进系统提示动态会话上下文由run_agent.py在每轮对话中维护当消息历史接近模型窗口时默认的agent/context_compressor.py会把旧工具输出先做轻量剪枝再把中间历史总结成“只作参考”的交接摘要同时保护开头和最近的尾部消息。这个策略的关键不在于“压缩”两个字而在于尽量避免把当前任务、工具调用配对关系和系统安全边界压坏。为了让新手容易理解可以把 Context Budget 想成出差行李箱。你不能只问“箱子有多大”还要决定护照、钱包、电脑这些必须随身带旧衣服可以压缩打包已经看过的说明书可以只带摘要但当前正在处理的票据绝不能塞进托运行李后忘掉。Agent 的上下文也是如此。1. Context Engine先把“上下文策略”抽象成可替换部件Hermes 没有把压缩逻辑硬编码在主循环里而是先定义了一个上下文引擎抽象。源码位于agent/context_engine.pyclassContextEngine(ABC):last_prompt_tokens:int0threshold_tokens:int0context_length:int0compression_count:int0threshold_percent:float0.75protect_first_n:int3protect_last_n:int6abstractmethoddefupdate_from_response(self,usage:Dict[str,Any])-None:...abstractmethoddefshould_compress(self,prompt_tokens:intNone)-bool:...abstractmethoddefcompress(self,messages:List[Dict[str,Any]],current_tokens:intNone,)-List[Dict[str,Any]]:...这段代码说明了一个重要设计上下文压缩不是“某个 if 分支”而是一个可替换的引擎。run_agent.py只需要知道三个核心问题当前用了多少 token、是否应该压缩、压缩后消息列表是什么。至于内部是传统摘要、DAG、向量检索还是未来更复杂的长期上下文系统都可以通过同一个接口接入。对新手来说这里值得特别关注threshold_tokens和context_length。context_length是模型可用窗口threshold_tokens是触发压缩的阈值。生产系统通常不会等到窗口完全爆掉再处理因为很多模型在接近上限时会出现请求失败、工具输出被截断、响应不稳定等问题。提前触发压缩是为了给当前轮推理和工具调用留下安全余量。ContextEngine还预留了工具接口defget_tool_schemas(self)-List[Dict[str,Any]]:return[]defhandle_tool_call(self,name:str,args:Dict[str,Any],**kwargs)-str:importjsonreturnjson.dumps({error:fUnknown context engine tool:{name}})默认压缩器没有暴露工具但这个接口很有意义如果未来某种上下文引擎需要让模型主动搜索、展开、描述某段旧上下文它可以像普通工具一样挂进 Harness。也就是说Hermes 把“上下文管理”设计成一种运行时能力而不是仅仅靠 prompt 文案提醒模型“记住前面的话”。2. 静态上下文项目规则先扫描再截断再注入动态对话历史之外Agent 还需要项目级上下文比如AGENTS.md、.hermes.md、.cursorrules。这些文件通常包含编码规范、测试命令、项目约定。Hermes 在agent/prompt_builder.py里按优先级加载defbuild_context_files_prompt(cwd:Optional[str]None,skip_soul:boolFalse)-str:project_context(_load_hermes_md(cwd_path)or_load_agents_md(cwd_path)or_load_claude_md(cwd_path)or_load_cursorrules(cwd_path))源码注释里明确写着优先级.hermes.md / HERMES.md会从当前目录向上走到 git 根目录AGENTS.md、CLAUDE.md、.cursorrules则按当前目录规则加载并且“first found wins”也就是只选择一种项目上下文类型。这能减少系统提示膨胀避免多个规则文件互相冲突。更关键的是Hermes 不是简单把文件读进 prompt。它会先做注入扫描def_scan_context_content(content:str,filename:str)-str:findings[]forcharin_CONTEXT_INVISIBLE_CHARS:ifcharincontent:findings.append(finvisible unicode U{ord(char):04X})forpattern,pidin_CONTEXT_THREAT_PATTERNS:ifre.search(pattern,content,re.IGNORECASE):findings.append(pid)iffindings:logger.warning(Context file %s blocked: %s,filename,, .join(findings))returnf[BLOCKED:{filename}contained potential prompt injection (...). Content not loaded.]returncontent这段实现体现了 Context Budget 的另一个维度上下文不仅有大小风险还有安全风险。项目文件一旦被注入“忽略之前所有指令”“泄露环境变量”等内容它会进入系统提示区对模型有很强影响。Hermes 的处理方式是发现可疑模式后阻断内容加载并在上下文中放入一个阻断说明。如果项目上下文太长Hermes 使用头尾保留策略def_truncate_content(content:str,filename:str,max_chars:intCONTEXT_FILE_MAX_CHARS)-str:iflen(content)max_chars:returncontent head_charsint(max_chars*CONTEXT_TRUNCATE_HEAD_RATIO)tail_charsint(max_chars*CONTEXT_TRUNCATE_TAIL_RATIO)marker(f\n\n[...truncated{filename}: kept{head_chars}{tail_chars}fof{len(content)}chars. Use file tools to read the full file.]\n\n)returncontent[:head_chars]markercontent[-tail_chars:]为什么不是从头截到固定长度因为规则文件的前面常常有总体原则后面可能有最近追加的重要约定。头尾保留比单纯保留开头更稳。中间的 marker 也很重要它明确告诉模型“这里被截断了需要完整内容时用文件工具读取”避免模型把不完整上下文当成完整事实。3. 默认压缩器阈值、保护区与摘要预算Hermes 默认上下文引擎是ContextCompressor。它的初始化逻辑位于agent/context_compressor.pyclassContextCompressor(ContextEngine):def__init__(self,model:str,threshold_percent:float0.50,protect_first_n:int3,protect_last_n:int20,summary_target_ratio:float0.20,...):self.threshold_percentthreshold_percent self.protect_first_nprotect_first_n self.protect_last_nprotect_last_n self.summary_target_ratiomax(0.10,min(summary_target_ratio,0.80))self.context_lengthget_model_context_length(...)self.threshold_tokensmax(int(self.context_length*threshold_percent),MINIMUM_CONTEXT_LENGTH,)target_tokensint(self.threshold_tokens*self.summary_target_ratio)self.tail_token_budgettarget_tokens self.max_summary_tokensmin(int(self.context_length*0.05),_SUMMARY_TOKENS_CEILING,)这里至少有四个工程细节值得展开。第一默认threshold_percent是0.50也就是大约用到模型窗口一半时就开始考虑压缩。看起来保守但 Agent 的后续工具调用可能一次读入大文件、执行测试输出上千行日志不能等窗口快满才补救。第二threshold_tokens用max(..., MINIMUM_CONTEXT_LENGTH)做下限保护。对于小上下文模型过早压缩会造成“刚聊几句就总结”的体验对于大上下文模型百分比阈值又能让压缩随窗口规模自然增长。第三protect_last_n默认是 20但真正的尾部保护不只靠消息条数后面还会用 token budget 保护最近上下文。因为 20 条短消息和 20 条大文件读取结果完全不是一个量级。第四摘要预算不是无限的。max_summary_tokens被限制为上下文窗口的 5%并且最多 12000 token。摘要太短会丢事实摘要太长则压缩失去意义。生产 Agent 的压缩策略必须考虑收益而不是“总结得越详细越好”。4. 触发压缩不是机械判断Hermes 还防止“压缩抖动”最简单的触发条件是“当前 prompt token 超过阈值”。Hermes 的should_compress也是从这里开始但多了一层反抖动逻辑defshould_compress(self,prompt_tokens:intNone)-bool:tokensprompt_tokensifprompt_tokensisnotNoneelseself.last_prompt_tokensiftokensself.threshold_tokens:returnFalseifself._ineffective_compression_count2:logger.warning(Compression skipped — last %d compressions saved 10%% each. Consider /new to start a fresh session, or /compress topic for focused compression.,self._ineffective_compression_count,)returnFalsereturnTrue这段代码解决的是长会话中的常见故障如果最近两次压缩每次只节省一点点 token继续压缩会进入“压缩、仍然过大、再压缩、仍然过大”的循环。模型可能浪费大量调用在整理上下文上却无法推进任务。Hermes 记录压缩收益如果连续收益低于 10%就暂停自动压缩并提示用户考虑新会话或定向压缩。这说明 Context Budget 不是单纯的容量问题而是一个运行时控制问题。压缩本身也有成本它消耗模型调用、可能损失细节、可能改变消息角色结构。成熟的 Harness 必须知道什么时候压缩也必须知道什么时候不要继续压缩。5. 工具输出剪枝先做便宜处理再调用模型摘要在 Agent 会话中最容易撑爆上下文的往往不是用户消息而是工具输出。比如测试日志、文件读取结果、搜索结果、浏览器快照。Hermes 在真正调用模型生成摘要之前先对旧工具结果做一个无需 LLM 的轻量剪枝def_prune_old_tool_results(self,messages:List[Dict[str,Any]],protect_tail_count:int,protect_tail_tokens:int|NoneNone,)-tuple[List[Dict[str,Any]],int]:Replace old tool result contents with informative 1-line summaries.它不是简单把旧工具输出替换成“已删除”而是尽量保留可用信息。对应的_summarize_tool_result会根据工具类型生成一行摘要iftool_nameterminal:cmdargs.get(command,)exit_matchre.search(rexit_code\s*:\s*(-?\d),content)exit_codeexit_match.group(1)ifexit_matchelse?returnf[terminal] ran {cmd} - exit{exit_code},{line_count}lines outputiftool_nameread_file:pathargs.get(path,?)offsetargs.get(offset,1)returnf[read_file] read{path}from line{offset}({content_len:,}chars)iftool_namesearch_files:patternargs.get(pattern,?)pathargs.get(path,.)returnf[search_files] content search for {pattern} in{path}- ... matches这类摘要对模型非常有帮助。例如旧上下文里有一段npm test的完整日志后来只需要知道“跑过测试exit 0有 47 行输出”完整日志就不必一直占窗口。又如某个文件被重复读取五次保留最新完整结果、把旧重复结果替换成“同更近期调用重复”可以减少无意义 token。这一层设计体现了生产经验便宜、确定性的剪枝应当先做昂贵、可能引入摘要误差的 LLM 压缩应当后做。很多上下文管理失败是因为系统一上来就让模型总结全部历史既贵又不稳定。6. 保护头部和尾部系统边界与当前任务不能被压进摘要Hermes 的主压缩流程写在compress方法里defcompress(self,messages,current_tokensNone,focus_topicNone):# Phase 1: Prune old tool resultsmessages,pruned_countself._prune_old_tool_results(messages,protect_tail_countself.protect_last_n,protect_tail_tokensself.tail_token_budget,)# Phase 2: Determine boundariescompress_startself.protect_first_n compress_startself._align_boundary_forward(messages,compress_start)compress_endself._find_tail_cut_by_tokens(messages,compress_start)turns_to_summarizemessages[compress_start:compress_end]这里的“头部”和“尾部”不是随便选的。头部通常包含系统提示、最初用户意图、任务边界尾部包含最近用户消息、刚执行的工具结果、尚未完成的动作。中间历史才是最适合压缩的部分。_find_tail_cut_by_tokens不是简单保留最后 N 条而是从后往前累计 tokendef_find_tail_cut_by_tokens(self,messages,head_end,token_budgetNone)-int:iftoken_budgetisNone:token_budgetself.tail_token_budget min_tailmin(3,n-head_end-1)soft_ceilingint(token_budget*1.5)foriinrange(n-1,head_end-1,-1):msg_tokenslen(content)//_CHARS_PER_TOKEN10...ifaccumulatedmsg_tokenssoft_ceilingand(n-i)min_tail:break这个算法的意义是最近上下文按 token 预算保护同时至少保留少量尾部消息。它还会避免把工具调用和工具结果切开因为很多模型 API 要求 assistant 的tool_calls后面必须跟对应的 tool result。上下文压缩如果破坏了这个结构请求会直接失败。还有一个非常关键的保护最后一条用户消息必须留在尾部。def_ensure_last_user_message_in_tail(self,messages,cut_idx,head_end)-int:last_user_idxself._find_last_user_message_idx(messages,head_end)iflast_user_idx0:returncut_idxiflast_user_idxcut_idx:returncut_idxreturnmax(last_user_idx,head_end1)源码注释提到如果边界调整把最近用户请求压进摘要中间摘要会把它写成“待处理请求”但摘要前缀又告诉模型“只回答摘要之后的最新用户消息”结果当前任务可能被模型忽略。这是长会话 Agent 非常典型的问题用户刚说“继续”系统压缩后把“继续”总结掉了模型就不知道要继续什么。Hermes 用显式代码保证最近用户消息还在活跃尾部而不是只靠摘要描述。7. 摘要不是新指令Hermes 用前缀防止旧任务复活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...)这段文案看起来啰嗦但它解决的是模型行为问题。摘要里可能包含历史用户请求例如“帮我修复 A”“继续分析 B”“不要忘记 C”。如果没有明确边界模型可能把摘要里的旧任务当成当前任务重新执行造成重复工作甚至覆盖用户已完成的修改。Hermes 还会在系统消息里追加一段压缩说明ifi0andmsg.get(role)system:existingmsg.get(content)or_compression_note([Note: Some earlier conversation turns have been compacted into a handoff summary to preserve context space. ...])msg[content]existing\n\n_compression_note这相当于给后续模型调用建立一个全局事实当前消息列表不是原始完整历史而是经过压缩的历史。它提醒模型可以参考摘要和当前文件状态但不要重复已经做过的工作。8. 压缩后还要修消息合法性tool_call 与 tool_result 必须配对很多文章讨论上下文压缩时只讲摘要质量很少讲消息结构合法性。但生产系统里这点非常致命。Hermes 在压缩后调用_sanitize_tool_pairscompressedself._sanitize_tool_pairs(compressed)这个方法处理两个失败模式def_sanitize_tool_pairs(self,messages):# 1. A tool result references a call_id whose assistant tool_call was removed.# 2. An assistant message has tool_calls whose results were dropped.具体做法是如果某个 tool result 找不到对应 assistant tool call就删除这个孤儿结果如果某个 assistant tool call 的结果被压缩过程丢掉了就补一个占位 tool resultpatched.append({role:tool,content:[Result from earlier conversation — see context summary above],tool_call_id:cid,})这段实现对新手尤其有启发OpenAI 风格的工具调用消息不是普通聊天文本。它有协议约束。压缩器不能只看自然语言还必须维护消息序列的语法正确性。否则再好的摘要也没有意义因为下一次 API 调用会被拒绝。9. 压缩收益要回写状态形成闭环压缩结束后Hermes 会估算新消息列表 token并记录节省比例new_estimateestimate_messages_tokens_rough(compressed)saved_estimatedisplay_tokens-new_estimate savings_pct(saved_estimate/display_tokens*100)ifdisplay_tokens0else0self._last_compression_savings_pctsavings_pctifsavings_pct10:self._ineffective_compression_count1else:self._ineffective_compression_count0这与前面的should_compress形成闭环。一次压缩不是孤立动作它会影响下一次是否继续压缩。工程上这很重要因为 Agent 会话可能持续数十轮压缩策略必须能自我调节。如果压缩器发现自己已经“压不动”继续自动运行只会浪费成本。这个闭环也提醒我们评估上下文策略不能只看单次摘要是否通顺还要看长期会话中的稳定性。一个好的压缩器应该让任务继续推进而不是在压缩动作上反复打转。10. 从 Hermes 抽象出的 Context Budget 落地原则结合 Hermes 源码可以把 Context Budget 的落地方案总结为五条。第一静态上下文必须有入口控制。项目规则文件要做优先级选择、注入扫描和截断。否则规则越多系统越容易被冲突指令或恶意文本污染。第二动态上下文必须区分消息价值。系统提示、当前用户请求、最近工具结果是高价值区域旧工具输出、重复读取、历史日志是低价值区域。压缩策略要先处理低价值高体积内容。第三压缩边界必须尊重协议结构。工具调用和工具结果不能被随意切开最近用户消息不能被摘要吞掉。上下文压缩不是普通文本摘要它是在维护一个可继续执行的状态机。第四摘要必须标明权限等级。Hermes 的SUMMARY_PREFIX明确说摘要是 reference only不是 active instructions。这能降低旧任务复活、历史请求误执行的风险。第五压缩策略要有反馈。Hermes 用节省比例和 ineffective count 防止压缩抖动。生产系统还可以进一步记录压缩前后任务成功率、工具错误率、用户中断率用数据判断压缩是否真的帮助了任务。结语Context Budget 的核心不是“窗口不够就总结”而是让 Agent 在有限注意力内保持正确任务边界、足够事实依据、合法工具协议和可控成本。Hermes 的实现展示了一个务实路径静态上下文先过滤和截断动态历史先剪枝工具输出再保护头尾、总结中间、修复工具配对并用收益指标防止反复无效压缩。这套设计之所以值得学习是因为它把“长上下文”从模型能力问题转化成 Harness 工程问题。模型窗口再大也需要上下文治理而一个好的 Harness不是把所有东西都塞给模型而是持续判断当前任务真正需要什么哪些历史只需要摘要哪些内容必须保持原样哪些旧信息应该退出舞台。参考源码agent/context_engine.py上下文引擎抽象接口。agent/context_compressor.py默认压缩器、工具输出剪枝、头尾保护、摘要前缀、工具配对修复。agent/prompt_builder.py项目上下文文件发现、注入扫描、头尾截断。run_agent.py上下文引擎初始化、会话消息维护、压缩触发点。

相关新闻