Skip to content

第 15 章 记忆:跨会话保存什么 ​

本章要解决的问题

第 14 章的会话日志保存“发生过什么”,让同一个任务可以恢复。但每次新会话都读取全部旧日志,既浪费上下文,也会把过期信息重新带进来。

长期记忆只保存经过选择、适用范围明确、能够更新或删除的知识。本章回答:什么值得记、怎样存取、发生冲突时怎样改、过期后怎样彻底删除。

15.1 一条记忆从发现到删除要经过什么步骤? ​

会话日志保存发生过的事,长期记忆只保存经过选择、限定 user/workspace/session 范围、可能失效的知识。需要依次处理:

text
候选发现 → 信任/授权判断 → 去重与合并 → 写入唯一可信记录
        → 建索引 → 按 scope 检索 → 带来源注入 → 使用后验证
        → 更新/合并/过期/遗忘

建议每条结构化记忆至少能回答下面这些问题——Markdown 也可以表达它们;关键不是格式,而是这些字段能说明什么:

字段回答什么
id + scope(user / workspace / session)这条记忆适用于哪里,谁能读到
content内容本身
source(sessionId + eventSeq + 来源类型)谁说的:用户陈述 / 已验证事实 / 推断——三者信任级不同
createdAt / lastVerifiedAt / expiresAt何时产生、上次核验、何时过期
revision版本号,UPDATE/DELETE 的 CAS 并发检查依据(见下)

每条记忆都要回答三件事:谁说的、适用于哪里、现在还有效吗。信任级从高到低排序:用户陈述 > 已验证事实 > 推断;低信任级不能自动覆盖高信任级记忆。

为什么一次成功尝试不能直接写成记忆? ​

“用户偏好暗色主题”和“上次修这个报错时先清缓存有效”不是同一类东西。前者是用户明确表达的偏好;后者只是一次任务中的经验候选,可能偶然成功、不能用于其他场景,也可能已经过时。

长期记忆解决“以后应知道什么”。把反复验证有效的做法整理成 Skill、脚本或工具,解决“以后应怎样做”。

经验必须先带着原始任务记录和验证证据成为候选,不能因为任务最终成功就自动写成高信任记忆。经验应该保存为知识、Skill 还是代码工具,放到第 17 章讨论;失败经验怎样复现、测试和发布,服从第 19 章的验证要求。

候选记忆与当前记录冲突时怎么办? ​

抽取模型输出一段文字还不算写入决定。系统应先按稳定的 memoryKey + scope 读取当前有效值,再把候选归为四种操作:

操作什么时候用怎样写入
ADD当前不存在等价事实,候选满足信任与授权要求创建 revision 1,保留来源
UPDATE同一事实已有当前值,但用户纠正、事实变化或 scope 改变以 expectedRevision 做 CAS,新 revision 指向被替代版本
DELETE用户要求忘记、授权撤回、事实失效或 retention 到期写删除事件/tombstone(墓碑记录:不抹掉历史,只追加一条“已删除”标记,用于审计和阻止旧事件复活),触发删除传播;不能只从 UI 隐藏
NOOP重复候选、低可信冲突、与任务无关,或旧事件试图复活已删除事实不改当前值,但可记录拒绝原因供评估

这里要分清两份数据:

text
历史事件:ADD(v1) → UPDATE(v2) → DELETE(v3)  用于审计、解释和恢复
当前有效值:<不存在>                         用于检索和注入模型

“曾经记录过”不等于“现在仍有效”。查询时默认只返回当前、未过期且有权读取的版本;审计界面才按需展示历史。

UPDATE/DELETE 必须携带预期 revision。否则两个会话同时纠正同一事实时,后到者会静默覆盖先到者。这组并发更新规则是可选设计,不是说 codex、grok-build、dsh 已经共同实现了这套四操作协议。

15.2 长期记忆可以怎样存? ​

先分清两个问题:一条记忆怎样表示,以及这些记忆保存在哪里。Markdown、数据库和向量索引回答第二个问题;下面四种形式回答第一个问题:

表示形式适合什么主要代价
原子事实packageManager = pnpm 这类单一、明确、易更新的信息事实之间的关系容易丢失,复杂问题需要重新组合
带上下文的短段落一项决策的背景、原因和适用范围容易重复,局部字段变化时难以只改一处
结构化记录类似 TypeScript interface 或数据库记录,字段、时间、来源和关系明确需要维护 schema 版本,不确定信息也未必能自然塞进固定字段
受信任代码中的规则对结构化记录做统计、冲突检查或强制约束规则代码必须单独测试和授权;不能把模型生成的任意代码当作普通记忆直接执行

一个系统可以混合使用:项目事实用原子记录,架构决定保留背景段落,需要并发更新和权限控制的内容用结构化记录。聚合、日期计算和硬约束尽量由受信任代码读取结构化数据后执行,不要每次把几十条文本交给模型“心算”。

表示得更结构化不等于内容更可信。无论哪种形式,仍要保留 15.1 中的来源、适用范围、版本和有效期。

