Ch05: Profile vs User Memory——写给 Agent 的 vs Agent 自己学的
Part 2: 记忆基础概念 前置章节:Ch04: 记忆的四种类型——从工作记忆到外部记忆 后续章节:Ch06: Memory vs RAG——经验和知识不是一回事
1. 从教练手册和训练日志说起
假设你是一名体育教练。你的桌上有两样东西。
第一样是一本教练手册。手册是上级或者你自己编写的,上面写着训练原则:”热身时间不少于 15 分钟”“重点训练核心力量”“每个动作示范三遍再让运动员练”。这些原则不会因为今天训练顺利就改变,也不会因为某个运动员说”我不喜欢热身”就作废——它是你的行为规范,除非你主动修订手册,否则每次训练都遵循同样的原则。
第二样是一本训练日志。日志是每天训练结束后你自动记录的:”小王右膝旧伤,深蹲时要注意”“小李今天动作进步明显,可以加量”“全队第三次测试成绩比第一次提升了 12%”。这些不是你预设的原则,而是从实际训练中积累的观察。日志需要经常翻阅才能指导训练——你可能忘了小王膝盖有伤,如果不去翻日志就可能在下次训练中让他做负重深蹲。日志也需要定期更新——小王的膝盖可能已经恢复了,如果你还按旧日志”注意右膝”来安排训练,反而限制了他的进步。
教练手册对应 Profile Memory——人工编写、确定性执行、不会自动更新。训练日志对应 User Memory——自动积累、需要检索、可能过时。
这两样东西各司其职,不能互相替代。如果你把”小王右膝有伤”写进教练手册,那它就变成了一个”每次训练都要遵循的原则”——但小王膝盖恢复后你忘了更新手册,每次训练还是会限制他,这就过度了。如果你把”热身时间不少于 15 分钟”写进训练日志,那它就变成了”一条可能被翻到也可能被忽略的记录”——但热身原则是每次都必须遵循的,不应该有”翻不到就不执行”的可能。
核心结论:Profile Memory 和 User Memory 应该分开设计、分开存储、分开更新。混在一起会出问题——要么动态信息过时了还在被当作原则执行,要么原则信息可能因为检索不到而被忽略。
下面逐一深入。
2. Profile Memory 详解
2.1 定义再强调
Profile Memory 是开发者或用户主动写给 Agent 的行为规范和背景知识。三个关键属性:
- 来源是人工编写:不是 Agent 自动生成的,是人类用编辑器一个字一个字敲出来的
- 注入是确定性的:每次会话开始时,Profile Memory 的内容一定会被加载到 system prompt 中,不存在”检索不到”的问题
- 更新是手动的:要改 Profile Memory 的内容,你需要打开文件编辑、保存,下次会话才会生效
这三点合在一起,使 Profile Memory 成为”最可控”的记忆类型——你知道 Agent 会看到什么、不会看到什么,没有不确定性。
2.2 CLAUDE.md(Anthropic)
Claude Code 的 Profile Memory 体系是当前路径布局最复杂的。
发现路径:Claude Code 在启动时沿以下路径逐级查找 CLAUDE.md 文件:
~/.claude/CLAUDE.md——全局用户级,所有项目共享的个人偏好<project-root>/CLAUDE.md——项目级,当前项目的编码规范和行为约束<cwd>/CLAUDE.md——目录级,当前工作目录下的特定规则
每一层都可以存在,内容累加而非覆盖。也就是说,如果三层都有 CLAUDE.md,Claude Code 会把三层的内容全部加载——全局层放通用偏好,项目层放项目规范,目录层放模块特有规则。这种”逐级加载 + 累加”的设计提供了精细的控制粒度。
但复杂度也带来了问题。三层 CLAUDE.md 的总 token 消耗可能不小——如果每层都写了几千 token,加起来就占掉了上下文窗口的相当一部分。此外,三层之间如果存在矛盾(全局层说”用中文回复”,项目层说”用英文写注释”),需要 Claude Code 在运行时判断优先级——通常更具体的层级(目录级 > 项目级 > 全局级)优先。
实践建议:全局层只放最通用的偏好(语言偏好、风格偏好),项目层放项目规范(技术栈、代码风格、目录结构),目录层尽量少用——只在模块间差异很大时才需要。
2.3 AGENTS.md(OpenAI)
Codex 的 Profile Memory 体系在发现链设计上最精巧。
发现链:Codex 在会话开始时沿目录树向上查找 AGENTS.md 文件。每个 AGENTS.md 的作用范围限于它所在的目录及其子目录。子目录的 AGENTS.md 可以 override 父目录的规则——而不是简单累加。
override 机制:这是 AGENTS.md 和 CLAUDE.md 的关键设计差异。CLAUDE.md 是”逐级累加”——三层内容全部加载。AGENTS.md 是”逐级覆盖”——子目录的规则覆盖父目录的同名规则,未覆盖的继承。
举个例子。假设项目根目录的 AGENTS.md 写了:
- 测试框架:pytest
- 命名规范:snake_case
- 文档语言:中文
子目录 src/rust/ 的 AGENTS.md 写了:
- 命名规范:snake_case(Rust 标准)
- 文档语言:英文
在 src/rust/ 目录下工作时,Agent 会用 pytest 做测试(继承自父目录)、snake_case 命名(覆盖但碰巧相同)、英文文档(覆盖了中文)。
30+ 工具联合标准:AGENTS.md 不只是”写规则”的地方,它还定义了 Codex Agent 可以使用的工具集。每个 AGENTS.md 可以声明”在这个目录下允许使用哪些工具”——比如 src/docs/ 只允许读文件和搜索,不允许执行代码。这种”规则 + 工具权限”的联合标准,是 AGENTS.md 独有的设计。
实践建议:override 机制让 AGENTS.md 特别适合大型 monorepo——不同模块可以有不同的编码规范和工具权限,而不用在每个子目录都写完整的规则集。
2.4 Cursor Rules
Cursor 的 Profile Memory 体系在”注入粒度”上最精细。
四种 Apply 模式:
-
Always:始终注入。每次对话开始时,规则内容一定会被加载到上下文窗口。相当于”贴在桌角的便签纸,每次坐下都能看到”。适合:全局编码规范、安全约束、必须遵守的行为规则。
-
Auto Attached:自动关联。当打开的文件匹配指定模式(如
*.tsx)时,规则自动被注入。相当于”翻开特定课本时自动出现的备注”。适合:特定文件类型的编码规范(如 React 组件规范、Python 类型提示规范)。 -
Agent Requested:Agent 主动读取。规则不自动注入,但 Agent 在判断”可能需要这个规则”时主动读取。相当于”需要时去翻参考手册”。适合:不常用但重要的规范(如部署流程、紧急修复协议)。Agent 需要有足够的判断力来决定”什么时候该读这个规则”。
-
Manual:手动指定。用户用
@rule-name显式引用时才注入。相当于”你主动去书架上拿的那本书”。适合:高度特定场景的规则(如某个 API 的调用约定、某个数据库的迁移规范)。
从 Always 到 Manual 的光谱:这四种模式形成了一个”始终注入 → 按需注入”的完整光谱。Always 模式确保规则一定会被执行,但消耗上下文窗口空间;Manual 模式几乎不消耗窗口空间,但依赖用户或 Agent 主动想起。Auto Attached 和 Agent Requested 则是两种折中——前者基于文件模式自动触发,后者基于 Agent 判断主动读取。
实践建议:默认用 Always(安全第一),只在规则内容很长且不常用时降级到 Agent Requested 或 Manual。Auto Attached 适合”和文件类型强相关”的规范。关键原则:宁可多注入一个不常用的规则,也不要漏掉一个必须遵守的规则。上下文窗口的空间可以优化,但规则遗漏的后果难以弥补。
2.5 三个系统的共同特点
尽管 CLAUDE.md、AGENTS.md、Cursor Rules 在发现路径、覆盖机制、注入粒度上有显著差异,它们共享三个核心特点。
人工维护。三个系统的 Profile Memory 都是人类编写的——没有一个是系统自动生成的。这意味着内容的准确性和及时性完全取决于维护者的勤勉程度。如果你把”用 React”写进了 CLAUDE.md,然后项目迁移到了 Vue 但忘了更新 CLAUDE.md,Agent 就会一直按 React 的规范工作。人工维护是 Profile Memory 最大的优势(确定性)也是最大的风险(可能过时)。
确定性注入。三个系统都保证”Profile Memory 的内容一定会被注入到上下文窗口中”——不存在”检索不到”的情况。CLAUDE.md 每次都加载三层内容,AGENTS.md 每次都加载当前目录链上的规则,Cursor 的 Always 模式每次都注入。这种确定性是 Profile Memory 区别于 User Memory 的根本特征。
版本可控。因为 Profile Memory 是文件,天然支持 git 版本控制。你可以追踪”谁在什么时候改了什么规则”,可以回滚到之前的版本,可以在 code review 中审查规则的变更。这种可审计性是 User Memory 很难做到的——mem0 的向量数据库里的记忆条目,不容易 git diff。
3. User Memory 详解
3.1 定义再强调
User Memory 是 Agent 从与用户的交互中自动学到的关于用户的知识。三个关键属性:
- 来源是自动提取:不需要用户显式说”请记住这个”,系统从对话中自动识别值得记住的信息
- 注入需要检索:不像 Profile Memory 自动注入,User Memory 存储在外部,需要检索机制才能进入上下文窗口
- 需要考虑遗忘:自动提取的信息可能过时,需要遗忘或更新机制
这三点合在一起,使 User Memory 成为”最有用但也最难做对”的记忆类型——有用是因为它能让 Agent 越来越”懂”用户,难做对是因为自动提取可能出错、检索可能找不到、记忆可能过时。
3.2 自动提取机制
User Memory 的第一步是从对话中提取值得记住的信息。不同系统的提取方式差异很大。
mem0:LLM 提取独立事实
mem0 的提取方式最直接:把对话文本丢给 LLM,让它输出结构化的独立事实。比如用户说”我最近在学 Rust,之前一直写 Python”,LLM 会提取出两条事实:
{"user_id": "u123", "memory": "用户最近在学 Rust", "metadata": {"type": "preference"}}
{"user_id": "u123", "memory": "用户之前一直写 Python", "metadata": {"type": "experience"}}
这种方式的优点是简单、通用——任何对话都能提取,不需要领域知识。缺点是”粒度不可控”——有时一条记忆太细碎(”用户今天喝了咖啡”),有时太模糊(”用户对编程感兴趣”)。mem0 用 LLM 去重来处理冗余——如果新提取的事实和已有事实语义相同,就跳过或更新。
Claude Code:Forked Agent 提取
Claude Code 的 extractMemories 启动一个独立的”forked agent”来分析对话。这个 agent 在后台异步运行(fire-and-forget),不阻塞主对话,可以进行比单次 LLM 调用更复杂的推理。但它也更昂贵——每次提取都需要一次额外的 LLM 调用。
一个关键的实现细节:extractMemories 明确排除 CLAUDE.md 中已经包含的内容。如果 CLAUDE.md 写了”项目用 React”,即使对话中用户提到了 React,extractMemories 也不会提取这条信息——避免 Profile Memory 和 User Memory 之间的冗余和不一致。
Letta:Agent 自管理
Letta/MemGPT 让 Agent 自己决定记什么、怎么记——通过 tool call 执行 core_memory_append、archival_memory_insert 等操作。优点是”判断力最强”,缺点是”可靠性最不可控”——Agent 可能忘记调用记忆工具,或写入不重要的信息而遗漏重要的。
DB-Agent-Memory:分层提炼
从底层到顶层逐级提炼:Observation → Fact → Summary → Profile。优点是”信息保真度有保障”(原始数据保留在 Observation 层),缺点是”提取链路长”(每层提炼都是一次 LLM 调用)。
3.3 检索机制
提取之后的第二个问题是”怎么在需要时找到相关的记忆”。四个系统展示了四种不同的检索策略。
全量注入(Codex):每次会话加载完整 MEMORY.md。最简单可靠,但前提是记忆总量不大(通常几千 token),否则不可行。
语义搜索(mem0):把查询做 embedding 转为向量,在向量数据库中搜索余弦相似度最高的 K 条记忆。适合记忆量大、每条独立的场景,但基于”语义相近”不保证”逻辑相关”——”用户喜欢 Python”和”用户讨厌蛇”可能语义相近但逻辑无关。
LLM 筛选(Claude Code):用 LLM 自身判断”哪些记忆和当前对话相关”。MEMORY.md 作为索引列出不超过 200 个记忆文件,每轮对话 Sonnet 从中选最多 5 个注入。把检索从”算法问题”变成”推理问题”,理解力最强但成本高、不确定性大。
图遍历(Graphiti):从知识图谱节点出发,沿关系边遍历相邻节点,两阶段(向量粗筛 + Cross-Encoder 精排)。支持多跳推理,但前提是图谱结构足够完整。
3.4 遗忘需求
User Memory 的遗忘问题值得单独拿出来讨论,因为它和 Profile Memory 形成了最鲜明的对比。
Profile Memory 不需要遗忘机制——因为它的内容由人类维护。当信息过时的时候,人类打开文件改一下就好。CLAUDE.md 中的”用 React”改成了”用 Vue”,下次会话自动生效。没有”Agent 还记得旧版本”的问题——因为每次会话都是从文件重新加载的。
User Memory 必须考虑遗忘——因为它的内容是自动提取的,用户不会每次都主动说”之前那个已经不对了”。如果你半年前对 ChatGPT 说”我在学 Python”,它记住了。三个月后你转学了 Rust,但没有主动说”我不再学 Python 了”——ChatGPT 可能还会推荐 Python 学习资源。
当前各系统对遗忘的处理:mem0 用 LLM 判断矛盾并更新(无时间衰减),DB-Agent-Memory 的提炼过程本身是”软遗忘”(无显式触发),Graphiti 用 invalid_at 时间戳标记失效(最显式),Codex 用 usage_count 做隐式遗忘,Claude Code 用 autoDream 定期整合,Letta 理论上 Agent 可自删但实践中很少主动遗忘。
遗忘是 User Memory 设计中最不成熟的环节。十个系统中只有 Graphiti 的 invalid_at 和 Codex 的 usage_count 算”显式遗忘设计”。遗忘比记忆更难设计——判断”什么应该被忘”需要理解信息的时效性和当前有效性,这本身就是一个需要推理的复杂任务。
4. 三个经典误区
在理解了 Profile Memory 和 User Memory 的设计之后,来看三个最常见的混淆误区。每一个都来自真实的设计选择错误——不是理论推演,是有人真的这么做了然后出了问题。
4.1 误区一:把动态知识写进 Profile
错误认知:Profile Memory 可以存放任何”关于用户或项目的知识”,包括当前进度、最近偏好等动态信息。
正确理解:Profile Memory 只适合存放稳定的、声明式的规则。动态信息应该存放在 User Memory 中。
为什么:Profile Memory 的核心优势是”确定性注入”——写入的内容每次对话都会被加载。这同时意味着写入的内容每次都会生效。如果你在 CLAUDE.md 里写了”当前正在做项目 A 的用户认证模块”,Agent 每次对话都会认为你在做用户认证——即使你已经切换到了项目 B。Profile Memory 没有”自动过期”机制,过时信息不会自己消失。
真实案例:有人在 CLAUDE.md 中写了”项目使用 React 17”。升级到 React 18 后忘了更新,Claude Code 此后两周都按 React 17 的 API 写代码。
正确做法:Profile Memory 只放”项目使用 React”这种稳定声明,版本号等动态知识让 User Memory 从对话中自动学习。
4.2 误区二:期望 Profile 内容被 extractMemories 提取
错误认知:CLAUDE.md 中写的内容也会被 extractMemories 提取为 User Memory,不用担心两者之间的信息丢失。
正确理解:Claude Code 的 extractMemories 明确排除 CLAUDE.md 中已经包含的内容。这是有意为之,不是 bug。
为什么:如果 extractMemories 也提取 CLAUDE.md 内容,会导致两个问题。第一,冗余——同一条信息同时出现在 system prompt 和检索结果中。第二,不一致——CLAUDE.md 更新后,User Memory 中的旧版本不会自动更新,Agent 可能同时看到矛盾的信息。
这体现了”单一来源”原则——每条信息只有一个权威来源,避免多来源导致矛盾。
4.3 误区三:认为 Memory Bank 就是 User Memory
错误认知:任何能存储信息并在后续对话中读取的机制都可以叫 User Memory。
正确理解:User Memory 有三个核心能力——语义检索、冲突合并、自动触发。缺少其中任何一个,充其量是”记忆银行(Memory Bank)”,不是完整的 User Memory。
-
语义检索:根据语义找到相关记忆,不是只能全量加载或关键词匹配。mem0、Claude Code、Graphiti 都具备。Codex 的 MEMORY.md 全量注入——记忆量增长后效率下降。
-
冲突合并:新记忆和旧记忆矛盾时自动检测并更新。mem0 用 LLM 判断矛盾,Graphiti 用
invalid_at标记失效。Codex 的 Phase2 是批量合并,但两次合并之间可能出现矛盾记忆并存。 -
自动触发:提取和更新不需要用户显式触发。mem0 和 Claude Code 在每轮对话后自动提取,DB-Agent-Memory 在交互后自动记录。
为什么区分重要:如果系统只有”存和取”没有”冲突检测”,当用户说”我不再用 React 了”,系统只是新增一条而没有更新旧的——下次检索两条都出现,Agent 不知道该信哪条。
Codex 的案例:MEMORY.md 是 Memory Bank(存和取),但 Codex 的整体记忆管线(MEMORY.md + Phase1 + Phase2 + usage_count + 冷却期整合)具备了完整 User Memory 能力。
5. 分开设计、分开存储、分开更新
基于以上分析,Profile Memory 和 User Memory 应该在三个维度上保持独立。
5.1 分开设计
Profile Memory 的设计目标是”确保 Agent 行为符合预期”(约束性、声明式、确定性),设计时问”Agent 必须遵守什么规则?”User Memory 的设计目标是”让 Agent 越来越了解用户”(适应性、事实性、概率性),设计时问”从交互中学到什么?怎么找到它?”设计问题不同,系统架构自然不同——Profile 需要”文件系统 + 加载器”,User 需要”提取器 + 存储后端 + 检索器 + 遗忘器”。
5.2 分开存储
Profile Memory 存在文件系统中,天然支持版本控制。User Memory 存在数据库或独立文件系统中。分开存储的好处是安全边界清晰——Profile Memory 是”Agent 应该怎样”的规范,不应该被 User Memory 的提取结果覆盖或修改。分开存储从物理上杜绝了这种可能性。
5.3 分开更新
Profile Memory 的更新是人工的——打开文件,编辑,保存。User Memory 的更新是自动的——从对话中提取、检测矛盾时更新旧记忆。如果你把”用 Python”放在 User Memory 中,它可能因为你说了”我开始学 Rust 了”而被更新——即使你只是想学 Rust 而不是切换主力语言。分开更新还有一个实际好处:调试更容易——Agent 行为不符合预期时,可以快速定位是 Profile 的规则不对还是 User Memory 的记忆过时了。
5.4 但它们需要协调
分开不意味着隔离。Profile Memory 和 User Memory 在运行时需要协调,主要体现在两个方面。
优先级:当 Profile Memory 和 User Memory 的信息冲突时,Profile Memory 优先。原因很简单——Profile Memory 是人工维护的确定性信息,User Memory 是自动提取的概率性信息。确定性优先于概率性,这是一个合理的设计原则。Claude Code 的 extractMemories 排除 CLAUDE.md 内容就是这个原则的体现。
互补:Profile Memory 定义”Agent 应该怎样”,User Memory 提供”用户是什么样”。两者合在一起,Agent 既有行为规范(”我应该用简洁的风格回复”),又有用户理解(”这个用户喜欢 Python、偏好详细解释”)。没有 Profile Memory,Agent 可能”学到了错误偏好但一直执行”;没有 User Memory,Agent 只能”按规范做事但不了解用户”。
6. 设计决策速查表
当你需要决定”这条信息应该放在 Profile Memory 还是 User Memory”时,用以下决策表。
| 问题 | 如果是 → Profile Memory | 如果是 → User Memory |
|---|---|---|
| 这条信息多久变一次? | 很少变(季度级别) | 经常变(天/周级别) |
| 信息过时的后果有多严重? | 很严重(安全/合规风险) | 不太严重(推荐不精准) |
| 谁应该对信息的准确性负责? | 开发者/用户手动维护 | 系统自动提取和更新 |
| 信息需要每次都生效吗? | 是,每次对话都必须遵守 | 否,只在相关时才需要 |
| 信息量有多大? | 小(几百到几千 token) | 大(可能几百条记忆) |
| 检索方式 | 确定性注入(不需要检索) | 语义搜索/LLM 筛选/图遍历 |
快速判断口诀:如果这条信息写错了会导致 Agent 做危险或错误的事 → Profile Memory。如果这条信息写错了只会让推荐不够精准 → User Memory。
举几个例子:
- “项目使用 React” → Profile Memory(写错了会用错误的框架,后果严重)
- “项目使用 React 18.2.0” → 不应该出现在 Profile Memory 中(版本号经常变),让 User Memory 管理
- “用户偏好详细解释” → User Memory(偏好可能随时间变化,过时后果不严重)
- “不要删除数据库中的数据” → Profile Memory(安全约束,每次都必须遵守)
- “用户之前用过 PostgreSQL” → User Memory(事实性信息,可能过时)
7. 本章小结
回顾本章的核心要点:
- Profile Memory 像教练手册(人工编写、确定性执行、不会自动更新),User Memory 像训练日志(自动积累、需要检索、可能过时)
- 三种 Profile Memory 实现各有特色:CLAUDE.md 路径布局最复杂(逐级累加),AGENTS.md 发现链最精巧(override 做本地覆盖 + 30+ 工具联合标准),Cursor Rules 注入粒度最精细(四种 Apply 模式形成始终到按需的光谱)
- 三者共同特点:人工维护、确定性注入、版本可控
- 四种 User Memory 检索策略:全量注入(Codex,简单但受容量限制)、语义搜索(mem0,通用但缺乏逻辑推理)、LLM 筛选(Claude Code,理解力最强但成本高)、图遍历(Graphiti,支持多跳推理但依赖图谱完整度)
- 遗忘是 User Memory 的核心挑战,Profile Memory 不需要遗忘——这是两者最根本的差异之一
- 三个经典误区:把动态知识写进 Profile(过时后永远生效)、期望 Profile 内容被 extractMemories 提取(Claude Code 明确排除)、认为 Memory Bank 就是 User Memory(缺少语义检索/冲突合并/自动触发)
- 两者应该分开设计、分开存储、分开更新,但在运行时需要协调优先级和互补性
- 快速判断口诀:写错了会导致危险 → Profile Memory;写错了只是推荐不够精准 → User Memory
下一章我们把对比维度从”记忆内部”扩展到”记忆和 RAG 的关系”——很多人分不清”Agent 的记忆”和”检索增强生成”到底有什么区别,这恰恰是理解记忆系统定位的关键。
导读目录:README.md 上一章:Ch04: 记忆的四种类型——从工作记忆到外部记忆 下一章:Ch06: Memory vs RAG——经验和知识不是一回事