Agent 做到一定阶段以后,记忆会变成一个很自然的需求。

用户不希望每次都重新介绍自己,不希望反复说明项目背景,也不希望每次都把偏好、约束、历史决策重新复制一遍。一个长期运行的 Agent,如果完全没有记忆,就会像一个每次重启都失忆的工具。它可以回答当前问题,却很难真正进入用户的工作流。

但记忆系统也很容易被做歪。

最直觉的做法是:把所有历史都存下来,然后在下一次调用模型时全部塞进 prompt。

用户说过什么,工具返回过什么,模型回答过什么,手动保存过什么事实,都放进上下文。这样看起来最完整,也最不容易漏信息。

问题是,这不是记忆。

这只是堆历史。

真正的 Agent 记忆,不是把越来越多的文本带回 prompt,而是要判断:哪些信息只是这次对话发生过,哪些信息代表稳定身份,哪些信息只是一个候选线索,哪些信息真的值得长期影响之后的决策。

记忆系统的核心,不是存得更多。

是更谨慎地决定什么值得回来。

把所有历史塞进 prompt 的问题

早期原型里,把历史直接放进 prompt 很有吸引力。

它简单。

它也确实能解决一部分连续性问题。

比如用户刚刚发了一段需求,下一轮模型需要看到这段需求。工具刚刚读取了一个文件,下一轮模型需要基于文件内容总结。模型刚刚问了一个澄清问题,用户的回答也应该进入上下文。

这些都是 session history 的价值。

但 session history 不是长期记忆。

一段会话里会有很多临时信息。用户可能在调试一个一次性问题,可能要求改某一句话,可能临时试探某个方案,可能给出一个后来又推翻的判断。如果这些内容全部长期回到 prompt,Agent 的上下文会越来越脏。

脏上下文有几个后果。

第一,噪声会变多。

模型看到很多已经不重要的信息,就需要在更多文本里判断什么仍然相关。这不但浪费上下文,也会增加误读概率。

第二,旧信息会变 stale。

用户一周前说“先用方案 A”,今天已经改成方案 B。如果旧信息没有被区分和整理,它仍然可能在 prompt 里影响模型。

第三,矛盾会变多。

长期 Agent 一定会遇到用户偏好改变、项目方向改变、事实被修正的情况。如果系统只会追加历史,不会整理记忆,prompt 里迟早会同时出现多个互相冲突的版本。

第四,成本会失控。

长上下文不是免费的。越多历史进入 prompt,每次调用模型的成本和延迟都会上升。更重要的是,模型的注意力也会被稀释。

所以,把所有历史都放回 prompt,不是长期记忆系统的目标。

它只是一个没有边界的缓存。

Session history 负责最近发生了什么

Agent 仍然需要 session history。

没有 session history,工具调用循环就无法成立。用户说“读一下这个文件,然后总结”,模型调用 file.read,工具返回 observation,模型再基于 observation 输出总结。这整个过程都依赖最近消息被保留下来。

session history 记录的是:

这次对话里发生过什么

它适合服务近期连续性。

比如:

  • 用户刚刚提出的任务
  • 模型刚刚做出的中间判断
  • 工具刚刚返回的 observation
  • 当前 session 内还没有结束的上下文

但长期记忆回答的是另一个问题:

过了这次对话以后,什么仍然应该影响 Agent

这两个问题不能混在一起。

可以把边界理解成:

session history = what happened recently
long-term memory = what should still matter later

前者是时间线。

后者是结论。

时间线里有很多证据,但不是每个证据都应该变成结论。用户在某个任务里说“这段先写短一点”,这可能只是当前文章的要求。用户反复说“和我讨论计划时用中文”,这就更像一个长期偏好。

如果系统不区分这两者,就会把一次性的上下文误当成长期事实。

Agent 会显得“记得很多”,但实际上是不加判断地携带历史。

身份和 profile 不应该混在普通事实里

记忆里还有一类东西,比普通事实更稳定:身份和 profile。

Agent 自己是谁,应该怎么说话,默认用什么工作风格,这不应该只是一个随机的 key/value fact。

用户是谁,偏好什么语言,喜欢什么沟通方式,当前主要项目是什么,也不应该和“某个网页标题是什么”放在同一个平面里。

因为这些信息进入 prompt 的方式不一样。

