Agent 上下文治理:从"定时清空垃圾桶"到"持续耕耘知识矿脉"

一、上下文管理的本质困境

如果你用过 coding agent,一定经历过这个场景:对话进行到关键时刻,agent 突然停下来——「正在压缩上下文,请稍候…」几分钟后它回来了,但已经忘了刚才在讨论什么。

这不是 bug。这是将上下文管理简化为一招「压缩」的必然结果。

压缩的四重罪

┌──────────────────────────────────────────────────────┐
│  对话增长 → 超出窗口 → LLM 压缩 → 丢弃原文           │
│     ↑                                        ↓        │
│     └──── 再溢出 ← 更多对话 ← 摘要不完整 ←────┘       │
└──────────────────────────────────────────────────────┘

用压缩应对上下文增长,本质是在和时间赛跑,而且注定会输:

问题 表象 根因
中断用户 agent 暂停 30 秒做 compaction 压缩是同步的,用户必须等
丢失信息 「我们为什么选 SQLite?」→ 永远消失了 压缩不可逆,摘要替代原文
缓存报废 每次压缩后 Anthropic 缓存命中率归零 prompt 前缀变化 = 缓存全部重置
无法跨会话 新会话从零开始,不记得任何事 压缩是本地的,不产生持久知识

把这四个问题拆开看,你会发现它们指向同一个根本缺陷:把上下文当成一个需要清空的垃圾桶,而不是一块需要耕耘的土地。

每一个对话回合——你解释的一个架构决策、agent 发现的一个配置陷阱、你们争论后达成的命名约定——都是知识。压缩只是把它们碾碎成摘要然后扔掉。次数多了,会话就从「长期合作关系」退化成「失忆症患者」。

压缩 vs 治理:两种哲学

维度 压缩(Compaction) 治理(Governance)
目标 减少 token 数量 管理知识生命周期
时间范围 一次性操作 持续运行的多时间尺度系统
缓存影响 每次操作都可能 bust 操作精确对齐到自然缓存失效边界
信息保存 摘要替代原文 分层:原文 → compartment → decay → archive
跨会话 无 记忆在会话间持久化并持续维护
用户感知 暂停 → 等待 compaction 零感知,全部后台运行

本文将以 Magic Context 为例,系统拆解一个完整的 Agent 上下文治理方案:它如何在不中断用户、不丢失知识、不报废缓存的前提下,让一个会话持续运行数周甚至数月。


二、第一性原理:上下文是时间序列问题

在设计任何上下文治理方案之前,需要先厘清基础约束。这些约束不是可选的——它们是物理定律级别的:

约束一:上下文窗口有限。 即使 200K token 也会用完。更糟的是,提供商的缓存窗口有 TTL——Claude Pro 5 分钟,Max 1 小时。缓存的 token 比未缓存便宜 90% 以上。

约束二:用户不能被中断。 任何同步的 compaction 操作都会暂停 agent,破坏工作流的连贯性。对长时间自主运行的 agent 来说,这尤其致命。

约束三:旧信息可能在未来关键时刻变得重要。 一个六个月前的技术选型讨论,可能在今天的重构决策中是唯一可靠的上下文。

约束四:每次无谓的 prompt 前缀变化都是一笔隐形账单。 修改 m[0](前缀)的任何字节,整个 Anthropic prompt cache 全部报废。

这四条约束相互对抗,形成了一个经典的「治理」问题——不是单次操作能解决的,需要一套持续运行的调度系统。Magic Context 的答案是把它拆成三个时间尺度:

// magic-context.jsonc 核心调度参数
{
  "cache_ttl": "5m",                      // 缓存有效期,匹配 provider 的缓存窗口
  "execute_threshold_percentage": 65,      // 上下文使用率达此值时强制执行排队操作
  "history_budget_percentage": 0.15,       // 保留给历史块的上下文比例
  "protected_tags": 20                     // 不会被立即丢弃的最新 N 个标签
}

这三个参数定义了一个「缓冲区」:在缓存仍然存活时,把所有修改操作排队;等缓存自然过期后,一次性执行所有排队操作。这避免了为每个单独操作都报废一次缓存。


三、三层时间架构:实时 / 异步 / 离线

