§ 11.3 · Section

记忆系统

Agent Memory

有一件事初学者几乎都会想错:他们以为大模型「记得」刚才聊过什么。它压根不记得。每一次 API 请求对模型来说都是人生第一次——你之所以觉得它有记忆,是因为你的程序每次都把之前的全部对话重新念了一遍给它听。这个真相一旦接受,接下来所有问题就都变成了工程问题:对话变长以后念不完怎么办?超过上下文窗口被截断怎么办?昨天的事今天还要记得怎么办?这一节要把这套「外挂账本」从头拆开——为什么必须往外存(有 Anthropic 官方给的最具体理由)、上下文为什么是「有限资源」而不是「越长越好」、Lost in the Middle 到底证明了什么、又没有证明什么(这是中文互联网上错得最普遍的一处),以及怎么用向量检索给 Agent 装一套能长期回忆的记忆,附一段可跑的代码。

生活场景
🧑‍⚕️ 每天上班都失忆的门诊医生

想象一位很厉害的医院门诊医生,医术极好,唯一的问题是——他每天早上一睁眼,就把前一天的事忘得一干二净。

那这家医院怎么还能正常运转?靠病历本

你走进诊室,他不认识你。但护士递上一本病历:上面写着你三个月前来过、当时诊断是什么、开过哪些药、对哪种消炎药过敏。他花两分钟读完,然后就能像老熟人一样接着上次的进度给你看病。你感觉他记得你,其实他记得的是那本病历。

再往下想,你会发现病历本身有讲究。它不可能把你每次说过的每句话都逐字记下来——三年下来那得多厚?所以病历上写的是压缩过的、结构化的、只留下会影响后续判断的信息:「2023-04 青霉素过敏(皮试阳性)」,而不是「患者当日情绪略紧张,抱怨候诊时间长,提到孩子上小学」。

而且病历还分两种存法:桌上摊着的这一本(今天要看的病人,随手就能翻)和档案室里的几万本(要用时按名字和编号去调)。

先破一个幻觉:模型天生没有记忆

把这件事说得再直白一点:大模型的 API 是「无状态」的。所谓无状态,说白了就是——服务器不替你记任何东西,你这次发过去什么,它就只看到什么。你在聊天界面里看到的「上下文连贯」,是客户端替你做的一件很笨的事:每发一句新话,就把整段历史重新打包发一遍。

用代码看这件事最清楚,一眼就懂:

# 第 1 轮
messages = [
  {"role": "user", "content": "我叫张伟,我对花生过敏。"}
]
# → 模型回:"好的张伟,我记住了。"   ← 它其实什么都没记

# 第 2 轮:你必须把第 1 轮整段带上,一个字都不能少
messages = [
  {"role": "user",      "content": "我叫张伟,我对花生过敏。"},
  {"role": "assistant", "content": "好的张伟,我记住了。"},
  {"role": "user",      "content": "推荐一道适合我的菜。"}
]
# → 模型回:"建议清蒸鱼,避开含花生的菜。"

# ★ 如果第 2 轮你只发了最后那一句:
messages = [
  {"role": "user", "content": "推荐一道适合我的菜。"}
]
# → 模型回:"推荐宫保鸡丁!"        ← 里面就有花生
#   它不是忘了,它是从来没知道过。

这个「每次重念一遍」的机制,是理解本节所有后续问题的钥匙。它直接导出三个必然后果,每一个都是工程上的硬约束:

换成大白话总结:模型不是记忆力差,它是压根没有记忆这个器官。所有你以为的「记忆」,全是你在外面搭的脚手架。相当于那位失忆医生本人没有记忆功能,是整个医院的病历系统在替他记。

为什么必须往外存:一个来自官方的具体理由

「所以要加个外部记忆」这句话很容易说得很空。这里给一个非常具体、来自 Anthropic 官方多 Agent 系统博客的理由,它比任何抽象论证都有说服力:

官方实践 · 为什么要写进 Memory

在 Anthropic 描述的多 Agent 研究系统里,负责统筹的 LeadResearcher(主研究员 Agent)会先把自己的计划写进 Memory(外部记忆),然后才开始干活。