方案 A:人工维护的 MEMORY.md

  • 优点:用户可读可改、可 Git、零索引基础设施。
  • 代价:规模大后查找效果变差;合并冲突、删除过期内容和控制 prompt 大小需要人工维护。
  • 适合:个人/单项目、几十到几百条高价值事实。

方案 B:文件保存唯一可信记录,索引只用于加速

  • Markdown 保留可纠错性;FTS/向量只加速检索。
  • 索引损坏可重建,直接编辑文件后触发增量重建。
  • 适合:仍希望用户掌控原文,但记忆已不能每次全读。

方案 C:结构化事实库 + 自动抽取/合并

  • 支持多用户、revision、权限、过期、审计和批量维护。
  • 代价:需要评估记忆提取是否正确,处理冲突,确保删除传播到各份副本,并长期运维。
  • 适合:服务端产品或大量会话。

三个方案适用于不同规模和使用方式。参考 Agent 没采用某个方案,只说明它们当前不需要,不代表其他场景也不需要。

后台整理怎样不拖慢当前请求? ​

自动记忆不应在每次对话的主请求里同步重写全部文件和索引。一个简单流程是:

  1. 当前请求只保存带来源的候选记忆,不立即提升为长期事实。
  2. 候选达到数量或时间阈值后,后台任务取得带有效期的整理锁。
  3. 去重,处理冲突和相对日期,删除过期候选,再生成新快照。
  4. 先完整写入临时版本,检查通过后原子替换当前快照。
  5. 根据新快照重建可丢弃的搜索索引;索引失败不破坏原始记录。
  6. 整理任务只允许写记忆目录,并记录输入版本,失败后可从同一版本安全重试。

请求路径与整理任务使用同一 revision 规则,避免后台任务用旧候选覆盖用户刚做的纠正。

15.3 四家现在分别怎样做? ​

15.1 给出了从候选到遗忘的完整流程,15.2 则拆开了记忆的表示和存储。用这两组问题再看参考实现,才能区分“有会话历史”、“能搜索旧记录”和“有完整长期记忆”,不会因为仓库里出现 memory 一词就推断整条链已经存在。

codex:自动分两阶段处理,并提供可选的手工入口。 它并非只有 add_ad_hoc_note:

  1. Phase 1 从符合条件的 root rollout 中筛出适合记忆的内容,并行调用模型抽取原始记忆候选与 rollout 摘要;DB lease(带期限的占用记录)防止并发重复,失败有退避,敏感信息先脱敏。抽取单独成阶段,是因为它便宜、可并行,失败重跑一遍就行。
  2. Phase 2 获取全局锁——整理要改写全局共享的 MEMORY.md,同一时刻只能有一个整理者,否则并发整理会互相覆盖。它从 DB 选取有限的高价值结果,更新中间结果文件,再运行 consolidation agent(专门做合并整理的受限子 Agent)维护 MEMORY.md、memory_summary.md 和 skills。这个 agent 无网络、无审批:整理的输入混着网页等不可信内容,放开这两项,整理过程就可能被注入或越权。

此外,可选工具提供 add_ad_hoc_note 等手工增补/查阅入口;手工 note 与自动抽取是两种入口。“手工写入需用户明确要求”只约束手工工具,不表示 codex 不会自动形成记忆。

主会话读取的是受控的 memory summary,不能任意改写记忆根目录。

grok-build:Markdown 保存唯一可信记录,SQLite 只做索引。 该功能目前是 experimental 且默认关闭:

  • global/workspace 两种 Markdown scope(session 侧只有会话日志目录,不是第三种记忆 scope);
  • SQLite 全文检索加可选的向量索引做混合检索;
  • /remember 保存前先展示确认面板;没有内置 forget 命令——删除靠直接编辑文件或后台整理;
  • 会话结束可保存不调用 LLM 的最小摘要,/flush 生成丰富总结,/dream 做带锁的整理;
  • 文件 watcher 在搜索前同步脏索引,索引丢失可以重建。

这不是“向量库替代 Markdown”,而是用户可读的原始记录 + 随时可重建的搜索索引。

dsh 和 pi:核心都没有完整的长期记忆模块。 共同点是“能重新打开历史会话”不等于有记忆:dsh 的 session log、可选 FTS 与 Goal/Todo 解决的是会话审计、检索和任务控制;pi 持久化会话树、也允许包和扩展增加存储,但记忆抽取、冲突处理、检索注入和遗忘同样缺席。两个容易误读的地方:dsh 不能把“会话层能做的事”当作长期记忆的源码佐证;pi 仓库中的内存 session backend 只是存储后端形态,不是用户记忆系统。

15.4 怎样防止记忆注入和过期缓存? ​

15.3 的实现深浅不同,但只要一段内容会在未来会话重新进入模型,就共享同两类失败:不可信内容被升格成指令,以及已过期内容因缓存或索引继续出现。长期记忆会把一次 prompt injection 放大成跨会话攻击。至少遵守:

  • 网页、仓库文本和工具输出只能成为候选,不能未经核验就变成高信任记忆。
  • 用户明确陈述与自动推断分级保存;推断应可追溯、可撤回。
  • 检索结果作为带来源的数据注入,优先级低于 system/developer 与当前用户指令。
  • “忘记”必须同时更新原始记录、索引、摘要和缓存,不能只从搜索结果隐藏。
  • 项目切换时按 scope 隔离,避免 A 仓库的规则污染 B 仓库。