把上下文治理拆成三个时间尺度的子系统,是 Magic Context 架构的核心创新:

┌──────────────────────────────────────────────────────────┐
│ 实时层 — 每轮 transform pass,< 1ms,零 LLM 调用        │
│   标签打标 → 丢弃排队 → 启发式清理 → 紧急溢出回收        │
├──────────────────────────────────────────────────────────┤
│ 异步层 — historian 触发,秒到分钟级,可用便宜模型        │
│   分段历史 → 分层 compartment → 事实提取 → 项目记忆       │
├──────────────────────────────────────────────────────────┤
│ 离线层 — dreamer 调度,小时到天级,夜间运行              │
│   记忆合并/验证/归档 → docs 维护 → key files 识别         │
└──────────────────────────────────────────────────────────┘

3.1 实时层:标签系统与延迟执行

每一轮对话,Magic Context 给每个消息、工具调用、文件引用打上 §N§ 标签。这些标签不是装饰——它们是缓存安全的内容寻址系统的核心。

关键设计:丢弃操作不立即执行。 当 agent 调用 ctx_reduce 标记某段内容可丢弃时,该标签只是进入了一个队列。队列里的内容在下一次缓存自然过期时才真正从 prompt 中移除。

这个「延迟」是刻意的。因为如果在缓存命中时执行丢弃,改变的字节会立即报废整个缓存前缀。而等到缓存自然过期时,所有排队操作可以一次性执行——一次报废,完成所有累积的清理。

这引出整个系统最核心的概念:Pass 分类学。

Pass 类型 触发条件 m[0] 状态 m[1] 状态 缓存影响
SOFT+ (defer) 上下文压力低,缓存仍有效 字节级不变 字节级不变 零影响
SOFT (execute) 缓存 TTL 过期或压力达阈值 字节级不变 重新渲染增量 仅 bust 到 m[1] 断点
HARD (fold) 模型切换、系统 prompt 变化、长时间空闲 重新物化(合并 m[1]) 重置为空 整个前缀重建

核心不变量:SOFT+ pass 必须字节级完全复现。任何内容的首次丢弃/剥离都必须发生在 cache-busting pass 上,defer pass 只重放已冻结的 ID 集合。

3.2 异步层:Compartment 系统

Historian 是一个后台运行的子 agent,负责将原始对话压缩成结构化 compartment。但与普通压缩不同,它产出的不是一段不可逆的摘要,而是多层 fidelity 的、确定性可渲染的历史块。

每个 compartment 携带:

  • 4 个 paraphrase tier(P1 详细 → P4 仅锚点)——同一个 compartment 在不同上下文压力下渲染不同详略
  • importance 分数——决定 decay 速率(重要对话 fading 更慢)
  • episode_type——code_change / discussion / debugging 等分类
  • facts 块——按 5 类别分类法提取的持久知识

Historian 的触发也是智能的,不是简单地等窗口满了就触发:

// historian 触发条件(伪代码)
const shouldFire = (
  // 条件1: 压力触发 — 未总结的 raw tail 超过阈值
  unsummarizedTailTokens >= contextLimit * executeThreshold * 0.05  // 5K-50K token 范围
  // 条件2: commit 簇触发 — 检测到完整工作单元
  || commitClustersInTail >= 3
  // 条件3: tail-size 触发 — 大量未处理内容
  || unsummarizedTailTokens >= triggerBudget * 3
);

3.3 离线层:Dreamer — agent 的睡眠

Historian 负责会话内的知识聚合。Dreamer 负责跨会话、跨时间的知识维护。它的七个任务全在夜间窗口(默认 02:00-06:00)执行:

任务 做什么 为什么需要
consolidate 合并语义重复的记忆 同一事实被记录三次 → 三倍 token 浪费
verify 对照当前代码库验证记忆 代码重构了,旧约束已过时
archive-stale 归档过时记忆 「使用 React 16」在升级到 18 后是误导
improve 精简冗长记忆 叙述式长句 → 运维级一句话
maintain-docs 维护 ARCHITECTURE.md / STRUCTURE.md 代码改了,架构文档自动跟上
user_memories 从多轮对话提取用户行为模式 沟通风格、审查偏好、工作节奏
pin_key_files 识别 agent 反复阅读的文件 每次上下文重载都要重读 → 直接注入