原因写得非常直接:因为超过 200,000 token 的上下文会被截断。计划如果只躺在对话历史里,一旦对话长到超过窗口,这份计划就会连同早期内容一起被切掉——而计划恰恰是最不能丢的东西

把它翻译成人话:它怕自己干到一半忘了自己原本要干什么。所以先把「我要干什么」抄到一个不会被冲掉的地方。

(说明:该博客的具体发布日期我没有取到,所以这里不标注日期。)

这个例子妙在哪里?妙在它暴露了「截断」这件事真正的危险之处——被切掉的不是随便某段闲聊,而可能是最关键的目标声明。而且截断通常是从最早的内容开始切,偏偏「我要做什么」这句话总是出现在最开头。

用生活场景说:你去菜市场买菜,出门前想好了「今天做红烧肉,要买五花肉、生姜、冰糖」。结果你在市场里逛了两个小时,看见什么便宜买什么,篮子塞满了,最后发现五花肉忘了买。正确做法是什么?在纸上写一张清单塞进兜里——不管中途看见多少打折的东西,清单永远在兜里,不会被挤掉。

这就是 LeadResearcher 那个动作的全部含义:把目标从「容易被冲掉的短期记忆」搬到「不会被冲掉的外部存储」。这也是本节要给你的第一条实操准则:任何长任务的 Agent,都应该在开工之前把计划落到外部存储里,并在每一轮开头重新读一遍。

Analogy · 装修工地上的三样东西

要把 Agent 的记忆分层讲清楚,装修工地是我见过最贴切的场景。工地上同时存在三样东西,各管一段时间尺度。

第一样:手里正拿着的那把工具和刚量出的那个尺寸。工人此刻脑子里记着「这面墙 3.2 米」,五分钟后钉完这块板,这个数字就没用了,自然忘掉。这对应模型的上下文窗口——容量小、取用快、用完即弃,而且塞不进太多东西。

第二样:贴在墙上的那张施工图。它不在任何人脑子里,它挂在那儿。谁需要就走过去看一眼:主卧要不要打通、插座留几个、水管走顶还是走地。工人换班了,图还在墙上——这就是外部记忆的价值:它不随「谁在干」而消失。这对应写进 Memory 的计划、待办清单、已完成事项。

第三样:物业档案室里这栋楼的原始图纸。平时压根不看,但真遇到「这根管子能不能砸」的时候,必须去调出来。这对应长期记忆库——量很大,只在需要时按线索检索出相关的那几页。

三样东西的关系值得再说一句:它们不是三个可选方案,而是三个必须同时存在的层次。只有第一样,工人一换班全乱;只有第三样,量个尺寸也要跑档案室,慢得干不了活。好的 Agent 记忆设计,就是搞清楚哪些信息该待在哪一层,以及信息怎么在层之间流动——量出来的尺寸要不要写到图上?图上的变更要不要归档?

而记忆压缩这件事,在这个场景里对应的是:竣工时把满墙的涂改、便签、临时标注,整理成一份干净的竣工图归档。不是删掉信息,是把「过程」压成「结论」。

业界常见的划分:短期记忆与长期记忆

说白了,这套划分就是把「这次对话里的事」和「跨越很多次对话都要记住的事」分开放。

讲 Agent 记忆几乎所有资料都会先分「短期 / 长期」两类。这里必须先做一个诚实标注「短期记忆 / 长期记忆」这套二分法在 Agent 领域没有权威论文给出过标准定义,它是业界通行的说法,不是论文结论。你在别处看到有人把它标成某篇论文的分类,那是加戏。

不过这个划分确实好用,因为它对应了两种物理上完全不同的存储方式:

短期记忆(业界常见说法)长期记忆(业界常见说法)
存在哪里就在这次请求的 messages数据库 / 向量库 / 文件,在模型外面
怎么进模型整段带上,模型全看得见按需检索,只把相关的几条捞进来
容量受上下文窗口硬限制理论上无限(受你的硬盘限制)
成本特性越长越贵,且每轮重复付费存储便宜,只在检索时付一点
能跨会话吗不能,关窗口就没了能,这是它存在的全部理由
典型内容本轮对话、刚才工具的返回值、当前计划用户偏好、历史结论、项目背景、过往对话摘要
生活类比桌上摊开的病历 / 手里的尺寸档案室的图纸 / 兜里的购物清单
失效的样子超窗口被截断,早期内容凭空消失检索没命中,等于没存