Agent identity 更像系统行为的一部分。它定义的是 Agent 的稳定人格、边界和协作风格。

User profile 更像长期协作上下文。它定义的是用户的稳定偏好和背景。

普通事实则更像可选材料。它可能有用,但不一定每次都要强影响模型。

如果全部放进一个 flat facts 表里,系统很快会失去语义。

project = TalyOS
language = Chinese
agent_persona = concise
last_web_title = ...
temporary_note = ...

这些看起来都是 key/value,但它们在 Agent Loop 里的意义完全不同。

所以记忆系统需要更明确的表面:

Agent identity
User profile
Project context
Preferences
Known facts

这不是为了把数据模型做复杂。

而是为了让模型看到上下文时,也能理解这些信息的层级。

身份和 profile 是稳定背景。

项目上下文是工作场景。

偏好会影响表达和默认选择。

已知事实只是可引用的信息。

这些东西如果结构化地进入 prompt,比一串混杂记忆更可靠。

自动记忆不能直接变长期记忆

手动记忆很清楚。

用户明确说“记住这个项目叫 TalyOS”,系统写入事实。用户明确设置 Agent 身份,系统写入 profile。这种记忆来自明确指令,风险相对低。

自动记忆更复杂。

Agent 可以在普通对话后主动提取候选信息。比如用户说:

以后我们讨论实现计划时默认用中文。

这很像长期偏好。

又比如用户说:

这段先写短一点。

这可能只是当前任务的局部要求。

再比如用户说:

我现在在 TalyOS 里处理记忆系统。

这可能是当前项目上下文,也可能只是这一次工作的背景。

如果自动提取一发现信息就直接写入长期记忆,Agent 很快会变得过度自信。

它会把临时指令当成永久偏好。

它会从一句话里推断太多。

它会把未确认的上下文带回之后的 prompt。

所以自动记忆应该先进入一个中间层。

这个中间层可以叫 memory signals。

signals 是证据,不是结论。

一个 signal 可以表达:

scope: user
kind: preference
content: Prefers Chinese for planning discussions.
source_session_id: default
confidence: 0.86

这里重要的不是字段本身,而是语义。

系统承认自己发现了一个可能有价值的信息。

但它还没有决定这个信息应该长期影响 Agent。

signal 不应该默认进入下一次 prompt。

它应该等待整理。

Dreaming 是整理边界

这就是 Dreaming 的位置。

OpenClaw 里的 Dreaming 给了一个很好的启发:记忆不一定要在在线对话中立刻定型。可以先把短期经验收集起来,再通过一个整理过程,把强信号晋升成长期记忆。

TalyOS 里更适合做一个小一点、SQLite-first 的版本。

它的形状可以是:

agent turn
-> MemoryExtractor
-> memory signals
-> Dreaming
-> long-term memories
-> structured prompt context

在线对话结束后,MemoryExtractor 只负责提取候选信号。

这些信号先留在短期层。

Dreaming 再作为显式整理步骤运行。

它可以分成三个概念阶段:

Light
收集还没有晋升的 signals
做基础清洗和归一化

REM
按 scope 和 kind 聚合同类信号
找出重复出现的主题

Deep
评分
晋升强信号
跳过弱信号或重复记忆
写入 Dream Journal

这里的 Dreaming 不需要神秘化。

它不是让系统凭空联想。

它做的是延迟判断。

在线阶段不要急着把一切变成长期记忆。离线或显式整理阶段再看:这条信息置信度够不够,是否重复出现,类型是否重要,是否已经有相同记忆。

这个延迟很关键。

因为很多信息只有放在一组 signals 里看,才知道它是不是稳定模式。

用户偶尔一次说“这次用中文”,不一定是长期偏好。

用户多次在计划讨论里切回中文,就更像稳定偏好。

一次提到某个项目背景,可能只是局部上下文。

多次围绕同一项目做决策,就更应该进入 project context。

Dreaming 的意义,就是给记忆一次从证据变成结论的机会。

Dream Journal 让整理过程可检查

自动记忆最怕黑箱。

如果用户不知道 Agent 为什么记住了某件事,也不知道它什么时候把某个 signal 晋升成长期记忆,记忆系统就会变得不可信。

所以 Dreaming 不应该只是悄悄改数据库。

它应该留下 Dream Journal。

