博客.
博客.
(联系方式)

保持联系

如果你想联系我,无论是讨论合作机会、分享想法,还是只是打个招呼,都可以通过以下方式找到我。

(最后更新)

版权所有 / 2026

(快捷键)C
返回学习路线
(02 · Agent 开发)2026年8月

记忆与状态

LLM 自己什么都不记得——记忆全是 Harness 演的戏。真正难的不是把对话存下来,而是下一次对话时,该想起什么、又该忘掉什么。

记忆与状态

一个反直觉的事实

先泼盆冷水:LLM 本身没有记忆。每次调用模型都是一次无状态的推理——它不知道你上一句话说了什么,不知道昨天聊过什么,甚至不知道自己是第几次被调用。你感觉 ChatGPT「记得你」,全是模型外面的那层程序(Harness)在演戏:每次请求都把历史对话拼进 prompt,模型只是读了一遍「会议纪要」然后接着演。

想通这一点,Agent 的记忆问题就从玄学变成了工程:记忆 = 你要在每次调用前,往上下文里塞什么。存下来不难,硬盘很便宜;难的是判断什么值得存、什么时候该想起、什么时候该忘掉。这一章我们用人脑的记忆系统作类比,把这个工程问题一层层拆开。


记忆分类学:不只三种

认知科学对记忆的分类,映射到 Agent 上意外地严丝合缝:

  • 工作记忆(Working Memory):你脑子里此刻正在处理的信息,容量极小(经典的 7±2 个组块)。对应 Agent 的短期记忆——当前会话的上下文窗口。
  • 情景记忆(Episodic Memory):「上周三我在咖啡馆遇到一个老同学」——带时间地点的具体经历。对应 Agent 存储的历史交互记录:某次对话、某次任务的过程与结果。
  • 语义记忆(Semantic Memory):「巴黎是法国首都」——从经历中抽象出来、与具体场景脱钩的知识。对应 Agent 沉淀的事实与偏好:用户是后端工程师、团队用 pnpm 不用 npm。
  • 程序性记忆(Procedural Memory):「怎么骑自行车」——关于「怎么做」的技能。对应 Agent 的 Skill / 工具使用经验:调试某类 bug 的标准流程、发布系统的操作步骤。

这个分类的价值在于,它直接告诉你不同记忆该用不同机制存:工作记忆靠上下文窗口,情景记忆靠日志和向量检索,语义记忆靠提炼后的结构化条目,程序性记忆靠 Skill 文档和少样本示例(few-shot examples)。很多团队用一个向量库统存所有东西,结果就是技能检索不出、事实对不上号——不是存储不行,是分类错了。

记忆分类学:人脑的工作、情景、语义、程序性记忆分别对应 Agent 的上下文窗口、历史交互记录、事实与偏好、Skill 文档

另一个推论:人脑是「小容量工作记忆 + 海量长期存储 + 按需提取」的架构。你不可能把所有经历同时装进脑子里,Agent 也一样——上下文窗口永远不够用,所以必须有长期记忆,所以必须有「检索」这个动作。后文的所有机制,本质上都是在回答同一个问题:有限的工作台,怎么摆出此刻最需要的那些东西。


短期记忆:会话内的上下文管理

短期记忆看起来最简单——不就是把消息列表存起来吗?但真实的坑在上下文窗口是有限的。聊得久了,消息总长度超过窗口,怎么办?三种常见策略,按实现成本排序:

  1. 截断(Truncation):只保留最近 N 轮。简单,但「N 轮之前你说过你海鲜过敏」就丢了。
  2. 摘要(Summarization):用一个 LLM 调用把旧对话压缩成一段摘要放在上下文开头,详细消息只保留最近几轮。这是主流做法。
  3. 分层摘要:摘要的摘要——对话很长时先按话题分段摘要,再对段摘要做总摘要。