有些资料还会再切出一类叫「工作记忆」「情景记忆」,划分方式各家不同。我建议你不必纠结名词,抓住三个真正要做的决策就够了:

上下文工程:把上下文当成有限资源

换成大白话:上下文窗口不是一个越塞越好的仓库,它更接近一张办公桌——桌面就那么大,你摊开的东西越多,真正在看的那份反而越容易被压在下面。

这里要引入一个近两年才立起来的概念——上下文工程(context engineering)。它有明确的官方出处:Anthropic 的文章《Effective context engineering for AI agents》,发布于 2025 年 9 月 29 日

先把这个词当场翻译:提示词工程(prompt engineering,§10 章讲过)关心的是「这一句话怎么写」;上下文工程关心的是「模型眼前那一整块内容里,该放什么、不该放什么、怎么排」。前者是措辞,后者是资源分配。相当于前者是「菜谱怎么写清楚」,后者是「一张只能摆四个盘子的桌子,摆哪四个」。

文章里最核心的一句判断是:上下文是一种有限资源,并且存在边际收益递减「边际收益递减」这个词听着像经济学,说白了就是:往里塞第一千个 token 带来的帮助,比塞第一个 token 小得多;塞到某个程度之后,再塞不但不涨,反而拖累。

官方给出的解释是「注意力预算」(attention budget)这个说法:模型有一份有限的注意力预算,每一个新塞进去的 token 都在消耗它。这个比喻很好懂——就像你在办公室一边开会一边回微信一边盯着报表,你的注意力总量不变,事情多一件,每件事分到的就少一点。

更硬的原因来自架构层面,官方文章给了两条,都值得记住:

官方还引用了一项来自 Chroma 的研究,术语叫 context rot(上下文腐坏,字面意思是「上下文烂掉」):随着 token 数增加,模型准确召回信息的能力会下降。

★ 但请注意:这是「性能梯度」,不是「断崖」

这一小节是专门用来防止你把上面那些结论用歪的。因为中文互联网上一个极普遍的说法是:「上下文一超过某个长度,模型就废了」。这是把梯度当成了断崖。

Anthropic 官方文章在讲 context rot 时特别强调了一句限定,原文是 「a performance gradient rather than a hard cliff」——这是一条性能梯度,而不是一道硬悬崖

这两个词的差别很关键,值得画出来:

误解版(断崖)              事实版(梯度)
准确率                       准确率
 │████████                    │████████
 │████████                    │ ███████
 │████████                    │  ██████
 │████████                    │   █████
 │████████▏                   │    ████
 │        ▏← 一超就废          │     ███
 └────────┴──────► 长度       └──────────────► 长度
  「128k 以内满血,              「越长越吃力,但是
   超一点就完全不能用」            一点一点地下降」

为什么这个区别对你有实际影响?因为它决定了你该怎么做决策。

如果你以为是断崖而事实是梯度,所以应该
「只要不超窗口就没事」,于是把窗口塞到 95% 满塞到 95% 满时性能已经明显退化了。该主动控制上下文长度,而不是以「不报错」为标准
「超了就完全不能用」,于是长文档任务直接放弃超长上下文仍然有用,只是要接受召回率下降,并配合检索、分块处理等手段兜底
纠结「到底多少 token 是分界线」没有分界线。该做的是给自己的任务实测一条曲线,看在什么长度上准确率掉到你不能接受的程度

用生活场景说这件事:这就像开车带人。车上多坐一个人,油耗高一点、加速慢一点,但车不会「多一个人就抛锚」。它是一条平缓变差的曲线,不是一个开关。所以正确的态度是「能少带就少带」,而不是「只要没超载就随便塞」。

★★ Lost in the Middle:它测的是位置,不是长度

接下来这一小节,我认为是本节最该慢慢读的部分——因为它是中文技术文章里出错率最高的一处,而且错得非常一致,说明大家都在互相抄。

先把论文坐标摆清楚:

它的实验做法是这一切的关键,请务必看清:研究者移动关键信息在输入中的位置也就是说——输入的总长度是固定的,里面的内容也是同一批,唯一被改变的变量是「那条有用的信息被放在开头、中间、还是结尾」。

