AI Agent的记忆与技能系统:从失忆到越用越懂你#
大语言模型本质上是无状态的——每次调用,模型对"你是谁"“上次聊了什么"“你喜欢什么风格"一无所知,除非这些信息被显式塞进当前的上下文窗口。这和 RAG 要解决的问题不是一回事:RAG 补的是模型"没见过的知识”,而这里要聊的是模型"没记住的经历”——同一个用户、同一个项目,换一次会话就打回原形,昨天纠正过的错误今天还会再犯一遍。
要让 Agent “越用越懂你”,就得给它一套外部记忆机制。这篇聊聊业界现在两种主流的记忆范式,以及一个更具体的案例——本文写作过程中实际在用的 Claude Code 的 Skills + Memory 系统是怎么设计的,包括它的取舍和坑。
一、为什么无状态的 LLM 需要外部记忆#
LLM 的"记忆"完全依赖上下文窗口,这带来两层限制:
- 会话内:上下文窗口再大也是有限资源,历史消息终究会被压缩或截断,早期的关键信息可能被后面的内容稀释掉。
- 会话间:这是更根本的问题——默认情况下,新开一次对话,模型对之前的一切都是空白。用户上次说过"别用 mock 数据库测试",这次还得再说一遍;项目上次讨论过要在某个日期前完成的截止时间,这次也得重新提供。
外部记忆要解决的正是"会话间"这层。思路很朴素:把值得跨会话保留的信息写到磁盘或数据库里,下次对话开始前,用某种方式把相关的部分捞回上下文。分歧只在于用什么结构存、用什么方式捞——这就引出了两种主流范式。
二、两种主流记忆范式对比#
2.1 向量检索式记忆#
以 MemGPT(现在的 Letta)和 Mem0 为代表的这一派,思路借鉴了操作系统的分页机制:
- 核心记忆(Core Memory):始终驻留在上下文里的一小块结构化信息,比如用户画像、当前任务的关键约束,容量有限但访问零延迟。
- 归档记忆(Archival / Recall Memory):存在外部向量数据库里的大量历史片段,模型通过函数调用主动检索——把当前对话内容 embedding 成向量,做相似度检索,召回 Top-K 条相关记忆插入上下文。
- 自我编辑:模型可以调用工具主动写入、修改、删除自己的记忆,理论上能持续自我进化。
Mem0 走的是更轻量的路子:从每轮对话里用 LLM 提炼出"事实"(比如"用户是数据科学家"“用户不喜欢被追问细节”),存成向量+图谱的混合结构,下次对话按需检索注入。
这一派的优点是召回不依赖精确匹配——哪怕用户换了个说法提问,语义相近的历史记忆也能被检索到;缺点是检索结果不可预测,同样的输入不一定每次都召回同一批记忆,调试和复现比较麻烦,而且提炼"事实"这一步本身依赖 LLM,容易把细节压缩丢或者提炼错。
2.2 文件/技能式记忆#
另一派更直接:记忆就是磁盘上的结构化文件(通常是 Markdown + frontmatter),检索不靠向量相似度,靠一个显式的索引文件——一个精简的目录,列出"有哪些记忆、分别是什么主题",模型先读索引,判断哪条记忆和当前任务相关,再按需读取对应文件的全文。
这一派的核心优势是可解释、可审计、可人工编辑——记忆是纯文本文件,人可以直接打开看、直接改、放进 git 追踪变更历史,出了问题一眼就能看出模型"记错了什么",而不用像调试向量检索那样去猜为什么召回了不相关的片段。代价是召回精度依赖索引的质量——如果索引写得含糊,或者相关记忆分散在措辞完全不同的多个文件里,模型可能判断不出该读哪个。
Skills 系统是同一套思路在"能力"而不是"事实"上的延伸:与其把所有操作流程都塞进一个巨大的系统提示,不如把每个流程打包成一个独立文件(一段简短描述 + 详细步骤),模型先看到的只是一份"技能目录"(每个技能一行描述),判断当前任务和哪个技能相关,再按需加载该技能的完整内容。这也是渐进式加载(progressive disclosure)的思路——不该在每次对话里都占着上下文的东西,先不加载。
2.3 对比一览#
| 维度 | 向量检索式 | 文件/技能式 |
|---|---|---|
| 存储结构 | 向量数据库 + embedding | 结构化文本文件 + 索引 |
| 召回方式 | 语义相似度检索 | 显式索引匹配 + 模型判断 |
| 可解释性 | 低——召回结果是黑盘 | 高——人可直接读、可 diff |
| 可编辑性 | 需要通过接口增删改 | 直接编辑文本文件 |
| 版本管理 | 通常没有 | 天然适合 git 追踪 |
| 召回精度依赖 | Embedding 模型质量 | 索引和描述的写法质量 |
| 适用场景 | 海量、非结构化、模糊召回 | 少量、高价值、需要可审计 |
两者不是互斥的——不少实际系统是混合的:高价值、少量的信息(用户身份、强约束)用文件式常驻,海量、低频访问的历史用向量式按需检索。
三、实战案例:写这篇文章时正在用的记忆与技能系统#
这篇文章本身就是一个现成的案例。写之前,Claude Code 先做了两件事,都是文件/技能式记忆的具体应用。
第一步:调用了一个叫 brainstorming 的 Skill。 系统提示里只有一份技能目录,每个技能一行描述,比如"用于创意工作之前——创建功能、搭建组件、增加新功能……在实现之前梳理用户意图、需求和设计"。判断这次任务(“找一个前沿方向写博客”)符合"创意工作"这个触发条件后,才去读取这个 Skill 的完整内容(一份几百行的流程说明:先问澄清问题、再提出几种方案、确认设计后才动笔)。整个过程中,其余没被触发的技能(调试流程、代码评审流程等)连内容都没有被加载过,不占上下文。
第二步:维护一份分层的记忆文件。 这套记忆系统把要记住的东西分成四类:
- user:关于用户角色、偏好、知识背景的记忆,比如"用户是资深 Java/Golang 工程师,第一次接触某个新领域时需要用已知领域类比"。
- feedback:用户明确给过的纠正或认可,比如"不要在回复末尾总结做了什么,用户看 diff 就够了"——这条记忆一旦写下,之后所有会话都会遵守,不用用户每次重复。
- project:项目相关的、代码里推导不出来的背景,比如某个截止日期、某个决策背后的动机。
- reference:外部系统的指针,比如"bug 追踪在 Linear 的某个项目里"。
每条记忆是一个独立的 Markdown 文件,带 frontmatter(name、description、metadata.type),文件之间用 [[name]] 互相链接。所有记忆文件的一行摘要汇总进一份 MEMORY.md 索引,这份索引常驻在上下文里,但索引本身很短——真正的内容只有被判断为"相关"时才会去读对应的文件。这和前面说的技能目录是同一个模式:先加载目录,再按需加载全文。
这套机制此刻正在真实运作:它记得这个博客用 Hugo + Blowfish、部署靠 deploy.sh 手动 rsync 而不是 CI,这些内容不是写在这次对话里,是上一次协作时被沉淀进项目记忆、这次对话开始前被重新读取的。
四、踩坑与设计取舍#
记忆写太多会变成噪音。 这套系统明确排除了一类信息:能从代码、git log、git blame 里直接推导出来的东西不存进记忆——因为那些信息一旦过时就是错误信息,而代码本身永远是最新的。记忆该存的是"看代码看不出来的东西":决策背后的动机、用户没写进代码但表达过的偏好。这个边界如果不划清楚,记忆库会迅速膨胀成一堆和当前代码状态脱节的过时描述,比没有记忆更糟——模型会基于错误的旧记忆做判断。
索引层不是可选项,是必需的。 如果没有 MEMORY.md 这一层,模型要么得把所有记忆文件全读一遍(拖慢速度、占满上下文),要么得靠语义检索去猜哪条相关(回到向量检索式的老问题)。索引本身是一种权衡——它要求写记忆的时候,摘要必须写得足够具体(“一句话钩子”),太笼统的索引条目等于没有索引,模型判断不出该不该展开读。
“哪怕只有 1% 相关也必须触发"这类强规则容易过度触发。 这次写博客的过程就是个例子——为一篇博客文章走了一遍完整的"brainstorming"流程(问澄清问题、提两三个方案、等设计确认),这套流程原本是为软件工程任务设计的,套在内容创作上多少有点形式大于内容。触发规则写得越"宁可错杀不可放过”,就越容易在不完全匹配的场景里被激活,执行时就需要判断力去裁剪掉不适用的步骤(比如这次跳过了"把设计写成 spec 文档提交到 git"这一步,因为博客文章不是软件实现计划)。规则本身没法覆盖所有边界情况,需要执行时的具体判断来补足。
并行写入的冲突问题。 如果多个 Agent 实例并行运行、同时想更新同一份记忆文件,会遇到和多智能体协作里"并行写操作几乎必然冲突"同样的问题——这也是为什么记忆系统通常设计成单线程写入、由一个 Agent 在对话过程中串行地读写,而不是开放给并发写入。
五、总结#
Agent 记忆系统的本质不是"让模型记住一切",而是可控的状态外部化——把值得跨会话保留的信息,从"隐式地依赖模型参数或对话历史"变成"显式地写在某个人和模型都能读懂、都能修改的地方"。向量检索式和文件/技能式只是这个思路下两种不同的存储和召回策略,选哪种取决于你更在乎召回的模糊容错率,还是可解释性和可审计性。
对个人博客这种规模的项目而言,文件式记忆几乎是唯一合理的选择——数据量小、更新频率低、但每一条都值得被人工检查。而这恰恰也是为什么"记忆"和"技能"值得放在一起讲:两者用的是同一套设计哲学,一个记住"发生过什么",一个记住"该怎么做",都靠一份轻量索引 + 按需加载全文来避免污染上下文。
通过邮件回复