// 一个最小可用的滑动窗口 + 摘要策略
async function buildContext(history: Message[]): Promise<Message[]> {
  const recent = history.slice(-10);              // 最近 10 轮原样保留
  const old = history.slice(0, -10);              // 更早的部分压缩
  if (old.length === 0) return recent;
  const summary = await llm.summarize(old);       // 一次额外的模型调用
  return [{ role: "system", content: `前情提要:${summary}` }, ...recent];
}

注意这个设计的本质:摘要就是工作记忆的「压缩算法」,和人脑把一天的经历浓缩成「今天开了三个会」是同一件事。代价是信息有损——压缩必然丢细节。工程上的补救是「摘要 + 可回溯」:摘要里带上原始消息的索引,模型发现摘要不够用时,可以通过工具把原文捞回来(MemGPT 的 recall memory 就是这个思路,后面细讲)。

还有一个常被忽视的点:摘要要增量更新,不要每次全量重算。每轮都把全部历史重新摘要一遍,token 成本是平方级增长。正确姿势是维护一个滚动摘要,每轮只把新溢出的消息「折叠」进去。

滑动窗口加滚动摘要:更早的消息增量折叠进滚动摘要,最近 10 轮原样保留,一起拼进下一次调用的上下文


会话状态:记忆不只是聊天记录

很多人把「记忆」和「消息历史」划等号,其实会话里还有更多状态要管:

  • 任务进度:这个任务进行到哪一步了,完成了什么,还剩什么
  • 中间产物:已经生成的文件、查到的资料、工具的执行结果
  • 环境状态:当前在哪个目录、分支是什么、测试上次跑没跑过

工程上常见的做法是把状态显式建模成一个对象,随会话持久化:

interface SessionState {
  sessionId: string;
  messages: Message[];        // 对话历史(会被摘要压缩)
  task?: { goal: string; doneSteps: string[]; pendingSteps: string[] };
  artifacts: Record<string, string>;  // 产物名 -> 路径
  updatedAt: number;
}

这样「恢复一个三天前的会话」就不是翻聊天记录,而是加载一个状态快照(checkpoint)。Claude Code 的 --resume、LangGraph 的 checkpointer,干的都是这件事。checkpoint 和长期记忆的区别在于:checkpoint 是「这个会话的存档」,只对恢复当前任务有意义;长期记忆是「跨会话的沉淀」,面向未来的所有会话。一个是游戏存档,一个是玩家的经验值。


长期记忆:写入、过滤与遗忘

长期记忆解决「下次对话还能记得」。架构就三个环节:写入、存储、读取。存储后面单独讲,这里先说最容易被做砸的写入侧。

什么值得记:写入过滤标准

把原始对话整段存起来是最懒也最差的方案——噪音太多,检索时什么都搜不准。好的做法是写入前先用 LLM 提炼,并给「值得记」一个明确标准。一条信息值得进入长期记忆,通常要满足至少一条:

  • 稳定性:三个月后大概率仍然成立(用户职业、技术栈、长期偏好)
  • 复用性:未来会话里会被用到(项目约定、部署约束、协作习惯)
  • 不可推导:从公开信息或当前上下文推不出来(用户私下说的决定)

反面清单同样重要:寒暄、一次性的上下文(「这个报错我刚重启解决了」)、会话内临时状态,一律不写。

// 会话结束后,提炼值得长期记住的东西
const memories = await llm.extract(`
  从以下对话中提取值得长期记住的事实(用户偏好、项目约定、重要决定),
  忽略寒暄和一次性细节,每条一句话:
  ${transcript}
`);
// -> ["用户偏好简洁的回答", "生产环境禁直连数据库"]
await memoryStore.save(userId, memories);

这就是语义记忆的形成过程:从具体经历中抽象出通用知识,丢弃经历本身

遗忘与过期:记忆只进不出就是垃圾场