结论是:关键信息放在开头或结尾时性能最高,放在中间时性能显著下降;而且即使是那些声称支持长上下文的模型,也一样有这个毛病

把实验形状画出来,误读就无处藏身了:

论文实际做的实验(长度不变,只挪位置)

  实验 A:★ 在最前面
     [★关键文档] [无关1] [无关2] ... [无关19]   → 准确率 高
  实验 B:★ 在正中间
     [无关1] ... [无关9] [★关键文档] [无关11] ... → 准确率 低  ← 就是这里
  实验 C:★ 在最后面
     [无关1] [无关2] ... [无关19] [★关键文档]   → 准确率 高

  ★★ 三个实验的输入长度完全一样!
     变的只有那一份关键文档摆在第几个位置。


很多文章误以为它做的是这个实验(长度在变)

     [★] [无关×5]      → 准确率 高
     [★] [无关×50]     → 准确率 中
     [★] [无关×500]    → 准确率 低
  ✗ 这是「提示越长越差」,论文没有用这个设计得出主结论。

所以必须把两句话分开说,一句是论文说的,一句不是:

说法论文支持吗说明
「关键信息放中间,模型容易漏掉」✔ 支持,这就是主结论论文正是通过移动位置测出来的:首尾高、中间显著下降
「声称支持长上下文的模型也躲不过这个问题」✔ 支持论文明确提到了这一点
「提示词越长,模型表现越差」✘ 不能由这篇论文直接得出它测的是位置这个变量,不是长度这个变量。长度效应是另一个话题(参见上面 context rot 那一小节,而且官方强调那是梯度不是断崖)
「所以上下文超过 X 千 token 就不能用了」✘ 双重错误既把位置问题说成长度问题,又把梯度说成断崖

为什么这个区分对你有真金白银的价值?因为两种理解会导出完全不同的做法:

这件事有一个特别贴切的生活类比:会议。你在办公室开一个一小时的会,要传达十件事。大家散会后能记住的,通常是最先说的那件和最后说的那件;夹在中间的第五、第六件,第二天问起来没人有印象。那怎么办?解法不是「把会缩短到十分钟只讲三件事」(那七件事还是要办),而是「把最重要的那件放在开头,再在结尾复述一遍」。

落到 Agent 记忆设计上,这条给出三个非常具体的动作:

短期记忆怎么管:四种常见策略

打个比方,这四种策略相当于整理冰箱的四种思路:塞不下了,你可以扔最旧的、可以把剩菜合并成一盒、可以只留今天要吃的、也可以拿个本子记下都放了什么。

知道了「上下文是有限资源」之后,第一个要解决的具体问题是:对话越来越长,该扔掉什么?下面四种做法都是业界常见实践(不是论文结论、也不是官方规范,请这样引用):

策略怎么做好处代价生活类比
全量带上历史一个字不删,整段发过去最简单,信息不丢越聊越贵;迟早撞窗口把整箱旧账本搬到会议室
滑动窗口只保留最近 N 轮,更早的直接丢成本可控且恒定早期的关键信息(过敏、需求)会凭空消失只保留最近三天的便签,之前的撕掉
摘要压缩历史攒到一定长度,让模型把它总结成一段,用摘要替换原文省很多 token,还保住了要点摘要会丢细节;摘要本身也要花钱生成;反复摘要会「越摘越走形」把一个月的工作日志写成一页月报
分层混合硬约束永久固定 + 最近几轮原文 + 更早的摘要 + 长期库按需检索实践中最稳的一种实现最复杂,要自己定规则兜里的清单 + 桌上的病历 + 档案室

关于「摘要压缩」有个坑值得单独说:反复摘要会失真。第一次把 20 轮压成一段,第二次把「那段摘要 + 新的 20 轮」再压成一段,第三次再压……这跟复印件再去复印一样,每一代都损失一点,几代之后细节全糊了。缓解办法是把关键事实单独抽成结构化条目永久保存(比如一个 facts 列表:用户名、过敏、偏好、已排除的方案),摘要只负责叙事部分。

换成大白话:摘要负责「故事讲到哪儿了」,结构化条目负责「有哪些不能忘的死规矩」。两者分开存,死规矩就永远不会在传话游戏中走形。