因为 dreamer 在空闲时间运行,所以可以使用本地甚至很慢的模型——没人等着。


四、Decay 渲染:确定性历史衰退

这是 Magic Context 区别于所有 LLM 压缩方案的核心机制。

LLM 压缩的根本问题:不可重现。同一个对话,每次压缩的结果都略有不同。结果不同 → prompt 字节不同 → 缓存报废。这意味着你越压缩,缓存越差——恶性循环。

Magic Context 的 decay 渲染替代了 LLM 压缩。它是一个纯数学函数,零 LLM 调用:

// Decay half-life 公式
half_life = H50 × 2^((importance − 50) / D) / max(budget_pressure, 0.10)

其中:
  H50 = 24 (基准半衰期 24 轮)
  D   = 25 (重要性衰减系数)
  budget_pressure = 当前历史块 token 使用率

这个公式决定了每个 compartment 在哪个 tier 渲染:

Tier 半衰期范围 内容详略 触发条件
P1 0 – 0.201 完整叙事 + U:/A: 行 最近 + 高重要性
P2 0.201 – 0.729 精简叙事 + 删掉低信号 U: 行 age 适中
P3 0.729 – 1.322 段落摘要 age 较大
P4 1.322 – 2.587 仅核心锚点 age 很大或压力很高
P5 > 2.587 归档(仅占位,ctx_expand 可恢复) 极老

关键洞察:因为是纯数学函数,相同的 compartment 集合永远渲染出相同的文本。 你停了一周回来,同一个 <session-history> block 的字节和一小时前一模一样。缓存是绝对稳定的。

还有一个巧妙的「愈合」机制:historian 生成最后一个 compartment 时,如果该 compartment 的边界恰好在 chunk 边缘(缺乏前瞻上下文),这个 compartment 会被丢弃而不是持久化。下一次 historian 运行时会从那个边界重新读取,带着完整的前瞻上下文重新生成。


五、记忆系统:知识不是附加功能,是压缩的副产品

绝大多数「agent + 记忆」方案是把对话摘要存进向量数据库,然后语义检索。Magic Context 的做法不同:记忆是压缩的副产品。

Historian 为生成 compartment 必须读完所有对话。在同一个 pass 中,它顺便提取出值得永久保留的事实——零边际成本。

5.1 五类别分类法

所有记忆被归入 5 个互斥类别:

// ctx_memory 示例
ctx_memory(action="write", category="CONSTRAINTS",
  content="Dashboard Tauri 构建需要 RGBA PNG,不能是灰度图")
类别 示例 用途
PROJECT_RULES 「发版前必须跑完整测试套件」 操作规程
ARCHITECTURE 「Order 模块使用 Event Sourcing」 架构决策
CONSTRAINTS 「Pi 的 SQLite 绑定不能用数组形式」 硬约束
CONFIG_VALUES 「embedding 使用 local Xenova/MiniLM」 配置事实
NAMING 「plugin 使用 @cortexkit/ 前缀」 命名约定

5 个类别足够覆盖项目知识的所有维度,又不会让 agent 在「该归哪一类」上浪费决策时间。

5.2 m[0]/m[1] 双层槽:缓存友好的注入路线

记忆如何注入到每轮对话中,是一个容易被忽视但影响深远的设计决策。

普通的做法:把记忆插在 conversation 最前面。问题:每次新记忆都在改变整个提示前缀 → 缓存全部报废。

Magic Context 的解决方案是一个双层 slot 结构:

┌──────────────────────────────────────────┐
│ m[0] — 冻结基线                          │
│  <project-docs> + <user-profile>         │
│  + 已 decay 的 compartment 历史           │
│  在普通轮次中绝不变更                      │
├──────────────────────────────────────────┤
│ m[1] — 易失增量                          │
│  新记忆 + 新 compartment + key-files      │
│  只在 cache-busting pass 上更新            │
└──────────────────────────────────────────┘

m[0] 在绝大多数 pass 上保持字节级不变——这意味着 Anthropic 的 prompt cache 几乎永远存活。

