最近在做 LLM 应用落地时,最常被问到的一个能力不是“能不能调用工具”,也不是“指令遵循得怎么样”,而是“它能不能记住我上次说过的话”。这个问题听起来简单,真正做起来却非常棘手。聊天记录一长,上下文窗口很快就撑不住了;超过窗口限制之后,前面的信息就像被剪断的录像带一样再也找不回来。这个问题的本质,是 LLM 缺少长期记忆。
近期在 Hacker News 上看到有人分享 Long Term Memory for LLM 的架构实验,结合自己的一些项目实践,我整理了一篇系统性的实操笔记。这篇文章会先讲清楚 LLM 长期记忆的核心概念和架构难点,再拆解常见的记忆设计模式,最后用 Python 实现一个可运行的“外部记忆层”,把“记忆”从对话框里搬出来,放到可持久化、可检索的存储中。
本文适合正在做 LLM 应用开发、Agent 搭建、知识库问答的开发者,也适合刚接触大模型应用、想理解“记忆机制”到底是怎么回事的初学者。读完你会掌握:
- LLM 长期记忆的核心问题与边界
- 短期记忆、长期记忆、外部记忆的区别
- 四种主流记忆架构设计模式
- 一个基于向量检索的长期记忆层完整实现
- 生产环境中记忆模块的排错与最佳实践
1. 背景与核心概念
1.1 为什么 LLM 需要长期记忆
先来看一个最常见的场景。
你正在和 AI 助手讨论一个项目方案,前面已经确定了技术栈、模块划分、数据库选型,甚至约定了命名规范。聊了 20 轮之后,你随口问一句“那刚才说的用户表用软删除还是物理删除”,模型却回答得模棱两可,因为它已经记不清前面提到的数据库设计细节了。
这不是模型“变笨”了,而是模型的上下文窗口是有限的。目前主流的大模型上下文窗口虽然已经扩展到几十万 token,但依然存在两个制约:
- 成本问题:把整段历史对话全部塞进每次请求里,token 消耗会随对话轮数线性增长,长对话成本非常高。
- 注意力衰减问题:即便窗口放得下,模型对中间部分内容的注意力也会明显下降,历史信息容易被“稀释”。
所以,仅靠“把历史都拼进 prompt”不是一个可持续的解决方案。LLM 需要一种机制,把重要的历史信息沉淀下来,在需要时再快速、准确地取回,这就是长期记忆要解决的问题。
1.2 什么是长期记忆
这里先做几个概念区分,很多新手容易混在一起。
短期记忆(Short-term Memory):指当前会话中模型可以直接“看到”的信息,也就是上下文窗口内的内容。它的特点是读取快、无需额外处理,但受限于窗口长度,会话结束后往往就丢失了。
长期记忆(Long-term Memory):指跨会话、跨时间维度的信息留存。它通常不直接放进 prompt,而是存储在外部介质中,需要时通过检索机制取回相关片段,再注入当前上下文。
外部记忆(External Memory):从架构角度看,长期记忆往往以外部存储的形式存在,可以是向量数据库、关系型数据库、Redis、文件系统,甚至是一张结构化的 JSON 表。
用一个比喻来理解:短期记忆是办公桌上的便签,手边就能看到,但桌面空间有限;长期记忆是资料室的档案柜,容量大、可以持久保存,但需要一套编码和检索系统才能快速找到有用的文件。
1.3 长期记忆的难点在哪里
长期记忆看起来只是“多存一份数据再查回来”,实际上难点不少:
- 记忆的取舍:不是所有对话都值得记,如何区分“重要信息”和“一次性寒暄”?
- 记忆的结构化:对话是自然语言,但很多记忆其实是结构化的,例如用户偏好、项目约束、实体关系,怎么统一表达?
- 记忆的时效性:一条记忆可能有时效,比如“本周三的会议待办”,过期了就不应该再被检索出来。
- 检索的准确性:从大量历史记忆中找到当前问题真正需要的那几条,对检索策略要求很高。
- 记忆的更新与遗忘:用户修正了之前说过的话,旧记忆如何失效?需要一套写覆盖或冲突消解机制。
理解这些难点之后,再看各种架构实验,思路就会清晰很多:所有长期记忆方案,本质上都是在“存储”和“检索”两端做设计取舍。
2. LLM 记忆架构的常见设计模式
近一年来,社区里出现了很多长期记忆相关的实验方案,比如 MemGPT、Generative Agents、Letta、Mem0,以及各类知识库问答系统。从架构上归纳,主流设计模式大致有四类。
2.1 纯上下文扩展模式
这是最朴素的方式,也就是把所有历史记录拼进 prompt。
系统提示词 + 最近对话历史 + 当前用户问题优点是不需要额外模块,实现最简单。缺点是:
- 成本随对话长度线性上升
- 超过窗口后必须截断,截断策略很难做到“刚好去掉无关内容”
- 超长上下文会稀释模型对关键信息的注意力
适合:短会话工具、一次性问答、模型能力演示。
2.2 摘要压缩模式
每次对话达到一定长度后,用一个摘要模型把历史对话压缩成几句话,然后用压缩后的摘要替代原始历史。
[摘要] 用户正在开发一个Python项目,确定了FastAPI+React技术栈... [最近对话] ...优点是可以保留较长时间跨度内的核心信息,比直接截断优雅。缺点是摘要过程会丢失细节,且重复摘要会产生“摘要的摘要”,信息逐层衰减。
适合:需要跨轮次延续信息、但细节要求不高的场景。
2.3 向量检索记忆模式
这是目前最主流的长期记忆方案。核心思路是:
- 把记忆文本通过 embedding 模型转为向量。
- 向量存入向量数据库。
- 用户提问时,把问题也转为向量,在向量库中做相似度检索。
- 将检索结果注入 prompt,作为“记忆上下文”。
向量检索 + 原始用户问题 → prompt优点是信息密度高、可扩展性强、支持跨会话检索,是知识库问答和 Agent 长期记忆的主流基础方案。缺点是需要额外的 embedding 模型和向量存储组件,检索质量直接受 embedding 模型影响。
2.4 混合分层记忆模式
更复杂的架构会把上面几种模式组合起来,形成分层记忆。
- 第一层:最近几轮对话,直接放窗口。
- 第二层:稍早的对话,做摘要压缩。
- 第三层:历史中需要精确留存的事实,写入记忆库,需要时检索。
- 第四层:长期稳定的用户画像、偏好,单独存储并定期更新。
这种分层架构是目前社区实验的主要方向。Hacker News 上关于 LLM long term memory architecture 的讨论,大多数也是围绕“如何组织这四层信息”展开的。分层的好处是兼顾成本、精度和时效,代价是系统复杂度明显上升。
3. 记忆架构关键组件拆解
不管采用哪种模式,一个可落地的长期记忆层通常包含四个关键组件:输入处理、记忆存储、检索模块、写入策略。
3.1 输入处理组件
输入处理负责决定“哪些信息值得进入记忆系统”。原始对话往往是口语化的,直接存储会浪费存储空间,也会降低后续检索质量。
常见做法:
- 先做对话清洗,去除无意义的口头语、重复表达。
- 再过滤敏感信息和无效内容。
- 对长对话做信息抽取,只保留关键实体、偏好、决策结论。
- 可以调用一次 LLM 对记忆内容做规范化重写。
3.2 记忆存储组件
存储层决定了记忆的容量和查询方式。目前有以下几种选择:
| 存储方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 关系型数据库 | 事务可靠,结构清晰 | 不适合语义检索 | 用户画像、结构化配置 |
| Redis | 读速快,支持过期 | 无语义检索能力 | 短期记忆、临时会话 |
| 向量数据库 | 支持语义相似度检索 | 需要 embedding 与索引维护 | 长期记忆核心存储 |
| 本地 JSON/文件 | 最简单,无额外依赖 | 查询能力弱 | 小规模实验 |
在长期记忆实验中,向量数据库是检索记忆的主流存储方式,常见的有 Chroma、Milvus、Qdrant、Weaviate、pgvector 等。选型时要考虑数据量、部署难度、是否支持持久化、是否有成熟客户端。
3.3 检索模块
检索模块把当前问题映射到历史记忆中。最基础的检索方式是向量相似度检索,也就是把用户问题转为向量,在向量库中查询最相似的若干条记忆。
单纯向量检索的常见问题是“语义相近但实际无用”的干扰结果。一个更稳的做法是“向量召回 + 重排序”:
- 先用 embedding 相似度召回候选记忆,数量可以多一些,比如 20 条。
- 再用一个重排模型(reranker)或基于规则的过滤逻辑,从候选中筛选出最相关的 3-5 条。
- 最后按时间权重调整排序,兼顾“语义相关”和“信息时效”。
3.4 写入策略
写入策略决定了记忆系统如何新增、更新、遗忘。最简单的是“无条件写入”,每条对话都存进去,但这会让记忆库迅速膨胀,检索噪声越来越大。
推荐策略:
- 阈值写入:对话轮次达到 N 轮后,批量提取这次对话中的关键信息写入记忆库。
- 去重更新:写入前先检索是否有相似旧记忆,如果有则更新原记录,而不是新增一条。
- 遗忘机制:为记忆增加时间戳和重要度评分,定期清理超过有效期的数据,或者将低重要度数据归档。
4. 环境准备与实验设计
下面进入实操环节。我们来实现一个最简但完整的长期记忆层,采用“向量检索 + 记忆库”模式,不依赖重型组件,方便你理解全链路。
4.1 实验环境
- 操作系统:Windows 10 / macOS / Linux 均可
- Python:3.10 或以上
- 安装包:openai、chromadb、pandas
pip install openai chromadb pandas如果你的环境没有配置 openai 库,请先安装。本文示例使用 OpenAI 兼容的 embedding 与 chat 接口,如果你的模型是本地部署,也支持 OpenAI 兼容协议,思路是一样的。
需要说明的是,各家依赖版本迭代较快,本文代码以常见版本为参考,重点演示设计思路。如果某个 API 在你的版本中不存在,请以官方文档为准调整。
4.2 项目结构
llm-memory-lab/ ├── memory/ │ ├── __init__.py │ ├── vector_memory.py │ ├── summarizer.py │ └── agent.py ├── data/ │ └── mem.db # Chroma 持久化目录 ├── demo.py └── requirements.txt4.3 设计要点
我们要实现的记忆系统有三个模块:
vector_memory.py:基于 Chroma 的记忆存取与检索。summarizer.py:使用 LLM 将对话压缩成结构化记忆。agent.py:把记忆检索结果与用户问题拼成 prompt,交给 LLM 生成回复。
实际运行流程:
用户问题 → 向量化 → 在记忆库中检索 top-k 条历史记忆 → 记忆 + 最近对话 + 用户问题 → LLM5. 核心代码实现
5.1 向量记忆模块
创建memory/vector_memory.py,负责初始化向量数据库,并提供增、查两个核心方法。
# 文件路径:memory/vector_memory.py import chromadb from chromadb.config import Settings class VectorMemory: """ 基于 Chroma 的长期记忆存储与检索。 """ def __init__(self, persist_directory: str = "./data/mem_db"): self.client = chromadb.PersistentClient( path=persist_directory, settings=Settings(anonymized_telemetry=False), ) # collection 可以理解为一张记忆表 self.collection = self.client.get_or_create_collection( name="llm_long_term_memory", metadata={"hnsw:space": "cosine"}, ) def add_memory( self, memory_id: str, text: str, embedding: list, metadata: dict = None, ): """ 写入一条记忆。 """ self.collection.add( ids=[memory_id], documents=[text], embeddings=[embedding], metadatas=[metadata] if metadata else None, ) def search_memory( self, query_embedding: list, top_k: int = 5, where: dict = None, ): """ 根据 query 向量检索最相似的记忆。 """ result = self.collection.query( query_embeddings=[query_embedding], n_results=top_k, where=where, ) return result这里有几个关键点:
PersistentClient会把数据持久化到本地目录,重启后记忆还在。hnsw: space选择 cosine 余弦距离,更适合语义相似度检索。documents保存记忆原文,embeddings保存向量,metadatas可以附加时间戳、来源、重要度等字段。
5.2 Embedding 与对话压缩模块
创建memory/summarizer.py,这个模块负责两件事:生成文本向量、把对话压缩成结构化记忆。请注意:不同模型的向量维度可能不同,使用前先确认你用的 embedding 模型维度与向量库索引维度一致。
# 文件路径:memory/summarizer.py from openai import OpenAI client = OpenAI() # 读取环境变量中的 API key def get_embedding(text: str, model: str = "text-embedding-3-small") -> list: """ 将文本转换为向量。 """ resp = client.embeddings.create(model=model, input=text) return resp.data[0].embedding def summarize_dialogue(dialogue: str, model: str = "gpt-4o-mini") -> str: """ 将一段原始对话压缩为结构化记忆文本。 """ prompt = f""" 你是一个记忆抽取助手。请从下面的对话中提取值得长期记住的信息, 包括:用户偏好、项目决策、重要事实、待办事项。 不要输出无关内容,不要编造,用简洁的中文条目输出。 对话: {dialogue} """ resp = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": "你是一个专业的记忆抽取助手。"}, {"role": "user", "content": prompt}, ], temperature=0.2, ) return resp.choices[0].message.content def build_memory_text(meta_type: str, content: str) -> str: """ 将抽取结果与记忆类型组合成规范化文本,便于检索。 """ return f"[{meta_type}] {content}"build_memory_text的作用是给记忆加一个标签前缀,比如[偏好]、[项目决策]、[待办]。这个前缀在向量检索时能提供额外的语义锚点,提高命中率。
5.3 Agent 主逻辑
创建memory/agent.py,这是整个记忆层与 LLM 交互的入口。
# 文件路径:memory/agent.py from .vector_memory import VectorMemory from .summarizer import get_embedding SYSTEM_PROMPT = "你是一个有长期记忆能力的 AI 助手。" class MemoryAgent: """ 带长期记忆的 AI Agent。 """ def __init__(self, top_k: int = 5): self.memory = VectorMemory() self.top_k = top_k def run(self, user_input: str, dialogue_id: str = "default"): """ 接收用户输入,检索相关记忆,生成回复。 """ # 1. 将用户输入转成向量 query_vec = get_embedding(user_input) # 2. 检索历史记忆 result = self.memory.search_memory( query_embedding=query_vec, top_k=self.top_k, where={"dialogue_id": dialogue_id}, ) # 3. 组织记忆上下文 memory_context = "" if result["documents"]: docs = result["documents"][0] memory_context = "\n".join(docs) # 4. 拼装 prompt prompt = f""" 以下是与你对话用户的长期记忆,请结合记忆回答问题。 如果记忆与问题无关,请忽略记忆,直接根据你的知识回答。 长期记忆: {memory_context} 用户问题:{user_input} """ from .summarizer import client resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}, ], temperature=0.7, ) return resp.choices[0].message.content def save_dialogue(self, dialogue_text: str, dialogue_id: str = "default"): """ 将一段对话抽取成记忆并写入向量库。 """ from .summarizer import summarize_dialogue, build_memory_text summary = summarize_dialogue(dialogue_text) memory_text = build_memory_text("对话记忆", summary) vec = get_embedding(memory_text) memory_id = f"{dialogue_id}_{int(time.time())}" self.memory.add_memory( memory_id=memory_id, text=memory_text, embedding=vec, metadata={"dialogue_id": dialogue_id, "timestamp": time.time()}, )注意:上面用到了time模块,记得在文件头部import time。
这个 Agent 的核心思想是:每次回答前先“回忆”,再把回忆结果作为上下文的一部分交给 LLM。这就是外部记忆层的雏形。
5.4 完整演示脚本
创建根目录下的demo.py,模拟一次完整的“写入记忆 → 检索记忆 → 生成回复”流程。
# 文件路径:demo.py from memory.agent import MemoryAgent agent = MemoryAgent() # 第一步:模拟一次对话,并保存记忆 dialogue = """ 用户:我们项目准备用 Python 开发后端 API。 助手:好的,建议使用 FastAPI 框架,性能好,生态也成熟。 用户:可以,数据库就用 PostgreSQL。 助手:明白,PostgreSQL 加 FastAPI 是很常见的组合。 """ print("==> 正在保存对话记忆...") agent.save_dialogue(dialogue, dialogue_id="project_a") print("保存完成。") # 第二步:新会话,用户提问,检索相关记忆 question = "我们项目后端用什么框架?" print(f"用户问题:{question}") answer = agent.run(question, dialogue_id="project_a") print(f"AI 回答:{answer}")运行:
python demo.py预期效果是:即使这是一次全新的会话,AI 也能根据历史记忆回答出“项目使用 FastAPI 和 PostgreSQL”。这就是长期记忆最基本的价值。
6. 进阶架构思路与工具选型
6.1 引入记忆总结与分层
上面示例只做了最简单的“整段对话压缩后存入记忆”。实际项目中,对话可能很长,直接压缩整段对话会丢失细节。更好的做法是:
- 将对话按轮次切分,每 3-5 轮抽取一次。
- 抽取结果先写入“候选记忆区”。
- 当候选记忆区积累到一定规模后,再做一次合并压缩,写进“长期记忆区”。
- 检索时优先从长期记忆区检索,其次从候选区补充。
这种分层方式把信息按生命周期切分,避免把重要细节和临时内容混在一起。
6.2 记忆更新与遗忘策略
长期记忆最大的风险不是“没有记忆”,而是“记住了错误信息后一直生效”。比如用户先说“数据库用 MySQL”,后来改口“还是用 PostgreSQL 吧”,如果记忆系统没有覆盖机制,AI 会一直按旧信息回答。
一个有效的做法是更新写入。
# 伪代码:更新写入逻辑 def upsert_memory(query_text: str, new_memory: str): # 先检索相似旧记忆 old_memories = memory.search_memory(get_embedding(query_text), top_k=3) for doc in old_memories: if similarity(doc, new_memory) > 0.85: # 删除旧记忆,用新记忆覆盖 memory.collection.delete(ids=[doc["id"]]) # 再写新记忆 memory.add_memory(...)同时,还需要引入时间衰减:检索结果不仅看相似度,还要乘以时间因子,让两周前的记忆权重自然降低。
6.3 向量数据库选型参考
不同规模项目的向量数据库选型策略:
| 项目规模 | 推荐方案 | 理由 |
|---|---|---|
| 个人实验 / 小项目 | Chroma | 安装简单,支持本地持久化 |
| 中型项目 | Qdrant / Milvus | 检索性能好,支持过滤,部署可控 |
| 已用 PostgreSQL 的团队 | pgvector | 避免额外引入组件,复用现有数据库 |
| 大规模生产 | Milvus / Weaviate | 分布式能力强,运维生态成熟 |
选型时不要只看宣传的性能数字,还要关注团队的运维能力。嵌入在业务代码里的向量库往往比单独维护一套分布式系统更省心。
7. 常见问题与排查思路
在实现长期记忆的过程中,我遇到和看到过很多问题,下面整理成一张排查表。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 检索结果与问题完全无关 | embedding 模型选型不当或领域差异大 | 换更强的 embedding 模型;增加记忆标签,如[偏好][决策] |
| 查询返回空结果 | collection 为空,或 where 过滤条件写错 | 检查记忆是否写入;先去掉 where 条件测试 |
| 记忆写入但重启后丢失 | Chroma 持久化路径未设置 | 使用 PersistentClient 并指定 path |
| 检索到的记忆太旧 | 缺乏时间衰减或过时清理机制 | 在 metadata 中记录时间戳,检索后按时间重排 |
| 向量维度不一致导致报错 | embedding 模型换了,向量维度变了 | 统一 embedding 模型,或者重建 collection |
| 对话太长导致压缩丢失关键信息 | 一次性压缩整段对话 | 按轮次切分,分批压缩;对关键实体单独抽取 |
| 用户修改了旧信息但 AI 仍按旧信息回答 | 没有更新覆盖机制 | 写入前先查找相似记忆,用新信息覆盖旧记忆 |
| Token 费用过高 | 每次检索都塞入过多记忆条数 | 降低 top_k;增加过滤条件;对记忆做重要度评分 |
排查这类问题有一个通用思路:把“写入”和“检索”两步分开验证。
先在记忆库里直接查询某条内容,确认已经写入;再用同一个 embedding 模型对测试问题做相似度对比,确认检索链路没问题;最后才去检查 prompt 组装是否正确。
8. 最佳实践与工程建议
8.1 记忆内容要“结构化 + 标签化”
不要把原始对话直接扔进向量库,至少要抽取、格式化、加标签。通过[偏好]、[决策]、[待办]、[实体关系]这样的前缀,能让检索命中率提升一个台阶。这种设计在检索时天然形成“类别过滤”的能力,可以配合 metadatas 里的 type 字段做二次过滤。
8.2 严格控制写入“记忆”的门槛
记忆不是越多越好。建议设置写入门槛:
- 寒暄、问候、表情类内容不写入。
- 单轮无信息量的内容不写入。
- 明确包含用户偏好、项目决策、重要事实的内容才写入。
- 写入前先让记忆抽取模型判断“这条对话是否有长期价值”。
门槛越高,记忆库越干净,检索质量越高。
8.3 上下文组装顺序有讲究
在构建 prompt 时,记忆内容不宜放在用户问题之后。推荐顺序:
系统提示词 注入记忆内容(长期记忆) 最近对话(短期记忆) 用户当前问题因为模型对输入内容的注意力分布并不均匀,放在中间的内容容易被忽略,放在开头和结尾的信息被利用得更充分。记忆内容最好放在 system 或对话开头的位置。
8.4 单独管理记忆的版本与回滚
长期记忆在生产环境中相当于“用户状态数据”,和代码一样需要版本管理。可以给记忆增加类似memory_version的字段,当算法或模型升级时,可以按版本切换。涉及记忆批量修改的场景,务必先在测试环境验证,再对生产环境操作,并保留数据备份。
8.5 安全与隐私边界
长期记忆一旦落地,就会涉及用户数据的保密、删除、导出等合规问题。至少要考虑:
- 敏感信息(密码、身份证号、密钥等)不得写入记忆库。
- 提供“清除指定用户记忆”的运维接口。
- 记忆检索和写入需要做权限隔离,不同用户不能互相看到对方的记忆数据。
- 如果需要删除用户数据,应同时清除向量库、关系库和备份中的数据。
9. 总结与下一步
这篇文章从 LLM 长期记忆的核心痛点出发,梳理了长期记忆与短期记忆的区别、四种主流架构模式,并实现了一个基于向量检索的完整记忆层示例。
你现在应该掌握了:
- LLM 长期记忆要解决的是“跨会话信息留存与准确恢复”问题
- 向量检索记忆是最主流的长期记忆实现方案
- 一个可运行的内存层至少包含:存储、写入、检索、注入四部分
- 记忆不是越多越好,必须有抽取、过滤、更新、遗忘机制
下一步,可以沿着这几个方向继续深入:
- 给记忆层接入真实对话流,接入 LangChain 或自研 Agent 框架
- 尝试 Milvus 或 pgvector 等生产级向量数据库
- 尝试引入 reranker 模型,提升检索精度
- 研究 MemGPT、Generative Agents 等论文中的分层记忆思想
长期记忆是 LLM 走向 Agent 化、个性化的重要基础设施,也是这个阶段最值得花时间研究的工程方向之一。建议你先跑通本文的示例,再根据你的业务场景设计属于自己的记忆模型。代码写多了,踩坑踩多了,自然就有了自己的经验体系。