长期记忆的技术底座:同一套向量检索原理的复用

其实就是把 § 9.2 讲过的那套东西原封不动搬过来用了一遍——本质上就是「把内容变成一串数字,再比谁离得近」。

长期记忆的核心问题是:存了一万条历史信息,这次对话该捞哪几条出来?如果用关键词匹配,用户说「我不能吃坚果」,就检索不到三个月前存的那条「花生过敏」——字面上一个字都不重样

所以要按「意思接近」来找,这就是 §9.2 讲过的 Embedding(嵌入向量)和向量检索。这里值得单独点明一件事:Agent 记忆用的检索技术,和 RAG 用的是同一套原理。不是两套东西,是同一套原理在两个场景里复用。区别只在于「库里存的是什么」——RAG 存的是文档片段,长期记忆存的是过往对话与结论。

快速回顾一下这套原理,因为它是长期记忆能不能用起来的全部关键。Embedding 说白了就是:把一段文字变成一串数字(一个向量),意思相近的文字对应的向量在空间里也靠得近。比较两个向量有多近,用余弦相似度——说白了就是量两个箭头之间的夹角,夹角小就是意思近:

cos_sim(A, B) = (A · B) / (|A| · |B|)

  A · B  是点积:对应位置相乘再全部加起来
         [1, 2, 3] · [4, 5, 6] = 1×4 + 2×5 + 3×6 = 32
  |A|    是向量长度(模):各分量平方和再开根号
         |[3, 4]| = √(9 + 16) = 5

结果范围 -1 ~ +1
  +1  完全同向 → 意思几乎一样
   0  互相垂直 → 毫不相关
  -1  完全反向 → 意思相反

两个非常实用的工程结论,直接影响你怎么写代码:

下面这个小工具让你亲手拖两个向量、看夹角和余弦值怎么变。建议把两个箭头分别调到「几乎重合」「垂直」「反向」各试一次,你对「相似度 0.82 到底算近还是不近」会形成一个手感——这比读十遍公式管用。

几十万条记忆怎么秒级查完:HNSW

知道怎么比两个向量之后,下一个问题是速度。假设你的记忆库攒到了 50 万条,每条 1536 维。老老实实跟每一条都算一遍点积(这叫暴力搜索),是 50 万 × 1536 ≈ 7.7 亿次乘法。换算成可感知的量:现代 CPU 单核每秒能做几十亿次这种运算,所以一次查询要花零点几秒——单人用还行,几十个人同时用就堵住了。

于是需要索引。今天最主流的算法叫 HNSW(Hierarchical Navigable Small World,「分层可导航小世界」),坐标很清楚:

跳表这个类比一说就懂,用地铁来讲:最底层是每站都停的普通线,中间层是只停大站的快线,最高层是只停三四个枢纽的机场快轨。你要从城市一头到另一头,聪明的走法是先坐机场快轨跨过大半个城市,再换快线靠近,最后换普通线走完最后两站——而不是从头到尾坐每站都停的车。

HNSW 的查询就是这个过程:从最高层的少数枢纽点出发,朝着「离查询向量更近」的方向跳;这一层跳不动了(周围邻居都比自己远),就下沉一层,在更密的图上继续跳;一直沉到最底层,得到答案。那个「指数衰减概率」保证了高层点稀少(枢纽就该少)、底层点密集(普通站就该多)——正好是地铁网络的形状。

要记住一句限定:HNSW 给的是「近似」最近邻,不保证百分百找到真正最近的那一条(专业叫法是 ANN,Approximate Nearest Neighbor,近似最近邻)。做记忆检索完全能接受——你要的是「相关的几条回忆」,漏掉排第 4 相关而拿到排第 6 相关的,差别很小。但如果你要做的是「精确查用户 ID 12345 的档案」,那压根不该用向量检索,该用数据库的主键索引。

动手:给 Agent 装一套向量记忆

把上面说的全部串起来。这段代码实现了一个完整可跑的记忆层:写入记忆、按语义检索、拼进提示词。为了彻底透明、不依赖任何框架,检索这里手写成暴力搜索(几千条以内完全够用):

import json, time
import numpy as np
from openai import OpenAI

client = OpenAI()          # 需要环境变量 OPENAI_API_KEY
EMB_MODEL = "text-embedding-3-small"

