记忆系统实战:让 Agent 记住用户、越用越懂你
- 记忆分三层:会话内(上下文,第 4 篇管)、任务级(scratchpad)、跨会话(长期记忆);只有「继续对话、断点恢复、跨会话记住」才需要最后这层。
- 官方实现用的是「文件」而不是向量库:Claude 的 memory tool 让模型读写 /memories 目录——文件隐喻对 LLM 最友好。
- 写入要过门槛:显式「记住」优先、自动提炼兜底;只记四类——用户偏好、稳定事实、重要决定、教训。
- 检索先简后繁:几百条以内关键词甚至全量注入就够;跨用户隔离是底线,用户要求删除是硬要求。
上篇回顾:上一篇用 MCP 解决了「工具从哪来」。但我们的 Agent 还是条金鱼——会话一关,昨天聊了什么、用户喜欢什么,全部归零。本篇兑现第 2 篇立下的欠账:给 Agent 装上跨会话的长期记忆。
一、三层楼:你的 Agent 真需要长期记忆吗
先把「记忆」这个大词拆成三层楼,对照你家的楼层的装修进度:一楼是会话内记忆——就是循环里的 messages,怎么装满、怎么裁剪,第 4 篇已经讲透;二楼是任务级工作记忆(scratchpad)——「处理到第几个文件」这类进度状态,第 2 篇也埋过伏笔;三楼才是今天的主角:跨会话长期记忆。LangGraph 的官方划分与此一一对应:短期记忆靠检查点(checkpointer)管理线程内状态,长期记忆靠存储(store)——「让 Agent 跨不同的对话与会话存取信息」,「跨线程持久,随时可取回」[3][4]。
确认需要之后,市面上有两条现成路线可抄:消费级产品普遍内置了记忆功能,对用户是黑盒;开发者平台的实现则完全透明——Anthropic 的 memory tool 把记忆做成了「模型可调用的文件工具」[2]:模型通过 view、create、str_replace、insert、delete、rename 六个命令请求文件操作,实际执行发生在你的应用里,存储后端可以是每个用户一个目录、也可以是数据库,唯一约束是操作必须限制在 /memories 目录内 [1]。看到「模型只发指令、你的代码执行」是不是很眼熟?对,这就是第 5 篇 Function Calling 的原则在记忆上的重演。
要不要上三楼,判据第 2 篇给过,这里正式收个账:Agent 需要「继续一段对话、中断后恢复、故障后重跑、或跨会话记住信息」中的至少一条,才需要持久化 [4]。一次性任务型 Agent(整理发票、批量转格式)不需要记忆,需要的是把任务做完。三层楼的装修成本是递增的:一楼几乎免费(messages 本来就在),二楼一个文件的纪律,三楼则是一整套「写入-检索-遗忘」的制度。所以在把「记忆」当成产品卖点之前,先问一句:用户真的要它,还是我们想炫技?记忆是一份产品承诺——你记下的每一句话,将来都要么帮到你,要么反噬信任。
值得停一秒的问题是:为什么官方实现用的是「文件目录」,而不是听起来更「智能」的向量库?因为文件隐喻对 LLM 最友好——训练语料里全是文件操作,模型天然理解「写进备忘录、回头翻出来」;Anthropic 的设计意图正是让模型「把学到的东西记进 memory 文件,按需读回」,而不是把全部历史塞进上下文 [1]。Manus 团队把这个思路推到极致:「把文件系统当作最终的、无限大的上下文」,他们的 todo.md 就是一份「结构化、外部化的记忆」(转述)[6]。这也回答了本篇最重要的工程判断:最小可行记忆 = 一个 JSON 文件,不是一套向量数据库。
二、写什么,什么时候写
记忆系统坏掉的头号方式不是「记不住」,是「什么都记」。垃圾进,垃圾出——检索时捞上来的全是噪音。我实践中只记四类:
- 用户偏好:称呼、语言、输出格式、「不要用项目符号」这类直接指令;
- 稳定事实:技术栈、团队规模、业务约束——会话结束后仍然成立的信息;
- 重要决定:为什么选 A 不选 B——半年后你或用户问起,这条记忆就是会议纪要;
- 教训:「用户纠正过我的地方」。Manus 有一句被引用很多的话:抹掉失败就是抹掉证据,「没有证据,模型就无法适应」(转述)[6]。
四类落在真实条目上长这样:「称呼用小 Q,回复不要用项目符号」(偏好)、「他们的后端是 Python + FastAPI,数据库是 Postgres」(事实)、「选 Postgres 是因为团队已有 DBA,放弃 MongoDB」(决定)、「用户指出过:报价类数字必须注明日期」(教训)。检验一条记忆值不值得写的标准很简单:把它删掉,下次会话的质量会不会实实在在变差?不会的,就不配占用检索位。
什么时候写?两条通道。显式通道:用户说「记住,我喜欢简洁的回复」,或模型在对话中判断某句话值得记、主动调记忆工具——Anthropic 的 memory tool 走的就是这条路 [1]。自动通道:会话结束后用一次便宜的 LLM 调用提炼「本段对话有哪些值得跨会话保留的信息」,写回记忆库——mem0 这类记忆框架的主打卖点就是这条「自动抽取 + 合并更新」的流水线 [5]。我的搭配是显式优先、自动兜底:显式写的内容用户有预期,错了会有人纠正;自动提炼必须有门槛——「宁可少记,不可错记」,一个错误记忆被反复检索,比十个缺失记忆危害大得多。
门槛之外还要去重。最小实现是条目字段带齐元数据——{content, type, updated_at},写入前先检索一遍,同义内容覆盖而非新增——下面代码的最小版只做同文精确匹配,同义改写靠提炼模型把表述归一后再入库。LangGraph 的 store 用命名空间 + key 的设计天然支持这件事:同 key 写入即覆盖,namespace 里通常放用户或组织 ID [3]——顺带解决了跨用户隔离,这条我们第四节展开。
三、怎么取:检索与注入
取记忆比写记忆更容易过度设计。先说结论:记忆条目在几百条以内,关键词匹配甚至全量注入就够了。证据是 Anthropic 自家的 memory tool 完全基于文件名和全文读取,没有任何向量检索 [1];LangGraph 的 store 支持向量相似排序,但那是给十万级条目准备的进阶配置 [3]。原型阶段上向量库,等于给单人厨房装中央空调。
检索命中后注入到哪?两个位置,分内容类型:小而稳定的用户画像(三五条以内)直接拼进 system prompt,常驻;检索命中的事件记忆放在消息前缀,按需出现。但这里有个第 4 篇读者应该警铃大作的细节:注入内容每轮都在变,会打垮 KV-cache(键值缓存)——缓存命中与未命中的输入价差可达约十倍(Manus 以其所用模型计价为例;转述)[6]。所以注入内容要保持稳定排序、放上下文靠后位置,或者干脆学 memory tool 的思路:平时不注入,给模型一个「查记忆」的工具,按需去读——把记忆当成工具调用的对象,而不是上下文的常驻户口。
「记忆即工具」还有一个隐藏红利:写入与删除本身也变成了可观察、可门控的调用。第 5 篇的确认门控可以直接套在 delete 上,审计日志天然记得模型每次「翻备忘录、写备忘录」的时间与目的——常驻注入的可观察性做不到这一点。安全与工程,在这里又一次指向同一个设计。
最后一层是优雅降级:检索不到,坦然说「我没有相关记录」,绝不编造;记忆存储整个挂掉,Agent 应该照常工作,只是变「笨」不变「坏」——记忆系统是增强件,不是依赖项。
四、忘与改:更新、冲突、隐私
记忆不是 append-only 日志(第 2 篇说过它「带租约」),这一节把租约条款补全。更新:用户改了主意怎么办?基础策略是同 key 覆盖 [3];进阶策略是以新覆旧、旧值留痕——记忆条目带时间戳,冲突时新的生效,旧的标记为已废弃但不物理删除,「压缩要有可恢复性」的道理在记忆上同样成立(转述)[6]。
遗忘有三种,别只做第一种:定期清理过时条目(比如「三个月前的项目状态」);按需删除(关键词命中即删,下一节的代码里就有);以及用户主动要求删除——这一条是隐私硬要求,不是功能选项。memory tool 专门提供了 delete 命令 [1],消费级产品也都做了「查看并删除记忆」的入口,原因相同:记忆越懂用户,用户越有权要求被忘记。
最后一条底线:跨用户隔离。namespace 按用户 ID 组织 [3],A 用户的记忆永远不能出现在 B 用户的上下文里——多租户系统里这必须做成结构保证(每用户独立存储键前缀),而不是靠检索时的过滤条件。过滤条件会写错,结构不会。冲突的处理再给一个具体样例:旧记忆写着「用户喜欢吊灯」,本次用户说「换成筒灯」——正确动作是:新条目覆盖旧条目、旧条目留痕标废弃,并在回复里确认一句「已记住你这次的选择,以后默认按筒灯推荐」。沉默地改 + 明确地确认,是记忆更新最体面的姿势。
五、动手:一个 JSON 文件的最小记忆
把以上全部原则压进一个类——JSON 存储、关键词检索、去重、删除,五十行以内:
import json, time
from pathlib import Path
class MemoryStore:
"""最小长期记忆:一个 JSON 文件 + 关键词检索。几百条以内,不需要向量库。"""
def __init__(self, path: str = "memory/store.json"):
self.path = Path(path)
self.path.parent.mkdir(exist_ok=True) # 每用户一个文件,跨用户隔离靠结构
self.items = json.loads(self.path.read_text("utf-8")) if self.path.exists() else []
def save(self):
self.path.write_text(json.dumps(self.items, ensure_ascii=False, indent=2), "utf-8")
def add(self, content: str, type_: str = "fact") -> str:
self.items = [i for i in self.items if i["content"] != content] # 去重:同文不重记
self.items.append({"content": content, "type": type_,
"updated_at": time.strftime("%Y-%m-%d")})
self.save()
return f"已记住:{content}"
def search(self, query: str, limit: int = 3) -> list:
words = [w for w in query.split() if len(w) >= 2]
scored = [(sum(w in i["content"] for w in words), i) for i in self.items]
return [i for score, i in sorted(scored, key=lambda x: -x[0])[:limit] if score > 0]
def forget(self, keyword: str) -> str:
before = len(self.items)
self.items = [i for i in self.items if keyword not in i["content"]]
self.save()
return f"已删除 {before - len(self.items)} 条相关记忆"
# 预期输出:store.json 出现去重后的条目;search 按空格分词匹配关键词,
# 上面这句会命中「用户偏好:回复要简洁……」一条;重复 add 同文不会重记
memory = MemoryStore()
memory.add("用户偏好:回复要简洁,不要用项目符号", "preference")
memory.add("用户偏好:回复要简洁,不要用项目符号", "preference") # 同文重复:验证去重
memory.add("技术栈:后端 Python + FastAPI", "fact")
print(memory.search("回复 格式"))
接入循环只有两处改动:开场检索注入,收尾提炼入库(循环体沿用第 3 篇的 react):
# client、messages 定义见第 3 篇 run();本段只展示「开场注入 + 收尾提炼」两处改动
def system_prompt(query: str) -> str:
# 教学最小版:命中拼进 system;生产按第三节建议——放靠后消息,或做成「查记忆」工具
base = "你是长期服务这位用户的助理;「已知记忆」与其冲突时,以用户本次表述为准。"
hits = memory.search(query)
known = "\n".join(f"- {i['content']}" for i in hits) or "(暂无相关记忆;不要编造)"
return f"{base}\n\n[已知记忆]\n{known}"
# 1) 组装时:messages 的 system 换成 system_prompt(goal)
# 2) 收尾时:一次便宜的调用提炼「值得跨会话保留的信息」,过门槛再入库 ——
summary = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "从对话中提炼值得跨会话记住的信息(偏好/事实/决定/教训),"
"每条一行;没有就只输出 NONE。宁可少记,不可错记。"},
{"role": "user", "content": json.dumps(messages, ensure_ascii=False)[:12000]},
]).choices[0].message.content
for line in summary.splitlines():
line = line.strip("- ").strip()
if line and line != "NONE":
memory.add(line)
这就是「最小可行记忆」的全貌。升级路线也是清晰的:条目过千换 SQLite,检索不准再上向量,想省事就评估 mem0 这类托管框架——它们本质是把「抽取-去重-检索-更新」流水线做成了服务 [5]。但请按这个顺序升级,而不是从向量库起步:记忆系统的难度从来不在存储,在取舍纪律。
小结
- 三层楼对号入座:会话内靠上下文管理、任务级靠 scratchpad、跨会话才需要长期记忆;判据不满足就别上三楼;
- 只记四类(偏好/事实/决定/教训),显式优先、自动兜底,写入过门槛、同义去重;
- 检索先简后繁,注入注意缓存稳定性,或干脆做成「查记忆」工具按需读;检索不到就承认,记忆挂了 Agent 要照常能跑;
- 遗忘三件套:过期清理、按需删除、用户主动删除(隐私硬要求);跨用户隔离靠存储结构,不靠过滤条件。
下一篇预告:一个 Agent 记性再好,也只有一个脑子。当任务大到要并行探索、要不同角色分工时,就该从「独狼」走向「团队」——下一篇《多智能体协作》讲编排者与工作者的组织学:什么时候才真的需要多智能体,以及最常见的三种反模式。
参考来源
- Memory tool, Anthropic, 2025,platform.claude.com
- Introducing advanced tool use on the Claude Developer Platform, Anthropic, 2025,anthropic.com
- Long-term memory, LangChain, 2025,docs.langchain.com
- LangGraph Persistence, LangChain, 2025,docs.langchain.com
- Mem0 Documentation, Mem0, 2025,docs.mem0.ai
- Context Engineering for AI Agents: Lessons from Building Manus, Manus, 2025,manus.im(标注「转述」处为要点转述)