被静态知识库折腾过的人,一定懂那种感觉:文档喂了几百份,问答效果还是“看似会答,实则空洞”。问文档里有的东西,答得还行;一旦用户问题换了个说法,或者需要结合上一次聊天的上下文,它就原形毕露——只会从固定知识库里搜索拼接,完全没有“记性”。我这次想聊的,不是又一份eRAG(增强检索生成)教程,而是把 Agent 和 Milvus 向量数据库搭在一起,给AI助手真正装上“动态记忆”的过程。这套思路不只在做文档问答,而是让助手能自己判断“哪些信息值得记住、哪些该忘掉、哪些需要跨会话复用”,从一个只会读资料的问答机器人,变成一个“越用越懂你”的长期陪伴型助手。如果你正在做智能客服、私域运营助手、个人AI知识管家这类项目,这篇内容大概率对你有用。1. 静态知识库的天花板:为什么“喂再多文档”也救不了助手先定义一下我口中的静态知识库:把一堆文档切块、向量化,导入向量数据库,用户提问时就做一次相似度检索,再把最相关的若干个片段丢给大模型生成答案。这是目前绝大多数RAG应用的默认形态,也是大部分人理解中的“知识库问答”。它确实解决了“大模型不掌握私有知识”的问题,但解决不了另外三个关键问题。1.1 知识时效性的死结文档切块入库之后,这个知识库就“定格”了。今天导入了产品手册,明天产品功能升级,后天价格调整,如果没有重新跑一遍拆分和嵌入的流程,助手回答的永远是旧信息。有人会说不就是重新导入吗?可现实项目里,知识库动辄几万个片段,每次全量重建嵌入的时间和成本都不小,更别提很多团队连“哪个文件更新了”都难定位。我在之前某项目里就看到过这样的尴尬:助手被问到新版本是否支持某项功能,它拿着旧手册信誓旦旦说支持,客户照做后翻车,最后一堆人排查了半天,才发现知识库三个月没更新。1.2 会话型需求才是真正的“记忆黑洞”静态知识库的另一个硬伤,是它的检索上下文完全来自“用户当次提问”。它不关心用户昨天问过什么、上次反馈过什么偏好、之前是否已经否定了某个方案。举个很现实的例子:一位用户昨天让助手帮忙筛选数据库选型方案,明确说了“不考虑需要额外购买License的商业产品”。今天他再问“帮我推荐几个数据库”,静态RAG会老老实实地把文档里所有主流数据库都列出来,包括他昨天已经排除的商业产品。助手完全“失忆”,用户感受到的就是:这AI根本不懂我。1.3 静态RAG为什么解决不了上下文个性化有些团队会试图通过拼长上下文来缓解这个问题,比如把历史对话全部塞进大模型上下文窗口。这种方法短期内有效,但窗口再大也有上限,对话一长,要么截断、要么费用暴涨、要么模型被无关历史带偏。更关键的是,历史消息里真正重要的往往是“用户偏好”“待办事项”“关键约束”这类结构化记忆,而不是一堆原始对话流水账。这也是我后来转向 Agent + 向量数据库这套架构的直接原因:我们需要的不只是一个“能查资料的搜索引擎”,而是一个能对记忆进行写入、更新、召回、遗忘的“记忆管理系统”。而向量数据库在其中扮演的角色,远不止存几个embedding这么简单。动态记忆本质上是把“记忆”当成一种可以持续写入、检索、重组的数据资产。这句话听起来抽象,拆开了就三条:每个对话里抽取出值得长期保留的事实片段,写入记忆库;用户每次提问时,除了查静态文档,还要查“历史记忆”,把相关的过往结论一起交给模型;记忆有生命周期,会过期、会被修正、会被覆盖,而不是只增不减。这三条对应到技术上,就是Agent负责“判断什么值得记”,Milvus这类向量数据库负责“存得下、查得快”,而大模型负责综合静态知识与动态记忆生成答案。下面我把这套架构拆开讲。2. 动态记忆的分层模型:Milvus在Agent里的真实位置很多人一听“动态记忆”就去翻专门的记忆框架,其实没必要。只要理解了分层的思路,用通用技术栈完全能搭出来。我习惯把助手的记忆分成三层:短期工作记忆、长期情节记忆、语义偏好记忆。2.1 把记忆分成短期、长期和语义记忆短期工作记忆对应的是当前会话内需要随时引用的信息,比如用户刚才说“我预算大概20万以内”。它通常不落地到向量库,放在Agent的会话状态里就好,属于“用完即走”的临时变量。长期情节记忆对应的是跨会话的关键事件,比如“用户上次确认了登录模块用统一身份认证方案”“用户已经否决过某款云服务”。这类信息时间跨度大,不立即生效于当前所有问题,但会在后续决策中反复用到。这类记忆最适合向量化存储。语义偏好记忆则更稳定,比如“用户团队以Java为主”“用户偏好开源方案”“汇报要求带成本对比表”。它类似于“画像”,会在几乎所有交互中影响Agent的风格和推荐倾向。在设计上,我不建议把所有记忆塞进一个collection里。混在一起会让检索噪声变大、权限控制变难。更合理的做法是按类型拆collection,或者至少用标量字段区分memory_type,后面检索时强制过滤。这个细节直接决定了召回质量,后面调优部分还会细讲。2.2 Milvus负责的部分:向量化、索引与可回溯检索把记忆抽象成“记忆碎片”之后,下一个问题是谁来管这些碎片。有人会图省事,直接用临时文件或者Redis里的数组来存embedding,几百条记录时确实能跑,但一旦到了几万条、几十万条,并且要支持多租户、标量过滤、实时写入,就完全不是一回事了。这轮我选择Milvus,原因有几个:第一是它把向量检索和元数据管理放到了同一套体系里。记忆碎片不仅是一个embedding数组,还伴随用户ID、场景标签、时间戳、记忆类型、来源会话ID等标量字段。Milvus可以在这套结构上直接做“标量过滤 + 向量相似度检索”,不需要在业务代码里自己维护两套索引再合并。第二是它的索引类型完善,尤其HNSW和IVF系列在中小规模数据上表现稳定,兼顾召回率和查询延迟。对记忆场景来说,通常在百万级以内,HNSW基本是首选,后面给参数建议。第三是支持写入后近实时可见。动态记忆要求“本轮对话结束后写入的内容,下一轮提问最好能立刻被召回”,Milvus的可调一致性级别正好覆盖这个需求,不至于为了等数据可见而阻塞对话流程。2.3 写入路径与读取路径的分离设计真正让记忆系统变复杂的,不是某一条记忆的存取,而是写入与读取两条链路互不干扰。写入链路发生在每轮对话结束后。Agent拿到完整对话,先做信息抽取:哪些是新事实、哪些是对旧事实的修正、哪些只是寒暄不值得记。抽取出来的结果经过摘要压缩、实体归一化,再生成embedding写入Milvus。这个过程不需要用户等待,属于异步任务。读取链路发生在用户提问时。Agent把当前问题embedding化,先到记忆库检索相关历史记忆,再结合静态知识库结果,组合成上下文交给大模型。读取链路对延迟敏感,通常控制在200毫秒以内,所以要把写入和读取分离开,避免写入时的索引构建拖慢检索。我当时在架构图里画得很简单:一条异步写入管道,一条同步读取管道,中间用Milvus连接。看起来简单,但真正落地时,两条链路各自都有不少细节要注意,下面从写读两侧分别展开。3. 从零搭建带记忆的Agent:摄入、写入与召回3.1 选型:为什么用Milvus而不是旁路搞个HashMap这里先给我的选型逻辑。如果你只是做个Demo,用个Python列表或者Redis都行,但真实项目一定会遇到几个逃不掉的问题:数据量增长后检索效率下降、需要按用户隔离数据、需要按时间或类型过滤、需要高频写入的同时还要保证查询稳定。HashMap或简单Redis方案在这些需求面前会迅速变成“手写数据库”项目,光维护成本就够呛。Milvus在这条路上的优势是:向量存储、索引构建、标量过滤、动态schema都原生支持,而且开源版部署门槛不算高。对Agent场景来说,我们不需要分布式集群,单机模式加上合适的索引参数,就足够支撑中小型应用。另外一个细节选型要点:embedding模型和向量库的维度要提前定好。我自己踩过坑,一开始用了某个模型的768维向量,后来想换一个效果更好的1024维模型,结果要么重建collection,要么在兼容性上费一番功夫。建议初期就选定一个主用embedding模型,维度、距离度量(一般用余弦距离)都固定下来,后续只升级模型版本并做“新旧向量共存 + 离线迁移”。3.2 记忆写入链路:对话摘要、实体抽取与向量入库写入链路是动态记忆系统的“输入闸门”,做得不好,后面检索再快也白搭。我会给每轮对话加一个异步的“记忆归纳”步骤,核心动作有三步。第一步是抽取核心记忆项。最简单的方式是让大模型按固定JSON模板输出记忆候选,比如:[ { "memory_type": "preference", "subject": "数据库选型", "content": "用户明确排除需要额外购买License的商业数据库", "importance": 0.9, "expire_at": "2025-12-31" }, { "memory_type": "event", "subject": "登录模块", "content": "用户确认采用统一身份认证方案,预计下月实施", "importance": 0.8, "expire_at": null } ]这个“半结构化抽取”比直接把整段对话切块入库聪明得多。它能去掉重复废话,只留关键约束和决策,而且后续做冲突检测时,有subject字段可以精确关联。第二步是生成embedding并入库。这里我建议对content做embedding,而不是对subject做。原因很简单:用户下次提问时,表达方式大概率不会和subject完全一致,但语义上会和content更贴近。入库时记得把user_id、memory_type、created_at、source_session_id这些标量字段一起写进去,后面按用户隔离和按时间过滤全靠它们。第三步是同步处理记忆冲突。比如用户这轮说“预算调整为30万以内”,而库里已经有一条“预算20万以内”。直接append会让检索同时召回两条矛盾记忆,模型就会迷糊。冲突检测的简单方案是:按subject字段先搜索旧记忆,存在则做更新或标记过期,而不是无脑插入新记录。这个逻辑早期可以放进同一个大模型调用里完成,不一定要单独搞复杂的规则引擎。3.3 记忆召回链路:相关性检索与Agent工具调用召回链路的核心任务是:在用户提问时,从记忆库里找出“和当前问题相关且靠谱”的旧记忆,并把它们组织成可用的上下文。我建议把这步设计成Agent的一个工具调用,而不是直接把所有记忆都塞进prompt。例如,在Agent的function列表里加上两个工具:search_user_memory(query, memory_type, user_id, limit)search_document_knowledge(query, collection, limit)大模型会根据用户问题判断该调哪个工具、以及怎么组合。为什么要这样设计?因为直接无差别召回往往会让模型被不相关的旧记忆干扰,而 If the 系统性调用,可以为问题归类,避免额外噪声。比如用户问“MySQL兼容性如何”,Agent应该优先检索文档知识,而不必把“用户偏好开源”这类画像记忆也拉出来。召回策略还有个细节:记忆检索的embedding可以做一些轻量改写。用户口语化提问“上次说那个数据库选型排除了什么来着?”直接做向量检索可能效果不理想,因为句式太绕。可以在召回前让大模型把问题转成“查询关键词”或“陈述性查询”,比如转成“数据库选型中被排除的方案”,再去检索,命中率会明显提升。3.4 一段可复现的最小化代码骨架下面给一段用Python实现的最小写入/召回逻辑,方便理解整体流程。这个示例做了简化,不依赖具体Agent框架,只演示Milvus和embedding模型的核心配合。from pymilvus import MilvusClient, DataType client = MilvusClient("memory.db") schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False) schema.add_field("pk", DataType.VAR_CHAR, is_primary=True, max_length=64) schema.add_field("user_id", DataType.VAR_CHAR, max_length=64) schema.add_field("memory_type", DataType.VAR_CHAR, max_length=32) schema.add_field("subject", DataType.VAR_CHAR, max_length=255) schema.add_field("content", DataType.VAR_CHAR, max_length=2048) schema.add_field("importance", DataType.FLOAT) schema.add_field("created_at", DataType.INT64) schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=768) index_params = client.prepare_index_params() index_params.add_index( field_name="embedding", index_type="HNSW", metric_type="COSINE", params={"M": 16, "efConstruction": 100} ) client.create_collection("user_memory", schema=schema, index_params=index_params) def write_memory(user_id, memory_type, subject, content, importance, embedding): row = { "pk": f"{user_id}_{subject}_{int(time.time()*1000)}", "user_id": user_id, "memory_type": memory_type, "subject": subject, "content": content, "importance": importance, "created_at": int(time.time()), "embedding": embedding, } client.insert("user_memory", [row]) def recall_memory(query_embedding, user_id, memory_type=None, top_k=5): expr = f"user_id == '{user_id}'" if memory_type: expr += f" and memory_type == '{memory_type}'" res = client.search( collection_name="user_memory", data=[query_embedding], filter=expr, limit=top_k, output_fields=["content", "subject", "memory_type", "importance", "created_at"], ) return res从这段骨架出发,再接入Agent的function calling就很自然了。关键是要理解:搜索时的过滤条件和索引设计必须在建表时就想好,否则后面加字段、改类型都要重建collection,线上项目会非常折腾。4. 调优不是玄学:标量过滤、一致性、索引参数这三板斧跑通Demo只是开始。真正要上线上环境,或者要在评测里拿高分,你得把三个细节调明白。很多人觉得向量数据库调优是个黑盒,其实不是,核心就三件事。4.1 标量过滤:千万级向量里“先筛后搜”的威力记忆库和通用文档库最大的不同,是它天然带着很多强过滤条件:user_id、memory_type、expire状态。如果不做过滤,每一次检索都在整个库里跑,不仅慢,而且容易把别人的记忆或者已经过期的记忆捞出来。Milvus里的filter可以像SQL一样组合表达式,比如:user_id == 'u_1001' and memory_type == 'preference' and expire_at > now这里有一个性能技巧:user_id这类高基数、高频过滤的字段,建议单独建Inverted Index或使用支持filtering的索引类型,否则过滤本身会成为瓶颈。我在实测中发现,同一个collection,给user_id加上标量索引之后,过滤查询延迟能从300毫秒降到50毫秒左右,提升非常明显。还有一点容易忽略:先过滤再向量检索和先向量检索再过滤的结果在精排上有区别。Milvus支持在向量检索时直接带filter,它会先做向量候选集的粗筛,再根据scalar条件过滤。想要更稳,可以把filter写成强条件,配合较小范围的候选集参数,确保返回的都是合法记录。4.2 索引参数:HNSW的M值与efConstruction到底怎么设HNSW是目前中小数据量下最稳妥的索引类型。它的核心参数有两个:M和efConstruction,查询时还有ef参数。M是每个节点的最大连接数。M越大,图越稠密,召回率越高,但内存占用和构建时间也越高。我的建议是16到32之间,数据量在百万级以下选16足够。efConstruction是建图时的动态候选集大小。越大建图越慢,但图质量更好。100到200是个甜点区间,太低了召回率掉得厉害。查询时的ef是每次检索的候选集大小。它不参与建索引,在每次查询时控制。线上建议从64开始,根据延迟和召回率调优。不要一上来就追求100%召回率,那是离线评测的事。在线场景下,90%以上召回率配合低延迟往往体验更好。我自己通常把目标设定为:p95 延迟在80毫秒以内,召回率不低于95%,在这个前提下尽量调小ef。4.3 一致性级别:写入后多久能被查到动态记忆要求“刚记住的内容很快能用上”,但Milvus这种分布式架构里,写入和查询可能落到不同节点,默认强一致性会带来额外延迟。我看到大多数项目都选Bounded一致性或Eventually,即可在几十到几百毫秒内看到新增记录。实际建议是:记忆写入本身不是高频操作,平均每秒几十条已经算很多了,所以不必为了吞吐牺牲一致性。我一般用Bounded,把staleness控制在2秒以内,既不会让用户体验到“刚说完就忘了”,也不会让写入链路变慢。这里还有一个经验:如果对话结束后的记忆写入和用户的下一轮提问几乎同时发生,偶尔会出现“这轮没检索到上轮记忆”的情况。缓解办法是在前端对话状态里带上最新几轮的关键提取信息,而不是完全依赖向量库即时查询。5. 真正麻烦的是“遗忘”和“冲突”:记忆管理的实战避坑很多人觉得动态记忆最难的是写入和检索,真做起来以后你会发现,最难的是“如何让系统不越记越乱”。记忆不是越多越好,甚至很多时候,“懂得忘”比“记得牢”更关键。5.1 过期记忆不会自己消失,需要衰减与归档用户偏好会变、项目状态会推进、临时约定会过期。如果所有记忆一视同仁永久存储,召回时就会频繁出现“过时信息误导答案”的情况。我的做法是给每条记忆加三层控制:显式过期时间expire_at。比如“用户下月进行系统切换”这类临时性记忆,到点就自动标记失效;活跃度衰减。每次被成功召回时,给last_access_at更新;长时间未被访问的高龄记忆,逐渐降低检索权重;定期归档任务。每天或每周扫描一次,将过期且低价值的记忆从主collection中移除或迁移到冷存储。这样设计的目的很简单:让高频使用的记忆始终保持在检索质量最好的区域,不让冷门旧数据干扰新决策。5.2 同主题多轮冲突,不能只靠“最新优先”用户的偏好不是一成不变的。今天说“优先考虑开源”,明天可能看到某商业产品后说“这个也可以看看”。如果系统简单粗暴地“最新写入覆盖旧记忆”,还是会有问题——因为有些旧约束只是暂时被搁置,不是被推翻。我后来采用的办法是:不物理删除旧记忆,而是给记忆加“状态”字段,active表示当前有效,superseded表示被新记忆替代但仍然可追溯。写入新记忆时,把同subject的旧记忆标记为superseded,检索时默认只召回active记忆。同时,Agent在最终回答之前会做一个简单的“记忆自检”步骤:如果召回的多个记忆之间存在明显矛盾,就把这些矛盾点一并呈现给大模型,让它结合当前问题判断用哪一条,必要时主动向用户求证。这个设计虽然多一次模型调用,但对客服类场景特别值钱,能避免自作主张的翻车。5.3 隐私与删改问题:向量库里删一条记录有多贵如果做的是C端产品,用户有一天要求“删除我的所有数据”,你的动态记忆系统得能快速响应。向量库删数据本身不复杂,按主键删除即可,但有两个坑。一个坑是embedding本身不可逆。即便删了原始记录,embedding向量仍然保留着语义痕迹,严格来说无法彻底“遗忘”。纯合规视角下,如果隐私要求极高,可能需要连embedding一起删除,并考虑索引碎片的清理。这个讨论有点复杂,但在产品层面至少要做到底层数据可删、索引可重建。另一个坑是缓存和日志。如果记忆在被删除之前已经进入了某些对话的历史响应,这些信息残留在服务日志、模型调用记录里,单靠删库是清不干净的。我的建议是:在系统设计之初就约定“记忆数据只作为运行时上下文,不做长期日志落库”,避免后续疲于应付删改请求。6. 实测效果与后续扩展:从“能记住”到“记得准”6.1 一组针对会话型场景的实测数据我在某个模拟会话式导购场景里,跑了约两周的动态记忆版Agent,对比之前的静态RAG方案,结果很有参考意义。最初用静态RAG方案时,用户多轮对话成功率大概在六成左右,经常出现“前面刚确认过预算,后面推荐超预算方案”的情况。换成Agent+Milvus动态记忆后,这类同主题一致性错误大幅下降;连续对话场景下,助手对用户偏好的命中率接近九成。数据只是参考,但它验证了一件事:动态记忆解决的其实不是大模型“变聪明”的问题,而是“信息在不同轮次之间保真传递”的问题。模型还是同一个模型,但因为每一次决策都能基于历史约束,整体表现自然就上去了。6.2 再往前走一步:分层记忆TTL、知识图谱融合与多用户隔离这套架构跑顺之后,我考虑过几个扩展方向,也建议你按这个顺序去升级。第一是更细致的TTL策略。现在很多记忆是统一过期策略,其实可以分类型:用户画像类记忆有效期最长,项目事件类记忆跟随项目周期,临时意图类记忆只存活几分钟。目前Milvus在标量字段上做过期标记并不难,真正需要投入的是确定各类记忆的合理生命周期。第二是与知识图谱融合。向量记忆擅长相似语义召回,但在“实体之间的关系链”上比较弱。比如“用户A关联的项目B使用了方案C,而方案C与用户偏好冲突”,这需要图结构才能推出来。我目前的做法是把关系型关键信息额外维护成三元组,只在需要多跳推理时启用,日常单跳问题还是走向量库。第三是多用户隔离。前面代码里已经体现了按user_id过滤,但真做多租户还得考虑collection隔离还是数据隔离。小规模场景一个collection加过滤字段就够;用户量上来后,更推荐按租户拆collection,避免查询性能互相干扰。6.3 最后分享几点让我少走弯路的心得走到这里,这套“Agent+Milvus”的动态记忆方案基本成型了。最后分享三条从实际部署中总结出的经验。一,不要一上来就追求复杂。先用一个最小闭环:对话结束异步抽取记忆、写入向量库、下次提问带过滤召回。跑通再逐步加冲突检测、过期归档、记忆自检。很多人一开始就设计五六个模块,结果调了半个月还在原地。二,评估指标一定要包含“记忆是否被正确复用”。不要只看最终回答是否合理,要单独统计“系统召回的记忆里有多少是真正相关的”。我把这个概念叫做“记忆采纳率”,它才是动态记忆系统本身的质量指标。三,embedding模型值得单独投入时间。向量库和索引参数能调的空间有限,真正决定召回质量上限的是embedding模型对中文长句、口语化表达的支持程度。预算允许的话,用领域微调过的embedding模型,效果提升非常明显。动态记忆不是一个神奇开关,打开就万事大吉。它更像一个长期维护的数据工程问题:要想让助手“懂你”,就得让它有写入、有检索、有遗忘,而这些每一环都需要认真设计。希望这篇内容能帮你少踩一些我踩过的坑,把AI助手从“查得着资料”真正推到“记得住用户”。
被静态知识库折腾过的人,一定懂那种感觉:文档喂了几百份,问答效果还是“看似会答,实则空洞”。问文档里有的东西,答得还行;一旦用户问题换了个说法,或者需要结合上一次聊天的上下文,它就原形毕露——只会从固定知识库里搜索拼接,完全没有“记性”。我这次想聊的,不是又一份eRAG(增强检索生成)教程,而是把 Agent 和 Milvus 向量数据库搭在一起,给AI助手真正装上“动态记忆”的过程。这套思路不只在做文档问答,而是让助手能自己判断“哪些信息值得记住、哪些该忘掉、哪些需要跨会话复用”,从一个只会读资料的问答机器人,变成一个“越用越懂你”的长期陪伴型助手。如果你正在做智能客服、私域运营助手、个人AI知识管家这类项目,这篇内容大概率对你有用。1. 静态知识库的天花板:为什么“喂再多文档”也救不了助手先定义一下我口中的静态知识库:把一堆文档切块、向量化,导入向量数据库,用户提问时就做一次相似度检索,再把最相关的若干个片段丢给大模型生成答案。这是目前绝大多数RAG应用的默认形态,也是大部分人理解中的“知识库问答”。它确实解决了“大模型不掌握私有知识”的问题,但解决不了另外三个关键问题。1.1 知识时效性的死结文档切块入库之后,这个知识库就“定格”了。今天导入了产品手册,明天产品功能升级,后天价格调整,如果没有重新跑一遍拆分和嵌入的流程,助手回答的永远是旧信息。有人会说不就是重新导入吗?可现实项目里,知识库动辄几万个片段,每次全量重建嵌入的时间和成本都不小,更别提很多团队连“哪个文件更新了”都难定位。我在之前某项目里就看到过这样的尴尬:助手被问到新版本是否支持某项功能,它拿着旧手册信誓旦旦说支持,客户照做后翻车,最后一堆人排查了半天,才发现知识库三个月没更新。1.2 会话型需求才是真正的“记忆黑洞”静态知识库的另一个硬伤,是它的检索上下文完全来自“用户当次提问”。它不关心用户昨天问过什么、上次反馈过什么偏好、之前是否已经否定了某个方案。举个很现实的例子:一位用户昨天让助手帮忙筛选数据库选型方案,明确说了“不考虑需要额外购买License的商业产品”。今天他再问“帮我推荐几个数据库”,静态RAG会老老实实地把文档里所有主流数据库都列出来,包括他昨天已经排除的商业产品。助手完全“失忆”,用户感受到的就是:这AI根本不懂我。1.3 静态RAG为什么解决不了上下文个性化有些团队会试图通过拼长上下文来缓解这个问题,比如把历史对话全部塞进大模型上下文窗口。这种方法短期内有效,但窗口再大也有上限,对话一长,要么截断、要么费用暴涨、要么模型被无关历史带偏。更关键的是,历史消息里真正重要的往往是“用户偏好”“待办事项”“关键约束”这类结构化记忆,而不是一堆原始对话流水账。这也是我后来转向 Agent + 向量数据库这套架构的直接原因:我们需要的不只是一个“能查资料的搜索引擎”,而是一个能对记忆进行写入、更新、召回、遗忘的“记忆管理系统”。而向量数据库在其中扮演的角色,远不止存几个embedding这么简单。动态记忆本质上是把“记忆”当成一种可以持续写入、检索、重组的数据资产。这句话听起来抽象,拆开了就三条:每个对话里抽取出值得长期保留的事实片段,写入记忆库;用户每次提问时,除了查静态文档,还要查“历史记忆”,把相关的过往结论一起交给模型;记忆有生命周期,会过期、会被修正、会被覆盖,而不是只增不减。这三条对应到技术上,就是Agent负责“判断什么值得记”,Milvus这类向量数据库负责“存得下、查得快”,而大模型负责综合静态知识与动态记忆生成答案。下面我把这套架构拆开讲。2. 动态记忆的分层模型:Milvus在Agent里的真实位置很多人一听“动态记忆”就去翻专门的记忆框架,其实没必要。只要理解了分层的思路,用通用技术栈完全能搭出来。我习惯把助手的记忆分成三层:短期工作记忆、长期情节记忆、语义偏好记忆。2.1 把记忆分成短期、长期和语义记忆短期工作记忆对应的是当前会话内需要随时引用的信息,比如用户刚才说“我预算大概20万以内”。它通常不落地到向量库,放在Agent的会话状态里就好,属于“用完即走”的临时变量。长期情节记忆对应的是跨会话的关键事件,比如“用户上次确认了登录模块用统一身份认证方案”“用户已经否决过某款云服务”。这类信息时间跨度大,不立即生效于当前所有问题,但会在后续决策中反复用到。这类记忆最适合向量化存储。语义偏好记忆则更稳定,比如“用户团队以Java为主”“用户偏好开源方案”“汇报要求带成本对比表”。它类似于“画像”,会在几乎所有交互中影响Agent的风格和推荐倾向。在设计上,我不建议把所有记忆塞进一个collection里。混在一起会让检索噪声变大、权限控制变难。更合理的做法是按类型拆collection,或者至少用标量字段区分memory_type,后面检索时强制过滤。这个细节直接决定了召回质量,后面调优部分还会细讲。2.2 Milvus负责的部分:向量化、索引与可回溯检索把记忆抽象成“记忆碎片”之后,下一个问题是谁来管这些碎片。有人会图省事,直接用临时文件或者Redis里的数组来存embedding,几百条记录时确实能跑,但一旦到了几万条、几十万条,并且要支持多租户、标量过滤、实时写入,就完全不是一回事了。这轮我选择Milvus,原因有几个:第一是它把向量检索和元数据管理放到了同一套体系里。记忆碎片不仅是一个embedding数组,还伴随用户ID、场景标签、时间戳、记忆类型、来源会话ID等标量字段。Milvus可以在这套结构上直接做“标量过滤 + 向量相似度检索”,不需要在业务代码里自己维护两套索引再合并。第二是它的索引类型完善,尤其HNSW和IVF系列在中小规模数据上表现稳定,兼顾召回率和查询延迟。对记忆场景来说,通常在百万级以内,HNSW基本是首选,后面给参数建议。第三是支持写入后近实时可见。动态记忆要求“本轮对话结束后写入的内容,下一轮提问最好能立刻被召回”,Milvus的可调一致性级别正好覆盖这个需求,不至于为了等数据可见而阻塞对话流程。2.3 写入路径与读取路径的分离设计真正让记忆系统变复杂的,不是某一条记忆的存取,而是写入与读取两条链路互不干扰。写入链路发生在每轮对话结束后。Agent拿到完整对话,先做信息抽取:哪些是新事实、哪些是对旧事实的修正、哪些只是寒暄不值得记。抽取出来的结果经过摘要压缩、实体归一化,再生成embedding写入Milvus。这个过程不需要用户等待,属于异步任务。读取链路发生在用户提问时。Agent把当前问题embedding化,先到记忆库检索相关历史记忆,再结合静态知识库结果,组合成上下文交给大模型。读取链路对延迟敏感,通常控制在200毫秒以内,所以要把写入和读取分离开,避免写入时的索引构建拖慢检索。我当时在架构图里画得很简单:一条异步写入管道,一条同步读取管道,中间用Milvus连接。看起来简单,但真正落地时,两条链路各自都有不少细节要注意,下面从写读两侧分别展开。3. 从零搭建带记忆的Agent:摄入、写入与召回3.1 选型:为什么用Milvus而不是旁路搞个HashMap这里先给我的选型逻辑。如果你只是做个Demo,用个Python列表或者Redis都行,但真实项目一定会遇到几个逃不掉的问题:数据量增长后检索效率下降、需要按用户隔离数据、需要按时间或类型过滤、需要高频写入的同时还要保证查询稳定。HashMap或简单Redis方案在这些需求面前会迅速变成“手写数据库”项目,光维护成本就够呛。Milvus在这条路上的优势是:向量存储、索引构建、标量过滤、动态schema都原生支持,而且开源版部署门槛不算高。对Agent场景来说,我们不需要分布式集群,单机模式加上合适的索引参数,就足够支撑中小型应用。另外一个细节选型要点:embedding模型和向量库的维度要提前定好。我自己踩过坑,一开始用了某个模型的768维向量,后来想换一个效果更好的1024维模型,结果要么重建collection,要么在兼容性上费一番功夫。建议初期就选定一个主用embedding模型,维度、距离度量(一般用余弦距离)都固定下来,后续只升级模型版本并做“新旧向量共存 + 离线迁移”。3.2 记忆写入链路:对话摘要、实体抽取与向量入库写入链路是动态记忆系统的“输入闸门”,做得不好,后面检索再快也白搭。我会给每轮对话加一个异步的“记忆归纳”步骤,核心动作有三步。第一步是抽取核心记忆项。最简单的方式是让大模型按固定JSON模板输出记忆候选,比如:[ { "memory_type": "preference", "subject": "数据库选型", "content": "用户明确排除需要额外购买License的商业数据库", "importance": 0.9, "expire_at": "2025-12-31" }, { "memory_type": "event", "subject": "登录模块", "content": "用户确认采用统一身份认证方案,预计下月实施", "importance": 0.8, "expire_at": null } ]这个“半结构化抽取”比直接把整段对话切块入库聪明得多。它能去掉重复废话,只留关键约束和决策,而且后续做冲突检测时,有subject字段可以精确关联。第二步是生成embedding并入库。这里我建议对content做embedding,而不是对subject做。原因很简单:用户下次提问时,表达方式大概率不会和subject完全一致,但语义上会和content更贴近。入库时记得把user_id、memory_type、created_at、source_session_id这些标量字段一起写进去,后面按用户隔离和按时间过滤全靠它们。第三步是同步处理记忆冲突。比如用户这轮说“预算调整为30万以内”,而库里已经有一条“预算20万以内”。直接append会让检索同时召回两条矛盾记忆,模型就会迷糊。冲突检测的简单方案是:按subject字段先搜索旧记忆,存在则做更新或标记过期,而不是无脑插入新记录。这个逻辑早期可以放进同一个大模型调用里完成,不一定要单独搞复杂的规则引擎。3.3 记忆召回链路:相关性检索与Agent工具调用召回链路的核心任务是:在用户提问时,从记忆库里找出“和当前问题相关且靠谱”的旧记忆,并把它们组织成可用的上下文。我建议把这步设计成Agent的一个工具调用,而不是直接把所有记忆都塞进prompt。例如,在Agent的function列表里加上两个工具:search_user_memory(query, memory_type, user_id, limit)search_document_knowledge(query, collection, limit)大模型会根据用户问题判断该调哪个工具、以及怎么组合。为什么要这样设计?因为直接无差别召回往往会让模型被不相关的旧记忆干扰,而 If the 系统性调用,可以为问题归类,避免额外噪声。比如用户问“MySQL兼容性如何”,Agent应该优先检索文档知识,而不必把“用户偏好开源”这类画像记忆也拉出来。召回策略还有个细节:记忆检索的embedding可以做一些轻量改写。用户口语化提问“上次说那个数据库选型排除了什么来着?”直接做向量检索可能效果不理想,因为句式太绕。可以在召回前让大模型把问题转成“查询关键词”或“陈述性查询”,比如转成“数据库选型中被排除的方案”,再去检索,命中率会明显提升。3.4 一段可复现的最小化代码骨架下面给一段用Python实现的最小写入/召回逻辑,方便理解整体流程。这个示例做了简化,不依赖具体Agent框架,只演示Milvus和embedding模型的核心配合。from pymilvus import MilvusClient, DataType client = MilvusClient("memory.db") schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False) schema.add_field("pk", DataType.VAR_CHAR, is_primary=True, max_length=64) schema.add_field("user_id", DataType.VAR_CHAR, max_length=64) schema.add_field("memory_type", DataType.VAR_CHAR, max_length=32) schema.add_field("subject", DataType.VAR_CHAR, max_length=255) schema.add_field("content", DataType.VAR_CHAR, max_length=2048) schema.add_field("importance", DataType.FLOAT) schema.add_field("created_at", DataType.INT64) schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=768) index_params = client.prepare_index_params() index_params.add_index( field_name="embedding", index_type="HNSW", metric_type="COSINE", params={"M": 16, "efConstruction": 100} ) client.create_collection("user_memory", schema=schema, index_params=index_params) def write_memory(user_id, memory_type, subject, content, importance, embedding): row = { "pk": f"{user_id}_{subject}_{int(time.time()*1000)}", "user_id": user_id, "memory_type": memory_type, "subject": subject, "content": content, "importance": importance, "created_at": int(time.time()), "embedding": embedding, } client.insert("user_memory", [row]) def recall_memory(query_embedding, user_id, memory_type=None, top_k=5): expr = f"user_id == '{user_id}'" if memory_type: expr += f" and memory_type == '{memory_type}'" res = client.search( collection_name="user_memory", data=[query_embedding], filter=expr, limit=top_k, output_fields=["content", "subject", "memory_type", "importance", "created_at"], ) return res从这段骨架出发,再接入Agent的function calling就很自然了。关键是要理解:搜索时的过滤条件和索引设计必须在建表时就想好,否则后面加字段、改类型都要重建collection,线上项目会非常折腾。4. 调优不是玄学:标量过滤、一致性、索引参数这三板斧跑通Demo只是开始。真正要上线上环境,或者要在评测里拿高分,你得把三个细节调明白。很多人觉得向量数据库调优是个黑盒,其实不是,核心就三件事。4.1 标量过滤:千万级向量里“先筛后搜”的威力记忆库和通用文档库最大的不同,是它天然带着很多强过滤条件:user_id、memory_type、expire状态。如果不做过滤,每一次检索都在整个库里跑,不仅慢,而且容易把别人的记忆或者已经过期的记忆捞出来。Milvus里的filter可以像SQL一样组合表达式,比如:user_id == 'u_1001' and memory_type == 'preference' and expire_at > now这里有一个性能技巧:user_id这类高基数、高频过滤的字段,建议单独建Inverted Index或使用支持filtering的索引类型,否则过滤本身会成为瓶颈。我在实测中发现,同一个collection,给user_id加上标量索引之后,过滤查询延迟能从300毫秒降到50毫秒左右,提升非常明显。还有一点容易忽略:先过滤再向量检索和先向量检索再过滤的结果在精排上有区别。Milvus支持在向量检索时直接带filter,它会先做向量候选集的粗筛,再根据scalar条件过滤。想要更稳,可以把filter写成强条件,配合较小范围的候选集参数,确保返回的都是合法记录。4.2 索引参数:HNSW的M值与efConstruction到底怎么设HNSW是目前中小数据量下最稳妥的索引类型。它的核心参数有两个:M和efConstruction,查询时还有ef参数。M是每个节点的最大连接数。M越大,图越稠密,召回率越高,但内存占用和构建时间也越高。我的建议是16到32之间,数据量在百万级以下选16足够。efConstruction是建图时的动态候选集大小。越大建图越慢,但图质量更好。100到200是个甜点区间,太低了召回率掉得厉害。查询时的ef是每次检索的候选集大小。它不参与建索引,在每次查询时控制。线上建议从64开始,根据延迟和召回率调优。不要一上来就追求100%召回率,那是离线评测的事。在线场景下,90%以上召回率配合低延迟往往体验更好。我自己通常把目标设定为:p95 延迟在80毫秒以内,召回率不低于95%,在这个前提下尽量调小ef。4.3 一致性级别:写入后多久能被查到动态记忆要求“刚记住的内容很快能用上”,但Milvus这种分布式架构里,写入和查询可能落到不同节点,默认强一致性会带来额外延迟。我看到大多数项目都选Bounded一致性或Eventually,即可在几十到几百毫秒内看到新增记录。实际建议是:记忆写入本身不是高频操作,平均每秒几十条已经算很多了,所以不必为了吞吐牺牲一致性。我一般用Bounded,把staleness控制在2秒以内,既不会让用户体验到“刚说完就忘了”,也不会让写入链路变慢。这里还有一个经验:如果对话结束后的记忆写入和用户的下一轮提问几乎同时发生,偶尔会出现“这轮没检索到上轮记忆”的情况。缓解办法是在前端对话状态里带上最新几轮的关键提取信息,而不是完全依赖向量库即时查询。5. 真正麻烦的是“遗忘”和“冲突”:记忆管理的实战避坑很多人觉得动态记忆最难的是写入和检索,真做起来以后你会发现,最难的是“如何让系统不越记越乱”。记忆不是越多越好,甚至很多时候,“懂得忘”比“记得牢”更关键。5.1 过期记忆不会自己消失,需要衰减与归档用户偏好会变、项目状态会推进、临时约定会过期。如果所有记忆一视同仁永久存储,召回时就会频繁出现“过时信息误导答案”的情况。我的做法是给每条记忆加三层控制:显式过期时间expire_at。比如“用户下月进行系统切换”这类临时性记忆,到点就自动标记失效;活跃度衰减。每次被成功召回时,给last_access_at更新;长时间未被访问的高龄记忆,逐渐降低检索权重;定期归档任务。每天或每周扫描一次,将过期且低价值的记忆从主collection中移除或迁移到冷存储。这样设计的目的很简单:让高频使用的记忆始终保持在检索质量最好的区域,不让冷门旧数据干扰新决策。5.2 同主题多轮冲突,不能只靠“最新优先”用户的偏好不是一成不变的。今天说“优先考虑开源”,明天可能看到某商业产品后说“这个也可以看看”。如果系统简单粗暴地“最新写入覆盖旧记忆”,还是会有问题——因为有些旧约束只是暂时被搁置,不是被推翻。我后来采用的办法是:不物理删除旧记忆,而是给记忆加“状态”字段,active表示当前有效,superseded表示被新记忆替代但仍然可追溯。写入新记忆时,把同subject的旧记忆标记为superseded,检索时默认只召回active记忆。同时,Agent在最终回答之前会做一个简单的“记忆自检”步骤:如果召回的多个记忆之间存在明显矛盾,就把这些矛盾点一并呈现给大模型,让它结合当前问题判断用哪一条,必要时主动向用户求证。这个设计虽然多一次模型调用,但对客服类场景特别值钱,能避免自作主张的翻车。5.3 隐私与删改问题:向量库里删一条记录有多贵如果做的是C端产品,用户有一天要求“删除我的所有数据”,你的动态记忆系统得能快速响应。向量库删数据本身不复杂,按主键删除即可,但有两个坑。一个坑是embedding本身不可逆。即便删了原始记录,embedding向量仍然保留着语义痕迹,严格来说无法彻底“遗忘”。纯合规视角下,如果隐私要求极高,可能需要连embedding一起删除,并考虑索引碎片的清理。这个讨论有点复杂,但在产品层面至少要做到底层数据可删、索引可重建。另一个坑是缓存和日志。如果记忆在被删除之前已经进入了某些对话的历史响应,这些信息残留在服务日志、模型调用记录里,单靠删库是清不干净的。我的建议是:在系统设计之初就约定“记忆数据只作为运行时上下文,不做长期日志落库”,避免后续疲于应付删改请求。6. 实测效果与后续扩展:从“能记住”到“记得准”6.1 一组针对会话型场景的实测数据我在某个模拟会话式导购场景里,跑了约两周的动态记忆版Agent,对比之前的静态RAG方案,结果很有参考意义。最初用静态RAG方案时,用户多轮对话成功率大概在六成左右,经常出现“前面刚确认过预算,后面推荐超预算方案”的情况。换成Agent+Milvus动态记忆后,这类同主题一致性错误大幅下降;连续对话场景下,助手对用户偏好的命中率接近九成。数据只是参考,但它验证了一件事:动态记忆解决的其实不是大模型“变聪明”的问题,而是“信息在不同轮次之间保真传递”的问题。模型还是同一个模型,但因为每一次决策都能基于历史约束,整体表现自然就上去了。6.2 再往前走一步:分层记忆TTL、知识图谱融合与多用户隔离这套架构跑顺之后,我考虑过几个扩展方向,也建议你按这个顺序去升级。第一是更细致的TTL策略。现在很多记忆是统一过期策略,其实可以分类型:用户画像类记忆有效期最长,项目事件类记忆跟随项目周期,临时意图类记忆只存活几分钟。目前Milvus在标量字段上做过期标记并不难,真正需要投入的是确定各类记忆的合理生命周期。第二是与知识图谱融合。向量记忆擅长相似语义召回,但在“实体之间的关系链”上比较弱。比如“用户A关联的项目B使用了方案C,而方案C与用户偏好冲突”,这需要图结构才能推出来。我目前的做法是把关系型关键信息额外维护成三元组,只在需要多跳推理时启用,日常单跳问题还是走向量库。第三是多用户隔离。前面代码里已经体现了按user_id过滤,但真做多租户还得考虑collection隔离还是数据隔离。小规模场景一个collection加过滤字段就够;用户量上来后,更推荐按租户拆collection,避免查询性能互相干扰。6.3 最后分享几点让我少走弯路的心得走到这里,这套“Agent+Milvus”的动态记忆方案基本成型了。最后分享三条从实际部署中总结出的经验。一,不要一上来就追求复杂。先用一个最小闭环:对话结束异步抽取记忆、写入向量库、下次提问带过滤召回。跑通再逐步加冲突检测、过期归档、记忆自检。很多人一开始就设计五六个模块,结果调了半个月还在原地。二,评估指标一定要包含“记忆是否被正确复用”。不要只看最终回答是否合理,要单独统计“系统召回的记忆里有多少是真正相关的”。我把这个概念叫做“记忆采纳率”,它才是动态记忆系统本身的质量指标。三,embedding模型值得单独投入时间。向量库和索引参数能调的空间有限,真正决定召回质量上限的是embedding模型对中文长句、口语化表达的支持程度。预算允许的话,用领域微调过的embedding模型,效果提升非常明显。动态记忆不是一个神奇开关,打开就万事大吉。它更像一个长期维护的数据工程问题:要想让助手“懂你”,就得让它有写入、有检索、有遗忘,而这些每一环都需要认真设计。希望这篇内容能帮你少踩一些我踩过的坑,把AI助手从“查得着资料”真正推到“记得住用户”。