
1. 从“金鱼记忆”说起AI 对话为什么总是聊完就忘如果你高频使用各类 AI 对话工具大概率遇到过这种让人抓狂的场景花了半小时跟 AI 把项目背景、技术栈、命名规范、代码风格全部交代清楚聊到第十轮它突然开始用完全不同的风格写代码甚至把你之前明确否定的方案又端了上来。你不得不重新贴一遍上下文像哄一个失忆的同事重新熟悉工作。这种“聊完就忘”的体验圈内人戏称为 AI 的“金鱼记忆”。这个问题的根源并不神秘。大语言模型本身是无状态的每一次请求对它来说都是全新的开始。你在对话框里看到的历史记录实际上是客户端在每次请求时把之前的对话内容重新拼接进去一起发给模型。一旦对话轮次变多、内容变长就会撞上上下文窗口的上限早期的信息被截断或压缩模型自然就“忘了”。更麻烦的是不同工具、不同会话之间彼此隔离你在 A 窗口调教好的偏好到了 B 窗口完全归零。“Claude-Mem”这个项目标题指向的正是围绕这一痛点的记忆增强方案。它要解决的核心问题可以拆成三层第一层是跨会话记忆让 AI 记住你是谁、你在做什么项目、你的偏好是什么第二层是记忆的组织与检索不是把所有历史一股脑塞回去而是按需召回最相关的片段第三层是记忆的写入与更新什么该记、什么该忘、旧信息如何被新信息覆盖。这三层合起来就是一套完整的“AI 记忆系统”。这篇文章适合三类人看一是天天跟 AI 结对编程、被上下文丢失折磨的开发者和写作者二是想给自己搭一套本地记忆库、对数据隐私有要求的技术爱好者三是单纯好奇“AI 记忆”到底怎么实现、想动手复现一个最小可用版本的学习者。我会从设计思路讲到实操细节把踩过的坑和验证过的参数一并交出来尽量让你看完就能动手。2. 记忆系统的整体设计与思路拆解2.1 为什么不做“无限上下文”而要做“记忆检索”很多人第一反应是既然问题是上下文不够长那把窗口开到无限大不就行了这个思路在工程上走不通原因有三。第一是成本上下文越长每次请求的 token 消耗越大而且是线性甚至超线性增长聊得越久越贵。第二是效果业界大量实测表明当上下文超过一定长度后模型对中间部分信息的注意力会明显衰减也就是所谓的“lost in the middle”塞得越多反而越容易忽略关键内容。第三是延迟长上下文意味着更长的推理时间交互体验直线下降。所以更务实的做法是把历史信息存到外部每次只召回跟当前问题最相关的一小部分拼进上下文。这就像人脑的工作方式——你不会在每次对话时回忆一生中所有事情而是根据当前话题调取相关记忆。Claude-Mem 这类方案的本质就是给 AI 外挂一个“记忆库 检索器”用检索的精准度换取上下文的长度。2.2 三层记忆架构短期、长期、工作记忆参考认知科学的分类一套好用的 AI 记忆系统通常分三层。短期记忆就是当前会话的原始对话直接放在上下文里保证连贯性。长期记忆是持久化存储的事实、偏好、项目信息存在数据库或文件里跨会话保留。工作记忆是每次请求时从长期记忆里检索出来、临时拼进上下文的那部分内容用完即弃。这个分层的好处是职责清晰。短期记忆负责“这一轮聊得顺不顺”长期记忆负责“跨天跨周还记得你”工作记忆负责“这次该带哪些记忆上场”。三者解耦之后任何一层出问题都好排查对话不连贯查短期跨会话失忆查长期答非所问查工作记忆的检索质量。2.3 存储选型为什么向量库不是唯一答案一提到记忆检索很多人立刻想到向量数据库。向量检索确实擅长语义相似度匹配比如你问“怎么优化查询速度”它能召回“数据库索引调优”这类语义相近但字面不同的记忆。但它不是万能的。对于精确匹配需求比如“我的项目名叫什么”“上次定的端口号是多少”关键词或结构化查询反而更准更快。我在实际搭建时采用的是混合方案结构化信息项目名、技术栈、偏好配置用键值对或轻量数据库存走精确查询非结构化的经验、讨论、结论用向量库存走语义检索。两路结果合并后再做一次重排序。这样既避免了纯向量方案在精确问题上的“幻觉召回”也弥补了纯关键词方案在语义理解上的不足。选型上没有绝对优劣关键看你的记忆内容以哪类为主。3. 核心细节解析与实操要点3.1 记忆的写入什么该记什么该忘记忆系统最容易翻车的地方不是检索而是写入。如果什么都记记忆库很快会被噪音淹没检索质量断崖式下跌如果记得太少又起不到作用。我的经验是设定明确的写入触发条件而不是每轮对话都写。值得写入的内容包括用户明确表达的长期偏好“我习惯用 TypeScript 严格模式”、项目的关键决策“数据库选型定为 PostgreSQL”、反复出现的实体项目名、模块名、常用路径、以及用户主动要求记住的内容。不值得写入的包括一次性的临时问题、闲聊、已经被后续对话推翻的结论。这里有个关键技巧写入时做一次“记忆提炼”不要把原始对话整段存进去而是让模型先总结成一句或几句结构化的事实。比如原始对话是五轮关于数据库选型的讨论最终存进去的应该是“项目 X 数据库选型PostgreSQL理由需要 JSONB 支持和事务完整性”。这样存储体积小、检索命中率高、拼进上下文时也不浪费 token。注意写入环节一定要做去重和冲突检测。同一个事实被反复写入会稀释检索权重新旧信息冲突时要有覆盖策略通常以时间较新的为准但涉及关键配置的变更建议保留历史版本以便回溯。3.2 记忆的检索召回策略决定成败检索环节的核心问题是给定当前这轮对话该召回哪些记忆最朴素的做法是把最近 N 条记忆全带上但这既浪费上下文又引入噪音。更好的做法是“相关性优先 时效性加权”。具体来说我会把当前用户输入作为查询分别走向量检索和关键词检索各取 Top-K我常用 K5合并去重后按一个综合分数排序。综合分数的计算大致是语义相似度占 0.6时间新鲜度占 0.2访问频次占 0.2。时间新鲜度用指数衰减越久远的记忆权重越低访问频次高的记忆说明经常被用到适当加权。这个权重不是拍脑袋定的是我在自己的使用场景里反复调出来的你可以根据自己的记忆规模调整。还有一个容易被忽略的点检索结果要做长度预算控制。召回的记忆拼进上下文时不能无限膨胀。我会给工作记忆设一个 token 上限比如 2000 token超出就按分数截断。否则聊到后期光记忆就占满了窗口反而挤掉了当前对话本身。3.3 记忆的更新与遗忘让系统保持“新鲜”记忆不是只增不减的。一个健康的记忆系统必须有遗忘机制。我的做法是给每条记忆打上“最后访问时间”和“访问次数”两个标签定期做一次清理超过一定时间比如 90 天且从未被召回过的记忆降权或归档被新信息明确覆盖的旧记忆标记为失效而非直接删除保留可追溯性。更新策略上我倾向于“追加 标记”而不是“原地修改”。因为原地修改一旦改错原始信息就丢了追加一条新记忆并标记旧记忆失效既保留了历史又保证了检索时优先返回新信息。这个思路跟很多版本控制系统的设计是一致的用空间换安全和可回溯。提示遗忘机制一定要可配置。有些场景比如长期项目跟踪需要记忆长期保留有些场景比如临时咨询希望用完即清。把保留策略做成参数而不是写死在代码里。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先说明下面这套方案是我在本地环境验证过的用 Python 实现依赖尽量精简。核心依赖就几个一个向量库用于语义检索一个轻量数据库用于结构化存储一个 embedding 模型用于把文本转向量。向量库我选的是本地文件型的方案不需要额外起服务适合个人使用如果你要多人共享可以换成客户端-服务端架构的。pip install numpy pip install sentence-transformers pip install sqlite-utilsembedding 模型我选的是轻量级的多语言模型本地推理不依赖外部接口隐私和成本都可控。第一次运行会自动下载模型权重大概几百 MB之后就走本地缓存。如果你机器性能有限可以选更小的模型代价是语义匹配精度略降。4.2 记忆库的初始化与表结构设计结构化记忆用 SQLite 存建一张 memories 表字段设计如下CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, category TEXT, embedding BLOB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_accessed TIMESTAMP, access_count INTEGER DEFAULT 0, status TEXT DEFAULT active );几个字段的用意category用来区分记忆类型偏好、事实、决策等检索时可以按类型过滤embedding存向量用 BLOB 序列化status支持 active、archived、invalid 三种状态配合遗忘机制使用。last_accessed和access_count是时效性加权的依据每次召回后更新。向量部分我单独存一个文件用 numpy 的 .npy 格式跟 SQLite 里的 id 一一对应。这样检索时先算向量相似度拿到 id 列表再回 SQLite 取内容和元数据分工明确。4.3 写入流程的代码实现写入的核心是“提炼 去重 落库”。先让模型把当前对话总结成结构化事实再检查是否与已有记忆重复。def write_memory(raw_dialogue, categoryfact): # 第一步提炼成简洁事实 summary llm_summarize(raw_dialogue) if not summary: return None # 第二步生成向量 vec embed(summary) # 第三步去重检测相似度超过阈值视为重复 similar search_similar(vec, top_k1) if similar and similar[0][score] 0.92: update_access(similar[0][id]) return similar[0][id] # 第四步落库 mem_id db_insert(summary, category, vec) return mem_id去重阈值 0.92 是我实测下来比较稳的值。太低会误判不同记忆为重复太高则漏掉真正的重复。这个值跟你的 embedding 模型有关换模型后建议重新校准。4.4 检索与上下文拼装的完整链路检索环节把前面讲的策略串起来def retrieve_memories(query, top_k5, token_budget2000): q_vec embed(query) # 向量检索 vec_hits search_similar(q_vec, top_ktop_k * 2) # 关键词检索 kw_hits keyword_search(query, top_ktop_k * 2) # 合并去重 merged merge_and_dedupe(vec_hits, kw_hits) # 综合打分 scored [] for hit in merged: score (0.6 * hit[similarity] 0.2 * time_decay(hit[last_accessed]) 0.2 * freq_weight(hit[access_count])) scored.append((score, hit)) scored.sort(reverseTrue) # 按 token 预算截断 selected [] used 0 for score, hit in scored: cost estimate_tokens(hit[content]) if used cost token_budget: break selected.append(hit) used cost update_access(hit[id]) return selectedtime_decay我用的是半衰期 30 天的指数衰减freq_weight用对数函数压缩避免高频记忆权重过大。这些参数都可以调关键是先跑起来再根据实际召回效果微调。4.5 把记忆接进对话流程最后一步是把检索到的记忆拼进发给模型的上下文。我的做法是在系统提示里加一段“已知背景”把召回的记忆按类别组织好def build_prompt(user_input, memories): memory_block \n.join( f- [{m[category]}] {m[content]} for m in memories ) system f你是一个有记忆的助手。以下是关于用户的已知背景 {memory_block} 请在回答时参考这些背景但不要生硬复述。 return system, user_input这里有个细节提示里要明确告诉模型“参考但不要复述”否则模型容易把记忆内容原样吐出来显得很机械。另外记忆块要放在系统提示靠前的位置实测比放在用户输入后面效果更稳。5. 常见问题与排查技巧实录5.1 记忆召回不准的排查思路召回不准是最常见的问题表现是“明明记过但答非所问”。排查按这个顺序走先看记忆有没有写进去查数据库里有没有对应记录再看向量有没有生成成功有时候 embedding 失败会静默跳过然后看检索分数如果目标记忆的相似度分数很低说明 embedding 模型对你的领域词汇不敏感考虑换模型或补充关键词检索最后看是不是被 token 预算截断了高分记忆没进最终列表。我踩过的一个坑是早期没做去重同一个事实被写了七八遍导致检索时全是重复内容真正有用的其他记忆被挤出去了。加上去重逻辑后召回质量立刻上了一个台阶。5.2 记忆冲突与过期信息的处理当新旧信息冲突时如果两条都处于 active 状态检索会同时召回模型可能被旧信息误导。我的处理方式是写入新记忆时主动检索是否有语义冲突的旧记忆如果有把旧的标记为 invalid。判断冲突不能只看相似度因为“端口是 8080”和“端口是 9090”语义很像但内容冲突需要额外做一次模型判断或规则比对。对于时间敏感的信息比如“当前使用的框架版本”我会在记忆内容里带上时间戳检索时优先返回最新的。更彻底的做法是给这类记忆设过期时间到期自动归档。5.3 性能与成本优化记忆系统跑起来后性能瓶颈通常在两处embedding 计算和向量检索。embedding 可以加缓存相同文本不重复计算向量检索在记忆量小于一万条时暴力计算完全够用不必上专门的向量索引。真正费钱的是写入时的“提炼”和冲突判断这两步都要调模型如果每轮对话都触发成本会上去。我的优化是只在检测到“值得记忆”的信号时才触发写入比如用户说“记住”“以后都这样”“这个项目用”而不是每轮都跑。5.4 常见问题速查表问题现象可能原因排查方向解决建议跨会话完全失忆记忆未持久化或未加载检查数据库文件是否落盘确认写入流程被触发召回内容不相关embedding 模型不匹配看相似度分数分布换模型或加关键词检索重复记忆泛滥缺少去重逻辑统计相同内容条数加相似度去重阈值回答机械复述记忆提示词未约束检查系统提示加“参考但不要复述”后期响应变慢上下文膨胀看拼装后 token 数设工作记忆 token 上限旧信息误导回答冲突未处理查是否有冲突记忆写入时标记旧记忆失效提示这张表建议打印出来贴在工位上出问题时按行排查比盲目改代码高效得多。6. 我实际用下来的一些体会这套记忆系统我在自己的日常工作和几个小项目里跑了小半年最大的感受是记忆系统的价值不在于“记得多”而在于“记得准”。早期我贪多什么都往里塞结果检索质量惨不忍睹后来狠心砍掉大量低价值记忆反而效果更好。记忆库跟人脑一样需要定期整理垃圾进垃圾出。另一个体会是写入时机的判断比检索算法更影响体验。什么时候该记、什么时候不该记这个判断做对了后面的事都顺做错了再好的检索也救不回来。我现在倾向于让用户显式触发记忆或者用明确的信号词触发而不是让模型自作主张这样可控性高很多。最后分享一个小技巧给记忆加一个“置信度”字段模型提炼出来的事实标中等置信度用户明确确认过的标高置信度检索时高置信度优先。这个改动很小但对召回准确率的提升立竿见影。后续如果要做多用户隔离只需要在表里加一个 user_id 字段检索时带上过滤条件即可架构上不用大动。