1. 从“金鱼脑”到“活档案”:为什么AI需要长期记忆
如果你用过市面上主流的AI对话产品,无论是ChatGPT、Claude还是国内的各类大模型,一个共同的痛点很快就会浮现:它们记性太差了。一次对话里,你告诉它“我叫张三,是个后端工程师,喜欢用Go语言”,聊了十几轮后,你问“我刚才说我用什么语言来着?”,它很可能已经忘得一干二净,或者开始胡编乱造。这种“金鱼脑”式的体验,极大地限制了AI在复杂、长周期任务中的应用,比如作为个人学习助手、项目协作伙伴,甚至是模拟一个拥有固定人设和背景故事的虚拟角色。
“给AI Chat加上长期记忆”这个需求,正是在这种背景下被强烈催生出来的。它远不止是让AI记住你的名字那么简单。一个真正有效的长期记忆系统,应该能让AI记住对话的上下文、用户的核心偏好、历史决策的逻辑、乃至整个项目的演进脉络。这听起来像是给AI装上一个外置的“海马体”,让它从一次性的、孤立的对话,进化为一个持续学习、不断成长的智能体。
然而,实现这个目标绝非易事。简单粗暴地将所有历史对话记录都塞进下一次的上下文窗口(Context Window)是行不通的。一方面,主流大模型的上下文长度有限(从几K到几十万Token不等),成本高昂;另一方面,无关信息的噪音会严重干扰模型当前任务的判断,导致回答质量下降。因此,核心挑战在于:如何从海量的历史信息中,精准、高效地提取出对当前对话最有价值的“记忆片段”,并安全、可控地注入到当前的思考流程中?
我最近在为一个内部知识问答系统设计记忆模块时,深入实践了“模型提取 + 程序把关 + 规则召回”这套组合拳。这并非某个现成框架,而是一种工程化的解决思路。它的核心思想是分层过滤、多级校验,将智能(模型)的灵活性与确定(程序与规则)的可靠性结合起来,确保召回的记忆既相关又安全。接下来,我将拆解这套方案的每一个环节,分享其中的设计逻辑、实操细节以及我踩过的那些坑。
2. 记忆的“原材料”处理:从原始对话到结构化记忆单元
在讨论如何“回忆”之前,我们得先解决“记什么”和“怎么存”的问题。原始对话流是一连串非结构化的文本,直接存储和检索效率极低。第一步,也是整个记忆系统的地基,是将这些原始信息加工成易于管理的“记忆单元”。
2.1 定义记忆单元的结构
一个基础的记忆单元(Memory Chunk)至少应包含以下几个字段:
- 核心内容:这是记忆的精华。不能是整段对话的复制粘贴,而是经过提炼的陈述句。例如,从对话“我最近在做一个电商项目,用Spring Boot和MySQL,前端打算用Vue3”中,可以提取出:“用户正在开发一个电商项目,技术栈为后端Spring Boot + MySQL,前端Vue3。”
- 实体与关键词:从核心内容中提取的关键实体(如“电商项目”、“Spring Boot”、“MySQL”、“Vue3”)和主题词(如“技术选型”、“项目开发”)。这是后续向量化检索和规则匹配的基础。
- 元数据:
timestamp: 记忆产生的时间戳。session_id: 所属对话会话的ID。importance_score: 记忆的重要性分数(初始可基于规则或简单模型设定,后续可动态更新)。memory_type: 记忆类型,例如user_preference(用户偏好)、fact(事实陈述)、task_context(任务上下文)、decision(决策逻辑)等。分类有助于针对性召回。
- 嵌入向量:将核心内容通过文本嵌入模型(如OpenAI的
text-embedding-3-small,或开源的BGE-M3、voyage-2)转换为高维向量。这是实现语义相似度搜索、突破关键词字面匹配局限的关键。
在实际存储时,我选择了PostgreSQL(关系型) + pgvector(向量扩展)的组合。关系型数据库负责存储所有结构化的元数据和关键词,pgvector则专门用于存储和检索向量。这种混合方案兼顾了灵活性和性能。
-- 示例表结构 CREATE TABLE memory_chunks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), core_content TEXT NOT NULL, entities TEXT[], -- 数组类型,存储实体 keywords TEXT[], -- 数组类型,存储关键词 timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), session_id TEXT NOT NULL, importance_score FLOAT DEFAULT 1.0, memory_type TEXT, embedding vector(1536) -- 假设使用1536维的向量 ); CREATE INDEX ON memory_chunks USING ivfflat (embedding vector_cosine_ops); -- 创建向量索引2.2 利用轻量模型进行信息提取
如何从一句用户发言“我讨厌下雨天,它让我心情低落,但喜欢雨后的清新空气”中,自动生成结构化的记忆单元?这里就是“模型提取”的第一站。
我们不需要动用GPT-4这样的大型生成模型来做这件相对模式化的事情,成本太高。更经济的做法是使用经过微调的小模型(如7B/13B参数的模型),或者直接利用大模型的“函数调用”或“结构化输出”能力,但以批处理、异步的方式进行以降低成本。
核心任务是命名实体识别和文本摘要。对于上面的例子,一个设计良好的提示词可以引导模型输出:
{ "core_content": “用户表达了对下雨天的厌恶(因其导致心情低落),但喜欢雨后的空气。”, "entities": ["下雨天", "心情", "空气"], "keywords": ["厌恶", "喜欢", "清新"], "memory_type": "user_preference" }实操心得一:提取模型的提示词工程。直接让模型“提取信息”效果不稳定。更好的方式是给它一个明确的“角色”和“输出格式”。例如:“你是一个专业的对话信息整理助手。请将用户的输入转化为一条简洁的事实陈述,并识别其中的关键实体和情感倾向。严格按照以下JSON格式输出:...” 同时,在memory_type的定义上要尽量具体且有限,避免模型自由发挥导致后续分类混乱。
踩坑记录:异步处理与错误容忍。记忆提取不应该阻塞主对话流程。我们需要建立一个异步处理队列。用户说完,主流程立刻响应,同时将这条对话扔进队列,由后台工作进程慢慢处理。这里的关键是错误容忍:提取模型可能失败、可能输出格式错误,我们的存储逻辑必须能捕获这些异常,将原始对话文本降级存储,而不是让整个记忆流程崩溃。一条格式错误的记忆也比丢失记忆要好。
3. “程序把关”:在存储前筑起安全与质量的防火墙
经过模型提取的记忆单元,还不能直接存入数据库。它可能包含敏感信息、无意义的废话,或者与已有记忆严重冲突。这就是“程序把关”环节的职责:通过一系列确定性的规则和逻辑检查,确保入库记忆的质量和安全性。
3.1 敏感信息过滤与脱敏
这是安全红线。即使用户在对话中透露了手机号、身份证号、邮箱密码等,我们的记忆系统也绝对不应该存储明文。
- 正则表达式过滤:第一道防线。编写针对常见敏感信息模式(手机号、身份证、银行卡号)的正则表达式,对
core_content进行扫描。一旦匹配,立即触发脱敏逻辑,例如将“我的电话是13800138000”处理为“我的电话是[电话号已脱敏]”。 - 关键词黑名单:第二道防线。维护一个敏感词黑名单(根据业务需求定制),检查记忆内容是否涉及。这可以过滤掉一些明显的违规内容。
- 重要性降权或拒绝存储:对于触发了敏感过滤的记忆,我们的策略不是简单丢弃(以免影响用户体验,比如用户后来问“我上次说的手机号是多少”,AI完全没记忆也奇怪),而是将其
importance_score设为极低(如0.1),或者在存储时打上needs_review的标签。在后续召回时,这些低分或待审核记忆会被优先级很低的策略处理,甚至不参与普通召回。
注意:脱敏后的记忆依然可能通过语义被关联,因此对于极高敏感场景,更安全的做法是记录“用户曾提供过个人联系信息”这一事实,而非任何具体内容。
3.2 记忆去重与冲突解决
用户可能多次表达同一偏好,比如反复说“我不吃香菜”。记忆系统不应该重复存储多条相同记忆,而应该进行合并或强化。
- 基于嵌入向量的语义去重:当一条新记忆产生时,计算其向量与近期(例如24小时内)或同类型记忆的余弦相似度。如果相似度超过一个阈值(如0.92),则判定为高度重复。此时,可以采取“强化”策略:更新原有记忆的时间戳
timestamp,并适当提升其importance_score,而不是新增一条。 - 基于实体的冲突检测:用户可能说“我最喜欢的颜色是蓝色”,但几天后又说“我觉得红色更好看”。这构成了事实冲突。程序可以检测到两条记忆的
entities都包含“颜色”,且core_content情感倾向相反。对于冲突,简单的覆盖可能不对。我们的策略是:同时保留,但附加冲突标记和上下文。将新记忆存储,同时在其元数据中链接到旧记忆的ID,并注明“可能更新了偏好”。在召回时,如果同时召回两条冲突记忆,可以优先选择时间戳更新的,或者将冲突信息一并提供给AI,让AI在上下文中自行判断(例如询问用户进行确认)。
实操心得二:把关规则的优先级与日志。过滤规则要有明确的优先级。例如,安全过滤(敏感信息)的优先级最高,必须立即执行且不可绕过;质量过滤(如过短的无意义内容)优先级次之;去重和冲突检测可以放在最后。所有被规则拦截或修改的记忆,都必须记录详细的审计日志(rule_fired,original_content,action_taken),这对于后期调试和规则优化至关重要。
4. “规则召回”:构建多维度的记忆检索策略
当用户开启一段新对话,我们需要从记忆库中召回相关的记忆。单纯依赖向量相似度搜索(语义召回)是不够的,它可能遗漏一些关键但表述不同的记忆。因此,需要引入“规则召回”作为补充和引导。
4.1 设计多路召回通道
我们将召回设计成一个多路并行的“召回器”集合,每路负责一种策略:
- 语义向量召回器:这是主力。将用户的当前问题或对话开头几句,转化为向量,在pgvector中使用
<=>(余弦距离)操作进行近似最近邻搜索,返回最相似的N条记忆。 - 关键词匹配召回器:从当前对话中提取关键词,在数据库中对
keywords和entities数组字段进行交集查询。这能保证字面完全匹配的关键信息不被遗漏,比如用户问“我提到的那个电商项目”,即使语义向量搜索没匹配上,“电商项目”这个关键词也能直接命中。 - 会话链召回器:如果当前对话有明确的
session_id(例如连续对话),优先召回同一会话内最近产生的M条记忆。这符合人类对话的短期连续性。 - 时间衰减与重要性加权召回器:这不是一个独立的召回器,而是一个对所有召回结果的重排序策略。一个记忆的相关性不仅取决于内容匹配度,也取决于其“新鲜度”和“重要性”。我们可以设计一个综合评分公式:
最终分数 = 语义相似度分数 * 时间衰减因子 * importance_score其中,时间衰减因子可以是exp(-λ * 时间差),让越近的记忆权重越高。这样,即使用户很久前提过喜欢蓝色,但最近提过喜欢红色,在重排序后,关于红色的记忆排名会更靠前。
4.2 规则融合与结果裁剪
多路召回会产生大量结果,需要融合和去重。通常采用“加权求和”或“取并集后重排序”的方式。例如,给语义召回的结果基础分高,关键词召回的结果基础分中等,然后统一用时间衰减和重要性加权公式再算一遍总分。
接下来是结果裁剪。我们不能把所有相关记忆都塞进上下文,必须做取舍。这里有两个关键策略:
- 多样性筛选:避免返回多条高度相似的记忆。在最终列表里,如果两条记忆的向量相似度极高,只保留分数最高的那条。
- 相关性阈值与容量控制:设定一个最低相关性分数阈值,低于阈值的直接丢弃。同时,设定一个总Token数上限(例如1024个Token),按记忆的综合分数从高到低选取,直到总内容长度接近上限为止。这确保了召回的记忆既是相关的,又是紧凑的。
实操心得三:召回策略的A/B测试。哪种召回器组合、哪种重排序公式最好?没有标准答案,完全取决于你的业务场景和用户数据。必须建立A/B测试机制。例如,为10%的用户启用一套新的召回权重,然后通过评估指标(如:用户对AI回答的满意度、用户是否减少了重复陈述信息的频率)来判断哪套策略更优。记忆系统的效果,最终要服务于对话质量的提升。
5. 记忆的注入与模型的高效利用
召回的记忆片段,如何有效地“告诉”AI模型?直接拼接在用户问题前面,像“以下是相关历史信息:... 当前问题:...”是一种方式,但不够优化。
5.1 结构化提示词模板
更好的做法是使用结构化的系统提示词或上下文管理。在对话开始时,或在每次需要记忆时,将记忆作为系统指令的一部分注入。例如:
你是一个拥有长期记忆的助手。以下是与当前对话相关的背景信息,请你在回答时充分考虑: 1. [记忆1的核心内容] (来源:用户于[时间]提及) 2. [记忆2的核心内容] (来源:在讨论[主题]时提到) ... 当前对话: 用户:新的问题...给记忆加上来源和时间戳,能增强模型对记忆可信度的判断。更重要的是,可以指令模型如何利用这些记忆,例如:“如果背景信息与当前问题明确相关,请依据背景信息回答;如果背景信息不足或无关,请忽略它们,基于你的通用知识回答。”
5.2 处理记忆冲突与不确定性
当召回的记忆之间存在冲突,或者记忆与当前问题仅有微弱关联时,简单的拼接可能导致模型混淆。这里可以引入更高级的提示技巧。
- 对于冲突记忆:在提示词中明确指出冲突的存在。“关于您最喜欢的颜色,历史记录中有两种说法:一条说您喜欢蓝色(记录于X月X日),另一条说您觉得红色更好看(记录于Y月Y日)。请问您当前更倾向于哪一种,或者是否有新的偏好?” 这样,AI就能基于冲突信息,生成一个更稳妥、甚至主动询问的回复。
- 对于弱相关记忆:如果记忆的相关性分数不高,可以尝试让模型先对记忆进行“摘要”或“评估”。例如,先让模型(或另一个小模型)判断:“以下记忆片段中,哪几条与‘如何优化数据库查询’这个问题直接相关?” 只将筛选后的强相关记忆注入最终对话。这相当于增加了一层基于模型的过滤,精度更高,但延迟和成本也相应增加。
踩坑记录:上下文窗口的“记忆污染”。这是最容易被忽视的问题。当你把多条记忆注入上下文后,模型有时会过度关注这些记忆,甚至在用户问题完全无关时,也强行引用记忆内容,显得答非所问。为了解决这个问题,我们除了设置严格的相关性阈值,还可以在系统指令中强调:“背景信息仅供参考,请优先直接回答用户当前问题。” 并监控那些被注入记忆但AI并未引用的对话案例,用于调整召回策略的阈值。
6. 让记忆“活”起来:动态更新与遗忘机制
一个静态的记忆库很快就会过时。用户的偏好会改变,项目信息会更新,某些记忆会随着时间流逝而失效。因此,记忆系统必须具备动态更新和主动遗忘的能力。
6.1 记忆强度的动态衰减与强化
我们可以为每条记忆引入一个动态的strength或importance_score字段,而非固定值。
- 强化:每当一条记忆被成功召回并利用(例如,AI在回答中明确引用了该记忆,或者用户对包含该记忆的回答给出了正面反馈),就增加它的强度值。
- 衰减:随着时间的推移,所有记忆的强度都缓慢衰减(例如,每天乘以一个小于1的衰减因子)。
- 冲突导致弱化:如果一条记忆与用户新提供的、且被确认为正确的信息冲突,那么旧记忆的强度应被大幅降低。
这样,常用的、新鲜的、正确的记忆会保持在“活跃区”,而不常用的、陈旧的、可能错误的记忆会逐渐“沉入”底层,在召回排序中优先级变低。
6.2 实施主动遗忘策略
纯粹的衰减只是降低优先级,数据仍然占据存储空间。我们需要一个“垃圾回收”机制。
- 基于强度的定期清理:设置一个极低的强度阈值(例如0.1)。定期(如每周)扫描所有记忆,将强度低于该阈值的记忆标记为“可归档”或直接删除。删除前,可以将其压缩转移到冷存储,以备极端情况下的审计或恢复。
- 基于类型的策略:对于
task_context(任务上下文)这类记忆,其生命周期可能与任务绑定。任务结束后一段时间,可以自动清理所有相关记忆。而对于user_preference(用户偏好)记忆,生命周期则要长得多。 - 用户显式控制:提供让用户管理记忆的界面或指令,例如“忘记我之前关于XX的所有信息”、“查看你记住了我的哪些信息”,让用户拥有最终控制权,这不仅是功能,更是建立信任的关键。
实操心得四:监控与评估体系。记忆系统不是一个“设好就忘”的模块。必须建立监控指标:记忆总量增长趋势、各类型记忆占比、记忆召回率、记忆利用率(被AI引用的比例)、以及因记忆产生的错误回答率。特别是错误回答率,需要通过人工抽样或模型自评的方式,检查AI是否因为错误的记忆(过时的、冲突的)而给出了错误答案。这套评估体系是迭代优化整个记忆管道(提取、把关、召回、注入)的指南针。
在我负责的项目中,引入这套记忆系统后,用户对于“AI记不住事”的负面反馈下降了超过70%,在涉及多轮复杂需求澄清和技术方案讨论的场景下,对话效率提升了近一倍。当然,这套系统也增加了复杂度和运维成本,但它所带来的体验提升是质的飞跃。技术决策总是权衡,而当你面对的是AI能否真正“理解”并“融入”一段持续关系时,为记忆付出的代价,往往是值得的。