接手某区域总部的智能问答项目时我原以为最棘手的是RAG链路里的向量数据库选型。真正跑起来才发现业务方对Agent的要求早就不是“能检索”而是“记住”。同一个用户昨天问过什么、上周说过什么偏好agent如果全忘了那体验和搜索引擎没什么两样。所以这一个月我大部分时间都在折腾一件事给AI agent设计一套长短期记忆架构让它既能应对会话内的连续追问也能跨会话沉淀用户画像。这篇就来复盘我在这套架构里怎么选向量数据库、怎么设计记忆分层以及实测下来踩过的坑。先说结论如果你的agent还停留在“每次对话都重新开始”的阶段赶紧把记忆架构补上。这篇内容适合正在做RAG应用、想把智能体推进到生产环境的团队也适合一个人捣鼓agent demo、想知道怎么设计记忆层的新手。我尽量少讲虚的把选型逻辑、架构分层、关键代码和排查过程都摊开讲。1. 为什么Agent需要长短期记忆先搞清楚要解决什么问题1.1 场景还原从一次“答非所问”说起刚接到这个需求时业务方给我举了一个例子用户上周在助手里面说过“我比较喜欢摩托车路线不喜欢高速”这周用户问“推荐一条周末短途路线”。如果agent没有记忆它只能根据当前这句问题给出一堆通用推荐甚至推荐全程高速的方案。用户觉得“这不是智能这是复读机”。这种场景不是个例。决策类、偏好类、身份类信息用户往往只会在某次对话里透露一次如果agent不把这些信息沉淀下来后续所有对话都是在“盲聊”。我当时给这个项目定的目标很朴素让agent拥有“记住关键信息”的能力而不是把所有信息都塞进上下文窗口。1.2 长短期记忆的本质是分层存储很多团队一上来就想着往向量数据库里塞东西但连“什么该记、记多久”都没想清楚。我的理解很简单短期记忆和长期记忆本质上是数据生命周期不同的两类存储。短期记忆工作记忆当前会话内的信息比如用户刚说“我只有半天时间”这种信息有效期只有几分钟用完就丢或者做摘要压缩。长期记忆持久记忆跨会话的稳定事实比如用户身份、偏好、历史决策结论这种信息需要长期保存且要能被随时检索。这个分层看起来简单实际设计时要考虑的问题不少短期记忆放在哪里、窗口多大、什么时候把短期记忆“固化”成长期记忆、长期记忆怎么检索、新旧记忆冲突了怎么办。后面几节我逐个拆解。2. 向量数据库选型我锁定的五个维度和两个反直觉结论2.1 选型前先列需求不能只看官网测评分数市面上的向量数据库测评文章一搜一大把召回率、QPS、内存占用这些指标确实重要但在我这个项目里真正决定选型的是另外五个维度第一是写入和更新的稳定性。记忆系统不是离线导入一次就完事而是随时都在增量写入。用户每产生一条值得记的信息就要往向量库里面写一条。如果向量库在频繁写入时检索性能急剧下降或者upsert操作经常超时这个系统就没法上线。第二是过滤能力。我们的长期记忆必须按用户ID隔离检索时只能检索当前用户的记忆不能A用户搜到B用户的偏好。这意味着向量库必须支持“向量相似度检索 标量字段过滤”的组合查询而且过滤条件要生效在向量检索之前否则数据量一大就会出现严重的精度问题。第三是索引参数的可调整性。不同业务场景对召回率和延迟的要求不一样我希望索引类型比如HNSW、IVF和参数邻居数、构建候选数是可以在线调整或者至少能快速重建的。买了一个黑盒服务结果索引参数全被定死后面优化会很被动。第四是运维成本。团队没有专职的数据库运维我不可能为了一个记忆模块专门养一个集群。所以自建方案必须足够轻托管方案必须在预算内关系库扩展方案要评估对现有库的压力。第五是生态集成。我们的技术栈主要是Python后续还要做定时任务做记忆清理和遗忘所以选型时特别关注有没有现成的客户端、有没有批量操作接口、有没有异步写入能力。2.2 三个候选方案的实测对比我当时把候选方案分成三类自建开源向量库、全托管向量服务、关系库内嵌向量能力。为了不涉及具体商业产品这里分别叫方案A、方案B、方案C。实测跑了一周把对比结果整理成一张表对比维度方案A自建开源向量库方案B全托管向量服务方案C关系库内嵌向量能力写入延迟1000条批量写入约1.2秒稳定约2.8秒受网络影响约3.5秒受关系库负载影响检索延迟百万级向量P99约20ms约35ms约80ms标量过滤支持好可过滤后向量检索好一般复杂条件容易走全表索引参数调整完全可调可重建部分可调依赖关系库版本运维成本需自己部署和监控零运维按量付费复用现有库但需评估压力数据隔离能力强可按集合/分区隔离强但需设计好key中等依赖表结构最终我选了方案A主要原因有两个一是写入延迟和检索延迟都最稳二是索引参数完全可调后面做优化时不会束手束脚。方案B适合不想维护基础设施的团队但记忆型业务的数据量增长很快按量付费的账单会越来越难看。方案C在数据量小时完全够用但一旦超过百万级向量复杂过滤场景下的检索延迟会上升到不可接受的程度。2.3 选型背后的两个“反直觉”经验第一个反直觉经验召回率不是第一优先级写入稳定性才是。很多人选型时盯着评测集的Recall10看但在记忆系统里用户记忆的写入是持续性的如果写入经常失败用户上一秒刚说过的偏好下一秒就丢了召回率再高也没有意义。第二个反直觉经验不用盲目追求高维向量。当时有人建议用1024维的向量模型说表达能力强。实测下来768维的模型在我们的数据上召回效果和1024维几乎一样但索引占用内存和检索延迟都明显更低。维度不是越高越好要看你实际的数据分布和模型能力。提示选型不要只看峰值性能要看“持续写入压力下的P99延迟”。拿小数据集测不出真差距最好用模拟的增量写入脚本压测至少3天再看结果。3. 长短期记忆架构设计把Agent当成一个会成长的系统3.1 短期记忆会话层的读写策略短期记忆我直接用缓存中间件保存会话原始文本没有放到向量库里。原因很简单短期记忆访问频率极高但单一记忆的保存时间很短用向量库去承载这种高频低价值的读写太浪费。短期记忆的窗口大小我一开始定的是最近10轮对话后来发现10轮不够。用户经常会先问几个问题、中间穿插别的话题、再回到最初的需求如果窗口太小前面的关键信息就丢了。后来调整成“按时间和轮数的双阈值”超过20轮或者距离当前时间超过2小时就触发一次摘要压缩。压缩策略是这样的把窗口内的对话用大模型生成一段摘要摘要里保留用户偏好、待办事项、明确结论这三类信息然后把摘要写入长期记忆。这样短期记忆像便利贴一样随写随撕长期记忆像档案柜一样只存放有价值的东西。3.2 长期记忆向量化存储与召回规则长期记忆是系统的核心主要存三类内容用户画像类身份、职业、地域、明确说过的偏好。决策结论类用户在对话里做过的选择比如“定了走山路路线”。业务事实类和具体业务相关的状态信息比如“合同还在审批阶段”。每条长期记忆写入时我先用嵌入模型把文本转成向量同时把记忆内容的结构化字段用户ID、记忆类型、创建时间、更新时间、置信度以payload形式一起存进去。向量用来做相似度检索payload用来做过滤和后续的逻辑控制。召回规则是用户发起新对话时先按用户ID过滤出该用户的全部长期记忆再用当前对话内容做向量检索取top5记忆拼接到系统提示词里。这个规则看着简单但实际写的时候有几个细节很关键记忆召回结果必须带上时间戳这样模型能判断哪些是旧偏好、哪些是新偏好检索分数低于阈值的记忆宁可不要也不能硬塞给模型。3.3 记忆控制器决定“什么时候记”“记什么”标题里提到的“指挥官”我落地成了一个叫记忆控制器的模块。它本质上是一个规则引擎加调度器负责三件事记忆写入决策。并不是每轮对话都值得写长期记忆。我的做法是先让大模型对每轮对话打一个标签是否包含值得长期保存的信息并给出记忆类型和置信度。置信度高于0.8才写入长期记忆0.6到0.8之间的进入待确认队列低于0.6直接丢弃。记忆冲突处理。典型场景是用户先说“我喜欢摩托车路线”三天后又说“摩托车太累了还是开车吧”。这两条记忆是冲突的。控制器会在写入新记忆时检查是否存在类型相同且内容方向相反的历史记忆如果存在就把旧记忆标记为“已过期”降权处理让新记忆在召回时排在前面。主动遗忘。长期记忆不代表永远保存。一些临时性信息比如“这个月月底前要提交材料”过期后就该被清理。控制器每天跑一个定时任务检查记忆里的过期时间字段自动归档或删除过期记忆避免向量库被垃圾数据塞满。注意记忆控制器是整个架构设计的核心也是和纯RAG系统的最大区别。RAG只知道“从库里检索”控制器知道“该不该记、记什么、什么时候忘”。4. 实操落地从一个向量集合到完整记忆系统4.1 数据准备与Embedding管道进入实操环节之前先说数据。很多团队犯的错是直接把对话原文切片后灌入向量库结果垃圾进垃圾出。我的做法是先做一层清洗和过滤。清洗规则按优先级排列去掉纯寒暄对话和“嗯”“好的”这类无意义回复合并同一用户在同一时间窗口内的连续相似表述对包含用户隐私信息的原始文本做脱敏处理只保留业务需要的字段。清洗完的数据再按“用户偏好、决策结论、业务事实”分别打标签方便后面按类型过滤。Embedding部分我选了一个中文效果稳定的向量模型输出维度768。写入前用了一个小技巧把记忆文本结构化后再嵌入。比如用户的原始说法是“我不喜欢走高速上次堵车堵了三个小时”我会让大模型先改写成结构化记忆“偏好出行避开高速公路原因是堵车体验不佳时间约在2025年某月”再把这条结构化文本送入嵌入模型。实测下来结构化后的记忆召回精度比直接存原始对话高了不少。4.2 向量库写入与检索代码骨架下面这段是核心记忆写入的伪代码基于我实际项目里的结构简化而来可以当作设计参考# 记忆写入模块伪代码 from vector_store import VectorCollection, SearchParams, DistanceMetric class LongTermMemory: def __init__(self, collection_nameagent_memory_v1, dim768): # 按用户ID分partition保证数据隔离和清理 self.collection VectorCollection( namecollection_name, dimensiondim, distance_metricDistanceMetric.COSINE ) # 索引参数HNSWM控制邻居数量ef_construction控制构建召回 self.collection.create_index( index_typeHNSW, params{M: 32, ef_construction: 200} ) def write_memory(self, user_id, memory_text, memory_type, confidence, expire_atNone): # 1. 文本结构化实际项目里这里会调用大模型做改写 structured_text self._normalize_memory(memory_text) # 2. 向量化 embedding self._embed(structured_text) # 3. 写入向量库payload保存结构化字段 self.collection.upsert( idget_memory_id(user_id, memory_text), vectorembedding, payload{ user_id: user_id, text: structured_text, type: memory_type, # preference / decision / fact confidence: confidence, created_at: now(), expire_at: expire_at } ) def recall_memory(self, user_id, query_text, top_k5, min_score0.55): # 检索时先按user_id过滤再做向量搜索 query_embedding self._embed(query_text) results self.collection.search( vectorquery_embedding, filter{user_id: user_id}, limittop_k, min_scoremin_score ) return [r.payload for r in results if not r.payload.get(expired)]这段代码里有几个设计点值得展开。按用户ID分partition而不是直接全局检索这是数据隔离的关键。如果不做过滤全局向量检索会频繁召回别人的记忆哪怕加了用户ID约束检索效率也会受到影响。分partition之后每次检索只扫描当前用户的数据延迟和精度都可控。cosine距离度量对文本记忆来说余弦相似度比欧式距离更符合语义比较的习惯因为向量的模长和文本长度相关性太强欧式距离会偏向短文本。实际用下来cosine的召回结果在业务场景里更稳定。min_score阈值这个是防“硬凑”。向量检索只要排序就能返回前5条但有时候这5条和当前问题根本不相关。设一个0.55的阈值低于阈值的记忆不返回宁缺毋滥。后面排查问题时会发现这个阈值很关键。4.3 关键参数的推导与调试记录top_k取值我先从3开始测发现3条记忆太少用户上一个会话刚提到的信息经常覆盖不到。调到5之后覆盖率明显提升。再往上调到8模型输入变长响应延迟增加而且记忆之间互相矛盾的几率也上升。最终定在5。chunk_size和embedding粒度我们不是做文档库的RAG而是记忆库所以不存在大文档切片的说法。一条记忆就是一个短句或摘要长度控制在100字以内。短的好处是记忆语义单一召回时不会出现一条记忆包含多个互相矛盾的偏好。HNSW参数M和ef_construction初始配置M32、ef_construction200召回率已经很理想。调大M到48后召回率只提升了不到1%但构建索引耗时增加了约40%果断回退。这里建议大家不要盲目照搬默认值先用数据集做一组小规模验证确认提升幅度再决定要不要加参数。记忆条数估算假设有10万活跃用户平均每个用户沉淀50条长期记忆总量就是500万条向量。每条向量768维对应原始浮点数据大约3KB加上payload里的文本和字段存储总量在10GB~20GB之间。这个量级方案A完全扛得住不需要做额外的分库分表只要按用户ID做partition就够了。5. 优化与排查我踩过的五个坑5.1 召回质量差从“换模型”到“混合检索”第一版上线后最明显的问题是召回质量不稳定。用户明明说过“不吃辣”用向量检索就是召回不出来。我先试了更换更强的向量模型提升有限。真正让效果质变的是改成混合检索。做法是把向量召回和关键词召回结合起来再用一个简单的排序融合算法取交集。关键词部分我用的是轻量级的BM25实现专门对记忆文本里出现过的具体名词做精确匹配。比如“不吃辣”这种关键词在向量语义空间里可能匹配到“饮食清淡”但在关键词空间里能精确命中。融合逻辑用的是经典的RRF公式给每个文档在向量召回和关键词召回里的排名取倒数求和后排序。实测下来混合检索把记忆召回准确率从78%提升到了92%。这一步可以说是整轮优化里性价比最高的一招。提示如果你的向量召回经常答非所问先别急着换模型看看是不是缺了关键词召回这一路。很多看似“语义理解不够”的问题其实是精确匹配没有兜住。5.2 写入瓶颈与批量策略记忆写入是一个持续不断的过程但业务方要求记忆写入不能阻塞正常对话。一开始我的实现是同步写入用户每产生一条记忆就立刻写库结果高峰期接口超时频发。排查后发现两个问题一是每条写入都要重新建连接网络开销大二是写入和检索共用一套资源写入压力上来后检索也跟着慢。解决方案引入批量写入和异步队列。具体做法是先写入一个内存队列后台每2秒或者攒够200条批量提交一次。检索走只读副本写入走主库。上线后P99写入延迟从2.5秒降到800毫秒检索完全不受影响。这里还有个小细节向量库的upsert操作对大量小请求不友好批量提交能显著提高吞吐但要注意batch大小不是越大越好我实测200条一批是最稳的。5.3 新旧记忆冲突与遗忘机制记忆冲突处理是控制器里最难的一块。刚开始我用的策略是“新记忆直接覆盖旧记忆”结果出了麻烦用户可能只是临时改主意第二天又改回来旧记忆被覆盖后就再也找不回来了。后来改成版本化存储。同一类型的记忆保留多个版本每条都带起始时间和结束时间。召回时默认取“当前时间仍在有效期内”的版本如果存在多条按时间倒序返回。用户改回原来的偏好时再写一条新版本旧版本自动归档但不清除。这个设计让冲突记忆的恢复成本大幅降低也方便后面做记忆审计时回溯。5.4 冷启动与脏数据清洗系统上线前需要从历史对话里回填一批记忆这部分坑最多。历史对话质量参差不齐很多对话没有明确的用户ID标识有些对话里还夹杂着客服的回复片段。我定的清洗规则是只有标记为“用户发言”且包含明确意图的文本才进入记忆库客服回复一律不写入。同时做一个“记忆置信度”校验对每条回填记忆计算一个来源可靠度分数分数低于0.7的不写入。跑完清洗后初始记忆库比预想的少了一半数据但质量干净了很多。这里给个建议冷启动阶段宁可少记也不要让垃圾数据污染向量库因为清洗脏数据的成本远高于重新生成。5.5 监控指标与灰度发布排查问题不能只靠用户反馈我把关键指标整理成了一张监控表挂在运维看板上指标预警阈值处理动作记忆写入成功率低于99.5%检查批量提交队列是否堆积、向量库连接是否异常记忆召回命中率低于85%检查min_score阈值是否过高、embedding模型是否有数据漂移检索P99延迟超过150ms检查索引是否需要重建、数据量是否突破了partition阈值记忆冲突率超过5%检查控制器版本化逻辑是否生效过期记忆清理完成率低于90%检查定时任务是否被阻塞灰度发布也很关键。我们按用户ID的hash值把流量切成10个桶每天放量12个桶先让小部分真实用户验证效果没有明显问题再继续放量。这个阶段抓到了几个在测试环境里根本复现不了的问题比如某类用户ID在过滤时因为编码不一致导致检索为空这类问题只能靠灰度流量来暴露。6. 后续扩展几点个人预期这套长短期记忆架构上线跑了两周效果比预想的好。业务方反馈最明显的是用户画像召回变准了同一个用户的连续会话里agent不再像第一次见面一样说话。我最深的一个体会是记忆架构的设计难点不在向量库本身而在“该记什么、什么时候忘”这套控制逻辑向量库只是最后的落地载体。后面我想把记忆做成“可视化记忆卡片”的形式用户可以在界面上手动确认或删除某条记忆这样agent的长期记忆就不再是黑盒用户对agent的信任感也会更强。另一个方向是把记忆召回从“单轮召回”升级成“多步规划召回”让控制器根据当前对话目标推测未来可能需要的记忆提前载入上下文。这个改动会复杂很多但值得尝试。最后说一个实操建议做记忆系统不要一上来就追求大而全的架构。先跑通“短期记忆存会话、长期记忆进向量库、控制器做调度”的最小闭环再按用户反馈迭代。记忆架构的优化空间很大但前提是先有一个能在生产环境稳定运行的地基。
阅读完成 · 觉得有帮助?