很多人第一次用龙虾时都会有一个直觉:
“聊久了,它应该会忘吧?”
这个直觉没错。模型的上下文窗口是有限的,不可能把几万句对话永远都放在眼前。
但你会看到系统表现得像这样:
- 不切 thread,也能连续推进一个长期任务
- 记得你之前的偏好、决定和待办
- 对话很长后也不会突然“失忆”
这篇文章讲清楚它背后的工程逻辑。
先讲结论:它靠的不是“超大脑子”,而是“分层记忆”
一个能长期连续工作的 AI 助手,通常不是在“硬记全部聊天记录”。
它更像一个做事很靠谱的人:
- 脑子里保留当前任务所需的短期信息
- 把长期重要信息记到笔记本
- 需要时再去查笔记,而不是靠猜
所以关键不是“记住一切”,而是“该记哪里、什么时候记、怎么取回来”。
1 为什么“一个对话一直聊”不会乱
要做到这件事,第一步不是 memory,而是“会话归属稳定”。
你可以把它理解成:
- 每条新消息进来,系统都要先回答“这条消息属于哪条连续会话?”
- 只要这个归属规则稳定,用户就会感受到“我一直在同一个对话里”
如果归属不稳定,会发生什么?
- 今天这句进 A 会话
- 明天那句进 B 会话
- 用户感觉就是“它忽然不记得了”
所以,连续性首先是路由问题,不是模型智商问题。
2 长对话为什么不会把模型撑爆
即使会话归属稳定,也还有第二个难题: 上下文窗口会满。
成熟系统会做三件事:
-
限制最近历史 :只保留与当前问题最相关的“最近若干轮”。
-
自动压缩旧历史(compaction):把很长的旧对话压成“结构化摘要”,保留关键决策和状态。
-
失败兜底:如果压缩中断或超时,回退到安全快照,不让会话进入半坏状态。
这和人工作很像:
- 桌上只放当前要处理的文件
- 老文件归档成摘要
- 归档出错就先回到上一个可用版本
3 真正关键:Memory 不是“备份聊天记录”
很多人把 memory 理解成“把聊天全存起来”。
这不够。
真正可用的 memory 系统至少要回答 4 个问题:
- 存什么
- 什么时候存
- 怎么找
- 找到后给模型喂多少
1 存什么
不是所有对话都值得永久存。
通常要存的是:
- 稳定偏好(口味、风格、边界)
- 已确认决策(做过什么决定、为什么)
- 长周期任务状态(进行到哪一步)
- 关键事实(日期、人物、账号约束)
2 什么时候存
好的系统会在“即将压缩上下文”前触发一次静默写入(memory flush):
- 先把耐久信息落盘
- 再去压缩历史
这样就不会因为压缩导致关键信息漂掉。
3 怎么找
常见做法是“混合检索”:
- 关键词检索(你说了某个明确词)
- 语义检索(你换了说法但意思相近)
然后把结果融合排序,优先给出最相关片段。
4 找到后喂多少
成熟系统不会把整本记忆库都喂给模型。
而是两步:
- 先 search,拿候选
- 再 get,只读取必要行
这样上下文干净,成本低,稳定性高。
4 一个通俗例子:为什么它看起来“真的记得你”
假设你连续三周都在推进“装修计划”。
第一周你说:
- 不要开放式厨房
- 预算上限 20 万
第二周你说:
- 客厅采光优先
- 工期希望 3 个月内
第三周你问:
“按我们之前的原则,周末我要见设计师,该先确认哪三件事?”
如果系统只靠当前窗口,它可能忘掉第一周。
如果它有分层记忆:
- 会话里保留最近讨论
- 长期记忆里有你前两周沉淀的偏好与约束
- 回答前先检索再取关键片段
最终回答会更像“基于你的长期上下文”,而不是一次性临场发挥。
5 一个点看懂工程质量
为什么 memory flush 不会乱触发,也不会重复触发。
很多系统的问题不是“没有 memory flush”,而是“flush 触发太随意”,结果变成:
- 该写入时没写
- 不该写入时反复写
- 一次 compaction 周期里重复写同样内容
OpenClaw 在这个点上做得很精细,核心是三段式控制。
1 先算阈值,只在快到上限时触发
在 src/auto-reply/reply/memory-flush.ts 里,触发条件本质是:
//当 totalTokens >= threshold 时,才考虑 flush
threshold = contextWindow - reserveTokensFloor - softThresholdTokens
这让 flush 从“拍脑袋触发”变成“窗口压力驱动”。
2 再做幂等控制,一轮 compaction 只 flush 一次
同一文件里还有一个关键判断(shouldRunMemoryFlush):
- 当前会话有
compactionCount - 上一次 flush 记录了
memoryFlushCompactionCount - 如果两者相等,说明本轮已经 flush 过了,直接跳过
这一步非常工程化。它不是靠“感觉上不会重复”,而是靠状态位严格去重。
3 触发位置放在主回合之前,保证先落盘再压缩
在 src/auto-reply/reply/agent-runner.ts 中,runMemoryFlushIfNeeded() 被放在主回合执行前。
src/auto-reply/reply/agent-runner-memory.ts 里会进一步检查:
- 不是 heartbeat
- 不是某些不适合的 provider 模式
- 工作区可写(只读沙箱不写)
通过后才跑 flush 回合,并在结束后把这两个字段写回 session store:
memoryFlushAtmemoryFlushCompactionCount
对应会话结构字段定义在 src/config/sessions/types.ts。
它把“写记忆”做成了可证明的状态机,而不是一段“偶尔执行的辅助逻辑”。
这就是为什么它在长对话压力下还能稳定,不会一边压缩一边把记忆策略搞乱。
6 这套设计的代价与边界
这套方案很强,但不是魔法。
优点
- 单对话体验稳定
- 长任务可持续
- 对“历史事实”更不容易胡编
代价
- 系统更复杂(路由、索引、压缩、检索都要配合)
- 记忆质量取决于写入质量
- 参数没调好会影响召回质量或成本
现实边界
- 记忆不是 100% 真相机,仍需要来源校验
- 高风险场景要保留“我不确定”与“可追溯引用”机制
六 如果你在设计类似系统,最值得抓住的三件事
- 先保证会话归属稳定
- 不要一上来就追求复杂 memory,先让“同一个人同一类对话”稳定落在同一会话。
- 把长期记忆外置
- 不要指望模型窗口长期记住一切。把耐久信息写进可检索存储。
- 强制“先检索再回答”
- 在系统策略层明确约束:涉及历史事实必须先查 memory。
- 这是把“看起来聪明”变成“工程上可靠”的分水岭。
结语
一个真正能长期协作的 AI,对外看起来像“记性很好”。
但从工程上看,它做的是更朴素、也更难的一件事:
把“记住”拆成可管理的流程。
- 谁的会话
- 当前保留什么
- 长期写入什么
- 回答前查什么
当这四件事同时做好,用户才会得到那种自然体验:
“我没有切 thread,但它一直跟得上我。”