每一次 API 调用对模型来说都是全新的开始。它「记得」上一轮对话,唯一的原因是你把上一轮的内容又发了一遍。所谓 agent 的memory(记忆),全部是应用层构造出来的幻觉——你决定把什么放进上下文,模型就「记得」什么。
这个认识把记忆设计还原成了一个朴素的工程问题:在有限的上下文预算里,放什么最划算?它有三重约束——容量(窗口上限)、成本(每 token 都要付费,且累积)、有效性(放进去不等于用得上,见 3.4)。
长对话或长循环必然会撑爆上下文。压缩策略的选择,本质是在「保真度」与「成本」之间取舍:
| 策略 | 做法 | 丢失什么 | 适用 |
|---|---|---|---|
| 滑动窗口 | 只保留最近 N 轮 | 早期信息完全丢失——包括最初的任务目标 | 信息价值随时间快速衰减的场景(闲聊);agent 循环中通常是危险的 |
| 摘要压缩 | 把早期内容交给模型压成摘要 | 细节、精确数值、原话;且摘要本身可能引入错误 | 长对话的通用方案,是目前最常用的做法 |
| 结构化抽取 | 抽成键值对/JSON(如「用户偏好=简洁」「已确认预算=5万」) | 叙事与上下文脉络 | 事实类信息的最佳方案——精确、可校验、可增量更新 |
| 外部化 + 检索 | 全量存外部,按需检索回来 | 不丢失,但可能检索不到 | 历史很长且访问稀疏时 |
实践中的组合形态通常是:固定的头部(system prompt + 工具定义,永不动)+ 结构化的事实区(抽取出的关键结论与约束)+ 早期对话的摘要 + 最近 N 轮的原文。这个结构同时照顾了「关键信息不丢」和「近期细节保真」。
压缩与 prompt caching 直接冲突,这是最容易踩的坑。2.2 讲过:缓存匹配是从第 0 个 token 开始的精确前缀匹配。而滑动窗口每轮都会切掉开头一段——前缀每轮都在变,缓存命中率恒为 0。
解法:让上下文只在末尾追加,压缩只在超过阈值时偶尔触发一次(触发时缓存失效一次,之后重新累积)。这把缓存失效频率从「每轮」降到了「每几十轮」,成本差异巨大。「累积 + 偶尔压缩」优于「每轮滑动」,几乎总是如此。
不是所有信息都值得进长期记忆。乱记会带来两个后果:检索噪声上升,以及过时信息污染——后者更危险。
① 稳定偏好(「用户习惯用 Python」「回答要简洁」)——值得记,变化慢、复用率高、体积小。适合结构化存储。
② 关键结论与约束(「已确认预算 5 万」「方案 A 已被否决,原因是…」)——值得记,且必须带时间戳与来源。这类信息一旦过时会造成严重误导,需要有更新与失效机制。
③ 过程细节(每一次工具调用的原始返回、中间推理)——不该进长期记忆。它们体积大、复用率极低,且会稀释检索质量。留在轨迹日志里供排查即可(4.2)。
还有一类必须单独处理:用户的个人敏感信息。「记住用户说过的一切」在合规上是危险的默认值——需要明确的存储范围、保留期限、以及删除机制。设计长期记忆时,「怎么删」和「怎么记」同样重要,且往往是事后才发现根本没法删。
设计:一个多轮任务助手,对话超过 20 轮后触发摘要压缩——把前面的内容交给模型压成一段几百字的摘要,替换掉原文。
症状:用户反馈在长会话的后半段,助手会违背早先明确说过的约束。典型情形是用户在第 3 轮说「预算上限是 5 万,绝对不能超」,到第 30 轮时助手推荐了一个 8 万的方案。
根因:摘要是有损压缩,而且它的损失不是均匀的。模型做摘要时倾向保留「叙事主线」——讨论了什么、达成了什么共识——而约束条件、否定信息、精确数值恰恰是最容易被压掉的部分。「预算不超过 5 万」在摘要里变成了「讨论了预算问题」,信息量归零。
修复:把「约束」从摘要里拿出来,单独结构化存储。每轮结束后用一次轻量抽取,把对话中出现的硬性约束、已确认的决定、被否决的选项写入一个独立的结构化区块(如 constraints: [{"预算上限": "50000", "确认于": "第3轮"}]),这个区块永远以原文形式保留在上下文中,不参与摘要。
为什么有效:因为它承认了一个事实——不同类型的信息,可压缩性完全不同。叙事可以压缩(丢一点细节无妨),约束不能压缩(丢一个字就反了)。用同一种策略处理两者,必然在约束上出事。
可迁移的原则:做压缩前先分类,对不可压缩的信息做无损保留。这在别处同样成立——日志可以采样,但审计事件不能;缓存可以过期,但一致性约束不能。识别出系统里那些「丢一点就全错」的信息,给它们单独的通道。
记忆库:Mem0、Zep、Letta 等提供了开箱的长期记忆能力(抽取、存储、检索、更新)。它们本质上是「RAG + 抽取管线 + 更新策略」的封装。引入前先确认它的更新与删除语义——记忆最难的部分不是写入,而是如何让过时的记忆失效,各家在这一点上的成熟度差异很大。
框架内置:主流 agent 框架普遍提供对话缓冲、摘要缓冲等 memory 组件。默认实现多为滑动窗口或简单摘要,注意它们与 prompt caching 的冲突(见正文),必要时自己实现「累积 + 阈值触发压缩」。
压缩用小模型即可:摘要与结构化抽取是相对简单的任务,用便宜的小模型做,成本可以忽略(2.3 的分级思路)。用旗舰模型做摘要是常见的浪费。
务必监控的两个指标:压缩触发频率(过高说明阈值设置不合理或上下文膨胀过快)与压缩前后的关键信息保留率(用一组已知约束做回归测试)。第二个指标很少有团队做,却是本章案例那类问题的唯一预警手段。
反直觉之处在于:滑动窗口发送的 token 更少,但花的钱可能更多。原因是 prompt caching。
缓存命中要求从第 0 个 token 开始的精确前缀匹配。滑动窗口每轮都从头部切掉一段,导致前缀每轮都变——缓存命中率恒为 0,每一个 token 都按全价计费。
累积式则相反:上下文只在末尾追加,前缀始终不变,除新增部分外全部命中缓存。即使发送的 token 总数更多,按折扣计费后的实际成本可能显著更低。
举例说明(假设缓存读取价为普通输入的 1/10):滑动窗口每轮发 4000 token 全价 = 4000 单位;累积式第 20 轮发 12000 token,其中 11500 命中缓存 = 11500×0.1 + 500 = 1650 单位。发得更多,花得更少。
更一般的结论:在有前缀缓存的系统里,「只追加」是一种极有价值的设计属性,它同时改善成本与 TTFT。任何会修改上下文头部的操作都应该被视为昂贵操作,尽量降低其频率。
A 语言偏好 → 结构化长期记忆。稳定、体积小、复用率极高,是长期记忆最理想的内容。存为键值对({"preferred_language": "en"}),每次会话开始时直接注入上下文,无需检索。
B 历史投诉 → 长期记忆,但必须带时间戳与状态。这类事件信息有时效与生命周期:如果投诉已解决而记忆没更新,agent 会在用户已经满意后还在道歉,体验反而更差。必须设计状态更新机制,或在检索时连带查询该工单的当前状态——更稳妥的做法是只记「用户ID关联工单#123」这个索引,具体状态每次实时查(3.1:实时数据用工具调用,不要存副本)。
C 信用卡后四位 → 不存。这是合规问题,不是技术问题。支付信息不应进入 LLM 的记忆系统;需要时通过专门的、受控的支付系统接口按需获取并最小化暴露。「模型能记住」不代表「应该让模型记住」——长期记忆的设计必须先过一遍数据分类与合规审查。
D 2000 字物流详情 → 短期记忆,会话结束即丢弃。典型的过程细节:体积大、时效性强(明天就过期)、复用率接近零。存进长期记忆只会污染检索。但要留在轨迹日志里供排查使用——日志和记忆是两个不同的系统,不要混用。
方向是对的(外部化 + 检索),但有五个会在生产中暴露的问题:
① 存了不该存的。「完整对话内容」包含大量过程细节和一次性信息,它们会稀释检索质量——真正有价值的偏好和约束被淹没在噪声里。应该先抽取再存储,而不是全量入库。
② 没有更新与失效机制。用户上个月说「我用 Python」,这个月改用 Go——两条矛盾的记忆同时存在,检索可能召回旧的那条。必须有覆盖/失效策略,这是记忆系统与普通 RAG 最本质的区别:知识库的文档是并存的,记忆是会被推翻的。
③ 检索不到 = 遗忘。依赖向量检索意味着继承了它的全部盲区(3.3)。关键约束若召回失败,效果等同于失忆——而这类失败是静默的。硬性约束不应依赖检索,应作为固定内容始终注入。
④ 破坏 prompt caching。每轮检索到的 top-5 内容不同,若拼在上下文靠前位置,会让缓存全部失效。应放在靠近用户提问的位置(2.2)。
⑤ 缺少合规设计。没有说明保留期限、删除机制、以及用户要求「删除我的数据」时如何执行。向量库中的记忆删除远比想象中麻烦,必须在设计阶段就解决,事后补极其困难。
改进后的方案:每轮结束后用小模型抽取结构化事实(偏好/约束/决定),带时间戳与来源写入结构化存储(不是向量库);稳定偏好每次直接注入而非检索;只有体量大的历史叙事才走向量检索;全部记忆关联用户 ID 以支持按需删除。