外观
记忆系统
LLM 本身是无状态的:每次调用只看到这一次请求里的 token,上一次对话对它而言从未发生。一个「记得你」的智能体,这种记忆能力 100% 来自 harness——是 harness 决定在每一步把哪些历史信息塞回上下文窗口。记忆系统(memory system)就是 harness 里负责持久化、筛选、注入历史信息的那组机制。
一句话定义
记忆系统 = 决定「哪些信息值得跨过上下文窗口的边界存活下来,以及下次以什么形式回到上下文里」的全部机制。
为什么需要记忆系统
表面上看答案很明显:上下文窗口装不下所有历史。但真正的工程动机有三层,一层比一层关键:
- 容量约束。即使上下文窗口到了 1M token,塞满它也是糟糕的:成本线性增长、延迟上升,且模型在长上下文中对中间段落的注意力会衰减("lost in the middle" 现象)。记忆的第一职责不是「存更多」,而是「让模型只看到值得看的」。
- 跨会话连续性。软件工程任务跨越数天,用户偏好跨越数月。没有持久记忆,每个会话都是一次冷启动——用户要重复交代项目结构、构建命令、代码风格。这是编码 agent 体验差异的最大来源之一。
- 经验积累。人类工程师的价值很大一部分来自「上次踩过这个坑」。如果 agent 能把「这个仓库的测试必须用 xvfb 跑」这类教训沉淀下来,它的能力会随使用次数复利增长;反之每个错误都会重犯。这是记忆系统最被低估的作用。
记忆不是免费的
每一段被注入上下文的记忆都在挤占工作记忆的预算,都在增加干扰风险。记忆系统的设计目标从来不是「记住一切」,而是用最小的上下文成本换取最大的连续性收益。
三层记忆模型
借鉴认知科学与操作系统的分层思想,实践中有效的记忆系统几乎都收敛到三层结构:
┌──────────────────────────────────────────────────────────┐
│ L1 上下文内工作记忆 (in-context working memory) │
│ 当前会话的消息历史、工具结果、scratchpad │
│ 延迟:0(已在上下文) 容量:受上下文窗口限制 │
│ 淘汰:压缩 / 截断 / 摘要 │
├──────────────────────────────────────────────────────────┤
│ L2 会话级记忆 (session-level memory) │
│ 会话摘要、任务状态、todo 清单、阶段性结论 │
│ 延迟:极低(文件或小数据) 容量:KB 级 │
│ 淘汰:会话归档、任务完结即废弃 │
├──────────────────────────────────────────────────────────┤
│ L3 跨会话长期记忆 (long-term memory) │
│ 用户偏好、项目约定、沉淀的教训、领域知识 │
│ 延迟:需显式加载或检索 容量:理论上无限 │
│ 淘汰:人工编辑 / 冲突合并 / 定期清理 │
└──────────────────────────────────────────────────────────┘三层的核心区别不是「存多久」,而是进入上下文的方式:
| 层级 | 进入上下文的方式 | 典型载体 | 谁在写 |
|---|---|---|---|
| L1 工作记忆 | 天然就在 | 消息列表 | harness 自动追加 |
| L2 会话记忆 | 压缩后注入 | 摘要、todo 文件 | harness 自动 + agent 主动 |
| L3 长期记忆 | 显式加载或检索 | 记忆文件、数据库、向量库 | 用户手写 + agent 学写 |
L1 的管理(压缩、截断、摘要)属于上下文工程的范畴;本章聚焦 L2 与 L3——真正需要「设计」的部分。
记忆文件模式:从 CLAUDE.md 到 AGENTS.md
2024 到 2026 年,编码 agent 的长期记忆方案出现了一个显著的收敛:纯文本 Markdown 文件,放在仓库里,会话开始时整体注入。
它长什么样
Claude Code 的记忆文件分三个作用域,会话启动时全部加载并拼接:
- 企业级策略:由组织管理员管理,对该组织所有项目生效,优先级最高;
- 项目记忆:仓库根目录的
./CLAUDE.md,提交进 git,全团队共享——放构建命令、代码规范、架构约定; - 用户记忆:
~/.claude/CLAUDE.md,个人偏好,跨所有项目生效。
此外还支持:嵌套的 CLAUDE.md(子目录下的文件按需加载)、@path/to/file.md 导入语法(把其他文件内容拉进来,保持主文件整洁)、/memory 命令直接编辑记忆文件、以及在输入开头键入 # 快速追加一条记忆。早期的 CLAUDE.local.md(个人项目级、不入库)已被标记为废弃,官方建议改用导入语法。
AGENTS.md 则把同一思路做成了跨厂商的开放约定:一个放在仓库根目录的 Markdown 文件,「给 agent 看的 README」。据其官网,该格式由 OpenAI Codex、Amp、Google Jules、Cursor、Factory 等共同推动,已被 6 万多个开源项目采用,现由 Linux 基金会旗下的 Agentic AI Foundation 托管。规则同样朴素:离被编辑文件最近的 AGENTS.md 优先;用户在对话里的显式指令高于一切文件。
为什么赢的是文本文件而不是数据库
这个收敛不是偶然,它说明长期记忆的真实需求结构:
- 可读、可改、可 diff。记忆文件是普通 Markdown,用户能直接看到 agent「以为」的是什么,能用 git 追踪每一次变化,能一键删除。检索式方案(向量库)天然做不到这一点——用户无法审查「检索器认为相关的东西」这个集合。
- 内容以指令和约定为主,而非海量事实。编码 agent 需要记住的是「这个项目用 pnpm 不用 npm」「提交前必须跑 lint」这类少量、高价值、强稳定的约定。这个量级(几百行)整体注入的代价很低,检索反而是过度工程。
- 天然支持分层与就近原则。嵌套文件让 monorepo 里每个子项目有自己的约定,「最近的文件优先」是一个零配置的消歧规则。
判断标准
当长期记忆的内容是「约定与偏好」(少而稳定、需要用户审计),选记忆文件;当内容是「历史事实与经历」(多而杂、按需召回),才需要检索式方案。大多数编码 agent 的主要需求是前者。
memory 目录:agent 自己写的记忆
记忆文件的下一步演化是写入权从人移交一部分给 agent。以 Claude Code 为例,除用户维护的 CLAUDE.md 外,社区资料显示其 v2.1.59(2026 年 2 月)引入了自动记忆(auto memory)并默认开启:agent 在工作中自主把值得保留的信息(调试结论、用户纠正过的偏好、关键决策)写入按项目隔离的 memory 目录(默认 ~/.claude/projects/<项目路径>/memory/),后续会话自动召回,可用 /memory 查看和清理。这个目录不入 git、本机私有,与团队共享的 CLAUDE.md 形成「人写的共享约定 + agent 写的私人笔记」的双层结构。
这个分工值得记住:共享的、需要审计的走仓库内文件;私有的、流水式的走本地目录。两者都是纯文本,都保留了可检查性。
写入时机:什么时候值得记
记忆系统最难的不是存,而是决定写什么。写得太少,agent 永远像个新人;写得太多,记忆库变成噪声坟场,检索/注入时被无关内容污染。
实践中行之有效的写入触发条件:
| 值得记 | 不值得记 |
|---|---|
| 用户显式纠正("不要用 X,用 Y") | 模型自己一次就猜对的事 |
反复出现才解决的非显然事实("这个测试要加 --no-sandbox") | 读一遍文档就能查到的事实 |
| 用户表达的稳定偏好(代码风格、沟通方式) | 一次性的任务细节("把这个变量改名为 foo") |
| 项目特有的隐性约定(不在任何文档里) | 已经写在 README / CLAUDE.md 里的内容 |
| 导致失败的环境陷阱 | 与当前任务无关的闲聊信息 |
写入口径可以归纳成一条规则:只记「下次没有它就会犯错或重复劳动」的信息。一个实用的自检——如果这条信息明天被删掉,会导致可预见的损失吗?不会就别写。
写入主体也有讲究:
- 用户触发(
#快捷记忆、"记住这一点"):最可靠,信噪比最高,应永远支持。 - agent 自主写入:需要在系统提示中给出明确的写入标准,否则模型要么什么都不记,要么把工具输出整段抄进去。Letta 的做法是给每个记忆块配一个
description字段告诉 agent 这个块该装什么——描述写得越好,agent 的写入判断越准。 - harness 自动沉淀(如会话结束时生成摘要):适合 L2,但要控制粒度,摘要的摘要会指数级失真。
遗忘与淘汰:被严重低估的一半
只进不出的记忆系统一定会腐烂。真实场景里遗忘策略至少包括四种机制:
- 冲突合并。新记忆与旧记忆矛盾时("我在备战马拉松" vs "我脚踝扭伤了"),要么让模型在写入时主动改写旧条目,要么在注入前做一致性检查。OpenAI 在 ChatGPT 新版记忆系统中明确提到,旧的「saved memories」机制的问题之一就是条目会互相矛盾,新版改为持续更新的「记忆摘要」来缓解这一点。
- 容量上限。Letta 的每个记忆块有字符上限(如 5000 字符),写满后 agent 必须自己决定压缩或替换什么——把「淘汰决策」变成 agent 的显式动作,而不是静默丢弃。
- 人工清理通道。
/memory编辑、ChatGPT 的记忆摘要页手动删除,都是必要设施。用户必须能回答「它到底记住了我什么」。 - 作用域隔离代替删除。项目级记忆不跨项目泄露、临时会话(如 ChatGPT 的 Temporary Chat)完全不读写记忆——很多「遗忘」需求其实是隔离需求。
别把向量相似度当淘汰策略
「按时间衰减」「按相似度去重」这类全自动淘汰听着优雅,实际上会删掉低频但关键的记忆(比如每年只用一次的发布流程)。淘汰决策要么交给人,要么交给模型做显式判断,不要交给统计启发式。
记忆 vs RAG:不是同一个东西
记忆系统和 RAG(retrieval-augmented generation)在工程上经常被混为一谈,因为它们都可能用到向量检索。但两者的目标函数不同:
| 维度 | 记忆系统 | RAG |
|---|---|---|
| 内容来源 | agent 自身的经历与交互产物 | 外部语料(文档、代码库、网页) |
| 核心问题 | 哪些交互产物值得沉淀、何时注入 | 如何从大规模语料中召回相关片段 |
| 写入方 | 用户 + agent(动态增长) | 通常是离线灌入(相对静态) |
| 个性化 | 强,面向「这个用户/这个项目」 | 弱,面向全体使用者 |
| 失败模式 | 记住错误信息、记忆污染 | 召回不准、切片不当 |
两者的配合方式也很清晰:RAG 管「世界知识」,记忆管「我和你的历史」。一个编码 agent 用 RAG 检索代码库和文档(见工具系统与上下文工程),用记忆系统存项目约定和用户偏好。共用一套向量设施没问题,但写入路径、淘汰策略、注入逻辑应该分开设计——把用户偏好和代码片段混在同一个向量库里,是「记忆污染」故障的经典温床。
向量检索式记忆的坑
向量检索依然是长期记忆的重要选项(尤其是对话历史、大规模经历召回),但这个方向有一组反复出现的坑:
- 相似度 ≠ 相关性。嵌入向量度量语义相似,而记忆需要的是「对当前任务有用」。用户说「帮我看看那个 bug」,向量检索会召回一堆谈论 bug 的记忆,却未必是「上次修复这个 bug 用了什么方法」那一条。时序、因果、任务上下文都不是嵌入能表达的。
- 写时无人审稿,检索结果不可预测。记忆文件模式下用户能看到全部记忆;向量库里存了几千条,用户根本不知道里面有什么,也无法预知某次查询会捞出什么。可审计性的丧失是向量方案最大的隐性成本。
- 退化与漂移。随着写入增多,库里语义近邻越来越密,top-k 检索的信噪比持续下降。没有淘汰机制的向量记忆库,半年后的表现往往不如刚建库时。
- 嵌入模型的隐性升级成本。换嵌入模型意味着全库重建索引,而记忆数据恰恰是最难迁移的——它包含大量只有原始上下文才能理解的碎片。
- 「检索到了但没被当回事」。即使召回正确,注入位置不佳(埋在上万 token 的中间)也可能被模型忽略。检索只是记忆链路的一半,注入策略同样重要。
这不是说向量记忆不能用——Letta 的 archival memory 就是向量化的,ChatGPT 的记忆摘要背后也有检索。而是说:向量检索适合做 L3 里「经历型」记忆的召回通道,不适合做唯一的记忆机制,更不适合替代可审计的记忆文件。
三个代表性案例
Claude Code:文件即记忆
Claude Code 代表了「极简可审计」路线:长期记忆就是分层的 Markdown 文件(企业/项目/用户三级 CLAUDE.md + 嵌套文件 + @ 导入),会话启动时整体读入;加上 agent 自维护的本地 memory 目录负责沉淀私有笔记。整个系统没有向量索引——社区流传的实现分析指出,它对记忆文件的选择依赖 LLM 读取文件头做判断,而非嵌入相似度。设计哲学很明确:用户能看到、能改、能进 git 的记忆,才是可信的记忆。代价是容量——整体注入决定了记忆文件必须保持精炼,几百行是健康量级,写几千行的 CLAUDE.md 反而会稀释关键指令。深度剖析见案例:Claude Code。
ChatGPT memory:持续更新的用户画像
ChatGPT 的记忆走的是「自动用户画像」路线:系统在后台持续把对话中显露的事实(职业、偏好、项目背景)合成为一份不断更新的「记忆摘要」(memory summary),取代早期需要用户手动管理的 saved memories。OpenAI 官方文档强调的改进点恰好印证了本章的两个原则:旧系统的问题在于条目会过期、会互相矛盾(合并与淘汰缺失);新系统的重点是可见性——用户能在摘要页看到、编辑、删除记忆,能点击回答下方的「来源」图标查看本次回答用到了哪些记忆。临时会话(Temporary Chat)则提供了彻底的隔离选项。这是消费级产品里「记忆治理」做得最完整的样本。
Letta(原 MemGPT):把操作系统内存管理搬进 LLM
Letta 的前身是 2023 年 10 月发布的 MemGPT(arXiv:2310.08560),其核心思想是「虚拟上下文管理」(virtual context management):像 OS 在快慢存储之间搬运内存页一样,在受限的上下文窗口与外部存储之间搬运信息,并用「中断」(interrupt)机制管理 agent 与用户之间的控制流。
这个思想在 Letta 产品里落地为一套清晰的分层:
- 记忆块(memory blocks):以 XML 结构固定在系统提示中、始终可见的上下文区段。每个块有
label、description、value、limit(字符上限)四个字段,agent 通过内置的记忆工具自主读写;块可设为只读(read_only),还能被多个 agent 挂载实现共享记忆。经典配置是persona(agent 的人设)与human(用户画像)两块。 - 上下文外存储:所有消息与状态都持久化在数据库中,被挤出上下文窗口的内容不会丢失,agent 可通过检索工具找回——对应 MemGPT 的 archival / recall memory 概念。
Letta 的贡献在于证明了「agent 自主管理自己的记忆」这条路的可行性:写入、压缩、淘汰全部由模型通过工具调用完成,harness 只提供结构和上限。它对记忆系统设计的启发是普适的——给记忆加上 label、description、limit 三个元数据,是把「无结构笔记」升级为「可管理内存」的最小成本。
设计一个记忆系统:决策清单
动手之前,按顺序回答这七个问题:
- 记什么? 列出你的 agent 真正需要的记忆类型:用户偏好、项目约定、任务状态、历史对话、领域事实。类型不同,载体不同。
- 每一类属于哪一层? 映射到 L1/L2/L3。能整体注入且需要审计的,进记忆文件;量大且按需召回的,才考虑检索。
- 写入权给谁? 用户手写 / agent 自主 / harness 自动,三者可以并存,但每类记忆必须有明确的唯一写入方,否则冲突无解。
- 写入标准是什么? 如果允许 agent 写,在提示词里给出像「只记下次缺了会犯错的信息」这样的硬标准,并配好块描述。
- 如何淘汰? 上限是多少?满了谁决定删什么?新旧冲突谁合并?——回答不了就先用文件 + 人工编辑。
- 用户能看到和删除吗? 记忆必须可审计、可清除。做不到这一点的方案(黑盒向量库)要给出强理由。
- 如何隔离? 项目之间、用户之间、临时会话的记忆边界在哪里?隔离往往比删除更重要。
最小可行方案
第一版记忆系统只需要两样东西:一个仓库内的 AGENTS.md(人写约定)+ 一个会话结束时的摘要文件(harness 自动写)。这两样覆盖了 80% 的价值,且零基础设施。等你被具体问题(跨会话经历召回)逼到墙角时,再上检索。
延伸阅读
- 上下文工程:L1 工作记忆的管理——压缩、截断与注入策略
- 智能体循环:记忆读写发生在循环的哪个位置
- 技能系统:技能与记忆的分工——可复用程序 vs 可复用事实
- 案例:Claude Code:文件式记忆的完整实现剖析
- 案例:LangGraph:框架化的 state 与 checkpoint 记忆
- 实践:常见陷阱:记忆污染、上下文膨胀等反模式
- 论文:核心论文:MemGPT 等记忆方向的论文脉络
参考资料
- Claude Code Memory & CLAUDE.md 指南(含三级作用域与导入语法)
- Claude Code Memory Explained: CLAUDE.md and Auto Memory(auto memory 版本信息)
- Claude Code Memory: auto memory 存储路径说明
- AGENTS.md 官网(格式规范、采用规模、托管信息)
- OpenAI 官方 Memory FAQ(记忆摘要、saved memories、Temporary Chat)
- MemGPT: Towards LLMs as Operating Systems(arXiv:2310.08560)
- Letta 官方文档:Stateful agents 与记忆概念
- Letta 官方文档:Memory blocks(label/description/limit/read_only)
核查说明
Claude Code 官方 memory 文档(code.claude.com/docs/en/memory)在本次写作时网络抓取失败,文中关于 CLAUDE.md 作用域、# 快捷键、/memory 命令、auto memory 的描述均来自上方列出的二手资料交叉印证;auto memory 的具体版本号(v2.1.59)仅见于第三方博客,引用时请留意。