人脑会遗忘,这不是缺陷而是特性——遗忘清理了检索空间。Agent 的记忆同样需要出口机制:

  • TTL(过期时间):时效性强的记忆(「本周在用 feature-x 分支」)写入时就带上有效期
  • 冲突更新:新记忆与旧记忆矛盾时(「团队从 Vue 迁到 React」),应该更新而不是并存,否则检索会同时捞出两条相反的事实。实现上可以在写入时先按相似度查一遍旧记忆,命中高度相似的条目就让 LLM 判断是「补充」「修正」还是「推翻」,再决定插入还是覆盖
  • 价值衰减淘汰:长期没被检索命中过的记忆,重要性随时间衰减,低于阈值就归档或删除。注意是「没被命中」而不是「时间久」——一条每年都会用到的记忆不该因为老而被清掉

读取侧:检索时机、query 与预算

写入做对只完成一半,读取侧同样有三个工程决策:

  • 什么时候检索:不要每轮无条件检索。常见做法是轻量判断先行——看当前用户输入是否涉及「人、项目、历史约定」这类长期主题,或者干脆让模型自己决定(MemGPT 的思路)。闲聊句检索出的记忆纯属噪音
  • 拿什么检索:直接拿用户最后一句话当 query 通常不准,因为代词和省略太多(「那这个怎么部署?」)。更稳的做法是让 LLM 结合最近几轮对话把 query 重写成一句自包含的问题,再走向量库
  • 注入多少:给记忆设 token 预算(比如不超过上下文的 10%),检索结果去重后按分数截断。记忆是上下文的「佐料」,不是主菜——注入太多会稀释当前任务,模型反而抓不住重点

这三条都做错的话,会出现一个典型的诡异现象:记忆库越大,Agent 表现越差。不是记忆的错,是读取策略把信噪比打崩了。


Embedding 与向量检索:记忆的「联想机制」

长期记忆存好了,怎么在需要的时候找到?关键词匹配太笨——用户说「数据库连不上」,记忆里存的是「生产环境禁直连数据库」,两句话几乎没有一个共同词。Embedding 解决的就是「意思相近但字面不同」的匹配

原理与距离度量

一个 Embedding 模型把任意文本映射成一个高维向量(常见 768~3072 维),语义相近的文本在向量空间里距离也近。检索 = 把查询也变成向量,找距离最近的 K 条。距离怎么算,三个主流选择:

  • 余弦相似度:只看向量夹角,不看长度。文本检索的默认选择,因为句子长短不该影响语义判断
  • 点积(内积):向量归一化后与余弦等价;不归一化时,模长会参与打分——有些模型(如部分推荐场景)刻意用模长编码「热度」,文本语义检索一般不用
  • 欧氏距离:对向量各维的绝对差敏感,适合向量本身有明确几何意义的场景,文本检索里较少用
import numpy as np

def cosine(a, b):
    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

store = VectorStore(dim=1024)
store.add(embed("用户是后端工程师,主要写 Go"))   # 写入:文本 -> 向量 -> 入库

query = embed("这个用户的职业背景是什么?")
top_k = store.search(query, k=3)                   # 返回最相关的 3 条,注入 prompt

模型选型:开源还是商用

选 Embedding 模型比选向量库影响更大,几个实际考量:

  • 开源自托管:BGE 系列(智源 FlagEmbedding,bge-large-zh 中文表现好)、m3e 是中文社区常用的选择。优点是数据不出内网、无调用成本、维度低(768/1024)存储省;缺点是要自己运维推理服务,多语言混合场景弱一些
  • 商用 API:OpenAI text-embedding-3 系列、Cohere embed 系列。开箱即用、多语言强,缺点按 token 计费、数据要出域
  • 维度不是越高越好:1536 维比 768 维的存储和检索成本翻倍,但很多任务上收益很小。先拿自己的真实语料做小规模评测再定

一个硬建议:不管选哪个,把「换模型」当一次性事件设计——Embedding 模型一换,历史向量全部作废要重算,所以向量库里一定要存模型版本号。


向量库选型:真实差异对比

存向量、查最近邻,理论上几十行代码能写。但数据量大了、要并发、要过滤,就得用专门的向量库。三个主流选项的真实差异:

