这两天总在想一个类比:Agent 发展到现在,Context 和 Memory 的各种管理手段,越来越像一门带 GC 语言的 Runtime。 程序想一直跑下去,但物理内存是有限的;Agent Loop 本质就是一个无尽循环,但是模型单次能接收的 Context Window 同样有一个确定的、强制不可突破的上限。这样说来两边其实在试图解决同一个问题:
怎么把一段看起来可以无限延续的运行,依靠一份有限的资源上?
刚开始这只是个“感觉很像”的直觉:Context 像内存,Message 像对象,Compaction 像 GC。如果两个事物有这么多的相似之处,那么肯定有一些本质上的共性。Agent Runtime 和 Context Window 的优化还是一个没有定论的东西,通过将 GC 模型和 Context 模型的映射比较或许能得到一些有趣的思路和方向。Context GC 和编程语言 GC 最关键的地方,或许不在它们相似的部分,而在于它们从哪一步开始不再相似。
编程语言 GC 概念映射到 Context
在一门带 GC 的语言里(下文我们都使用 JVM 中的概念近似地理解),程序不断创建对象,对象占用内存,又通过引用连接成一张图。GC 从一组 Root 出发遍历这张图:能够从 Root 到达的对象仍然活着,无法到达的对象则已经不可能再被程序访问,可以安全回收。
Agent 运行时也在不断产生东西:
- system prompt;
- user message;
- assistant message;
- tool call;
- tool result;
- 当前计划;
- 工具定义
这些信息最后都在争夺同一块有限资源:
是第 轮真正进入模型的 Context, 是这一轮不可突破的 Token Budget。
在我之前写的 一种 LLM Agent Runtime 的形式化模型 中,一轮 Agent Loop 被写成:
是 Runtime 保存并可访问的全部内部状态, 则只是这一轮真正送给 LLM 的有限输入。两者并不相同:
Runtime 可以保存很多东西,但模型这一轮真正能够“想到”的,只能来自 。LLM 模型某种程度上来说是海量概念地压缩,本身就能在一个更为广阔的上下文中工作(这个后面会用 Git 的例子说明)。
如果暂时把一条独立 Message 当成一个对象,那么 Context GC 至少有四种很自然的操作:
- 完整保留;
- 压缩内容;
- 彻底删除;
- 把多条 Message 合并成一个新的表示。
到这里为止,一切都很像 GC:Context Window 是有限堆空间,Message 是对象,新的对话和工具结果是分配,删除和压缩则是在回收空间。不过显然编程 GC 没有压缩和合并这个概念,有的话也更像是更底层虚拟内存物理内存的优化操作。严格来说只有完整保留和彻底删除两种操作。
传统 GC 回收的是确定无用的对象 Context GC 中没有确定无用的对象
传统 tracing GC 的安全性是确定的。一个对象只要无法从任何 GC Root 到达,程序就再也没有办法访问它,回收它不会改变程序未来的任何行为
Context 里的信息没有这种好事。删掉一句旧的用户约束、把一段 Tool Result 压成摘要,甚至只是这一轮不把某条 Message 递给模型,都可能改变模型下一步输出的概率分布:
这正是有 GC 语言的 GC 和 Agent Context 之间最关键的差异:程序 GC 销毁对象而回收的内存,是确定对未来毫无影响的数据,它们不被任何 GC Root 触达,无法被访问的数据自然不会产生影响,回收是安全的;而 Context 的处理永远是有损的,只是损失大小有差异(这个差异的度量就是我们设定的那些 Agent 指标:任务成功率、约束违反率、副作用安全、恢复成本、请求或中止后的人工接入等等)。Context GC 不能得到 JVM GC 那种严格的语义透明。它能争取的“无感知”,只能是从 Agent 外部看过去,GC 前后的任务行为近似不变。损失是一定的,只不过可以讨论“损失了多少、这个损失能不能接受”这类问题。
信息离开 Context 真的无法再访问了吗?
把两件事分开:信息还在 、只是这一轮没进 ,这像 Paging;信息从 里永久删掉,这才像真正的 GC。看起来似乎是成立的,但站在 LLM 的视角,这个区分却并不成立。
一条信息哪怕还躺在数据库里,只要模型不知道它曾经存在过,也不知道它在哪里、Runtime 不会再主动把它装载回来,它和被删掉就没有区别:物理上存在,却已经不属于这段计算了。
反过来也一样,一个巨大的 Tool Result 完全可以这样处理:
原始结果被移到文件、数据库或者对象存储中,Context 只留下摘要、Hash、地址和读取方式。从 Context Window 看,原文已经被 GC、Token 已经释放;从 Agent 的能力看,它随时能通过 Handle 把原文取回来。
所以 Context GC 不该按“数据存在哪块物理介质”来定义,而要按“Agent 还有没有一条有效的取回路径”来定义。这条路径也不只是一个指针,它可能是:
- Context 中的直接引用;
- Artifact Handle;
- Runtime 检索;
- 重新读取文件;
- 从其他信息重新推导;
- Runtime 在合适时机自动重新注入。
在一个代码 Repo 中,Context 里早已没有上一次调用 git 的记录(可能所有和 git 相关的信息都被丢弃了),但 LLM 依然可能根据当前目标主动再调一次 git,重新拿到 Commit 信息。
不过但这里藏着一个细小却要命的区别:重新调用 git 拿到的是当前事实,未必能还原过去那次观察。万一对历史的 commit 做了合并呢?所以至少要分开两件事:
原始 Message、历史事件和证据还能不能恢复;以及
它所表达的语义命题还能不能重新得到。“重新知道现在是什么”和“还原过去发生过什么”不是一回事。
良好定义的 Context GC Root
顺着 GC 的思路往下,绕不开的问题是:Context GC 的 Root 是什么?
按 Message 的 Role 来回答最省事:System 和 User 是 Root,Assistant 和 Tool 不是。这也是很多激进的压缩方法的实现。但这个答案显然不对:旧的 User Message 可能已经被新要求撤销;Assistant Message 里可能埋着“改完代码我会跑测试”这种还没兑现的承诺;某条 Tool Result 可能是“安全约束已经满足”的唯一证据。
Root 没法被定义为 Message 的类型。
在我的设想中,Root 真正的定义是:
Context GC Root 是 Agent Runtime 当前尚未解除的义务。
把理论上真实、完整的义务集合写成:
这些义务可能来自很多地方:
- 当前必须完成的目标;
- 用户明确给出的约束;
- System Policy 和安全边界;
- 尚未完成的计划;
- LLM 已经作出但尚未兑现的承诺;
- 结果未知、需要验证或回滚的外部副作用。
比如用户说:
修复登录问题,不得修改公开 API;修改完成以后运行测试,并告诉我问题的原因。
这句话至少产生四项义务:
- 修复登录问题;
- 不得修改公开 API;
- 修改后运行测试;
- 完成后解释原因。
只要这些义务还没有解除,它们就是 Context GC Roots。
而且 Root 不能因为 Message 太旧、太长或者“看起来相关性不高”就被 GC 算法自行删除。义务只能通过自己的生命周期解除:它被满足、被用户取消、被更高优先级的要求替代,或者任务以符合约定的方式终止。如果执行失败,义务甚至不会简单消失,而可能变成新的义务:
这个设想的启发来自于 Agent Context 压缩的目的:更好地完成用户地需求。Context GC 只是手段,手段为目的服务。GC Root 就是为这个目的服务的最小原子化的语义信息,这些语义信息被我称为“义务”:为了解决用户需求而需要的必要信息。
Root 不是整条 Message
一条 Message 里可能同时有目标、约束、猜测、情绪、重复描述和寒暄。如果其中一个约束仍然存活,不代表整条 Message 的每一个字都必须永久保留。Root 真正到达的是这条 Message 中与义务有关的语义投影:
可以把依赖关系写成:
是履行义务所需的最低信息保真度。有的内容必须保留原文,有的必须保留精确数值和来源,有的只需要语义等价,有的保留一个摘要就够了,还有的只需要留下有效的恢复路径。于是“可达”不再只是一个简单的布尔值,而是带着保真度要求:
| 保真度 | 含义 |
|---|---|
verbatim | 必须保留原始措辞 |
exact | 数值、标识符和语义必须精确 |
provenance | 必须保留来源、时间和证据关系 |
semantic | 保持等价含义即可 |
summary | 保留主要结论即可 |
recoverable | 保留有效恢复路径即可 |
这也把四种操作统一了起来:
- 没有 Root 再依赖某项语义,可以删除;
- Root 只需要较低保真度,可以压缩;
- Root 需要原始证据,只能完整保留;
- 多条 Message 的必要语义可以由一个新表示共同承载,可以合并。
“未来可能有用”是没有用的
在 Context GC 中我们还有一个绕不过去的话题:我们不知道未来。如果把存活定义成“只要未来可能再用到”,那所有历史信息都应该永久保留——用户以后也许会突然问起任何一句话,环境也可能发生任意变化。这显然会让 GC 彻底停摆。信息活性只能相对于一个被允许考虑的未来集合:
这个集合由当前未解除义务、已知环境、合法动作、时间与成本范围共同限定。也就是说,Context 里不存在脱离任务的绝对性。某段信息是否还需要保留,必须写成类似:
同一段信息,在一个任务里可能是必须精确保留的证据,在另一个任务里可能只是随时可以丢弃的噪声。
不同的 Root 配不同的风险预算
“不得泄露密钥”和“最后尽量简短总结”不应该共享同一个容错标准。所以每个义务都可以带上自己的保真度、风险预算和风险计算方式:
其中:
- :必须满足的条件;
- :最低信息保真度;
- :可以接受的行为损失;
- :怎样看待未来分支中的风险;
- :义务的解除条件;
- :义务当前所处的状态。
安全约束可能关注最坏情况,普通目标更关心期望损失,某些场景则需要特别关注低概率、高损失的尾部风险:
于是 Context GC 的“无感知”可以更准确地写成:
它不是说 GC 前后模型一定输出完全相同,而是说对于每一项未解除义务:
这里的 并不是严格的数学等价。带阈值的近似关系未必有传递性,连续多次“每次看起来只损失一点”的摘要,最后仍然可能把原始语义修改地面目全非。更准确的说法是:GC 后的 Context 仍处在相对于原始信息的可接受行为偏差范围里。
Runtime 做 GC 的时候并不知道未来,度量和运行需要分开
还有一个麻烦:Runtime 真正执行 GC 的那一刻并不知道未来,自然也算不出任务成功率到底会掉多少。这就得把真正运行的因果策略和对策略的评价度量两件事拆开。
运行时地策略
只包含时刻 已知的信息。GC 算法可以根据 Message 类型、年龄、长度、义务依赖、恢复路径等作出判断,但不能偷看未来。
对使用该策略的 GC 的事后评价:
也就是说,未来行为指标定义了“什么是一个好的 Context GC”,却不是 Runtime 执行回收时能直接拿到的输入。传统 GC 通常可以在运行时精确判断一个对象是否从 Root 可达;Context GC 的语义安全性只能靠事先定好的策略去近似,再在未来轨迹里接受检验。不确定和确定,是 Agent 区分于以往软件开发最深的一道分界线。
那么,Context GC 到底是什么?
把这些条件放在一起,Context GC 可以被写成一个受约束的表示优化问题:
要求:
并且对所有仍未解除的义务:
一句话:
在不让 Agent 忘记自己没有完成什么的前提下,尽量减少当前 Context 的 Token 占用,同时控制重新获取信息的成本和未来行为风险。
再回头看,Context GC 和传统 GC 的对应关系大概是这样:
| GC 语言 | Agent Context GC | 相同与不同 |
|---|---|---|
| 有限堆空间 | 有限 Context Window | 都要支撑潜在无限运行 |
| 堆对象 | Message 或派生信息表示 | Message 还有类型、顺序和自然语言语义 |
| GC Root | 未解除的 Runtime 义务 | 前者是结构引用起点,后者是任务和行为约束 |
| 对象引用 | 义务对语义投影的依赖 | Context 的边还可能包含工具、检索和动作 |
| Mark | 找到义务相关语义 | 从精确图遍历变成语义判断 |
| Sweep | 删除 Message 或语义片段 | 可能改变模型输出分布 |
| Compact | 压缩、合并 Message | 传统 Compact 无损,Context 压缩通常有损 |
| Moving / Paging | 原文外移,Context 保留 Handle | 重新装载需要时间、Token、权限和工具 |
| Memory Leak | 无有效路径却仍占据 Runtime 的信息 | Root 过度保守也会造成 Context 膨胀 |
| GC 透明性 | Agent 行为近似保持 | 一个接近确定保证,一个只能做风险约束 |
整个关系可以画成这样:
flowchart TD
A["Agent Runtime 状态 R_t"] --> B["未解除义务 O*_t"]
A --> C["Context 中的 Message 对象"]
B --> D["义务相关语义投影 P_o(m)"]
C --> D
B --> E["允许未来 F_adm"]
B --> F["保真度 q、损失预算 ε、风险函数 ρ"]
D --> G["Context GC 策略 G_φ"]
E --> G
F --> G
G --> H["保留 / 压缩 / 删除 / 合并"]
H --> I["新的 Context M'_t"]
I --> J["未来 Agent 行为"]
J --> K["成功率、约束违反、安全性、恢复成本"]
结论
最初我只是觉得 Context 很像内存。现在我更愿意把 Context GC 定义成:
Context GC 是在未解除运行时义务的约束下,对 LLM 可见信息以行为近似保持为目的而进行的 LLM 可见上下文优化。
它不只是传统意义上的 GC,而是把 GC、虚拟内存、缓存、分层存储和有损压缩混在了一起。但 GC 仍然是一个很有力量的出发点,因为从这个模型触发需要我们认真回答两个问题:哪些信息还活着?我们凭什么认为删掉它是安全的?
传统 GC 的答案是“从 Root 不可达,所以程序永远不可能再访问”。Context GC 的答案要模糊得多:
这项信息已经不再支撑任何未解除义务;或者即使降低它的保真度,也不会让未来行为风险超过我们愿意接受的范围。
这当然没有 JVM GC 那么干净,甚至注定拿不到同样强的安全证明。但也正因如此,Context GC 恐怕不只是一个工程优化问题——它其实在逼我们回答一件更根本的事:对一个持续行动的 Agent 来说,什么仍然值得记住,什么已经可以忘记,以及“忘记”究竟意味着什么。