有趣的是哪些不会触发 HARD fold:新的 compartment、user-profile 更新、project-docs 变更——这些只是 m[1] 的增量,不会专门为它们拆 m[0]。它们在下一次自然 HARD bust 时折叠进去。

5.3 三层回查

注入所有记忆是浪费,完全不注入是丢失。Magic Context 使用三层递进式回查:

层级 机制 Token 成本
自动注入 每轮将最相关的记忆注入 <session-history> ~4K budget
主动搜索 ctx_search 同时搜索记忆、对话历史、git commits agent 决定
延迟提醒 auto-search 低阈值被动提示 ~200 token

第三层特别精妙——它模拟人类「似乎想起点什么」的感觉,不占用 token,但足够让 agent 决定是否需要完整搜索。


六、紧急溢出:当一切失控时

即使有以上所有机制,如果用户连续疯狂操作,上下文压力可能达到临界点:

≥ 85% 使用率:force materialization,触发分层紧急丢弃。工具输出按重要性分级(T3 misc → T2 edit/search → T1 导航),从最旧的开始回收,但保留最近 20% 的 T1/T2 工具调用。最近被丢弃的工具调用保留 [dropped §N§] 骨架——维持 provider 的 tool-pairing 要求。

≥ 95%:紧急深度清理,零等待,立即回收空间。

Provider 溢出错误:解析 Anthropic/OpenAI/Copilot 的 context overflow 错误,提取实际窗口限制并持久化——后续 pass 使用更保守的值。

这个分层设计的关键:不会因为一次溢出就把整个会话毁了。被 drop 的工具输出可以通过 ctx_expand 恢复,被 remove 的消息在 historian 的 compartment 中有摘要保留。


七、缓存稳定性:不只是优化,是不变量体系

整个系统最令人敬畏的工程成就,是对缓存稳定性的偏执。这不是「尽量少改 prompt」——这是数学上可证明的不变量体系。

不变量 1:drop placeholder 是纯函数。[dropped §N§] 只依赖 tag id,永远不从被修改的内容重新派生子节。

不变量 2:所有首次应用的 strip/drop 只在 cache-busting pass 上执行。defer pass 只重放已冻结的 ID 集合。

不变量 3:historian publish 不 bust 缓存。它只追加 compartment 行到 SQLite,等待下一次自然的 execute pass 时 materialize。

不变量 4:HARD bust 意味着前缀已经没了 → 把所有事都倒进这个 bust。永远不要在 HARD bust 上「延迟」任何操作。

这四条不变量构成了完整的安全网:在正常运行时绝不多 bust 缓存,在必须 bust 时一次性做尽可能多的事。


八、治理 vs 压缩:最后的对照

维度 压缩 治理
架构 单次 LLM 调用 三层时间尺度的持续系统
缓存 每次操作都可能 bust 操作精确对齐到自然 bust 边界
历史 每次压缩覆盖上一次结果 decay 渲染:同一段历史永远输出相同文本
记忆 无(或需单独系统) 压缩的副产品,零边际成本
维护 无 dreamer 夜间自动 consolidate / verify / archive
可恢复性 压缩后原文永久丢失 compartment → ctx_expand 可恢复原始对话
跨会话 无 SQLite 持久化,跨会话甚至跨 harness
用户感知 暂停 30 秒等 compaction 零感知

最终的区别不在技术参数上,而在心智模型:压缩把上下文当垃圾,治理把上下文当矿脉。


结语

业界普遍把 agent 上下文当成一个「需要清空的缓冲区」,于是解决方案止步于「换个大窗口」或「写个摘要」。但真正的好 agent 应该像一位长期同事——你不需要在每次对话开始时重新介绍项目背景,她记得上次的技术讨论、记得为什么改了那个配置、甚至记得你的工作习惯。

这不是靠压缩实现的。

是靠一个持续的、多时间尺度的、缓存安全的治理系统——在正确的时间做正确的事:实时层的丢弃排队、异步层的 compartment 衰减、离线层的记忆维护。每一层都有自己的节奏,互不阻塞,最终形成一套自运行的上下文生命周期管理。

一个会话的生命周期可以从几小时延伸到几个月。关键不在于你有多少 token,而在于你知道什么时候该忘记什么——以及什么时候该记住什么。