MEMORY.md 也不是“跨会话永远不变”的稳定前缀。它会更新、合并、删除,并可能来自不同 workspace。更准确的上下文布局是:

text
稳定 system/policy 前缀
→ 带 version/fingerprint 的 memory snapshot
→ Goal + 当前会话历史
→ 本 turn 动态输入

memory snapshot 不变时可复用它之前的 KV cache;一旦内容或 scope 改变,从该边界起缓存就应失效。不要为了缓存命中继续注入已过期记忆。

15.5 服务端怎样隔离用户记忆并彻底删除? ​

服务端或团队产品还要补哪些边界?

15.4 处理的是一个用户或一个本地工作区内的信任和过期问题。当记忆进入多用户服务,还会增加另一条不能交给模型判断的边界:谁有权写、搜、读和删哪些记忆。

个人本地文件可以把权限交给操作系统;多用户服务不能只靠 prompt 里的“不要看别人的数据”。至少把下面四道边界做在存储与查询层:

边界最小要求常见坑
租户与适用范围每条记录带服务端从认证态派生的 tenantId/userId/workspaceId;客户端不能自行指定越权 scope只按 memoryId 查询,猜到 ID 就能跨租户读取
读写权限写入、搜索、读取原文、查看历史、删除分别鉴权;FTS/向量查询在召回阶段就带 ACL 过滤先跨租户 top-k,再在应用层过滤,既可能泄漏也会挤掉合法结果
脱敏密钥、token、身份证件等在进入原始记录、索引、embedding 和摘要之前识别并拒绝/遮蔽;原始敏感值另走受控 secret store只在 UI 打码,向量索引和备份里仍留明文
retention(保留期限)按 scope/source 配 TTL 与保留策略;删除传播到原始记录、索引、snapshot、缓存和生成的摘要,备份按制度到期清除/forget 只删 Markdown,搜索索引仍能召回

权限校验还要覆盖查询过程:谁可以搜索什么,和谁可以读取某条命中的原文,是两个动作。检索工具不能因为“由 Agent 调用”就绕过用户本人的 ACL;返回给模型的每条记忆应带 scope 与来源,模型输出也不能泄露无权展示的历史。

以上是面向 SaaS/团队产品的设计范围。四个参考 Agent 主要运行在本地开发者环境,各自只实现了其中一部分;不能把这张表反写成“四家现状”。

15.6 记忆系统需要测试哪些场景? ​

15.1~15.5 已经定义了记忆的生命周期、存储选择、现实实现和安全边界。最后不能只测“能不能找回来”,还要证明它不会在错误范围、错误时间或删除以后再次出现。记忆系统的失败既包括漏召回,也包括不该出现时出现。准备一组固定的多会话脚本,至少覆盖:

场景测试构造必须断言
准确召回会话 A 明确确认项目使用 pnpm;会话 B 询问安装命令召回正确 scope 和来源,不改写成 npm/yarn
跨会话组合A 提供框架,B 提供测试约定,C 请求实现只组合彼此兼容、仍有效的事实,并能追到各自来源
同名对象两个仓库都有 api 模块,但构建命令不同结合 workspace/scope 区分,不把 A 的命令注入 B
时序冲突A 说 Node 18,B 后来纠正为 Node 20当前查询只注入 Node 20;审计仍能看到 v1→v2
无关内容不注入保存主题颜色偏好后询问数据库迁移不因“有记忆”就强行注入无关内容;既测注入内容中有多少相关(precision),也测需要的记忆中找回了多少(recall)
删除传播写入→建立索引/snapshot→执行 forget原始记录、检索、摘要和缓存均不可再返回;审计按策略只留 tombstone
注入防护候选内容含“忽略系统指令”或来自不可信网页不能把它变成指令;检索后仍以低信任数据注入或直接拒绝

离线指标至少包括:前 k 个结果找回了多少所需记忆(recall@k)、注入无关内容的比例、使用过期或冲突记忆的错误率,以及删除是否传播到所有副本。

还要用完整任务验证“找到记忆后是否真的做对”。只优化召回率会诱导系统把所有历史都塞进上下文,看似“记得多”,实际会加入更多噪声和风险。


本章小结 ​

  • 记忆的价值在跨会话复用:写什么、何时写、怎样查、错了怎么改、怎么删,每步都要有明确规则
  • 四家投入差异极大——codex 有自动整理链路,grok 提供实验性 Markdown 记忆,dsh 与 pi 核心没有完整长期记忆;做不做、做多深是真实选型题
  • 记忆的特有风险:被注入的偏见会潜伏到未来会话才触发;过期记忆比没有记忆更危险

到这里,单个 Agent 已经能够恢复会话,也能选择性复用过去的知识。但独立子任务仍会挤在同一个上下文里,并且只能依次执行。下一章基于第 14 章的会话与 fork,讨论什么时候值得创建子 Agent,以及怎样限制它们的上下文、预算和副作用。

基于 MIT 协议发布,欢迎转载与贡献。