Journal 不需要一开始就很复杂。第一版只要记录:

看到了多少 signals
晋升了多少 memories
跳过了多少候选
这次整理的大致摘要

这会让记忆系统多一层可解释性。

当 Agent 以后表现出某种稳定偏好时,系统至少能回头看:这条记忆是怎么被整理出来的。

可检查不等于每次都需要人工审批。

但它意味着自动记忆不是无声发生的副作用。

对长期 Agent 来说,这一点很重要。

用户可以允许系统自动学习,但系统也应该让学习过程有迹可循。

长期记忆进入 prompt 时要分区

记忆被晋升以后,也不应该作为一堆无序文本进入 prompt。

模型看到上下文时,需要知道每类信息的意义。

更好的 prompt 形状是:

Agent identity:
- ...

User profile:
- ...

Project context:
- ...

Preferences:
- ...

Known facts:
- ...

这比一个混合列表稳定得多。

因为同一句话放在不同区域里,权重和语义是不一样的。

“回答要简洁”如果出现在 Agent identity 里,它像系统风格。

如果出现在 User profile 里,它像用户偏好。

如果出现在 Known facts 里,它可能只是某次任务记录。

结构化 prompt 不是装饰。

它是在告诉模型:这些记忆分别应该如何参与决策。

同时,这个结构也能保护短期 signals。

未晋升的 signals 不进入普通 prompt。只有通过 Dreaming 晋升后的长期记忆,才会出现在这些结构化区域里。

这样自动记忆就不会因为提取得太积极而立刻污染上下文。

第一版应该保持确定性

记忆系统很容易一开始就走向复杂方案。

比如 embeddings、vector search、语义去重、模型驱动的反思、自动后台整理、多用户 profile partition、可视化记忆图谱。

这些能力都可能有价值。

但第一版更应该先把边界站稳。

SQLite 足够表达几类核心状态:

profiles
memory_signals
long_term_memories
dream_journal

确定性规则也足够验证最关键的语义:

  • 明确身份可以稳定存储
  • 明确偏好可以被提取成 signal
  • signal 不会直接进入 prompt
  • Dreaming 可以晋升强信号
  • 重复和低置信度内容可以被跳过
  • 整理结果可以写入 Dream Journal

第一版不需要马上判断所有微妙语义。

它需要先证明:系统不会把所有东西混在一起。

确定性实现的好处是可测试、可解释、可回放。

当存储边界和晋升语义稳定以后,再把 extractor 换成模型驱动,或者加入更强的检索能力,系统也不会失去基本形状。

如果一开始就让模型直接决定长期记忆,系统看起来更聪明,但边界更模糊。

记忆系统最怕的不是第一版不够智能。

最怕的是第一版太愿意相信自己。

记忆失败不能打断普通回答

记忆应该增强 Agent,而不应该让每次对话都变脆。

一次普通回答里,最重要的是用户能得到结果。

如果回答已经生成成功,但自动记忆提取失败,系统不应该让整个请求失败。

如果 Dreaming 运行到一半失败,系统不应该把 signals 标记为已晋升。

如果某条 profile 或长期记忆为空,或者包含不应该进入 prompt 的内容,prompt assembly 应该跳过它。

记忆系统应该遵守一个很朴素的原则:

记忆失败,不破坏当前回答。
整理失败,不伪装成整理成功。
进入 prompt 的内容,必须经过边界检查。

这让记忆层保持低耦合。

Agent Core 可以从记忆里取上下文,但普通对话不应该被记忆副作用绑架。

长期来看,这也更符合用户感受。

用户需要的是一个越来越懂上下文的 Agent,而不是一个因为整理记忆失败就无法回答问题的系统。

这一篇的结论

Agent 记忆不是把更多历史塞进 prompt。

历史只是原料。

真正的记忆系统要把经验变成证据,再把足够可靠的证据变成长期上下文。

session history 负责近期连续性。

identity 和 profile 负责稳定背景。

memory signals 负责保存候选线索。

Dreaming 负责整理、去重、评分、晋升和记录。

long-term memories 才负责回到 prompt,影响之后的判断。

这个链路看起来比直接追加历史更麻烦,但它让 Agent 的记忆从“存过什么”变成“什么值得回来”。

而对长期运行的 Agent 来说,后者才是真正重要的问题。