第 10 章 · 记忆架构
Harness 锚点|判别清单第 3 项「状态持久化」——跨会话的长期记忆。
每天都要重新自我介绍的助理
那个卖家给自己的客服 Agent 起了个名字,叫“小助”。他每天早上打开对话,都得先来一段:我是做家居品类的、主打北美站、发货走海外仓、回复客户要客气但别啰嗦、报价一律用美元。交代完,小助干活确实利索。
问题是,第二天早上打开,小助又是一张白纸——“您好,请问有什么可以帮您?”仿佛昨天那一整天的磨合从没发生过。他得把偏好再讲一遍,把海外仓的规则再讲一遍,把“别用感叹号轰炸客户”再讲一遍。
一周下来他算了笔账:光是每天重新交代背景,就要花掉十几分钟。更糟的是,小助偶尔还会“记错”——因为它压根不记得,全靠他当场重讲,讲漏一句就出岔子。
这不是小助笨,而是它天生没有跨越会话的记忆。上一章讲的上下文工程,管的是“一次会话之内”怎么把信息安排好;可会话一结束,上下文窗口清空,一切归零。要让小助真正像个熟手助理,就得给它一样东西:跨会话的长期记忆(long-term memory)。
为什么上下文窗口不等于记忆
先厘清一个容易混淆的点:上下文窗口不是记忆。
上下文窗口里的内容,本质是“这一轮推理临时看得见的工作区”。它有三个特点让它当不了记忆:会话结束就清空、装不下太多(有 token 上限)、还会被压缩掉细节(第 9 章讲的 Compaction)。指望它记住你三个月前说过的偏好,等于指望便签纸当档案柜。
真正的记忆得满足两个条件:存在窗口之外的持久层(persistent store),以及用时能被取回并注入上下文。这就引出记忆系统绕不开的三个问题——它们是本章的主线:
- 存什么(what to store):不是所有对话都值得记。要提炼出“值得长期保留”的东西——用户偏好、关键事实、重要决定,而不是把每句闲聊都存下来。
- 怎么取(how to retrieve):下次会话时,如何从一堆记忆里找到与当前任务相关的那几条,并注入上下文?可以按时间、按关键词、或按语义相似度检索。
- 怎么忘(how to forget):记忆无限增长本身就是灾难。旧的、过时的、再不被触及的记忆,需要衰减(decay)或淘汰。
第 3 点最常被忽略,却最关键:一个什么都记得的系统,和什么都不记得一样没用——因为它无法区分重要和琐碎,取回时会被海量噪音淹没。人脑之所以高效,恰恰因为它会主动遗忘。
记忆的几种类型:短期、长期、语义、情景
认知科学给记忆分了类,这套术语被 Agent 领域直接借了过来。讲清楚它们,你才知道自己到底在存什么、该用什么方式存。
- 短期记忆(short-term memory):当前会话的上下文,也叫工作记忆(working memory)。它就是第 9 章那个上下文窗口里的东西——这轮对话、刚调过的工具结果。特点是容量小、时效短、会话结束即失效。
- 长期记忆(long-term memory):跨会话持久保存的信息,是本章的主角。它又可以细分成下面两类。
- 语义记忆(semantic memory):存“事实与概念”这类去情境化的知识。比如“这个卖家主营家居品类”“海外仓发货 3–5 天到”。它不关心你是什么时候、在哪次对话里说的,只关心这条事实本身。
- 情景记忆(episodic memory):存“发生过的具体事件”,带时间、带上下文。比如“6 月 12 日那次,客户 A 因为物流延迟投诉,我们补偿了一张 10 美元券”。它保留了事件的来龙去脉,回顾时能还原当时的情形。
一个成熟的助理,这几种记忆都要有:短期记忆让它跟得上当前这轮对话;语义记忆让它记住你是谁、有什么偏好;情景记忆让它能回忆起“上次那个投诉是怎么处理的”。小助之所以像陌生人,就是因为它只有短期记忆,一断电就全丢。
怎么取:向量检索与 RAG
记忆存下来还只是一半,取回(retrieval) 才是难点。如果长期记忆里攒了几千条事实,新会话一开始,你不可能把它们全塞进上下文——窗口装不下,也会淹掉信号。你只需要与当前任务相关的那几条。
最朴素的取法是关键词匹配:用户问“物流”,就把含“物流”二字的记忆捞出来。但这太脆——用户问“发货多久到”,关键词里没有“物流”,就漏检了。二者明明说的是一回事。
解决办法是语义检索(semantic retrieval),它的核心技术叫 embedding(嵌入向量):把每段文本用一个模型转成一串高维数字向量,让“意思相近”的文本在向量空间里距离也相近。于是“发货多久到”和“物流时效”虽然一个字不重合,向量却挨得很近,一查就能查到。
这套“先检索相关内容、再喂给模型生成回答”的范式有个响当当的名字:RAG(Retrieval-Augmented Generation,检索增强生成)。它的完整流程是这样:
- 建索引(indexing):把知识(文档、历史记忆、FAQ……)切成小块,每块算出 embedding,存进向量数据库(vector database)。
- 检索(retrieval):来了新问题,把问题也算成 embedding,在向量库里找出距离最近的 Top-K 条。
- 增强生成(augmented generation):把这 Top-K 条作为参考资料拼进上下文,让模型基于它们回答。
RAG 最初是为“大规模知识库问答”设计的,但它和长期记忆天然是一回事——记忆就是一个不断增长、需要按语义取回的知识库。所以你会看到:长期记忆做大了,底层往往就是一套 RAG。 记住这个等价关系,后面选型就不会晕。
怎么忘:巩固与衰减
人不会记住一生中的每一秒。反复用到的信息会被巩固(consolidation)成长期记忆,越记越牢;只见过一次、之后再没碰过的信息会随时间衰减(decay),慢慢淡出。这套机制叫类人遗忘(human-like forgetting),它不是缺陷,而是让记忆系统保持高效的关键。
把这个理念落到工程上,每条记忆通常带几个属性:
- 重要性(importance):这条信息有多要紧。高重要性的(如核心偏好)该留得久。
- 访问次数(access count):被取回、被用到的次数。用得越多,说明越该巩固。
- 最近访问时间(last accessed):越久没碰的,越该衰减。
有了这几个维度,就能给每条记忆算一个分数:重要性高、最近用过、用得频繁的分数高,长期没人理的分数低。取回时按分数排序取前几条;分数低到一定程度,就把这条记忆标记为“淡出”,不再主动出现在索引里(但需要时仍可按精确 key 找回)。这正好对应上面的第 3 个问题——遗忘和记住同样重要。
用 pi 实现:memory 扩展与类人遗忘
pi 把上面这套理念做成了一个 memory 扩展,源码就在 examples/extensions/memory.ts。它以扩展(第 8 章)的形式挂在 Agent 的生命周期上,而且 pi 在首次运行时会自动安装它——“有记忆”是开箱即得的默认体验,不用你手动配置。
它的几个设计决策,正好把前面的概念一一落了地:
- 持久层在哪:记忆存成一个项目本地的
.pi/memory.jsonl文件,按 key 组织,还是分支感知(branch-aware)的——不同会话分支的记忆不会互相污染。这就是“窗口之外的持久层”。 - 存什么:每条记忆有
key、value,还带category(分成fact事实 /decision决定 /preference偏好 /context背景四类)和importance(1–5 的重要性)。它甚至会拒绝存密钥、token、密码这类敏感信息,从源头堵住泄露。 - 怎么忘:这是它最有意思的部分。每条记忆按“最近访问时间、访问次数、重要性”算一个分数,随时间指数衰减;据此把记忆分成三种可见性(visibility)状态——
active(活跃,出现在索引里)、dormant(休眠,不主动出现但没删)、forgotten(淡出,连搜索都不返回,但仍能用精确 key 找回)。这就是类人遗忘:重要的、常用的被巩固,一次性的随时间衰减。
它怎么在会话间自动记与取?靠订阅生命周期事件:session_start 时把项目记忆重建(rebuild)进内存;context 事件里,它把最活跃的几条记忆压成一个紧凑索引,注入到上下文最前面——这就是“取回并注入”;session_before_compact(压缩前)会把当前记忆快照落盘,防止丢失。
pi 真实暴露给模型的,是一组以 memory_ 打头的工具。用概念代码描述模型是怎么用它们的:
// 会话中:模型判断这是一条值得长期记住的偏好,主动存下来
await pi.callTool("memory_set", {
key: "reply_style",
value: "回复用中文、简洁、不要用感叹号轰炸客户",
category: "preference", // fact | decision | preference | context
importance: 4, // 1–5,越高留存越久
});
// 新会话里:模型按语义/关键词把相关记忆搜回来
const hits = await pi.callTool("memory_search", { query: "怎么回复客户" });
// 命中的记忆被拼进上下文,Agent 于是"记得"卖家的回复偏好
注意 pi 用的是 memory_set / memory_search / memory_get 这组显式工具,而不是某种自动“读心术”——存什么、什么时候取,由模型在推理中自己决定并调用,你完全能在源码里看清它的判断依据。要点在于它给模型的工具说明(promptGuidelines)里明确写了“只存值得长期保留的项目事实、决定、偏好、背景,绝不存密钥”——这又呼应了第 4 章那句:工具描述就是写给模型的说明书。
记忆与上下文压缩(第 9 章)是一对搭档:压缩管“会话内怎么忘细节”,记忆管“会话间怎么留重点”。二者一内一外,共同决定了 Agent 能“记住”多少东西。
大规模知识库:叠一层向量检索
pi 的 memory 扩展主打“长期记忆 + 类人衰减”,取回用的是关键词加分数排序,对“几十到几百条项目记忆”这个量级足够了。但如果你要做的是成千上万篇文档的知识库——比如把整个产品手册、所有历史工单都变成 Agent 能查的记忆——关键词检索就不够了,你需要前面讲的语义检索 / RAG。
在 pi 里这不冲突,反而很自然:把向量检索做成另一个扩展或一个 MCP Server(第 5 章)。它内部维护向量数据库,对外暴露一个 knowledge_search 之类的工具;模型需要时调用,它算 embedding、查 Top-K、把结果回填。这样一来,pi 的 memory 扩展管“轻量的、类人衰减的项目记忆”,向量检索扩展管“重量级的语义知识库”,各司其职、按需组合——这正是 pi“控制权在你手里”的一贯风格。
动手看看
打开 pi 源码里的 examples/extensions/memory.ts,对照本章的三个问题一条条找:
- 存什么:看
MemoryEntry这个结构,数一数它给每条记忆记了哪些字段(category、importance、accessCount、lastAccessed……),想想每个字段服务于哪个问题。 - 怎么忘:找到
getMemoryScore和getMemoryVisibility两个函数,看它怎么用衰减公式把记忆分成active/dormant/forgotten三档——这就是类人遗忘的全部秘密,也就几十行。 - 怎么注入:找到
pi.on("context", ...)那段,看它如何把最活跃的记忆压成索引塞进上下文最前面。
读完你会发现,一个“有记忆”的 Agent,核心并不神秘:一个持久文件、一套打分排序、几个生命周期钩子。
实战中的几个坑
坑一:什么都往里记,记忆变垃圾场。
- 现象:跑一阵后记忆库塞满琐碎信息,取回时相关的反而被淹没。
- 原因:没约束“值得记什么”,把每句闲聊、每个临时结果都存了。
- 对策:只存耐久的偏好、事实、决定;给
importance打分,靠衰减机制自动淘汰低价值项。
坑二:把记忆当数据库,从不遗忘。
- 现象:记忆只增不减,越查越慢,还常取回过时信息。
- 原因:只实现了“存”和“取”,漏了“忘”。
- 对策:引入类人遗忘——按访问时间、次数、重要性衰减,让旧而无用的记忆淡出。
坑三:语义检索召回一堆不相关的东西。
- 现象:RAG 取回的 Top-K 里混进大量跑题内容,模型被带偏。
- 原因:文本切块太大或太杂,embedding 分不清主题;或 K 设得太大。
- 对策:合理切块(chunking)、按语义边界切;调小 K,必要时加重排序(rerank)过滤。
坑四:把敏感信息写进了记忆。
- 现象:API key、密码被当成“事实”存进了持久层,泄露风险极高。
- 原因:记忆写入没做敏感信息拦截。
- 对策:在写入路径上加校验拒绝敏感 key(pi 的 memory 扩展就内置了这道防线),密钥走专门的凭证管理。
对比:其他框架
“跨会话记住东西”这件事,各框架的支持程度差得很远:
| 框架 | 记忆机制 | 特点 |
|---|---|---|
| LangGraph | 用 store(长期记忆)配 checkpointer(会话内状态),按用户/命名空间存取 | 机制齐全,但存什么、何时取要自己写进图里 |
| CrewAI | 内建短期 / 长期 / 实体(entity)记忆分层,可一键开启 | 配置化程度高,团队跨任务自动记要点 |
| PydanticAI | 无内建记忆,只给消息历史 | 跨会话记忆需自接持久层或向量库 |
| Agno | 把记忆与知识(RAG)做成一等组件,内置会话记忆与用户记忆 | 开箱即用,适合快速搭“有记忆的助手” |
| pi | 以 memory 扩展提供,主打类人遗忘、首次运行自动安装 | 轻量、透明,控制权在你手里 |
抛开包装,每个记忆系统都在回答同样三个问题——存什么、怎么取、怎么忘。差别只在于框架把这三步做成了内建组件,还是留给你自己拼装;以及“忘”这一步是被认真对待,还是干脆没做。
小结
- 上下文窗口不是记忆:它会清空、装不下、还被压缩。真正的记忆要存在持久层(persistent store)里,用时取回注入。
- 记忆分几类:短期记忆(short-term) 就是当前上下文;长期记忆(long-term) 跨会话保存,又分存事实的语义记忆(semantic memory) 和存事件的情景记忆(episodic memory)。
- 取回大规模记忆靠语义检索:用 embedding 把文本变向量、按距离找相关,这套“检索+生成”范式就是 RAG(检索增强生成)。
- 遗忘和记住同样重要:类人遗忘(human-like forgetting) 通过巩固(consolidation) 与衰减(decay) 淘汰低价值记忆,pi 的 memory 扩展正是这么做的(active/dormant/forgotten 三档)。
- pi 的 memory 扩展首次运行自动安装,把记忆存进项目本地文件、按分数衰减、经生命周期事件自动记取;大规模知识库再叠一层向量检索扩展即可。
有了工具、上下文、记忆,Agent 已经相当完整。但它的知识仍局限在“模型训练时见过的”——你的私有资料、内部文档它并不知道。下一章我们就解决这个问题:用 RAG 让 Agent 基于你自己的知识库回答。