上下文压缩:摘要、驱逐与分层记忆。
一个长期运行的智能体,其轨迹无界增长;窗口不会。压缩是一组在缩小历史的同时保留继续推进所需信息的技术。做得好它是隐形的;做得差它会让智能体在任务中途悄悄失忆。这是让真正长时程智能体成为可能的操作。
压缩阶梯:先用最便宜、可承受的技术。
压缩不是单一动作。它是一架损失逐级增大的技术阶梯;用能让你保持在预算内的最温和那一级,只有当它不够时才升级。
- 截断——整批丢掉最旧的轮次。最便宜、损失最大,仅当被丢内容已持久化到长期记忆时才安全。绝不要把截断作为唯一机制。
- 去重/剪枝——移除冗余的工具输出、重复的检索、被取代的计划版本。高价值、近乎零风险:你删的是噪声,不是信号。
- 摘要——用一段模型生成的概要替换若干轮。主力。有损,但只要范围划得对就能保留信息。
- 分层摘要——摘要的摘要,使得即便非常古老的历史也作为一条细线索存活。这是解锁实际上无界时程的技术。
顺序很重要。先剪枝再摘要(别花钱让模型去摘要重复垃圾)。先摘要再截断(别删掉你还没提炼的东西)。只截断那些既被摘要又被持久化的内容。
保留智能体所需内容的范围化摘要。
一句通用的"摘要这段对话"恰恰会丢失智能体需要的东西:未闭合回路、决策及其理由、错误原因、硬约束。压缩摘要必须是任务结构化的,而非散文结构化的。
# memory/compact.py COMPACT = """Compress the following agent turns into a structured state summary. Preserve, do not narrate: DECISIONS: choices made and the reason for each FACTS: durable facts established (mark source turn) OPEN: unfinished sub-tasks / unanswered questions ERRORS: failures seen and their root cause CONSTRAINTS:rules that must still hold Drop: pleasantries, superseded plans, raw tool dumps already reflected in FACTS. Be terse. No prose. TURNS: {turns}""" def summarize_span(turns: list[dict], llm) -> str: body = "\n".join(fmt(t) for t in turns) return llm(COMPACT.format(turns=body), max_tokens=600)
结构化标题就是契约。一份让 OPEN 与 CONSTRAINTS 逐字忠实的摘要,可以替换 40 轮历史而智能体继续正确推进。一份对它们做了改述的摘要会丢失线头——经典的"智能体忘了它不许碰生产环境"失效。
绝不要摘要当前的未闭合回路。压缩作用于已解决或已老化的历史。活跃子任务和最近几轮以逐字形式留在工作集——摘要你正做到一半的东西,正是智能体丢失自己位置的方式。
分层记忆:MemGPT 式的分级。
决定性的思想,由 Packer 等在 MemGPT(2023)中阐明:把上下文窗口当作内存、把外部存储当作磁盘,由智能体自身(经由工具)管理各级之间的分页。三级:
TIER 0 WORKING in-window, verbatim, fully attended
→ current task, last k turns, scratchpad
TIER 1 RECALL out-of-window, fast retrieval
→ recent summaries, indexed episodics
TIER 2 ARCHIVE out-of-window, cold, summary-of-summaries
→ old-task traces, thin durable residue
paging: WORKING --evict--> summarize --> RECALL --age--> ARCHIVE
ARCHIVE --recall on demand--> RECALL --promote--> WORKING
关键在于,智能体拥有在各级之间搬运数据的工具——它可以选择"记住这个"(写入 recall)、"我们关于 X 决定了什么"(搜索 archive),或"把这个换页回来"。记忆管理成为智能体动作空间的一部分,而不只是它背后一个隐形的框架。这正是时程实际上无界的原因:智能体总能伸手够到更早的内容,只是为此付出一次检索往返。
触发:在压力下压缩,而非按定时器。
压缩昂贵(一次模型调用)且有损。从预算压力触发它,带迟滞,这样你不会在边界上来回抖动。
# memory/compactor.py class Compactor: def __init__(self, budget, hi=0.85, lo=0.60): self.budget = budget self.hi, self.lo = hi, lo # hysteresis band def maybe_compact(self, history) -> list: used = ntok(history) / self.budget.working if used < self.hi: return history # under pressure threshold keep_recent = tail_until(history, self.lo) old = history[:-len(keep_recent)] if not old: return history # nothing safe to compact persist_to_longterm(old) # write BEFORE you shrink digest = summarize_span(old, llm) return [{"role": "system", "content": "[compacted history]\n" + digest}] \ + keep_recent
迟滞带(在 85% 满时压缩,降到 60%)防止了那种病态情形:每一轮都把线轻轻推过去,触发一次新鲜、昂贵的摘要,却只换来勉强够用的余量。以能买到真正余量的块为单位压缩。
验证压缩,因为有损不等于损坏。
每一次压缩都是一个智能体可能悄悄丢失某个事实的地方,而你要到三轮之后它做了某件被禁止的事时才会发现。把压缩当作任何其他有损变换来对待:验证它。
- 约束存活检查。在带外维护一份硬约束的显式清单。压缩后,断言每一条仍(语义上)出现在摘要中;若否,逐字重新注入它。
- 未闭合回路存活检查。在压缩前后清点未闭合子任务数。下降是红旗,不是特性。
- 往返评估。离线地,针对压缩后状态提问那些只能从压缩前历史回答的已知问题。把答对率作为一等指标跟踪(见
evaluating-memory)。
最便宜的稳健保障:把硬约束和当前未闭合回路清单放在工作集顶部一个小的、永不压缩的钉住块里。压缩就在物理上吃不掉它们,无论它多激进。把令牌花在这里——这是你拥有的杠杆率最高的预算。