一、上下文管理的本质困境
如果你用过 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,而在于你知道什么时候该忘记什么——以及什么时候该记住什么。