做过 AI Agent 开发的朋友,可能都有过这种体验:

对话一长,模型就开始「失忆」,要么上下文窗口不够用,要么响应越来越慢。

我最近在研究 Hermes Agent 的架构设计,发现它在上下文处理上有一套很有意思的设计:分层系统提示 + 智能上下文压缩。这套方案解决的不只是「塞不进去」的问题,而是让 Agent 在长对话中依然保持高效的推理能力。

今天就来详细拆解一下 Hermes 的上下文处理机制,看看它是怎么做到的。

[[IMG: Digital transformation concept]]


一、为什么上下文处理这么重要?

做 Agent 开发的人都知道,大模型的上下文窗口是有限的。

拿 GPT-4o 来说,虽然有 128K tokens 的上下文,但实际使用时,超过 4 万 tokens 后响应速度就会明显下降。而 Claude 3.5 虽然有 200K tokens,但如果每次都把所有历史对话塞进去,前缀缓存的效率也会大打折扣。

更关键的问题是:对话越长,无关的历史信息就越多,模型越容易被「噪音」干扰,反而影响输出质量。

所以,上下文处理的核心目标不是「塞更多」,而是「塞得更准」

Hermes 在这个问题上,设计了一套完整的解决方案。

[[IMG: Technology trend concept]]


二、分层系统提示:让缓存命中更高效

Hermes 的系统提示构建,采用了三层架构

┌─────────────────────────────────────────────────────────────┐
│ Stable Layer(稳定层)— 每个会话构建一次,缓存复用          │
│  - 身份标识、工具指导、技能提示、环境提示                    │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Context Layer(上下文层)— 依赖 cwd                        │
│  - AGENTS.md、.cursorrules 等项目配置文件                   │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Volatile Layer(变动层)— 每轮都变化,**永不缓存**         │
│  - 内存快照、用户配置、时间戳                               │
└─────────────────────────────────────────────────────────────┘

这样做的好处是什么?

第一,稳定层最大化前缀缓存命中。身份定义、能力说明这些内容每个会话都一样,构建一次就缓存起来,所有后续 API 调用都能复用。对于支持前缀缓存的提供商(比如 Anthropic),这能显著降低延迟和成本。

第二,变动层独立管理。内存快照、用户配置这些每轮都可能变化的内容单独管理,不会污染缓存。

第三,按需重建。只有压缩事件触发后,才重建系统提示,其他时候保持缓存。

这套设计的精髓在于:把「不变」和「变」分离,让缓存真正发挥作用

[[IMG: Business chart and graph]]


三、ContextCompressor:智能上下文压缩

如果说分层提示是「前端优化」,那 ContextCompressor 就是「后端压缩」。

3.1 压缩算法的五步流程

def compress(self, messages, current_tokens=None, focus_topic=None, force=False):
    """压缩算法:
      1. 修剪旧工具结果(无 LLM 调用)
      2. 保护头部消息(系统提示 + 前 N 条)
      3. 保护尾部消息(按 token 预算)
      4. LLM 摘要中间轮次
      5. 迭代更新之前的摘要
    """

第一步:工具结果修剪。这一步最巧妙——它不调用 LLM,只是用规则去掉重复的、冗长的工具输出,把大型输出替换成信息性摘要。比如一个 ls 命令返回 100 个文件名,只需要保留「共 100 个文件」这样的摘要。

第二步和第三步:边界保护。头部保留系统提示和前 3 条消息,尾部按 token 预算保护最近的内容(约 20K tokens)。中间的部分才是真正需要压缩的区域。

第四步:LLM 摘要。用结构化模板让 LLM 生成中间轮次的摘要:

## Active Task        # 当前最重要的任务
## Goal              # 用户总体目标
## Constraints       # 约束和偏好
## Completed Actions # 已完成操作列表
## Active State      # 当前工作状态
## Key Decisions     # 重要技术决策
...

第五步:迭代更新。压缩后的摘要会保存下来,下次压缩时作为上下文提供,实现增量更新而非从头摘要。

3.2 压缩触发时机

Hermes 有三种压缩触发机制:

