8. 让 AI 记住你:记忆体系的原理与设计
你和 AI 编程助手合作了一整天。
上午你让它帮你重构了认证模块,过程中它了解了你的项目结构——Go 语言,monorepo,internal 目录放业务逻辑,pkg 目录放公共库。下午你让它帮你写一个新的中间件,它已经知道你的中间件都遵循 func(next http.Handler) http.Handler 的签名模式,知道你习惯用 slog 而不是 logrus 做日志,知道你的错误处理风格是返回自定义的 AppError 类型而不是裸的 error。
到傍晚的时候,你们的协作已经非常顺畅了。你只需要说"帮我给 OrderService 加一个缓存层",它就知道该用你项目里已有的 cache.Store 接口,知道缓存 key 的命名规范是 {service}:{entity}:{id},知道 TTL 应该从配置中心读取而不是硬编码。你甚至不需要解释这些——它在之前的对话中已经"学会"了。
第二天早上,你打开一个新会话。
"帮我给 UserService 加一个缓存层。"
它回了一段代码。用的是 go-cache 库——你的项目里根本没有这个依赖。缓存 key 是 user_123 这种格式——和你项目的命名规范完全不同。TTL 硬编码成了 5 分钟——你昨天刚告诉它不要硬编码。
它不记得了。昨天一整天的"学习",全部归零。
你叹了口气,开始重新解释项目结构、代码规范、技术选型。这些解释你昨天已经做过一遍了。明天你还会再做一遍。后天也是。
这不是某个产品的缺陷。这是大模型的基本架构决定的。
8.1 无状态的代价
第二章已经拆解过这个机制:大模型的每一次推理都是无状态的纯函数,多轮对话的"记忆"只是客户端每次把完整历史重新发送给模型的结果。会话内的"记住"靠的是上下文窗口还装得下历史消息,一旦窗口满了就截断,一旦会话关闭就归零。
那一章的重点是解释机制本身。这一章要讨论的是:这个机制在长期协作中造成的实际代价。
开头的场景就是最典型的代价——重复冷启动。
每次新会话,你都要重新告诉 AI 你的项目结构、技术选型、代码规范、团队约定。这些信息不是"偶尔需要"的背景知识,而是"每次都需要"的基础上下文。没有它们,AI 生成的代码就像一个刚入职第一天的实习生写的——语法正确,但和你的项目格格不入。
重复冷启动的代价不只是"浪费时间重新解释"。它还有几个更隐蔽的成本:
认知负担。 你需要记住"哪些信息 AI 还不知道"。昨天你告诉它用 slog 不用 logrus,今天你得记住这件事还没告诉它——否则它会用 logrus,你看到代码后才意识到"哦,我忘了说"。你的大脑在替 AI 维护一份"它应该知道但还不知道"的清单。
信息遗漏。 你不可能每次都完整地重复所有上下文。你会遗漏一些"觉得不重要但其实很重要"的细节。比如你忘了提"错误消息要用英文",AI 就会混用中英文。这些遗漏导致的返工,累积起来是巨大的时间浪费。
协作深度的天花板。 人类同事之间的协作效率会随着合作时间增长而提高——你们建立了共同的术语、默契的分工、对彼此风格的理解。但和 AI 的协作永远停留在"第一天"的水平。你们无法积累默契,因为它没有跨会话的记忆。
你可能会想到第五章讨论的 Skill——编码规范、项目约定、架构模式这些信息,不是可以通过 Skill 在每次会话开始时自动加载吗?确实如此。Skill 能覆盖那些预先可定义的、相对稳定的知识:团队的编码风格、项目的技术选型、错误处理的固定模式。这些信息写成 Skill 后,每次会话都会自动注入上下文,不需要你重复解释。
但 Skill 无法覆盖的是动态积累的知识——那些在协作过程中逐渐产生的、无法预先定义的信息。昨天讨论后决定的数据库表结构、上周约定的 API 命名变更、三天前发现的一个性能瓶颈的临时解决方案——这些信息是在对话中"涌现"的,不是你能提前写进 Skill 的。而且它们会不断变化:今天的决策可能推翻上周的决策,新的发现可能让旧的方案失效。
Skill 解决的是"静态知识的复用",记忆系统要解决的是"动态知识的积累"。两者互补,但不能互相替代。
这些代价在短期内可以忍受——一次性的问答、临时的代码片段生成,冷启动的成本可以忽略。但在长期的项目协作中,动态知识的丢失会成为严重的效率瓶颈。即使有了 Skill 覆盖基础规范,你仍然需要在每次新会话中重新解释那些"上次对话中才确定的事情"——而这些往往是当前任务最关键的上下文。
这道鸿沟不会随着模型能力的提升而消失。模型变得更聪明,只是意味着它在"看到"信息后能做出更好的判断——但如果信息根本不在上下文里,再聪明也没用。
要跨越这道鸿沟,需要在无状态的推理引擎之上,构建一个有状态的记忆层。这个记忆层负责在会话之间持久化关键信息,并在新会话开始时把相关信息注入上下文,让 AI "看起来"记得你。
这就是记忆系统要解决的核心问题:在一个无状态的引擎上,构建有状态的体验。
8.2 记忆系统到底在做什么
"有状态的记忆层"——这个说法容易让人产生一个误解:是不是模型本身变成有状态的了?是不是服务端给模型加了一块"硬盘"?
不是。模型仍然是那个无状态的纯函数。每次推理,它看到的仍然只是上下文窗口里的文本,窗口之外的一切对它来说仍然不存在。
所谓的"记忆系统",做的事情其实很朴素:在模型之外维护一个持久化的存储(可以是数据库、文件、甚至一个 JSON),把有价值的信息存进去;下次对话开始时,从存储中取出相关信息,拼进上下文窗口——通常是塞进 System Prompt。模型"看到"了这些信息,所以它表现得好像"记得"你。但如果你把这段注入的文本删掉,它立刻就不记得了。
换句话说,记忆系统不是给模型装了记忆,而是给模型配了一个"秘书"——这个秘书替模型记笔记,每次开会前把相关笔记念给模型听。模型本身的大脑没有任何变化。
理解了这个前提,我们来看看当前的 AI 工具具体是怎么实现这个"秘书"的。
2024 年初,OpenAI 给 ChatGPT 加上了 Memory 功能。如果你用过,你会发现它的工作方式出奇地简单——简单到你可能会怀疑"就这?"
当你在对话中提到"我是一个 Go 开发者"或者"我的项目用 PostgreSQL",模型会在回答你的同时,悄悄调用一个内部工具,把这条信息存进那个外部存储。存储的格式是一句简短的陈述——"用户是 Go 开发者,偏好使用 slog 做日志"。下次你开新对话时,系统把你所有的记忆条目拼成一段文本,塞进 System Prompt 的开头。
没有向量数据库,没有语义检索,没有复杂的相关性排序。就是全部塞进去。
为什么这么"粗暴"的方案能工作?因为每条记忆只有一两句话,一百条记忆加起来也就几百个 Token。对于动辄几十万 Token 的上下文窗口来说,这点开销就像往游泳池里倒了一杯水——你甚至感觉不到水位变化。与其花大力气做精准检索(还可能漏掉重要信息),不如全量注入,保证不遗漏。
这个设计揭示了记忆系统的第一个反直觉的事实:当记忆总量足够小时,最"笨"的方案就是最优方案。 零检索延迟、零遗漏风险、实现成本极低。只有当记忆量大到塞不进上下文时,才需要引入那些听起来很高级的检索机制。
但 ChatGPT Memory 有一个明显的局限:它不区分项目。你在前端项目中积累的"用 React"的记忆,在你写后端 Go 代码时也会出现在上下文里。它不会造成错误,但会浪费空间——就像你桌上堆满了所有项目的文件,每次工作都要在一堆无关的纸张中找到需要的那几张。
Cursor 的做法更精细一些。它面对的是编程这个具体场景,而编程有一个天然的边界——项目。你同时在做三个项目,每个项目的技术栈、代码规范、架构决策都不一样,把它们混在一起只会互相干扰。所以 Cursor 的记忆是项目级别的——每个项目有自己独立的记忆库,打开 A 项目时只看到 A 项目的记忆,不会被 B 项目的信息污染。这就像给每个项目配了一个独立的笔记本,而不是所有东西都记在同一张纸上。
Cursor 还做了一件有意思的事:当它判断某条信息值得记住时,不会悄悄存下来,而是会显式地告诉你——"我记住了:你的项目使用 gRPC 做服务间通信"。你可以确认,也可以说"不对,我只是在讨论 gRPC,我们实际用的是 REST"。
为什么要多这一步?因为一条错误的记忆比没有记忆更糟糕。想象一下:系统错误地记住了"项目用 REST"(实际上你只是在比较 REST 和 gRPC 的优劣,最终选了 gRPC),然后在后续的每一次对话中,它都基于这条错误信息生成代码。你会反复看到 REST 风格的代码,反复纠正,却不知道问题出在一条你从未确认过的记忆上。用户确认机制牺牲了一点自动化程度,换来的是更高的准确性——这个权衡在编程场景下几乎总是值得的。
Claude 走了另一条路。它的记忆不是一堆散乱的句子,而是按类别组织的结构化信息——用户偏好是一个区块,项目技术栈是另一个区块,编码规范又是一个区块。当你说"我们把日志库换成 zerolog 了",系统知道应该更新"编码偏好"下的"日志库"字段,而不是在记忆库里新增一条。这避免了一个棘手的问题:记忆库里同时存在"日志用 slog"和"日志用 zerolog"两条矛盾信息。
Claude 还给了用户一个完整的记忆管理界面——你可以查看、编辑、删除所有记忆条目。这看起来是个小功能,但它解决了记忆系统最深层的信任问题:你需要知道 AI 到底"记住了"什么。 如果记忆是一个黑箱,你永远不知道 AI 的某个奇怪行为是不是因为一条错误的记忆在作祟。给你一个审计入口,让你能看到全貌,信任才能建立起来。
拆解完这三个产品,你会发现它们虽然实现细节不同,但做的事情是一样的:在对话中识别有价值的信息,提取出来存下来,下次对话时再注入回去。差异只在于每个环节的具体策略——谁来判断什么值得记、存成什么格式、怎么处理冲突、给用户多大的控制权。
这些差异不是随意的产品决策,而是对不同场景的适配。通用对话场景(ChatGPT),用最简单的方案就够了——记忆量小,全量注入,不需要复杂的检索。专业编程场景(Cursor),需要项目隔离和更高的准确性——一条错误的记忆会导致持续的错误代码。深度协作场景(Claude),需要结构化管理和完整的用户控制——因为记忆会深刻地影响 AI 的长期行为,用户必须能审计和修正。
8.3 什么信息值得记住
记忆系统面临的第一个问题,也是最根本的问题:从海量的对话内容中,什么应该被存下来?
你和 AI 的一次对话可能有三十轮。其中二十五轮是"帮我把这个变量名改成 count""这个函数的第三个参数是什么意思""帮我加个空行"这样的临时性交互。这些内容在当下有用,但没有任何跨会话的价值——下次你不会需要知道你曾经改过一个变量名。
但在第 12 轮,你说了一句:"我们决定用 gRPC 而不是 REST,因为内部服务之间的调用频率很高。"这句话的价值完全不同——它是一个架构决策,会影响后续所有涉及服务间通信的开发工作。如果 AI 能记住这个决策,下次你让它写一个新的服务调用时,它就不会生成 REST 风格的代码。
所以记忆提取的核心挑战是区分信号和噪声。三十轮对话里,可能只有两三轮包含值得长期记住的信息。系统需要准确地找到这两三轮,忽略其余的二十七轮。
当前的主流做法是让模型自己来做这个判断。具体来说,系统会给模型一段提取指令,告诉它什么样的信息有跨会话价值——用户的偏好、项目的技术事实、确定的设计决策、团队的协作规范——同时告诉它什么不值得记——临时性的问答、被否定的方案、只在当前任务中有意义的操作。
这段指令的设计有一个微妙之处:它必须同时包含"要提取什么"和"不要提取什么"。如果只告诉模型"提取有价值的信息",它会过度提取——模型天然倾向于"宁可多记不可漏记",它会把几乎所有信息都标记为"有价值"。加上负面清单——"不要提取临时性问题""不要提取被否定的方案"——才能把提取精度控制在合理范围内。
还有一个容易被忽略的设计:允许模型说"这轮对话没有值得记住的"。如果不给这个退出机制,模型会"硬凑"——因为它被要求提取信息,它就会尽力找到一些东西来提取,哪怕那些东西其实没什么价值。结果就是记忆库里充满了"用户询问了 fmt.Sprintf 的用法"这样的噪声。
判断完"什么值得记"之后,下一个问题是"什么时候记"。
最直觉的做法是每轮对话结束后立即提取。但这有一个问题:你在第 5 轮说"我们用 REST",在第 15 轮说"算了,还是用 gRPC 吧"。如果每轮都实时提取,第 5 轮会存入"项目用 REST",第 15 轮会存入"项目用 gRPC"——记忆库里出现了矛盾。而如果等到会话结束再统一提取,模型能看到完整的讨论过程,知道最终结论是 gRPC,就不会把中间被否定的 REST 方案存下来。
但等到会话结束也有风险——如果浏览器崩溃了、网络断了,整个会话的记忆就全丢了。所以实践中大多数系统采用混合策略:用户显式说"记住这个"的信息立即写入(置信度极高,不需要等);模型自主判断的信息在会话结束时批量提取(需要完整上下文来做准确判断);特别长的会话,每隔一段时间做一次增量提取(防止意外中断导致全部丢失)。
8.4 存了不等于能用
信息存下来了,但存了不等于能用。想象你的记忆库里已经积累了五百条记忆——三个项目的技术选型、两年来的编码偏好、几十个架构决策。现在你开始一个新会话,说"帮我给 OrderService 加一个缓存层"。
如果记忆总量还在"全量注入"的范围内(几百个 Token),事情很简单——全部塞进去,让模型自己判断哪些相关。但五百条记忆可能有几千甚至上万 Token,全量注入不再可行。系统必须从中选出最相关的那几条。
"缓存策略是 Cache-Aside,TTL 从配置中心读取"——高度相关。"缓存 key 的命名规范是 {service}:{entity}:{id}"——高度相关。"前端用 React"——完全不相关。系统需要做出这个判断。
最常用的方式是语义检索——把你的消息和每条记忆都转换成向量,通过向量距离衡量语义相似度。"加缓存层"和"Cache-Aside 策略"在字面上没有重叠,但语义上高度相关,向量检索能捕捉到这种关联。
但语义检索有一个坑:语义相似不等于任务相关。"上次讨论过 Redis 的内存淘汰策略"和"给 OrderService 加缓存层"在语义上很相似——都和缓存有关。但如果上次讨论的是另一个项目的 Redis 配置,它和当前任务就毫无关系。向量检索只看语义距离,不看项目归属。
这就是为什么 Cursor 的记忆要带项目标识——检索时先按项目过滤,再在过滤后的子集内做语义匹配。这不只是为了"组织整洁",而是为了检索精度。没有这层过滤,跨项目的语义噪声会严重干扰检索结果。
还有一个更隐蔽的坑:用户的第一条消息往往信息量不足。你说"帮我写个 API"——这条消息太宽泛了,几乎和所有记忆都有一定的相关性,但和任何一条都不是高度相关。检索出来的结果是一堆"看起来都有点关系"的记忆,真正有用的被淹没在噪声里。
解决方案是延迟检索——不在第一条消息时就检索,而是等对话进行了两三轮、上下文更丰富之后再触发。或者结合 IDE 的上下文——你当前打开的文件、光标所在的位置——来增强检索查询的信息量。
检索到了相关记忆,最后一个问题是:放在上下文的什么位置?这个决定看起来是技术细节,但它直接影响模型对记忆的"态度"。放在 System Prompt 里,模型会把记忆当作指令来遵循——"用 slog 做日志"会被当作一条必须遵守的规则。作为独立的上下文块注入(比如用 <memories> 标签包裹),模型会把记忆当作参考信息——它知道这些是历史信息,会参考但不会盲目遵循。
实践中两种方式混合使用:用户偏好和团队规范放 System Prompt(它们是"规则",应该被严格遵守),项目知识和历史决策放独立块(它们是"参考",当前需求优先级更高)。
8.5 当新记忆和旧记忆打架
记忆系统运行一段时间后,你会遇到一个不可避免的问题。上周你说"我们的前端用 React"。这周你说"我们决定把前端迁移到 Vue"。两条信息都被存进了记忆库。下次 AI 检索到这两条记忆时,它会怎么处理?
如果它只看到"前端用 React"(因为这条记忆的语义和当前查询更匹配),它会生成 React 代码——这是错的。如果它同时看到两条,它可能会困惑。如果它足够聪明,能判断出"迁移到 Vue"是更新的信息——但这需要它理解时间顺序和"迁移"的语义含义。
这个问题的本质是:世界在变化,但记忆库中的旧信息不会自动消失。 不同的产品用不同的方式处理这个问题。Claude 的结构化存储天然地解决了一部分冲突——当记忆按 key 组织时,"前端框架"这个 key 只有一个值,新值直接覆盖旧值。冲突检测变成了简单的 key 匹配,不需要语义理解。但前提是记忆在写入时就被正确地分类了——如果"React"被标记为"前端框架"而"Vue"被标记为"迁移计划",系统就检测不到冲突。
另一种方式是在写入新记忆时,先搜索语义相似的旧记忆,然后让模型判断两者是否矛盾。模型能理解"REST"和"gRPC"在"服务间通信"这个语境下是互斥的选择,会判断为冲突,保留更新的那条。这种方式更灵活,但每次写入都要多一次模型调用来做冲突检测。
最可靠的方式其实是最简单的:依赖用户的显式表达。当你说"记住:我们现在用 gRPC 了,不用 REST 了"——这句话本身就包含了"覆盖旧信息"的语义。"现在"暗示了之前用的是别的东西,"不用"明确否定了旧信息。模型捕捉到这些语义线索,不只是存入新记忆,还会主动搜索并删除矛盾的旧记忆。
除了冲突,还有一个更缓慢的问题:记忆会过时。"项目用 Go 1.20"——如果项目已经升级到 1.22,这条记忆就是错的。但系统怎么知道它过时了?除非你显式地说"我们升级了",否则系统无法自动判断。"上次讨论了连接池大小的问题"——这条记忆在讨论后的一两周内可能有参考价值,但三个月后大概率已经没用了。
理论上可以设计基于时间的衰减机制——记忆的"重要性分数"随时间递减,长期未被使用的记忆逐渐淡出。但实践中大多数产品选择了最保守的策略:不主动遗忘,依赖用户管理。原因很务实——自动遗忘的风险太高。如果系统错误地删除了一条仍然有价值的记忆,用户可能根本不会意识到(他只会发现"AI 突然不记得我的偏好了"),而且无法恢复。相比之下,"记忆库里有一些过时信息"的代价要小得多——最多浪费一点上下文空间。
8.6 怎么让记忆系统真正为你工作
理解了原理之后,回到最实际的问题:你该怎么做?
大多数人使用记忆系统的方式是纯被动的——正常对话,让系统在后台自动提取。这能工作,但效果有限。系统可能漏掉你觉得重要的信息(它的"重要性"判断和你不一致),也可能记住一些无关紧要的东西。
更有效的方式是主动喂养。当你做出一个重要决策时,显式地告诉 AI "记住这个"。不是等系统自己发现,而是你主动标记。这就像和新同事合作时,你会主动说"这个很重要,记一下"——而不是指望他从你的只言片语中自己推断出什么是重要的。
新项目的第一次对话,是建立基础记忆的最佳时机。与其让系统在后续几十次对话中慢慢"发现"你的项目信息,不如一开始就把关键信息一次性告诉它——语言版本、框架选型、数据库、项目结构、编码规范、错误处理风格。这段信息会被一次性写入记忆库,后续所有对话都能受益。这比零散地在十次对话中提到这些信息高效得多,而且不会遗漏。
还有一个容易被忽略的习惯:定期审查记忆库。每隔一段时间——比如每月一次——花五分钟看看 AI 到底记住了什么。项目升级了依赖版本?更新相关记忆。换了技术方案?删除旧记忆。发现 AI 的行为不符合预期?检查是否有错误的记忆在误导它。这个投入很小,但能避免"AI 基于过时信息持续生成错误代码"的问题——这个问题一旦发生,排查成本远超五分钟。
最后一个实用的判断:如果你同时有记忆系统和规则文件(比如 Cursor 的 .cursor/rules),一条信息应该放在哪里?判断标准很简单:这条信息是团队共享的,还是你个人的? 团队共享的规范——所有人都应该遵守的编码风格、不会频繁变化的项目结构、需要版本控制的约定——放规则文件,提交到 Git,让所有团队成员都能看到。个人的偏好——你自己的习惯、动态变化的决策、不适合写进代码仓库的临时方案——放记忆。规则文件是"团队的共识",记忆是"你和 AI 之间的默契"。
回顾一下这一章的脉络。
我们从一个日常场景出发——AI 不记得昨天的对话——引出了"无状态引擎上构建有状态体验"这个核心矛盾。然后拆解了三个真实产品的实现,看到了它们共享的基本架构和各自的设计权衡。接着深入到工程层面:什么信息值得记、什么时候记、怎么存、怎么检索、怎么处理冲突和过时。最后落地到使用层面:主动喂养、项目初始化、定期审查。
记忆系统目前仍然是一个快速演进的领域。但核心问题不会变——在无状态的引擎上构建有状态的体验。理解了这个问题的本质和当前的解决思路,你就能在新方案出现时快速理解它的设计逻辑。
这一章还引出了一个更深层的问题。记忆注入占用的上下文空间,就是留给当前任务的空间减少。记忆越丰富,注入越多,留给实际工作的"带宽"就越窄。这个资源分配问题不只关乎记忆——System Prompt、工具描述、Skill 指令、检索结果、记忆注入,它们都在争夺同一个有限的上下文窗口。怎么在这些竞争者之间分配空间,让每一个 Token 都花在刀刃上——这是下一章要讨论的问题。