# ==================== 记忆层 ====================
class VectorMemory:
    """最小可用的长期记忆:一条记忆 = 一段文本 + 一个向量 + 元数据"""

    def __init__(self):
        self.texts = []        # 记忆原文
        self.metas = []        # 元数据(时间、类型、重要度)
        self.vecs  = None      # 形状 (条数, 1536) 的矩阵

    def _embed(self, texts):
        r = client.embeddings.create(model=EMB_MODEL, input=texts)
        return np.array([d.embedding for d in r.data], dtype=np.float32)
        # 注意:OpenAI 的 embedding 已归一化为单位长度,
        #      所以余弦相似度可直接退化成点积。

    def add(self, text, kind="fact", importance=1):
        v = self._embed([text])
        self.texts.append(text)
        self.metas.append({"kind": kind, "importance": importance,
                           "ts": time.strftime("%Y-%m-%d %H:%M")})
        self.vecs = v if self.vecs is None else np.vstack([self.vecs, v])

    def recall(self, query, k=3, min_score=0.30):
        """按语义相关度召回。min_score 是「宁可不给也不给错」的门槛"""
        if self.vecs is None:
            return []
        q = self._embed([query])[0]
        scores = self.vecs @ q                    # 点积 = 余弦相似度
        # 重要度轻微加权:让硬约束更容易被召回(业界常见实践)
        scores = scores + 0.02 * np.array(
            [m["importance"] for m in self.metas], dtype=np.float32)
        order = np.argsort(-scores)[:k]
        return [(self.texts[i], self.metas[i], float(scores[i]))
                for i in order if scores[i] >= min_score]

# ==================== 组装进对话 ====================
mem = VectorMemory()
mem.add("用户叫张伟,是 iOS 开发者。", kind="profile")
mem.add("用户对花生严重过敏,任何含花生的建议都要避开。",
        kind="constraint", importance=5)        # ★ 硬约束给高重要度
mem.add("上次排查内存泄漏,已排除「图片缓存未释放」和「循环引用」两种原因。",
        kind="episode")
mem.add("用户偏好简短回答,不喜欢客套话。", kind="preference", importance=3)

SYSTEM_TMPL = """你是一个长期陪伴用户的助手。

【必须遵守的硬约束】
{constraints}

【可能相关的过往记忆】
{recalled}

回答时如果用到了记忆,请自然地体现出来,不要机械复述。
再次强调,上面的硬约束不得违反。"""       # ★ 结尾复述,利用首尾位置优势

def build_prompt(user_input):
    hits = mem.recall(user_input, k=3)
    # 硬约束永久固定在最前面,不依赖检索能不能命中
    constraints = "\n".join(
        f"- {t}" for t, m, _ in
        [(t, m, 0) for t, m in zip(mem.texts, mem.metas)]
        if m["kind"] == "constraint") or "(无)"
    recalled = "\n".join(
        f"- [{m['kind']} {m['ts']}] {t}  (相关度 {s:.3f})"
        for t, m, s in hits) or "(没有检索到相关记忆)"
    return SYSTEM_TMPL.format(constraints=constraints, recalled=recalled)

def chat(user_input):
    sys = build_prompt(user_input)
    r = client.chat.completions.create(
        model="gpt-4o-mini", temperature=0.3,
        messages=[{"role": "system", "content": sys},
                  {"role": "user",   "content": user_input}],
    )
    return sys, r.choices[0].message.content

if __name__ == "__main__":
    for q in ["晚上想吃点下酒菜,给我推荐两个。",
              "那个内存问题还有什么方向可以查?"]:
        sys, ans = chat(q)
        print("=" * 60)
        print("问:", q)
        print("--- 实际拼出来的系统提示 ---")
        print(sys)
        print("--- 回答 ---")
        print(ans)

这段代码里有五处是刻意这样设计的,每一处都对应本节讲过的一条原理:

补一句工程实话:这段代码用暴力搜索,几千条记忆以内毫无问题。上到几十万条再换成带 HNSW 索引的向量库(Faiss、Milvus、pgvector、Qdrant 之类),架构不用改,只把 recall 里那一行矩阵乘法换成库的查询调用。不要一上手就套大框架——自己手写一遍,你会对每一步在干什么有彻底的掌控。