时机 触发条件 位置
预检压缩 用户切换到较小模型时 conversation_loop.py:474
轮次后压缩 API 响应后检测 token 使用率 conversation_loop.py:3553
错误触发 上下文窗口溢出等错误 error_classifier.py

关键设计:压缩阈值默认是 50%。当检测到 token 使用量超过上下文的 50% 时,触发压缩。这个比例可以配置,给了开发者很大的灵活性。

3.3 防抖动机制

连续两次压缩如果节省的 token 不足 10%,则跳过第二次压缩。这避免了在临界点反复压缩的性能损耗。

[[IMG: Data visualization dashboard]]


四、统一入口:所有 LLM 调用的上下文一致性

很多 Agent 实现中,不同模块的 LLM 调用各自处理上下文,容易出现不一致。Hermes 的做法是统一入口

def build_api_kwargs(agent, api_messages: list) -> dict:
    """Build the keyword arguments dict for the active API mode."""
    if agent.api_mode == "anthropic_messages":
        return _transport.build_kwargs(...)
    elif agent.api_mode == "bedrock_converse":
        return _bt.build_kwargs(...)
    elif agent.api_mode == "codex_responses":
        return _ct.build_kwargs(...)
    else:  # chat_completions (default)
        return _ct.build_kwargs(...)

所有 API 调用都经过 build_api_kwargs(),确保上下文处理的一致性。系统提示的缓存策略也在这个层面统一管理。

这个设计让我想到一个点:很多 Agent 实现里,system prompt 每次都重新构建。但 Hermes 的做法是把系统提示存到 SQLite 会话数据库里,跨进程恢复。这意味着即使用户关闭终端再打开,对话历史的上下文处理依然是连续的。

[[IMG: Abstract technology background]]


五、与传统方案的对比

特性 传统方案 Hermes
系统提示 每次构建 分层缓存,稳定层复用
压缩触发 手动判断 自动检测 token 使用率
压缩内容 简单截取 LLM 结构化摘要
压缩防抖 连续压缩节省 <10% 跳过
上下文入口 分散 统一 build_api_kwargs()
会话连续性 丢失 SQLite 持久化

传统方案的痛点是什么?往往是「上下文不够用了才想起来压缩」,而且压缩方式也简单粗暴——直接截取前面的历史。这种方式会丢失重要的中间上下文,而且压缩阈值不科学,有时候截了 50 条才发现 token 还是超了。

Hermes 的方案是预防性压缩 + 智能摘要。通过分层提示减少不必要的重复,通过阈值检测提前压缩,通过结构化摘要保留关键信息。

[[IMG: Network connections digital]]


六、这套设计给我的启发

研究 Hermes 的上下文处理机制,我有三点最深感受:

第一,「分离」比「统一」更灵活。分层提示的本质是把变化频率不同的内容分开管理。稳定的内容缓存复用,变化的内容独立更新。这种思路在很多场景都适用。

第二,「智能」比「规则」更高效。传统压缩是规则驱动(消息数、时间戳),Hermes 是智能驱动(token 使用率 + LLM 摘要)。虽然多了一次 LLM 调用,但压缩质量更高,后续调用的效率也更高。

第三,「一致性」比「灵活性」更可靠。统一入口的好处不只是性能优化,更重要的是保证所有 LLM 调用的上下文处理逻辑一致。分散的入口看起来灵活,实际上容易埋下 bug。


写在最后

上下文处理是 Agent 开发中的「隐形难题」。

表面上只是「塞进去、压缩掉」的操作,实际上涉及缓存策略、压缩算法、触发时机、入口统一等多个维度。Hermes 的方案不一定是唯一的正确答案,但它提供了一套可参考的设计思路

如果你也在做 Agent 开发,不妨思考一下:你的上下文处理,有没有做到「该缓存的缓存,该压缩的压缩,该统一的统一」?

好的架构设计,往往不是解决「能不能」的问题,而是解决「好不好」的问题。

[[IMG: Circuit board close up]]


本文基于 Hermes Agent 开源代码整理,仅供参考

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