很多人刚接触AI产品时都会有种感觉:它很聪明,但总记不住你。上轮聊过的偏好、约好的时间、改过的代码,切个会话就全部归零。这个问题本质上就是AI记忆缺失。今天我把自己在做agent记忆、历史对话记录、本地记忆迁移这些方向上的实操经验整理一遍,希望能帮到正在做AI应用开发、agent工程实践和AI产品设计的朋友。这篇不会只堆概念,我会把短期记忆和长期记忆怎么设计、记忆怎么召回、怎么迁移,以及上线后怎么排查问题都讲清楚。文章会偏向工程落地,但不会假设你已经很懂AI,我会把关键前置知识一并补上。
1. 内容整体设计与思路拆解
1.1 AI为什么需要记忆:从“无状态”聊起
先理解一个底层事实:绝大多数大语言模型本身是一个无状态函数。你调用一次接口,输入一段文本,它返回一段文本。模型不会主动记住上一次调用发生了什么,更不会在两次独立调用之间偷偷积累关于你的画像。这个问题在单轮问答里不明显,但到了聊天助手、AI编程、智能客服这类连续场景,就会立刻暴露:用户反复交代自己的偏好、项目背景、代码约束,AI却每次都在“失忆”。
所以我们在工程上要做的事情,就是给这棵“聪明的树”加上根。所谓AI记忆,本质上是在模型外部维护一套状态系统,再通过提示词、检索、结构化数据等方式,把记忆在合适的时候重新送回到模型面前。模型本身不变,但系统有了记忆,用户体感上就会觉得AI“越来越懂我”。
我经常跟产品经理说一句话:记忆不是模型的能力,是系统的能力。一个没有记忆系统的AI,再强的模型也只能做“一次性对话”;只有把记忆系统做好,AI才能真正像一个人一样,记住你的名字、你的习惯、你上周说过的话。
1.2 记忆的类型:不是只有“记住一句话”这么简单
很多人以为AI记忆就是“把聊天记录存下来”。真做起来你会发现,存储只是最基础的一层,关键在分类和使用方式。在我目前的工程实践里,会把AI记忆分成四类:
| 记忆类型 | 代表内容 | 典型实现方案 | 生命周期 |
|---|---|---|---|
| 短期记忆 | 当前会话的对话历史、上下文状态 | 上下文窗口、滑动窗口、会话摘要 | 会话结束或短期 |
| 长期事实记忆 | 用户偏好、姓名、公司、项目背景 | 用户档案、结构化数据库、KV存储 | 数天到数年 |
| 情境记忆 | 某次任务的过程、前因后果 | 对话日志、摘要、事件序列 | 按需保留 |
| 程序性记忆 | 用户期望的工作流程、习惯性指令 | 技能模板、Prompt规则、参数配置 | 长期复用 |
短期记忆对应人类的“工作记忆”,临时记住手头事情;长期事实记忆对应“我们对一个人的了解”;情境记忆更像是“想起上次那件事的经过”;程序性记忆则是“我知道你通常怎么做”。这四类不是互斥的,同一个信息可能同时落在多个类型里。
在实际落地时,我的顺序建议是:先把长期事实记忆做出来,因为它对产品体验提升最明显;再做短期记忆的压缩和摘要;情境记忆和程序性记忆属于进阶,等前两者稳定了再上。
1.3 短期与长期如何协同:一套“双网络记忆模型”
你可能看到过“双网络记忆模型”这个说法,我第一次听也觉得玄,拆开看其实很朴素:它指的是把记忆系统拆成“写入网络”和“读取网络”两条相对独立的路径。
写入网络负责感知信息、判断重要性、抽取要点、写入长期存储。它相当于记忆的编码器,决定什么值得记住、用什么形式记录。读取网络负责在每次推理前,根据当前对话内容去召回相关记忆、重新排序、组装上下文。它相当于记忆的提取器,决定哪些记忆需要在当下被唤醒。
为什么要这样拆?因为写入和读取的优化目标完全不同。写入侧更看重准确性和去重,读取侧更看重相关性和低延迟。如果两者混在一个模块里,你改一次存储结构就影响召回,改一次召回策略又会影响写入,后期基本没法维护。
另外,短期记忆和长期记忆的协同也遵循这个逻辑。短期记忆是在线的、高速的,直接拼进上下文;长期记忆是离线的、大规模的,通过检索才进入上下文。两者不是互相替代,而是互补。短期记忆保证连续对话不跑偏,长期记忆保证跨会话的连续性。你在产品里感受到的“AI越来越懂我”,绝大多数来自长期记忆被准确召回到短期上下文的那一刻。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 记忆的表示与存储:文本、JSON还是向量
想清楚记忆要记什么,接下来就是怎么表示。最常见的三种表示是原始文本、结构化JSON和向量Embedding。
原始文本最省事,直接把聊天记录、摘要原样存下来。优点是可读性强、便于人工审查,缺点是召回时不好精确匹配。结构化JSON适合存储用户档案、固定字段,比如姓名、公司、套餐等级、偏好标签。优点是查询灵活、更新方便,缺点是难以覆盖开放式的上下文信息。向量Embedding则把一段文本编码成高维向量,支持语义相似度检索,特别适合“用户问了一个问题,我们需要找到对话库里语义最接近的几条历史记忆”这种场景。
一次比较完整的落地,三种会一起用。用户画像用JSON放关系型数据库,关键对话摘要用文本放对象存储或数据库,同时为摘要和画像描述生成Embedding向量放进向量库。
存储选型我没有太复杂的偏好。数据量小,直接用SQLite或PostgreSQL;需要上百万元素的相似检索,才上专门的向量数据库,比如Milvus、pgvector、Qdrant、Chroma。不要一上来就上重组件,先跑通用型数据库,量到了再拆。
这里要特别提醒中文Embedding的效果问题。很多默认Embedding模型对中文的长文本理解并不理想,导致召回相关度低。我踩过这个坑后,项目里会优先选对中文支持好的模型,比如BGE系列、text-embedding-3-small等,并在上线前用自己场景的50~100条真实问题做一轮召回评测。评测指标简单直接:看Top5召回里用户真正关心的旧记忆占几条。不达标就换模型或调分块方式。
2.2 短期记忆和长期记忆的分离策略
很多人会把所有历史一股脑塞进Prompt里,结果就是Token爆炸。一个正常的聊天助手,如果对话超过20轮,原始记录可能就要一万多Token,再塞点系统提示、工具定义、检索出来的长期记忆,分分钟超过模型上下文窗口。
我的做法是把上下文预算分成四个区域:系统提示区、短期记忆区、长期记忆区、当前输入区。建模时给每个区域规划一个Token上限,比如系统提示2K、短期记忆8K、长期记忆3K、当前输入4K,总和控制在模型窗口的60%以内,留出余量给模型生成。
短期记忆不能是无限拼接,得做压缩。常用两种策略:一是滑动窗口,保留最近若干轮完整对话,更早的部分转成摘要;二是摘要合并,每经过一定轮次,就让模型把前面的对话压缩成一段摘要,新的对话接着摘要走。实际项目中,滑动窗口保证即时理解,摘要合并保证长期连贯,两者结合效果最好。
长期记忆的召回也必须有预算上限。不要把所有匹配到的历史记录都塞进去,我的经验是召回3到5条高质量记忆,每条控制在200~500字。宁可少而准,不要多而杂。
2.3 记忆召回:检索、重排与上下文组装
记忆召回的完整链路是:改写当前输入、生成查询向量、执行相似度检索、召回候选记忆、重排截断、组装成上下文。
改写这一步容易被忽略。用户当前这句话往往是“那个方案后来怎么样了”,直接拿这句话去向量检索,匹配效果很差。更好的是用模型把当前问题改写成“用户在询问之前讨论过的某套技术方案的具体细节和后续进展”,再拿改写结果去检索。这就相当于搜索引擎里的Query扩展,召回质量会有一次明显提升。
重排层在候选数量多时特别重要。向量检索只是第一道粗筛,召回20条候选后,需要按相关度和时效性重新排序。我会给每条记忆算一个权重分,相关度占60%,时间新鲜度占30%,来源可信度占10%。如果某条记忆和当前问题语义很接近,但已经过气半年,分数会被压下来。
组装上下文也要讲究格式。不要直接把一大段记忆丢进去,模型分不清哪是记忆哪是现场内容。我惯用的做法是在记忆外显处加 <memory> 标签,并且在系统提示里写清楚:“以下内容是该用户在历史对话中留下的记忆,可能包含旧信息,如果与当前对话冲突,请以当前对话为准。” 这样模型就能把记忆当作参考资料,而不会产生认知混乱。
2.4 记忆更新与遗忘:不能只写不删
记忆系统做得越久,越会发现“遗忘”和“记住”同等重要。用户昨天说“我喜欢简洁风格”,今天改口说“现在要更详细的报告”,如果系统还拿旧偏好硬套,体验会非常糟糕。
因此写入时要带时间戳、版本号和置信度。每次新信息写入,如果和旧信息冲突,默认以最新为准,但不直接删除旧记录,而是标记为“已被覆盖”。这样万一新信息只是一次性误发,系统还可以回退。
什么时候触发写入?我的标准很简单:用户显式表达偏好、重复出现两次以上的事实、任务完成的结论性信息。至于闲聊中的琐碎内容,不写。过度写入会制造大量无效记忆,反而污染后续召回。
遗忘机制也要纳入设计。可以定期扫描记忆库,把超过一定时间未使用、并且置信度低的记忆降权或归档。这不是简单的删除,而是让长期记忆库保持“新陈代谢”。我在生产环境里通常设置一个离线任务,每周对记忆做一次老化处理,将六个月未命中的低置信度记忆移到冷存储。
3. 实操过程与核心环节实现
3.1 搭建一个最小可用的AI记忆系统
我建议第一次做记忆系统的朋友,不要一上来就搞分布式向量库,而是先用一个单体服务跑通闭环。下面是我常用的最小架构,由四个部分组成:用户档案表、对话记录表、摘要和向量索引、召回服务。
用户档案表存长期事实记忆,字段不多,主要包括用户ID、姓名、公司、偏好标签、补充描述、更新时间。对话记录表存每一轮问和答,附带时间戳、会话ID、是否已经归档。摘要表存对某段历史对话的压缩描述。向量索引存摘要和关键描述的Embedding,用于语义检索。
这四个部分可以全部落在同一台服务器的SQLite或PostgreSQL里,向量部分用pgvector插件就能解决。整条链路清晰,后期要加大规模也方便,用统一的读写接口把底层存储换掉就行。
3.2 核心代码示例:记忆的保存与召回
下面是一个简化过的Python实现,展示记忆模块的核心接口。生产环境里我会在这里接真正的向量数据库和LLM调用,但核心逻辑是一样的。
python复制import sqlite3
import json
import time
import hashlib
class MemoryStore:
def __init__(self, db_path="memory.db"):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._init_tables()
def _init_tables(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS user_profile (
user_id TEXT PRIMARY KEY,
profile_json TEXT,
updated_at INTEGER
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS memories (
memory_id TEXT PRIMARY KEY,
user_id TEXT,
content TEXT,
scope TEXT,
importance REAL,
version INTEGER,
updated_at INTEGER
)
""")
self.conn.commit()
def save_profile(self, user_id, profile: dict):
data = {
"user_id": user_id,
"profile_json": json.dumps(profile, ensure_ascii=False),
"updated_at": int(time.time())
}
self.conn.execute(
"INSERT INTO user_profile (user_id, profile_json, updated_at) VALUES (:user_id, :profile_json, :updated_at) "
"ON CONFLICT(user_id) DO UPDATE SET profile_json=:profile_json, updated_at=:updated_at",
data
)
self.conn.commit()
def save_memory(self, user_id: str, content: str, scope: str = "default", importance: float = 0.5):
memory_id = hashlib.md5(f"{user_id}:{content}:{time.time()}".encode()).hexdigest()
self.conn.execute(
"INSERT INTO memories (memory_id, user_id, content, scope, importance, version, updated_at) "
"VALUES (?, ?, ?, ?, ?, 1, ?)",
(memory_id, user_id, content, scope, importance, int(time.time()))
)
self.conn.commit()
return memory_id
def recall_memories(self, user_id: str, query: str, top_k: int = 3):
# 简化版召回:在真实系统中这里会用向量检索 + 重排
# 这里先用关键词匹配兜底,保证机制可跑通
rows = self.conn.execute(
"SELECT content, importance, updated_at FROM memories WHERE user_id=? ORDER BY updated_at DESC LIMIT 100",
(user_id,)
).fetchall()
scored = []
query_words = set(query.lower().split())
for content, importance, updated_at in rows:
content_words = set(content.lower().split())
overlap = len(query_words & content_words)
if overlap > 0:
score = overlap * 0.6 + min(0.4, 1.0 / max(1, (time.time() - updated_at) / 86400.0))
scored.append((score, content))
scored.sort(reverse=True)
return [content for _, content in scored[:top_k]]
上面的代码里,save_profile负责更新长期用户档案,save_memory负责写入一条历史记忆,recall_memories是召回入口。真实项目里,recall_memories里应该换成Embedding模型和向量数据库,但接口设计可以保持不变。这样做的好处是,你后续无论换多少底层技术,上层调用方不需要大改。
3.3 历史对话记录与本地记忆迁移的完整流程
“本地记忆迁移”是很多工具型AI产品都需要的功能。用户在A设备积累了大量历史对话和偏好,换到B设备后如果能无缝迁移,体验会好很多。
这里有一个关键取舍:迁移什么。我的建议是优先迁移压缩后的摘要和用户画像,而不是动辄几十万Token的原始明文。既保护隐私,又省空间,召回效果也不差。
具体流程可以这样设计:
- 导出:在旧设备上把该用户的用户画像、对话摘要、长期记忆打包成一个JSON文件。
- 脱敏:对涉及密钥、手机号、身份证等信息做正则替换或删除。这一步不能省,否则就是在搬运风险。
- 重建摘要:如果原系统没有摘要,用LLM把历史对话分批压缩成事件摘要,格式例如“3月12日讨论了登录模块的重构方案,最终选择JWT + Redis方案”。
- 导入:在新环境的记忆服务中,将摘要和画像写入结构化表,同时生成向量索引。
- 校验:用几条典型问题测试召回,确认“新AI”能记住“旧事”。
我在WorkBuddy这类带本地工作记忆工具的做法里,也是同样的思路。历史对话被切成“可迁移的记忆单元”,用户在换设备或换工作区时,只需要迁移这个记忆包,就能把原来积累的上下文带过去。
3.4 在AI编程场景中召回“改到哪了”
AI编程是AI记忆落地最刚需的场景之一。你让AI助手帮你改过代码,过几天你回来问“上次那个登录模块改到哪了”,如果AI没有记忆,它大概率会一脸茫然。
我在做类似Opencode这类AI编程工具的工程实践时,会专门为代码修改事件建立一套结构化记忆流。每个文件修改都记录为一个事件,包含文件路径、改动类型、Diff片段、修改原因、涉及的需求或Issue编号。这些事件写入记忆库后,下次用户提出相关需求时,召回系统会优先返回该文件最近的修改记录。
关键点在于:不能只存改完后的最终代码,还要存“为什么改”。最终代码可以从Git里拿,但修改动机、讨论过程、备选方案,这些只存在于对话里。所以我会把代码变更和对话讨论交叉索引,形成“需求描述、方案选择、最终修改”这样一条完整记忆链。这样下次AI不仅能告诉你改了哪里,还能复述当时为什么这么改。
4. 常见问题与排查技巧实录
4.1 记忆召回不准确:先从查询改写和分块查起
最常遇到的问题就是“明明存在记忆,但需要时召不回来”。我排查这类问题时,会按下面顺序走:
- 检查查询改写质量。用户口语化输入没被改写成适合检索的语义向量,召回漏掉很正常。先优化这一步。
- 检查分块大小。一段摘要如果超过一定长度,Embedding的整体语义会被稀释。尝试把长记忆拆成更小的单元,或者增大召回条数。
- 检查相似度阈值。阈值设得太高,会把边缘相关记忆全部过滤掉。我在项目里会把阈值调低,让候选量变大,再靠重排去筛。
- 检查模型向量对中文的支持。这点前面说过,如果你的Embedding模型在中文上效果很差,召回不准几乎是必然的。
4.2 上下文爆掉:Token预算失控怎么办
上下文爆掉通常是短期记忆区域失控。最常见的原因是直接把所有历史对话原样拼接,没有压缩。解法就是我前面提到的滑动窗口加摘要合并。
另一个容易忽略的原因是工具返回内容过大。AI编程场景里,一次文件读取可能直接塞进几千甚至上万Token,几次工具调用后窗口就满了。此时要对工具返回做截断或摘要,不能全量进上下文。
如果预算还是很紧张,可以考虑做“分层上下文”:先把最关键的长期记忆和最新几轮对话放进上下文,把更早的历史压缩成摘要;如果摘要都没法完全放进窗口,再退一步,只放可能与当前任务最相关的部分。
4.3 记忆串线和过时信息:给每条记忆加上“有效期”
记忆串线是指A用户的问题带出了B用户的记忆,这个问题一旦出现,属于严重事故。我在代码里强制要求所有召回都带用户ID过滤,同时在写入时也会检查数据源,只允许来自该用户自己的对话、档案、操作事件进入记忆库。测试阶段要专门写一条跨用户串线的用例,防患于未然。
过时信息则是另一类“慢性病”。用户已经改了偏好,AI还拿老黄历说事。解决办法是版本化加时间戳,重排时给“新信息”更高的权重。如果某条记忆和一个高频业务字段(比如用户常用语言)冲突,以最新写入为准,旧版本只保留痕迹,不再参与召回。
4.4 数据安全与隐私:本地优先是一条捷径
做记忆系统时,数据合规不是加分项,是底线。我的习惯是“能本地存就本地存,能少收就少收,能删就删”。用户画像只保留产品功能必需字段,历史对话默认只保留摘要而不是全文,向用户提供“清除我的记忆”按钮。这个按钮不仅满足合规要求,还会显著提升用户对产品的信任感。
当记忆需要上云或跨端同步时,我会先做脱敏,再迁移。对于敏感信息字段,可以在写入时做关键词屏蔽,或用哈希方式存储。这样即使数据库被拖走,攻击者也很难还原出完整的用户画像。
4.5 部署与性能:把记忆服务和LLM服务解耦
最后说下上线阶段的工程问题。很多同学会把记忆模块嵌在Agent主进程里,前期确实方便,但一旦并发上来,记忆写入和检索的耗时就会拖累主链路。
我建议把记忆模块独立成一个服务,提供三个接口:保存记忆、召回记忆、更新档案。LLM推理服务和主Agent通过内部RPC调用这个记忆服务。这样做的好处是,记忆服务的缓存策略、连接池、扩缩容可以和主服务独立管理。向量检索的延迟如果偏高,可以在记忆服务里加一层Redis缓存,把高频召回的Top结果缓存起来,命中时直接返回。
模型部署方面,如果你用的是开源模型做Embedding,可以单独部署成一个GPU推理服务;如果是调用商业化Embedding接口,则要做好批量写入的限流和重试。LLM主模型的部署不用和记忆服务耦合,记忆服务本质上只是一个带语义检索能力的普通后端服务。
我自己在实际项目中体会最深的一点是:AI记忆不是“能存能查”就结束,它更像是产品的一等公民,从设计阶段就要考虑清楚记什么、怎么读、怎么更新、怎么遗忘。先做一个小而正确的记忆闭环,比一开始就铺一张巨大的“全量历史记录”要有效得多。你只需要把用户档案和最近的重点摘要管好,AI的“懂你感”就已经能上一个台阶。等跑顺了,再慢慢扩展情境记忆和程序性记忆,这条路走得通,也走得稳。
