Skip to main content

第5章:上下文工程 —— 在有限窗口里保住任务连续性

一、上下文不是消息数组​

对一个长期 Agent 来说,“上下文”至少由五个来源组成:

静态身份 + 工具 schema + 项目上下文文件 + 当前会话历史 + 动态记忆/环境信息

这些来源拥有不同生命周期:工具 schema 可能随环境变化,skills 需要按需加载,记忆只在相关时注入,历史要持久化但可能压缩,环境提示要防止把宿主信息泄漏到远端执行环境。

因此 Hermes 将上下文做成策略层,而不是在 run_conversation 里拼一条长字符串。

二、系统提示词的组装顺序​

agent/system_prompt.py:152 的 build_system_prompt_parts 和 :549 的 build_system_prompt 是入口;它们依赖 prompt_builder.py 的多个构建器:

  1. Agent identity / personality;
  2. 当前平台和交互方式提示;
  3. 工具使用规则与工具说明;
  4. 环境 hints、cwd、终端 backend;
  5. SOUL.md、HERMES.md、AGENTS.md、.cursorrules 等上下文文件;
  6. skills manifest 和动态能力;
  7. Nous subscription / tool gateway 等条件能力说明。

prompt_builder.py 会沿当前目录向上寻找上下文文件,并做 frontmatter 去除、字符截断和来源标记。这里的核心策略是:指令可以继承,但每一层都要知道它来自哪里、能占多少预算。

三、动态记忆不直接改历史​

MemoryManager.prefetch_all 在 turn 开始前根据用户输入查询 MemoryProvider;conversation_loop 在 API call 处把结果放进当前 user message 的 api_content sidecar。干净的 messages 保持不变。

这样做解决了三个问题:

  • 记忆可能随 provider 延迟返回,不应改写持久化 transcript;
  • 重试同一 turn 时,应该发送同一份 composed content,而不是重复嵌套记忆;
  • 重新打开 session 时,历史仍是事实记录,不会混入当时动态召回的偶然结果。

四、ContextEngine:把策略抽象出来​

agent/context_engine.py:89 的 ContextEngine 抽象出:

  • should_compress / should_compress_info;
  • compress;
  • prune_tool_results_only;
  • select_context;
  • on_turn_complete、session start/end;
  • 当前模型变化和工具 schema。

这说明压缩并不是 Loop 的一个 if,而是可替换的 context policy。ContextCompressor(context_compressor.py:1312)是当前主实现,但接口允许将来加入不同的窗口策略。

五、什么时候压缩:真实 usage 与 preflight 的双保险​

压缩触发不能只依赖消息字符数。Hermes 同时使用:

  • 模型元数据中的 context length;
  • API 返回 usage 的 prompt tokens;
  • preflight 的粗略 token 估计;
  • 当前 tail budget、summary budget 和 anti-thrash guard;
  • 压缩失败冷却、连续 fallback 次数和 session 持久化状态。

ContextCompressor.update_from_response 在真实响应后更新 token 状态;should_compress_preflight 负责在发出很可能失败的请求前拦截。对于 token 估算不可靠的模型,should_defer_preflight_to_real_usage 允许等待真实 usage,避免过度压缩。

六、切哪里:不能把消息数组从中间随便砍​

工具调用要求 assistant tool call 和 tool result 成对;任务又要求保留最近的用户目标和最新的 assistant tail。因此压缩器有一组边界函数:

  • _align_boundary_forward/backward:把切点对齐到合法消息边界;
  • _find_last_user_message_idx、_find_last_assistant_message_idx:保护最近语义锚点;
  • _ensure_last_assistant_message_in_tail、_ensure_last_user_message_in_tail:避免压缩后没有可继续的当前任务;
  • _sanitize_tool_pairs:修复或丢弃不完整的工具对。

七、摘要不是自由发挥,而是交接协议​

ContextCompressor._generate_summary 会把旧 turn 序列化后交给辅助模型;失败时还可以 _build_static_fallback_summary。摘要的有效性不只看“有没有文字”,还会检查:

  • 是否有真实 user turn,防止合成摘要伪造用户意图;
  • 是否保留目标、约束、进度、决定和下一步;
  • 是否记录相关文件路径;
  • 是否保留 skill 的 pruned/ghost marker;
  • 是否能在下一轮被识别为 context summary。

这是一个很重要的设计:摘要是下一阶段 Agent 的 handoff 文档,而不是聊天记录的文学改写。

八、Micro-compaction 与大压缩​

Hermes 不只有“大压缩”。context_compressor.py 后半部分实现 micro-compaction:针对单个 exchange 或过长 tool result 做滚动摘要/拼接,尽量不触发一次昂贵的全局摘要。

可把两者理解成:

策略作用域成本风险
tool result prune单个结果低丢失局部输出
micro-compaction一小段历史中需要维护 cursor/marker
full compaction多个旧 turn高摘要错误会影响任务连续性

测试文件名中的 anti_thrash、tool_pair_floor、summary_continuity、zero_user_provenance,正好对应这些容易出错的边界。

九、为什么压缩失败也要有状态机​

如果每次压缩失败都立即再试,长会话会进入“压缩失败 → 继续膨胀 → 再失败”的死循环。ContextCompressor 持久化 fallback streak、ineffective count、failure cooldown,并允许 fallback 到主模型或静态摘要。

所以压缩是:

观察 usage → 判断阈值 → 选择切点 → 摘要/裁剪
→ 校验摘要来源 → 写入 session → 更新 guard
→ 下一轮重新计算,而不是相信一次结果

十、可迁移的三条原则​

  1. 上下文工程同时包含加法和减法:加载相关信息,也要删除过期或过大的信息。
  2. 合法切点比最大压缩率重要:不破坏 tool pair、用户目标和当前 tail,才不会把 Agent 压缩成失忆状态。
  3. 压缩结果必须可验证、可恢复、可观测:摘要 metadata、session entry、telemetry 和 fallback guard 缺一不可。