该记什么、该忘什么:比技术更难的那个问题

这一条听着玄,其实就是在问一件很日常的事:什么值得写进本子,什么说过就算了。

技术部分到这儿其实讲完了。但真正让记忆系统好用或不好用的,是一个产品问题:什么值得存?把所有对话原文全存进向量库是最省事的做法,也是最糟的做法——因为库里塞满了「嗯」「好的」「谢谢」这类噪音之后,每次检索捞回来的都是垃圾。

一个可用的分类(这属于业界常见实践,不是标准):

类型例子存不存怎么处理
硬约束过敏、宗教饮食禁忌、法律合规要求必存永久常驻上下文,不参与检索、不允许被摘要压缩掉
身份与偏好职业、技术栈、喜欢简短回答必存常驻或高优先检索,条目化保存
结论与决定「已排除 A、B 两种原因」「方案定为 C」必存存结论,不存推导过程
过程性对话来回确认、修改措辞、闲聊不存压成一句摘要就够,原文丢掉
时效性信息「今天下午三点有会」「这周在出差」存但要带过期时间过期就删或标记失效,否则会造成严重误导
已被推翻的旧信息「上次说用 MySQL」(后来改成 PostgreSQL 了)必须更新新信息要覆盖旧信息,而不是并列存两条

最后两行是记忆系统最容易翻车的地方,值得单独说。矛盾的记忆比没有记忆更危险。假设库里同时存着「用户项目用 MySQL」和「用户项目用 PostgreSQL」,检索时两条都被捞出来,模型看到互相矛盾的资料,它会怎么办?它不会说「你的记忆自相矛盾」,它会挑一条看起来顺的,然后自信地讲下去。

这就像一本病历上前后写着「青霉素过敏」和「青霉素皮试阴性」,两句都没划掉。医生翻到哪一页就按哪一页开药——这已经不是信息不足,这是信息污染。所以记忆系统必须有「更新」这个动作,不能只有「追加」。

同理,时效性信息不带过期时间是个隐形炸弹:三个月前存的「这周在出差」,三个月后被检索出来,模型就会一本正经地说「考虑到您在出差……」。一条实操建议:凡是句子里有「今天」「这周」「最近」的记忆,写入时一律带上写入日期,并在拼进提示词时把日期一起给模型看——让它自己判断这条还新鲜不新鲜。

关于工程实践的来源标注

本节前后用到了一批工程手法,这里统一做一次来源交代,避免你把它们当成权威结论去引用:

为什么要花一整段讲这个?因为在一个演进这么快的领域里,「知道哪句话有出处、哪句话只是大家都这么干」本身就是一种关键能力。把工程惯例说成论文结论,是技术写作里最常见也最有害的一种失真。你自己在团队里做技术决策时也该这样区分——「有论文支持的」和「大家都这么做的」,能承受的质疑强度完全不同。

记忆系统的六个高频坑:一份排查清单

症状大概率的原因先查哪里
聊到第 30 轮,模型忘了开头的关键要求上下文被截断,或滑动窗口把早期内容丢了把硬约束改成永久常驻,不要指望它待在历史里
明明存过,就是想不起来检索没命中(问法与存法用词差太远)打印相关度分数;分数普遍低于 0.3 就是没检索到
记忆捞回来了,模型却没用塞的内容太多,关键那条被埋在中间减少召回条数;把最相关的挪到最前和最后(Lost in the Middle)
它坚持一个已经被推翻的旧结论记忆没更新,新旧两条并存加「更新/失效」机制,不能只有追加
它说「考虑到您在出差」,可你早回来了时效性记忆没带过期时间写入时带日期;拼提示时把日期一起给模型
越聊越贵,token 消耗失控全量带上历史,每轮重复付费上摘要压缩 + 分层策略;只有硬约束才配全程常驻

常见误解一次澄清

