
开头做了一段时间大模型应用开发我发现一个特别有意思的现象同样是调用同一个模型有的产品用下来像冷冰冰的问答机有的却像相处很久的老朋友。差别往往不在模型本身而在产品有没有给这个模型配上“记忆”。最近我们团队在做一款名叫“小度想想”的智能体产品核心目标就是让智能体记住每个用户的偏好、习惯和对话上下文真正实现千人千面的个性化体验。这篇文章把我们在设计记忆系统时踩过的坑、理清的思路、落地的实现方式整理出来希望能给正在做智能体、AI助手或个性化对话系统的朋友一些参考。记忆的分层、写入策略、召回调优、隐私边界这些我们都会展开聊内容偏工程落地不堆概念看完可以直接照着搭一版。1. 为什么智能体需要记忆产品痛点与设计原点1.1 “失忆”的智能体有多让人抓狂先讲个真实场景。早期我们上线了一版没有记忆能力的对话Demo用户每天进来都是陌生感拉满。有用户连续一周跟智能体说“我胆固醇偏高早餐不要推荐油腻的”智能体每次都像第一次听到一样下一条推荐还是煎饼果子配油条。用户后来在反馈里写了一句很扎心的话“它还不如一个记事本。”这句话直接推动我们把记忆系统提上了排期。失忆带来的问题不只是体验差还会让用户对产品的信任感快速流失。试想用户花了十分钟告诉智能体自己的作息、饮食禁忌、健身目标转头再问一句“我今天适合练什么”智能体完全接不上那一刻用户就会觉得之前的信息全白给了。更现实的是很多智能体产品的核心价值并不是单轮问答的正确率而是长期陪伴中形成的连贯服务。没有记忆就等于每次对话都是初次见面产品永远停留在工具层面做不出情感黏性和个性化价值。1.2 从“会聊天”到“懂你”记忆的分层模型想清楚记忆要解决什么问题后我们一开始也犯过“什么都想存”的毛病。对话记录、用户点击、停留时长、页面浏览全部塞进一张表里结果又乱又没法用。后来参考认知科学里关于人类记忆的分层方式把智能体记忆拆成四类短时工作记忆、长期情景记忆、语义知识记忆和用户画像记忆。短时工作记忆对应当前会话里的上下文比如用户前一句提到“周末去杭州”后一句问“帮我查高铁”智能体得知道“杭州”就是它要查的目的地。长期情景记忆则是跨会话的关键事件比如“上个月用户说过在准备省考”“去年年底换了新工作”。语义知识记忆更偏通用常识比如城市地图、菜系分类、履历常识这部分可以直接从外部知识库挂载不一定要从对话里学。用户画像记忆是最高抽象层把用户明确表达或行为推断出的偏好沉淀成标签比如“偏爱清淡”“工作时间上午九点到下午六点”“家里有一只五岁的柯基”。这四类记忆不是并列关系而是层层汇总的关系。会话上下文经过提炼变成情景记忆情景记忆再经过归纳和校验沉淀为用户画像。分清楚层级之后后面设置存储策略和更新逻辑都会省力很多。1.3 千人千面不是玄学记忆如何支撑个性化“千人千面”这个词在产品文档里出现频率很高但落到智能体身上本质就一句话让系统在面对不同用户时能够调取不同的背景信息生成不同的回答策略和表达风格。举个例子同一个“晚上吃什么”的问题面对一位高血糖用户和一位正在减肥的健身用户智能体给出的答案完全不同。前者侧重推荐的含糖量提示后者侧重蛋白质和热量控制。如果不用记忆做区分模型只能给出一份模板化的“均衡饮食建议”看起来没错但谁都觉得没说透。记忆支撑个性化还有一层递进它让模型越用越懂用户的表达习惯。有人喜欢简洁答案有人希望先解释原理再给结论有人习惯用短句提问。这些风格特征完全可以被记忆系统捕获然后在生成提示词里作为显性指令传给模型。我们实测下来加了风格记忆之后用户的对话满意度评分提升非常明显比单纯调模型参数还管用。这也是为什么我一直认为大模型应用拼到最后拼的不是模型本身而是模型外面的这套数据与记忆架构。2. 记忆系统整体架构与技术选型2.1 四层架构采集、存储、召回、应用记忆系统在我们项目里并不是一个单独的服务而是嵌在智能体主流程中的中间层。整体分成四层采集层、存储层、召回层和应用层。采集层负责从交互过程中提取原始线索包括用户输入、用户反馈、主动点击行为、系统触发的埋点等。这里有一个容易被忽略的点不是所有对话内容都值得进存储层采集层需要先做一轮预过滤把噪音话题、闲聊寒暄、重复试探性内容筛掉。存储层负责把采集到的线索变成结构化记录写入对应的存储介质。召回层是记忆系统的核心它的任务是当用户发起新一轮对话时从海量记忆里快速找到与当前问题相关的片段。应用层再把召回结果转成提示词中的上下文块交给大模型生成最终回答。四层之间的接口要尽量保持简单我们一开始设计时还加了消息队列做异步解耦后面发现对话场景下的记忆写入延迟其实没那么敏感反而是同步写入更容易排查问题。最后简化成了“采集层写一份、存储层存一份、召回层查一份、应用层拼一份”的直线流水代码清晰很多。这个经历也让我意识到架构是为场景服务的不要为了所谓的解耦而过度设计。2.2 技术选型向量库、缓存库和关系库怎么搭配存储层的选型是记忆系统设计里最花时间的环节。我们考虑过全用向量数据库也考虑过全用关系型数据库最后定下来的是“关系库 向量库 缓存库”的组合方案。关系型数据库用来存用户画像这类强结构数据。它字段明确、支持复杂条件查询、事务能力强标签体系变动的时候也好做迁移。向量数据库用来存文本类的记忆片段比如用户说过的一段话、事件摘要、偏好描述这类信息没有固定结构只能靠语义相似度来检索。缓存库用来存当前会话内的短时上下文需要低延迟读写所以对速度要求最高一般用键值型存储就能满足。三者的边界其实和前面说的记忆分层是对应的画像层偏结构化情景层偏向量化会话层偏缓存化。选型时还有个容易被忽视的细节向量数据库的索引类型和距离度量方式。如果记忆片段数量不大几万条以内用暴力检索都能满足需求一旦上了百万级就得考虑HNSW这类近似最近邻索引同时要根据文本长度选择合适的归一化方式。我们早期在评估阶段用默认配置跑了一版结果中文长文本的召回结果非常不稳定后来发现距离度量选成了不适合文本向量的方式换成适用的度量之后效果立刻改善。这个坑后面细说。2.3 记忆类型与生命周期管理不同记忆类型的生命周期差异很大不能一视同仁。会话内的短时记忆只在当前对话中有效对话结束就可以清理最多保留到用户关闭页面后十分钟。用户画像层的记忆则需要长期保存通常情况下只增不删除非用户主动修改或要求清除。情景记忆和语义记忆处于中间状态需要做时间衰减和定期归档。生命周期管理的核心是防止两件事一是记忆无限膨胀导致召回时噪声变大二是陈旧记忆误导模型比如用户两年前喜欢某个品牌现在可能早就换口味了。我们的处理方式是给每条记忆打上时间戳和置信度字段召回时按时间衰减系数做加权同时后台跑定期任务把超过半年的低置信度记忆降级超过一年且从未被命中的记忆直接归档。表格里可以看到我们对四类记忆的生命周期约定记忆类型存储介质默认保存时长更新策略召回优先级短时工作记忆缓存库会话结束 10分钟每轮对话全量覆盖最高长期情景记忆向量库180天事件抽取后追加中语义知识记忆向量库/知识挂载持久定期同步外部知识低用户画像记忆关系库持久显式确认后更新高这套生命周期管理规则上线之后最直观的变化是召回精度的稳定性变好了。之前出现过用户上一秒说“想去重庆”下一秒智能体突然引用他半年前说过的“不喜欢火锅”严重割裂。加了时间加权后近期的记忆天然占更大权重这种跨时空打架的情况明显减少。3. 核心细节解析从用户画像到记忆槽位设计3.1 用户画像字段设计与标签体系用户画像不是简单堆字段而是要具备可扩展性。我们把画像表设计成三块基础属性、动态偏好、行为统计。基础属性包括称呼、所在城市、年龄范围等这类信息通常由用户主动提供或对话中显式说出。动态偏好包括饮食口味、沟通风格、兴趣话题、常用时间段等这类信息会随着对话持续更新。行为统计则记录统计类的指标比如最近七天对话次数、平均对话轮数、主动点击次数。画像字段里我们特别设计了一个“标签来源”字段记录每个标签是被用户明确说出来的还是被系统推断出来的。比如用户直接说“我不吃香菜”这就是显式标签置信度直接拉满如果是通过用户多次提到“香菜好难吃”推断出来的就是隐式标签置信度低一些。这个设计非常关键因为在后续的记忆更新冲突中我们要靠置信度来决定听谁的。JSON示例可以这样表示{ user_id: u_10001, base_info: { nickname: A同学, city: 杭州, age_range: 25-30 }, preferences: [ { tag: 饮食偏好:清淡, source: explicit, confidence: 0.95, updated_at: 2025-06-01 20:13:00 }, { tag: 沟通风格:喜欢短答案, source: inferred, confidence: 0.72, updated_at: 2025-05-30 09:12:00 } ], behavior_stats: { recent_days_active: 12, avg_dialogue_rounds: 6.5, preferred_hour: 22:00-23:00 } }3.2 记忆写入策略什么时候记、怎么记、记多少记忆写入是整个系统里最需要克制的地方。我们的经验是宁缺毋滥。写入过多低质量的记忆会让系统变得“自作聪明”甚至产生错误的个性化。写入时机主要看三类信号。第一类是用户显式表达比如“我喜欢”“我不吃”“我住在”这类明确句这是最高优级的写入信号。第二类是重复行为同一个偏好在多轮对话或多次会话中反复出现系统可以自动打上隐式标签。第三类是外部行为反馈比如用户对某类推荐频繁点击或者对某类回答频繁点踩这些行为也能反映偏好。记多少的问题同样值得琢磨。我们尝试过把用户原话完整存进向量库后来发现效率极低且原话里的情绪化表达会干扰语义检索。最终方案是让大模型先做一次结构化抽取每条记忆只保留“主体 属性 偏好对象 时间上下文”的格式比如“用户不喜欢吃香菜”而不是“我记得用户上次吃饭的时候说香菜好难吃”。这样做至少带来两个好处一是记忆片段更干净向量检索的语义距离更准确二是后续做用户可读的记忆管理界面时可以直接把这些结构化描述展示给用户体验更友好。提示记忆写入要做幂等控制同一个标签如果已经被写入再次写入时不要生成新记录而是更新原记录的置信度和时间戳否则画像表很快就会堆满重复数据。3.3 记忆召回相似度计算与阈值调优召回模块直接决定了记忆能不能被“用起来”。我们采用的是多路召回策略每一路单独输出一批候选再做融合排序。第一路是根据用户当前问题文本向量与向量库中的记忆片段算相似度找语义上最接近的记忆。第二路是直接拉取当前用户的画像标签把置信度高的画像记录全量取出。第三路是查最近几轮会话的短时上下文。第四路是可选的根据当前场景和时间段做规则召回比如用户在晚间提问就把“晚间提问偏好”这一维度的记忆优先提出来。多路召回的难点在融合排序。不同路的打分口径不一样不能直接相加。我们的做法是给每一路设一个基础权重再用时间衰减因子加权。时间衰减采用指数衰减公式最终权重 基础权重 × exp(-λ × 天数间隔)。λ取值我们调到0.02表示一条记忆经过35天权重会衰减到原来的一半。这个参数可以根据产品节奏调整如果用户活跃周期是周级λ可以适当调大。阈值也不是拍脑袋定的。我们先拿了一批人工标注的测试数据跑索引方式下的相似度分布发现相关记忆的相似度集中在0.78以上不相关记忆大多在0.65以下。所以初始阈值设为0.76然后再根据线上召回日志微调。调阈值的核心指标是“召回命中率”也就是召回来的记忆里有多少条真正对回答有帮助。如果命中率低于40%说明阈值偏低或排序有问题需要收紧。3.4 个性化生成提示词组装与冲突消解召回的最终目的是给大模型提供一份干净的“记忆简报”。我们在提示词组装上迭代过很多版本总结出一个比较稳的思路system提示词中先固定角色人设然后放“用户画像”接着放“近期重要记忆”最后放“当前会话上下文”。画像是高置信度标签语气肯定近期记忆用“据用户最近提到”这类弱表述避免模型把过期信息当硬事实。示例模板大致如下【角色设定】 你是“小度想想”一位温暖、细心的个人智能助手。 【用户画像】 用户偏好清淡饮食喜欢200字以内的短答案常在工作日晚十点后提问。 【近期记忆】 用户在本周二提到周末想爬山用户在上次对话中反馈不喜欢氛围嘈杂的餐厅。 【当前上下文】 用户现在问这周六有什么合适的户外活动推荐注意这里的措辞控制。如果记忆置信度偏低我们在生成提示词时会把“用户偏好清淡饮食”改成“用户近期似乎比较关注清淡饮食”给模型留出怀疑和追问的空间。这个细节很重要它避免了模型过度自信地依据错误记忆生成答案。冲突消解是另一个大坑。最典型的情况是用户今天说“我要开始吃辣了”但历史画像里还写着“不吃辣”。我们的策略是当用户在当前对话中给出了与画像相反的信息时以当前对话为准同时把画像标签的置信度降低并且更新为“近期偏好可能改变”。但更新不是立刻覆盖而是进入一个“待确认”状态只有当下一次对话再次验证后才算彻底覆盖。这样设计是为了防止用户只是随口说了句违背习惯的话系统就永久修改画像反而矫枉过正。4. 实操过程与核心环节实现4.1 环境准备与工程目录设计这套记忆系统我们用的是Python实现依赖并不多。下面是基础的环境清单主要用到关系型数据库做画像存储、向量索引库做记忆片段检索以及一个嵌入式向量构建模块。如果你想快速复现建议先把基础环境跑通pip install numpy sqlalchemy redis工程目录我们按功能模块拆分核心就三个文件画像存储模块、向量召回模块、记忆组装模块。后期维护起来非常方便每个新同学接手后基本一天能看懂结构。目录设计如下memory_system/ ├── profile_store.py # 用户画像读写与冲突更新 ├── vector_memory.py # 记忆向量化、写入、召回 ├── memory_composer.py # 提示词组装与记忆简报生成 ├── tests/ # 单元测试与回归用例 └── config.py # 阈值、衰减系数等全局配置4.2 画像存储与记忆管理核心代码实现先说画像存储模块。这一层最核心的是写入时要判断该更新还是该新建我们用一个简单的按标签名查重再更新的逻辑实现。核心代码如下class ProfileStore: def __init__(self, db_connection): self.conn db_connection def upsert_tag(self, user_id, tag, source, confidence): sql SELECT id, confidence FROM user_tags WHERE user_id? AND tag? row self.conn.execute(sql, (user_id, tag)).fetchone() if row: new_conf max(confidence, row[confidence] * 0.9) update_sql (UPDATE user_tags SET confidence?, updated_atCURRENT_TIMESTAMP WHERE id?) self.conn.execute(update_sql, (new_conf, row[id])) else: insert_sql (INSERT INTO user_tags(user_id, tag, source, confidence, created_at) VALUES(?, ?, ?, ?, CURRENT_TIMESTAMP)) self.conn.execute(insert_sql, (user_id, tag, source, confidence)) self.conn.commit()这个实现里有一个小细节对于已存在的标签新置信度不是直接替换而是取原先置信度的九折和当前置信度之间的较大值。这么写是为了防止某次误判导致标签被瞬间拉得过高给系统留一点“犹豫空间”。记忆向量模块就更直接了。写入时把文本丢给Embedding模型转成向量随向量一起存上原文、类型和时间戳召回时用当前query向量去索引里做相似度搜索。核心代码片段如下class VectorMemory: def __init__(self, index, embed_func, top_k5): self.index index self.embed embed_func self.top_k top_k def add_memory(self, memory_id, text, memory_type): vec self.embed(text) metadata {id: memory_id, text: text, type: memory_type} self.index.upsert(memory_id, vec, metadata) def recall(self, query_text, user_idNone, top_kNone): k top_k or self.top_k query_vec self.embed(query_text) hits self.index.query(query_vec, kk) return [{text: h.metadata[text], score: h.score, type: h.metadata[type]} for h in hits]这里我刻意没有绑定具体向量库的API因为不同方案接口差异不大。实际工程里你需要把index.upsert和index.query替换成所用库对应的方法即可。4.3 完整示例模拟一次“千人千面”的对话做一个完整的走查。假设用户A同学第一次使用“小度想想”在周二晚上说了句“周末想去人少一点的山里走走”。采集层识别到“人少”“山里”“周末”三个关键偏好写入情景记忆同时把“偏好安静的自然环境”作为隐式画像标签置信度设为0.6。第二天用户再问“这周六有什么推荐的地方吗”系统进入召回流程第一步把用户问题向量化在向量库里做语义检索召回“周末想去人少一点的山里走走”这条记忆相似度约0.83超过了我们设定的0.76阈值。 第二步从画像表拉取用户标签看到“偏好安静的自然环境”和“喜欢短答案”。 第三步组装记忆简报得到类似下面的文本用户画像喜欢安静的自然环境答案偏好简洁。 近期记忆用户提到周末想去人少一点的山里走走。 生成要求请围绕江浙沪周边、人少、适合徒步的景点给出推荐控制在150字以内。模型依据这段简报生成答案给出的推荐自然就避开了热门景区而倾向那些徒步路线相对冷门、风景更自然的地点。用户如果觉得推荐满意点了赞这个点赞行为又会作为信号反馈给采集层再次提高“偏好安静的自然环境”这个标签的置信度。这条闭环跑通之后产品就具备了一个最基础的“越用越懂你”能力。4.4 效果评估别只盯着模型指标记忆系统上线后怎么评估效果我们分成了离线指标和在线指标两部分。离线阶段跑了三组测试召回命中率、记忆标签准确率、上下文冲突率。召回命中率看的是召回片段和当前问题是否相关标签准确率靠人工抽检来打标上下文冲突率是看提示词里是否出现互相矛盾的信息。这三项在开发阶段用回归测试集持续盯着就够用。在线阶段则更关注业务指标。我们重点看了四个用户次日留存、平均对话轮数、回答采纳率、差评率。记忆系统的上线会让对话轮数和使用时长明显上升如果一段时间内这些指标没有变化说明记忆没有形成正向体验。还有一类比较隐蔽的评估方式是“可解释性回访”随机抽选用户对照他们的记忆画像让用户自己判断画像准不准。我们做了一轮访谈发现用户对于“智能体记得我上次说过什么”的正面感知特别强烈哪怕只对了一条也会提升对整个产品的信任。这比任何算法指标都更能说明问题。5. 常见问题与排查技巧实录5.1 记忆污染聊错一次永久带偏记忆污染是我们上线后遇到最头疼的问题。典型症状是用户某次闲聊时随口说了句气话“我再也不喝咖啡了”系统立刻把“不喝咖啡”写入画像之后半个月的推荐全都避开咖啡用户无奈地说“我不是那个意思啊”。排查下来发现根因是写入策略太激进把低置信度的情绪化表达直接当成了偏好。解决方案从三个层面入手。第一层在采集层加情绪识别带有明显负面情绪的句子不直接入画像只进情景记忆存档。第二层对隐式标签设置“冷静期”新标签先以低置信度写入至少需要在三天内的两次对话中重复出现才会升级为正式标签。第三层提供用户可见的“记忆管理面板”让用户能直接对每条记忆做删除和修改。这个过程在智能体产品里不是额外功能而是必须项因为自动记忆一定会犯错要给用户一个纠错的出口。5.2 上下文爆炸长对话下的记忆裁剪记忆系统上线后另一个高频问题是上下文长度。用户的记忆越积越多提示词里的记忆简报越拼越长最终吃掉大量上下文配额甚至影响模型响应速度。我们遇到一个极端案例某位高频用户对话三个月后画像标签和记忆片段加起来接近8000字每次请求的延迟都比其他人高出近一倍。解决思路是给记忆简报设“总字数值”通常控制在600字以内。超出的部分通过优先级截断先保留高置信度画像再保留近期记忆最后才放中长期的情景记忆。与此同时后台做一个记忆摘要任务把三个月前的事件摘要合并压缩成一条全局摘要只保留“发生了什么、结果如何、涉及什么偏好”三要素。这个处理在实践里非常管用记忆简报的平均长度从1500字降到了500字左右模型生成质量和响应速度双双提升属于少见的“减法优化”。5.3 冷启动与召回不准数据稀疏怎么办新用户没有任何画像和情景记忆时记忆系统基本是空转的这个问题叫冷启动。我们尝试的方案有两个。第一是在第一次交互时主动提问采集信息但不是生硬地让用户填问卷而是把问题融进对话里比如用户问天气时顺手问一句“您在南方还是北方换季的时候我会留意提醒您”。第二是为新用户加载一套“通用常识包”把城市信息、季节信息、常见场景模板挂载到记忆上下文里虽然不是个性化内容但至少能让首轮回答不那么空洞。召回不准的问题更多出现在向量索引的调参不当。我们踩过的典型坑是文本长度差异太大造成的匹配偏差。长文本向量和短文本向量如果不做长度截断或归一化语义距离的计算结果会偏向长度相近的文本而不是含义相近的文本。解决方法是写入前把所有记忆片段做长度归一化统一截成256字符以内的摘要再进行向量化。这个改动看似不起眼却让召回命中率提升了差不多十个百分点。5.4 隐私与合规底线哪些记忆不能碰记忆系统天然会触碰个人信息这是逃不开的课题。我们的原则是能少存就少存能脱敏就脱敏能给用户控制权就给控制权。身份证号、银行卡、详细住址这类强敏感信息一律不写入记忆甚至在采集层就要做识别拦截。健康信息、政务信息等敏感类别只在用户主动提供且明确授权时才允许进入情景记忆并且使用时只保留必要描述不保留原始隐私细节。我们还在产品里加入了“记忆透明度”设计。用户在对话中随时可以问智能体“你记得关于我的哪些信息”系统会把已存储的记忆清单以通俗语言展示出来。用户也可以一键清空某类记忆或全部记忆。这个设计虽然不能带来直接的业务指标但对用户信任度和合规风险控制的价值非常大。很多从业者会忽略这一点我强烈建议把记忆可视化放到需求优先级前列。一个连自己记住了什么都不愿意展示给用户的智能体很难真正获得用户信赖。结尾最后再分享一点个人体会。在做“小度想想”这套记忆系统之前我一直以为最难的是算法和存储架构真正落地之后才发现难点其实是“克制”。克制写入、克制召回数量、克制对用户隐私的过度挖掘每一条克制都在为更好的长期体验让路。我们曾经因为召回命中率不够高而盲目加规则结果系统变得格外“自以为是”后来砍掉一半规则效果反而好了。所以给正在做类似项目的朋友一个建议先搭一个最简单的“画像 情景记忆”的闭环跑通核心体验再逐步加复杂策略。另外一个小技巧给每一类记忆都留一个“用户可解释入口”用户能看见的系统才更可信。这个领域还在快速演进今天单条记忆的存取逻辑说不定未来就会变成智能体之间协作的通用语言。