维度pgvectorQdrantMilvus
索引算法HNSW、IVFFlatHNSWHNSW、IVF 系列、DiskANN 等十多种
过滤检索SQL WHERE 全能力payload 条件过滤标量字段过滤表达式
部署形态PG 扩展,零新增组件单二进制/Docker,可集群分布式,依赖 etcd + 对象存储
舒适区规模百万级千万级亿级以上
运维成本复用现有 PG 运维

两个名词解释清楚:HNSW(分层小世界图)建一张多层图,查询时从顶层稀疏层跳到下层稠密层,召回率高、查询快,代价是索引占内存大;IVF(倒排文件)先把向量聚类成 n 个桶,查询只搜最近的几个桶,省内存快建索引,但召回率随桶数权衡。绝大多数场景两者都够用,不用纠结。

选型一句话:从 pgvector 开始,撞到墙再换。大多数 Agent 项目的记忆量级(每用户几十到几百条)远没到需要专用向量库的程度。但注意一个真实混淆点:pgvector 不是「传统数据库顺便支持向量」的玩具——它的 HNSW 实现在百万级上性能不输专用库;真正的差距在分布式能力和超大规模下的索引构建速度。


经典设计:Generative Agents 的三因素模型

「该想起什么」这个问题,2023 年斯坦福的 Generative Agents 论文给了至今仍被大量抄作业的回答。它给每条记忆打分,取 Top-K 注入上下文:

score = α × 相关性 + β × 新近性 + γ × 重要性
  • 相关性(Relevance):记忆与当前情境的 Embedding 余弦相似度
  • 新近性(Recency):指数衰减函数,论文用 0.995^(距上次被访问的小时数)——注意是「上次被访问」而不是「被创建」,被想起的记忆会「保鲜」,这很像人脑
  • 重要性(Importance):写入时让 LLM 打 1-10 分,「早饭吃了什么」是 1 分,「被裁员了」是 10 分
def score(memory, query_emb, now):
    relevance = cosine(memory.emb, query_emb)
    recency = 0.995 ** hours_since(memory.last_accessed, now)
    importance = memory.importance / 10      # 归一化到 0~1
    return relevance + recency + importance  # 论文里三个权重都是 1

top_k = sorted(memories, key=score, reverse=True)[:5]

三因素的妙处在于它们会互相纠偏:只靠相关性,陈旧的相似记忆会霸榜;加入新近性,近期发生的事自然优先;加入重要性,「用户随口一句」压不过「用户立下的规矩」。你实现自己的记忆系统时,这三个因子是最低配置,α/β/γ 按场景调。


经典设计:MemGPT 把记忆当操作系统管

MemGPT(2023)把上下文窗口类比为内存、外部存储类比为硬盘,让 LLM 自己通过函数调用管理记忆,像操作系统做分页调度一样。它的记忆分三层:

  • Core Memory(核心记忆):直接放在系统提示词里,模型每轮都能看见。存最关键的人设和用户档案,容量小。模型可以用 core_memory_append / core_memory_replace 自己改写
  • Archival Memory(归档记忆):无限容量的外部存储(向量库),模型用 archival_memory_insert 写入、archival_memory_search 检索——检索是模型主动发起的动作,不是框架自动注入
  • Recall Memory(回溯记忆):完整会话历史,模型用 conversation_search 按关键词/时间翻旧账——这就是前文说的「摘要 + 可回溯」的落地

这个设计的精髓是把记忆管理从「框架的固定逻辑」变成「模型可学习的技能」:什么时候该记、什么时候该查,由模型根据情境判断,而不是开发者写死的规则。代价也明显——每轮都要带一堆记忆管理函数的定义,prompt 膨胀,且模型需要足够强才玩得转。

MemGPT 的三层记忆:核心记忆放在系统提示词里,归档记忆是向量库,回溯记忆存完整会话历史,全部由 LLM 通过函数调用主动读写


争议:显式记忆模块 vs 超长上下文直通

上下文窗口涨到百万 token 之后,一个认真的质疑出现了:既然能把全部历史塞进去,还要记忆系统干嘛?

