
不知道你有没有过这种体验跟 AI 助手聊了很久它表现得特别懂你连你上周提过的项目偏好都记得清清楚楚。可一旦你关掉浏览器、刷新页面或者换一个新的对话窗口一切回到原点——它用客客气气的语气重新问你是谁、需要什么帮助。如果你正在做 AI 应用或者用 AI 搭过聊天机器人这个“失忆”场景几乎每天都在发生。“ai-memory”这个词最近在开发者社区里讨论度很高它解决的就是这个问题让 AI 拥有跨对话的长期记忆。它不是某个特定产品而是一类技术方案的总称——核心是让 AI 在多次会话之间记住用户偏好、关键事实、历史结论并在恰当的时机把记忆调出来参与决策。这篇文章我不打算只讲概念而是把我自己从零搭建一套 AI 记忆层的完整过程、原理拆解和踩坑记录分享出来。不管你是想给 ChatGPT 套一层记忆还是在自己的应用里接一个带记忆的助手这篇都能给你一条可以照做的路线。1. 大模型的“失忆”真相上下文窗口并不等于记忆先搞清楚一个基本问题为什么大模型看起来记性那么差很多人以为模型本身有记忆只是暂时没想起来。实际上不是这样。模型每一次推理都是“无状态”的——你发一句话它根据这句话和窗口里的历史文本算出下一句话仅此而已。1.1 上下文窗口的物理限制模型能“看到”的内容受限于上下文窗口也就是一次能塞进多少 token。比如某个模型窗口是 128K token换算成中文大约十几万字。看起来很大但真聊起天来很快就会被耗尽。我来算一笔账假设每轮用户输入约 300 字约 200 token每轮模型回复约 500 字约 350 token单轮对话合计约 700 token 左右加上系统提示词占用的 token128K token 的窗口大约能容纳 180 轮左右的来回对话。这已经算是很宽松的估算实际使用中如果你贴了长文档、让模型分析过代码几千轮的小型对话也可能直接把窗口撑爆。窗口一满要么报错要么系统自行截断早期内容——早期信息就静默消失了。1.2 遗忘的真正原因自注意力机制的成本更深层的原因是 Transformer 架构的自注意力机制。每一层都要计算输入序列中所有 token 两两之间的关系计算复杂度是 O(n²)。序列越长时间和内存消耗增长得越夸张。这不是简单地“多买点显卡”就能解决的问题。硬件约束决定了模型在处理超长序列时要么牺牲速度要么牺牲质量要么两者都牺牲。我记得自己第一次尝试让模型读一篇三万字的技术文档时等待时间从几秒直接飙升到几十秒期间显存占用居高不下。后来查阅资料才知道长序列下 KV Cache 的存储也是个巨大开销。所以大模型厂商普遍对窗口长度做了限制这个限制不是能力问题是工程和成本问题。1.3 为什么“把所有历史都塞进上下文”是最笨的方案早期做聊天机器人的时候最简单的思路是把用户之前说过的所有话拼接起来每次对话都发给模型。这个方案在 demo 阶段看起来很顺畅一旦进入真实使用就会崩——对话越久单次请求的 token 越来越多费用越来越高响应速度越来越慢。更要命的是模型会被大量无关信息干扰。你三小时前聊过吃饭偏好现在正在问代码报错模型如果只凭堆砌的上下文判断很可能跑偏。上下文窗口更像是模型的一块“临时工作台”而不是“长期仓库”。把工作台当仓库用迟早东西多到放不下。真正合理的做法是在对话过程中把重要信息提炼出来存到工作台之外的地方下次需要时再把相关的那部分放回工作台。这就是 AI 记忆层的核心思想。2. 记忆到底有哪些形态别一说记忆就想到向量库很多人看到“AI 记忆”第一反应就是向量数据库。这个方向没错但只对了一部分。如果一开始就把所有鸡蛋放进向量库这个篮子里后面会走很多弯路。我先按记忆的时效性、结构化程度把常见的方案梳理了一遍方便你按需选型。2.1 长期记忆 vs 短期记忆到底在说什么记忆系统通常分为两层短期记忆Session Memory只在一次会话内部有效目的是维持当前对话的连贯。比如你正在讨论一个需求说着说着忘了前面提到的细节AI 需要记得。这个最直接的实现方式就是上下文窗口本身或者再做一层简单的历史摘要。长期记忆Long-term Memory跨会话持久化今天聊的结论下周还能用。比如用户的工作领域、项目偏好、之前给过的反馈意见。这层必须依赖外部存储也就是数据库。实际落地时这两个层次经常混在一起。比如用户在一次会话里说“我偏好简洁的回答风格”这句话既应该是短期记忆本会话后续立即生效也应该是长期记忆以后每次对话都生效。如果只做短期用户下次来还是得重新强调如果只做长期会话中途调整偏好又反应不过来。所以好的记忆层是分层设计、互相配合的。2.2 显式记忆与隐式记忆显式记忆Explicit Memory用户明确告诉过你的比如“我叫小A做跨境电商”。这种以结构化数据存最合适查询准确、不依赖语义相似度。一块用户资料表一个键值存储就能解决。隐式记忆Implicit Memory需要从对话中推断出来的比如“用户每次问成本问题时都很在意预算”没有明说但数据里有信号。这种需要做信息抽取存入带语义索引的存储查询时靠相似度检索。我见过不少人过度强调隐式记忆的 AI 含量动辄上大模型做提取、做 embedding却忽略了最简单的用户资料表。实际上一个能稳定记录“用户说了什么”的系统远比一个能把“用户没说的话”猜个大概的系统可靠。优先做显式再逐步叠加隐式是我实践下来最稳的路线。2.3 结构化记忆、摘要记忆与向量记忆的三角对比到具体存储层面主流的方案有三种各有适用场景。我整理了一个表格方便对照方案存储形式查询方式优点缺点适用场景结构化记忆关系型数据库、键值存储SQL、精确匹配高准确率、低延迟、易维护无法覆盖开放式信息用户资料、偏好设置、统计计数摘要记忆纯文本摘要单段或多段每次都注入或按规则取用简单、节省 token、覆盖信息广粒度粗、无法精确检索历史对话压缩、会话交接向量记忆Embedding 向量 向量数据库语义相似度检索支持开放域语义匹配有误召回、依赖 Embedding 质量知识片段、项目上下文、跨会话事实一张图记不住没关系记住这个判断逻辑就行先问自己“用户给的信息能不能用字段表达”能就走结构化。信息量大但不需要精确找就走摘要。信息是碎片化的、需要按语义匹配的才轮到向量库。一上来直接上向量库除了让检索变复杂不会带来额外收益。2.4 记忆读取方式也是分层级的选择题存储只是第一步更关键的是怎么把记忆“放回”对话里。我实测过三种读取策略全量注入把记忆全部塞进系统提示词。简单粗暴适合记忆量极小的场景。一旦记忆超过几千 token效果和成本都会失控。规则筛选注入按预设规则挑一部分记忆注入。比如用户 ID、会话类型、关键词匹配。实现简单、稳定适合业务规则清晰的场景。语义召回注入把当前对话内容 embedding 化到向量库里找最相关的记忆片段拼进上下文。灵活性强适合开放式对话但有召回不准的风险需要阈值控制。多数项目不会只用一种。比如用户画像走结构化按用户 ID 直接查出来固定注入项目背景信息走向量召回根据当前问题动态匹配历史对话压缩成摘要只在新会话开场时注入一次。层各司其职组合使用。我把这套分层的思路叫作“三角记忆”结构化保底、摘要保覆盖、向量保灵活。三角互相补位缺一个都会在实际使用中露出短板。3. 动手搭一个最小可用的 AI 记忆层Python 实操理论说完了直接上代码。我用一个尽可能简单的方案来演示基于 OpenAI API Chroma 向量库 JSON 文件存储构建一个带记忆的对话循环。你不需要有深厚的技术背景按步骤来就能跑通。3.1 架构设计为什么先选 Chroma 而不是 Milvus很多教程一上来就教你部署一套分布式向量数据库我强烈不建议。项目从零开始阶段你需要的不是高并发而是快速验证记忆逻辑是否有效。Chroma 是嵌入式向量数据库一条命令就能装好数据存在本地磁盘没有独立服务需要维护。对单个应用来说性能完全够用。等你的用户量真正大到需要横向扩展再迁移到 Milvus 或 Qdrant 也不迟——迁移成本远低于一开始就陷入基础设施的泥潭。我选的技术栈是OpenAI Embeddings API文本转向量Chroma向量存储和相似度检索JSON 文件存结构化用户资料OpenAI Chat Completions对话生成整条链路跑通的核心逻辑用户输入 - 查询向量记忆语义召回 - 读取结构化记忆规则注入 - 组装提示词 - 调用 LLM - 获得回复 - 对话结束后提炼记忆并存储3.2 初始化记忆库别在库结构上费太多心思先安装依赖和初始化 Chroma 客户端。这一步极度简单pip install chromadb openai然后写一个记忆管理类import chromadb from chromadb.utils import embedding_functions class MemoryStore: def __init__(self, collection_nameai_memory): # OpenAI 官方 embedding 函数封装 self.embedding_fn embedding_functions.OpenAIEmbeddingFunction( api_keyyour-api-key, model_nametext-embedding-3-small ) self.client chromadb.PersistentClient(path./memory_db) self.collection self.client.get_or_create_collection( namecollection_name, embedding_functionself.embedding_fn ) def add_memory(self, user_id, text, metadataNone, memory_idNone): self.collection.add( documents[text], ids[memory_id or fmem_{user_id}_{int(time.time())}], metadatas[{user_id: user_id, **(metadata or {})}] ) def search_memory(self, user_id, query, top_k3): results self.collection.query( query_texts[query], n_resultstop_k, where{user_id: user_id} ) return results[documents][0] if results[documents] else []这里有几个细节值得解释persistent_client会把向量持久化到本地memory_db目录服务重启后记忆不会丢。where{user_id: user_id}是关键的过滤条件。它保证检索只在该用户的记忆范围内进行避免用户 A 的记忆污染用户 B 的对话。embedding 模型选择text-embedding-3-small对小规模应用性价比极高单次调用成本低到可以忽略。3.3 核心对话流程记忆的读写时机现在实现带记忆的对话函数。思路是收到用户消息后先从记忆库检索再把记忆注入系统消息最后调用对话模型。import time class MemoryAssistant: def __init__(self, memory_store, openai_client): self.memory_store memory_store self.client openai_client # 结构化记忆的简单存储JSON 文件 self.profiles_file user_profiles.json def get_user_profile(self, user_id): 读取用户结构化资料 import json, os if not os.path.exists(self.profiles_file): return {} with open(self.profiles_file, r) as f: profiles json.load(f) return profiles.get(user_id, {}) def save_user_profile(self, user_id, profile): 保存用户结构化资料 import json, os profiles {} if os.path.exists(self.profiles_file): with open(self.profiles_file, r) as f: profiles json.load(f) profiles[user_id] profile with open(self.profiles_file, w) as f: json.dump(profiles, f, ensure_asciiFalse, indent2) def chat(self, user_id, user_message): # 1. 向量记忆检索找出与当前问题相关的历史片段 relevant_memories self.memory_store.search_memory(user_id, user_message) # 2. 结构化记忆读取拿到用户偏好等固定信息 profile self.get_user_profile(user_id) # 3. 组装系统消息 system_parts [] if profile: profile_str .join(f{k}:{v} for k, v in profile.items()) system_parts.append(f用户资料{profile_str}) if relevant_memories: memory_str \n.join([f- {m} for m in relevant_memories]) system_parts.append(f你可能记得的历史信息\n{memory_str}) system_message { role: system, content: \n\n.join(system_parts) if system_parts else 你是一个乐于助人的AI助手。 } # 4. 调用对话模型 messages [ system_message, {role: user, content: user_message} ] response self.client.chat.completions.create( modelgpt-4o-mini, messagesmessages, temperature0.7 ) reply response.choices[0].message.content return reply def remember_after_conversation(self, user_id, conversation_text): 把一段对话提炼成记忆存下来。 # 这里简单起见直接存原文实际应该用 LLM 摘录关键点下一章详述 self.memory_store.add_memory(user_id, conversation_text)主流程运行起来长这样# 初始化 store MemoryStore() import openai client openai.OpenAI() assistant MemoryAssistant(store, client) # 第一次对话 reply1 assistant.chat(user_001, 你好我最近在做跨境电商想找一个物流方案推荐) print(reply1) # 模拟对话结束后记录记忆 assistant.remember_after_conversation(user_001, 用户在做跨境电商正在寻找物流方案) # 第二次对话模拟新会话 reply2 assistant.chat(user_001, 上次提到的物流方案你能再详细讲讲吗) print(reply2)第二次对话时由于向量检索召回了“用户在做跨境电商”这一条记忆模型自然知道“上次提到的”是指什么。即便中间隔了很久、甚至服务重启过这个上下文仍然能延续。3.4 跑通之后你会发现的第一个问题整段记忆太粗暴上面这段代码只是演示最小闭环。remember_after_conversation直接把整段对话原文存进去会导致两个严重问题冗余信息过多。聊了十轮存进去十轮的完整文本下次检索时噪音很大。没有提炼关键信息。用户真正值得记住的是“偏好、结论、承诺、目标”而不是流水账得做一层提取。这就要进入下一章的内容记忆写入策略。没有这一步你的记忆库很快会变成一个垃圾场——存得越多检索效果越差。4. 记忆写入与压缩什么该记、什么该忘、如何控制 token我很长时间里以为“记东西”比“找东西”简单。直到搭完第一版 demo把几千条杂乱文本扔进向量库后检索质量肉眼可见地下降我才意识到写入环节决定的不是存储量而是整个系统的上限。好的记忆系统在写入那一刻就已经完成了 80% 的筛选工作。4.1 记忆条目模板别记原文记要点最好的做法是让 LLM 在每轮对话结束后用固定模板提取值得记住的信息。我设计了一个 Prompt 模板要求模型输出 JSON 形式的结构化记忆片段。MEMORY_EXTRACTION_PROMPT 请从以下对话中提取值得长期记住的信息输出 JSON 数组。 每条记录必须包含三个字段 - type: user_preference用户偏好/ project_fact项目事实/ contact_info联系方式/ commitment用户承诺/ other其他 - content: 简短的一句话描述第三人称 - importance: 1-5 的整数5 为最重要 要求 1. 只输出真正有价值的信息不输出寒暄、临时情绪、与对话目标无关的内容。 2. content 里用“用户”指代对话的人类参与方。 3. 如果没有值得记录的信息输出空数组 []。 对话内容 {conversation} 调用时只需要把这段 Prompt 传给模型解析返回的 JSON 再写入存储def extract_and_save_memories(user_id, conversation_text): response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是信息提取引擎只输出 JSON 数组不输出多余文字。}, {role: user, content: MEMORY_EXTRACTION_PROMPT.format(conversationconversation_text)} ], temperature0 ) raw response.choices[0].message.content import json try: memories json.loads(raw) except: # 模型偶尔会输出 Markdown 包裹的 JSON需要清理 raw_clean raw.strip().strip(json).strip().strip() memories json.loads(raw_clean) for mem in memories: if mem[importance] 3: store.add_memory( user_id, mem[content], metadata{type: mem[type], importance: mem[importance]} ) return len(memories)这个设计的核心在于“重要性阈值”。低于 3 的条目直接丢弃比如“用户今天心情不错”这种瞬时状态的记录存了只会制造噪音。真正值得长期存在的是那种过了一个月还能影响对话决策的信息。4.2 摘要记忆把长对话压缩成可控 token有些场景下用户聊的内容无法拆成一条条零散记忆需要维持一个“整体印象”。比如一个项目讨论了需求背景、技术选型、分工计划单点细节可以条目化但整体脉络需要一份摘要。我的做法是维护一个“会话摘要”每次对话结束后增量更新。def update_session_summary(user_id, old_summary, new_conversation): 把旧摘要和新对话合并生成新摘要。 response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你负责维护对话摘要。请结合旧摘要和新的对话内容输出一份更新后的摘要。保持简洁保留关键决策和事实控制在 200 字以内。}, {role: user, content: f旧摘要\n{old_summary}\n\n新对话\n{new_conversation}} ], temperature0 ) return response.choices[0].message.content摘要存在哪呢最省事的方式还是 JSON 文件或普通 KV 存储不建议塞进向量库。因为摘要的使用逻辑是“每次都带上”而不是“按需检索”。以用户 ID 为主键存一份最新摘要文件新会话开始时就自动注入系统提示词。这样既不挤占上下文窗口也不会因为检索不到而漏掉重要背景。4.3 控制 Token 的三个上限单条、单次、总量记忆系统的 token 管理是个硬功夫。我给自己设了三条硬性约束单条记忆不超过 100 token还记不住说明拆得不够细。单条过长会导致两条记忆语义纠缠检索召回时连带噪声也放大。每次召回的片段总量不超过 800 token检索到 top_k3 条引入的 token 总量必须控制住。超过这个量模型注意力会被稀释反而忽视系统提示词里的指令。系统提示词 记忆 本轮对话总量不超过窗口的 70%留 30% 给模型输出。如果一次请求占满窗口回复很容易在生成中途截断质量也肉眼可见下滑。如果你记性好可能已经发现上面这套约束的代价——系统提示词里的记忆一多模型的行为稳定性会受影响。这就要靠优先级排序用户显式设置的偏好比如“别用专业术语解释”永远排在最前面向量召回的内容排其次最后才是历史摘要。4.4 记忆的更新与删除不只是在“增加”大多数记忆系统做出来只是不断往里加数据但现实中记忆是会过时的。用户上个月说喜欢短视频这个月可能就改口说不想看短视频了。设计记忆时我建议加一个简单的“置信度”机制每条记忆记录附带last_confirmed_at时间戳同一语义的新信息出现时覆盖旧记录而不是新增一条超过 N 天未确认的高重要性记忆定期提醒用户确认或自动降权和你维护真实的人际关系一样记忆系统也需要“保鲜”。只增不改的系统用久了就像堆积了无数过期消息的微信群——号还在价值已经毁了。5. 实测踩坑记录向量检索失效、记忆污染与时间衰减代码跑通第 40 天问题开始集中爆发。我记录了三个最典型的坑每个都是真实使用中会碰到的给大家提前避雷。5.1 检索阈值设太低召回了一堆“沾边”内容Chroma 默认的类似度阈值其实相当宽松我一开始没设任何阈值结果用户问今天天气系统召回的是“用户三月份提过想买雨伞”。从向量距离看确实相关但从对话意图看完全是噪音。后来我给similarity_threshold加了硬限制低于阀值的记忆强制丢弃def search_memory_with_threshold(self, user_id, query, top_k5, similarity_threshold0.75): results self.collection.query( query_texts[query], n_resultstop_k, where{user_id: user_id}, include[documents, distances] ) docs, dists results[documents][0], results[distances][0] filtered [] for doc, dist in zip(docs, dists): # chroma 的 distance 是 L2 距离越小越相似但对不同 embedding 模型 # 这个距离的绝对含义完全不同只能靠实测调参。 if dist (1 - similarity_threshold): filtered.append(doc) return filtered注意一个坑不同 embedding 模型输出的向量空间度量标准完全不同。text-embedding-3-small和text-embedding-ada-002的余弦距离分布差异很大。我建议你在自己的数据上加一层测试集跑几组典型查询画出距离分布再定阈值。别照抄网上的参数。5.2 多用户共享记忆库没加过滤导致的越权第二版时我把系统接上了多用户。因为图省事检索时没带where{user_id: user_id}过滤结果用户 A 问“我的订单在哪儿”系统召回了用户 B 的订单信息。虽然模型最后没有直接泄露内容但在回复中出现了模糊指代。多用户隔离是底线存储侧没有隔离应用侧再小心也没用。另外建议把用户 ID 同时写进记忆的元数据查漏时可以直接按元数据字段做修正。5.3 时间衰减缺失用户变了记忆没变这是最隐蔽的坑。一个用户两个月前明确说着“预算优先不考虑质量”两个月后项目升级邮件里提到“现在优先质量预算放宽”。这两条记忆在语义上是冲突的但向量库不会自动解决冲突检索时可能两条同时召回模型就被夹在中间左右为难。最终我引入了一个简单的规则同一 type 的记忆新写入时自动置顶用时间戳做判定发生冲突时以较新的为准。同时定期用 LLM 跑一个“记忆一致性检查”把同内容域的记忆拉出来让模型判定哪些已经过时。自动巡检很烧 token所以频率不用高每周一次足够。5.4 Embedding 模型的“语义漂移”换模型等于失忆中间有一次为了省成本我把 embedding 模型从text-embedding-3-small换成了text-embedding-3-large。后果是旧数据全部“失忆”——新旧向量分布的语义空间不一致导致召回质量暴跌。向量库里的历史数据必须和查询时使用同一套 embedder这是铁律。真要换模型得重跑全量数据的 embedding迁移前先做数据备份。5.5 记忆冲突的用户视角什么时候选择“不注入”最后一个容易忽略的点不是所有历史记忆都应该被注入。用户可能只是想临时聊一个全新的话题不希望 AI 一直拿旧信息来套话。所以我现在会在系统提示词里加一句软约束历史信息仅供参考。如果用户当前话题与历史记忆明显不相关不要生硬套用。这个小小的限定能显著降低“AI 老是提起我上周说的事情”这种尴尬体验。记忆的价值在于辅助理解而不是限制当下的对话。6. 最后再分享一点扩展思路从单机记忆到个人记忆中心到这里基础的记忆层已经能稳定运行了。但如果你认真做过一遍你会意识到“ai-memory”这条路还很长。我现在正在尝试把记忆从“应用绑定”变成“个人绑定”——也就是说用户的记忆不属于某个特定 AI 应用而是属于用户自己。通过标准 API 读写不同应用共享同一套长期记忆。这样用户迁移到新助手时不会再一次次重复自我介绍。这个方向目前的难点在于数据接口标准、隐私边界、以及记忆的编辑权限管理。如果让我给一个快速上手的建议就是从文章里的最小闭环开始先跑通单用户的记忆读写再造多用户隔离再加时间衰减和冲突处理。这套流程走过一遍你对 AI 记忆的理解会远超那些只是看过概念文章的人。反正踩坑这件事早踩早省心。