常听到的说法实际情况
「模型记得我们刚才聊的内容」不记得。API 是无状态的,是你的程序每次把全部历史重新发了一遍。所有「记忆」都是外部脚手架
「Lost in the Middle 证明了提示词越长效果越差」错。论文(arXiv 2307.03172TACL 2023)测的是位置不是长度——做法是移动关键信息在输入中的位置,输入长度不变。结论是首尾高、中间显著下降,它不直接证明「提示越长越差」
「上下文一超过某个长度模型就废了」错。Anthropic 官方强调 context rot 是「a performance gradient rather than a hard cliff」——性能梯度,不是硬悬崖。是逐渐变吃力,不是过线就报废
「只要不超上下文窗口就没有性能损失」不对。上下文是有限资源边际收益递减;模型有注意力预算,每个新 token 都在消耗它;n 个 token 产生 n² 对关系,越长越被摊薄
「短期记忆 / 长期记忆是论文里的标准分类」不是。这套二分法没有权威论文定义,是业界通行的划分方式。好用,但别当论文结论引
「512 token 一片、重叠 50、先粗筛再 rerank 是标准做法」chunking 策略、rerank、hybrid search 都没有权威一手来源,属于工程社区共识实践,不是论文结论也不是官方规范
「Agent 记忆和 RAG 是两套不同的技术」底层是同一套原理:Embedding + 向量检索。区别只在库里存的是文档片段还是过往对话
「向量库里选 cosine 还是 L2 很关键」对已归一化的单位向量(如 OpenAI 的 embedding),两者给出完全相同的排序,是个伪问题
「HNSW 一定能找到最相似的那一条」不能,它是近似最近邻(ANN),用一点召回率换了速度。要精确匹配请用数据库索引
「记忆存得越多越好」不对。噪音会淹没检索;矛盾的记忆比没有记忆更危险;时效性记忆不设过期会造成严重误导
Recap · 收束

本节的地基是一句话:模型没有记忆这个器官。API 是无状态的,你以为的「记得」,全是你的程序每轮把历史重新念一遍的结果。由此必然导出三件事——成本随对话线性增长、迟早撞上上下文窗口、跨会话彻底断片。

「为什么必须往外存」有一个最具体的官方理由:Anthropic 多 Agent 系统里的 LeadResearcher 会先把计划写进 Memory,因为超过 200,000 token 的上下文会被截断——它怕干到一半忘了自己原本要干什么。(该博客的发布日期未取得,故不标日期。)

上下文工程(Anthropic《Effective context engineering for AI agents》,2025 年 9 月 29 日)给了看待上下文的正确框架:它是有限资源,存在边际收益递减;模型有注意力预算,每个新 token 都在消耗它;架构上 n 个 token 产生 n² 对关系,越长越被摊薄;而且训练数据里短序列远多于长序列。context rot(Chroma 研究)说 token 一多、准确召回就下降,但官方强调那是「a performance gradient rather than a hard cliff」——性能梯度,不是断崖。

本节最该记牢的一处纠偏:Lost in the Middle(arXiv 2307.03172TACL 2023 正式期刊)测的是多文档问答键值检索,做法是移动关键信息在输入中的位置——它测的是位置,不是长度,不直接证明「提示越长越差」。结论是首尾性能最高、中间显著下降,声称支持长上下文的模型也躲不过。这条结论的现金价值是:把最关键的信息挪到最前和最后,一个 token 都不用减。

技术底座是 §9.2 那套原理的复用:余弦相似度 cos_sim(A,B) = (A·B)/(|A|·|B|);OpenAI 的 embedding 已归一化为单位长度,故可退化成点积,且与欧氏距离给出完全相同的排序;量大了上 HNSW(Malkov & Yashunin,arXiv 1603.09320,2016)——多层邻近图、指数衰减概率选层、对数复杂度,结构类似跳表,给的是近似最近邻。

来源边界也要记住:「短期 / 长期记忆」这套二分法没有权威论文定义,是业界通行说法;chunking、rerank、hybrid search 未取得权威一手来源,属工程社区共识实践。别把惯例当论文引。

最后是那个比技术更难的产品问题:硬约束必须永久常驻、不参与检索;只存结论不存过程;时效性信息一律带过期时间;新信息要覆盖旧信息——矛盾的记忆比没有记忆更危险。

下一节转向「怎么找资料」:把外部知识库接进来,让模型回答之前先翻书,这就是 RAG 检索增强。你会发现它用的正是本节这套向量检索,只是库里换成了文档。

☰ 主页
学海无涯 · 智能篇 · § 11.3