正方(超长上下文派)的理由:没有压缩就没有信息损失;省掉 Embedding、向量库、读写策略一整层复杂度;「大海捞针」测试表明模型在长上下文里找细节的召回已经很高。

反方(显式记忆派)的反击:

  • 成本与延迟:每轮请求都带上百万 token,费用和首 token 延迟都是数量级差距。记忆系统本质是缓存——你不会因为内存贵了就把 CPU 缓存砍掉
  • ** Lost in the middle **:研究表明模型对长上下文中部信息的利用明显弱于头尾,塞得多不等于看得全
  • 跨会话的结构化沉淀:用户偏好这种「从一百次对话里抽象出来的一条结论」,靠塞原始历史是让模型每轮重新做一遍归纳,浪费且不稳定
  • 遗忘是功能:有些记忆业务上就该过期、该删除(合规、隐私),全量塞历史没有这个开关

务实的共识是:超长上下文让「短期记忆」的容量焦虑消失了,但长期记忆的写入提炼、检索排序、遗忘机制一个都没被替代。两者是协作关系不是替代关系——上下文装当前任务,记忆系统管跨任务沉淀。


常见误区

  • 把聊天记录全量塞进上下文:窗口爆炸、成本爆炸、模型还会被无关信息带偏。上下文是稀缺资源,要精打细算
  • 认为向量检索 = 记忆:向量检索只是「联想」这一种提取方式。精确事实(API key、数据库地址)该用结构化 key-value 存,程序性知识该进 Skill 文档,别什么都向量化
  • 记忆只进不出:没有遗忘机制的记忆库会变成垃圾场,检索信噪比随时间崩盘
  • 每轮无条件检索记忆:注入一堆无关记忆会稀释上下文、白烧 token。该想的时候不想、不该想的时候乱想,比没有记忆更糟
  • 拿用户最后一句话直接当检索 query:结合会话上下文重写 query 再搜,准头会好得多
  • 为了记忆上重基建:上来就部署 Milvus 集群,结果全公司记忆数据不到十万条。先跑通读写策略,再谈规模

术语表

名词定义一句话直觉常见混淆
上下文窗口模型单次调用能看到的最大 token 数模型的「工作台」大小不等于记忆,只是记忆的展示位
工作记忆当前会话内正在被处理的信息脑子里此刻想的事常被当成全部记忆,忽视长期存储
情景记忆带时间地点的具体经历记录「上周三那次对话」与语义记忆混存导致检索噪音
语义记忆从经历抽象出的通用知识与偏好「用户是后端工程师」不是原始对话的搬运,需要提炼
程序性记忆「怎么做」的技能与流程会骑自行车该进 Skill 文档,不该进向量库
Embedding把文本映射为高维向量的模型输出给每句话一个「语义坐标」换模型后旧向量全部作废
余弦相似度两向量夹角的余弦值只比方向不比长短与点积混淆:归一化后才等价
HNSW分层小世界图索引算法从粗到细跳格子找人与 IVF 混淆:一个建图一个分桶
IVF倒排文件索引,先聚类分桶再搜桶先锁定小区再挨家找桶数设小了召回率会掉
过滤检索向量相似度之外的标量条件筛选「只在他的记忆里搜」先过滤后检索 vs 先检索后过滤结果不同
checkpoint会话状态的持久化快照游戏存档是单会话存档,不是跨会话长期记忆
TTL记忆的过期时间牛奶的保质期没有 TTL 的记忆库会越翻越不准

参考材料


小结

记住一张图:上下文窗口是工作记忆,摘要和截断是压缩算法,向量库加 Embedding 是情景记忆的联想检索,结构化条目是语义记忆,Skill 是程序性记忆,checkpoint 是会话存档。存记忆不难,难的是读写策略——什么值得写、按什么标准想起、什么时候忘掉。先把三因素打分和遗忘机制跑对,存储选型反而是最不重要的事。下一章我们会把这些记忆机制放进完整的 Agent Loop 里,看它们和工具调用、规划如何配合。