下午两点我坐在电脑前准备给 Agent 的记忆系统做个优化。起因是用户反馈说它好像失忆了。我那个 Agent 跑了两个月之前只保留了最近 10 轮对话的上下文窗口超出的直接丢弃。浅层交互够用但用户隔天回来继续之前的话题Agent 就一脸懵。标准做法嘛接个向量数据库。我用 pgvector把每轮对话的摘要做 embedding 存进去检索的时候用 cosine similarity 找 top-5塞回 prompt。思路很直觉教程也都这么教。跑起来之后我做了个测试构造了 30 个隔天回来继续之前话题的场景看 Agent 能不能正确回忆之前的关键信息。每个场景模拟两天的对话第一天用户聊一些个人信息和偏好第二天问一个需要用到前一天信息的问题。比如第一天说我住在北京朝阳区公司在望京第二天问附近有什么好吃的地方推荐。判断标准很简单Agent 回复里有没有正确引用前一天的关键信息。结果召回率 23%。三十个场景里只有七个能正确回忆。我是真没想到会这么低。问题出在哪翻了一下检索结果发现问题比想象的更基本。举个例子用户第一天说我对花生过敏第二天问推荐个零食。理想情况下 Agent 应该回忆起花生过敏这条信息。但向量检索返回的 top-5 是1. 用户喜欢辣味 (cosine: 0.87) 2. 用户之前问过天气 (cosine: 0.84) 3. 用户对花生过敏 (cosine: 0.82) 4. 用户住在北京 (cosine: 0.79) 5. 用户喜欢户外运动 (cosine: 0.77)花生过敏排第三cosine 0.82。如果 Agent 只看 top-3 就漏了。排第一的喜欢辣味跟推荐零食的语义确实更近但对 Agent 做决策来说花生过敏这条信息的优先级显然应该更高。说白了语义相似不等于上下文相关。向量数据库擅长找看起来像的东西但 Agent 需要的是此刻有用的东西。这两个概念之间的鸿沟比我预想的大得多。折腾过程接下来两个小时我做了这些尝试换 embedding 模型。从text-embedding-ada-002换到text-embedding-3-large维度从 1536 提到 3072。召回率从 23% 升到 28%。花了钱提升微乎其微。调 chunk 策略。原来每轮对话一个 chunk改成按话题分组每 3-5 轮对话合并成一个摘要 chunkoverlap 设 1 轮。召回率 31%。好了一点点但问题没质变。加 cosine 阈值。把低于 0.75 的结果过滤掉避免不相关的上下文污染。召回率反而降到 19%因为有些该召回的摘要 cosine 分数本来就不高被误杀了。上 re-rank 模型。用cross-encoder/ms-marco-MiniLM-L-6-v2对向量检索的 top-20 结果做重排序。这步效果最明显召回率升到 41%。但延迟从 200ms 涨到了 1.2s对于一个需要快速响应的 Agent 来说不太能接受。折腾到四点半我坐在椅子上发了会儿呆。去倒了杯咖啡站在窗边想了想。花了两个半小时召回率从 23% 提到 41%翻了一倍不到离能用还差得远。问题可能不在参数调优上而在整个思路的方向就有偏差。向量数据库的本质是做语义相似度匹配调参能改善的是找得更准但没法改变找的逻辑就不对这个根本问题。换个思路我重新想了一下。Agent 的记忆跟搜索引擎的检索不是一回事。搜索引擎要找跟 query 最像的文档Agent 的记忆要找此刻对话需要的上下文。这两件事的匹配逻辑完全不同。向量数据库解决的是前者。那后者呢我想到一个很笨的办法不用向量检索用结构化的 key-value store。让 Agent 自己判断什么信息值得记然后用 key 打标签存下来。检索的时候不做语义匹配直接按 key 精确查找。interfaceMemoryEntry{key:string;// 结构化标签如 user_preference, allergy, locationvalue:string;// 具体内容timestamp:number;// 写入时间source:string;// 来源对话轮次confidence:number;// Agent 自评的置信度 0-1}classAgentMemory{privatestore:Mapstring,MemoryEntry[];privaterecentWindow:Array{role:string;content:string};privatewindowSize:number;constructor(windowSize:number20){this.storenewMap();this.recentWindow[];this.windowSizewindowSize;}// Agent 主动写入——它自己判断什么值得记asyncremember(key:string,value:string,source:string):Promisevoid{constentry:MemoryEntry{key,value,timestamp:Date.now(),source,confidence:0.8,};if(!this.store.has(key)){this.store.set(key,[]);}constentriesthis.store.get(key)!;// 同 key 同 value 不重复写入constexistsentries.some(ee.valuevalue);if(!exists){entries.push(entry);// 每个 key 最多保留 10 条防止膨胀if(entries.length10){entries.shift();}}}// 按需检索——不做语义匹配直接按 key 查recall(key:string):MemoryEntry[]{returnthis.store.get(key)||[];}// 近期对话上下文滑动窗口getRecentContext():Array{role:string;content:string}{return[...this.recentWindow];}// 写入对话到滑动窗口addToWindow(role:string,content:string):void{this.recentWindow.push({role,content});if(this.recentWindow.lengththis.windowSize){this.recentWindow.shift();}}// 构建给 Agent 的完整上下文buildContext(currentQuery:string):string{constrecentthis.getRecentContext().map(m${m.role}:${m.content}).join(\n);// 把所有 key 列出来让 Agent 自己选需要哪些constallKeysArray.from(this.store.keys());constmemoryDumpallKeys.map(k{constentriesthis.recall(k);return[${k}]:${entries.map(ee.value).join( | )};}).join(\n);return## 近期对话\n${recent}\n\n## 长期记忆\n${memoryDump}\n\n## 当前问题\n${currentQuery};}}你可能会说这不就是个 JSON 文件吗。对它就是。但关键不在存储方式在于谁来决定什么值得记。向量数据库的逻辑是所有对话都存检索时按相似度找。问题是 Agent 没有参与什么重要的判断它只是被动地把所有东西塞进向量空间然后祈祷 cosine similarity 能做出正确的排序。我的做法是每次对话结束后让 Agent 自己跑一轮记忆提取判断这轮对话里有哪些信息值得长期保存然后用结构化的 key 存下来。// 对话结束后的记忆提取asyncfunctionextractMemories(agent:AgentMemory,conversation:Array{role:string;content:string}):Promisevoid{constsystemPrompt你是一个记忆管理器。分析以下对话提取值得长期记住的信息。 只提取未来对话可能需要的信息忽略寒暄、闲聊、一次性问题。 输出 JSON 数组每条包含 - key: 结构化标签如 user_preference, allergy, location, project_status - value: 具体内容简洁的一句话 - confidence: 你对这条记忆有用性的评估 0-1 如果这轮对话没有值得记住的信息返回空数组 []。;constresponseawaitllm.chat({system:systemPrompt,user:JSON.stringify(conversation),responseFormat:json,});constmemoriesJSON.parse(response);for(constmofmemories){if(m.confidence0.6){awaitagent.remember(m.key,m.value,turn_${conversation.length});}}}跑一下看看实际输出。用户说了这么一段话我叫张三对海鲜过敏住在深圳南山最近在做跨境电商。记忆提取的输出长这样[{key:user_name,value:张三,confidence:0.9},{key:allergy,value:海鲜过敏,confidence:0.95},{key:location,value:深圳南山,confidence:0.85},{key:project_status,value:正在做跨境电商,confidence:0.7}]四条记忆每条都有明确的 key。下次用户问附近有什么海鲜餐厅的时候Agent 直接按allergy和location两个 key 检索一秒就拿到海鲜过敏和深圳南山。不需要任何语义匹配也不会把喜欢辣味这种不相关的信息翻出来。这套东西跑起来之后我重新跑了那 30 个测试场景。召回率 89%。从 23% 到 89%中间省掉了向量数据库、embedding 模型、re-rank 模型整个系统的依赖少了一半延迟从 1.2s 降到 80ms。记忆提取是异步的不在对话主链路上。什么时候还是得用向量我不想把话说死。向量数据库不是没用是用错了场景。如果 Agent 需要检索大量非结构化文档比如产品手册、FAQ、技术文档那向量检索依然是首选。因为这些内容没法用简单的 key-value 结构化而且语义相似在文档检索场景下确实有意义。但如果做的是 Agent 的对话记忆记住用户说过什么、偏好是什么、项目进度到哪了结构化的 KV store 大概率比向量数据库好使。因为这些信息的检索逻辑是精确匹配不是模糊查找。用户对花生过敏这条信息你要么需要它要么不需要不存在跟花生过敏 87% 相似这种东西。我后来做了个混合方案KV store 管对话记忆向量数据库管文档检索。两套系统各干各的互不干扰。用户偏好、项目进度、历史决策这些结构化的信息走 KV storeAgent 自己提取、自己打标签、自己检索。产品文档和帮助中心这些非结构化的内容走向量数据库用传统的 RAG 管线。两套系统的检索结果在 prompt 组装阶段合并。这个组合在雷达鸭的客服 Agent 上跑了三周没出过一次失忆的问题。雷达鸭那个 App 本身是收录一人公司赚钱案例的客服需要记住用户之前聊过什么方向这套笨办法反而稳得很。反正我现在是信了Agent 的记忆系统简单的东西往往更好使。那些看起来高级的方案不一定适合你的场景。技术选型没有银弹适合的才是好的。你如果也在折腾 Agent 记忆不妨先试试最笨的办法。说不定有意外的惊喜。关于作者老三10 年软件开发经验软件设计师、人工智能应用工程师。专注鸿蒙应用开发ArkTS北向开发与 Web 前端业余探索 AI 自动化。不定期在 CSDN 分享鸿蒙和 AI 方向的技术笔记。本文遵循 MIT 协议转载请注明出处。