
1. 存储和检索早已不是瓶颈真正的坎在写入端做 Agent 记忆系统的这大半年我最大的感受是大部分团队把精力花在了向量数据库选型、召回策略调优、混合检索权重配比这些事上结果项目上线后最头疼的问题根本不是存不进去或者搜不出来而是 Agent 面对用户时该记住的没记住不该记的记了一大堆甚至记进去的东西过两天就变成了干扰源。换句话说存储和检索是书架和图书管理员的问题而真正难的是采编环节——决定一本书值不值得进馆、怎么写摘要、怎么归类、怎么处理内容冲突。先说说为什么存储和检索其实没那么难。以当前主流架构为例记忆的存储层无非是向量库加关系型数据库的混合方案向量库负责语义召回关系库负责精确过滤和时间排序。选型上开源的、托管的、云厂商内置的成熟方案一把抓吞吐量和延迟都有现成压测数据可以参考。检索层更不用说TopK 召回加 Rerank再加上关键词与向量混合的 Hybrid Search效果上限是可预期的。我踩过的坑集中在一个被严重低估的环节记忆的写入编码Encoding。同样一段对话A 方案记下来的是用户说他下周要去上海出差B 方案记下来的是用户出行计划2025-06-15 至 06-18 在上海目的为参加供应商会议关注点包括交通与住宿安排。这两个记忆项在存储层面没有任何区别但后续检索出来的可用性天差地别。一个更直观的类比图书馆买书存储和读者找书检索都有标准流程真正考验馆员水平的是这本书值不值得买、买回来怎么写编目摘要、放在哪个分类下。编目摘要写得潦草书再新也没有人找得到分类归错了检索系统再先进也是白搭。Agent 记忆完全同理——写入那一刻的编码质量决定了记忆在未来的每一次调用上限。所以在聊具体方案之前先把一个认知摆正记忆系统的问题清单里写入端的占比远高于存储端和检索端。后面我会展开讲写入端到底难在哪几个具体问题上以及我们项目里是怎么一步步解决这些问题的。2. 记进去的那一刻发生了什么写入端的完整动作拆解一个完整的记忆写入流程远不止把文本塞进向量库这么简单。我把一次写入拆开来看它至少包含五个连续动作触发Trigger→ 筛选Filter→ 编码Encode→ 融合Merge→ 存储Store。触发是什么时机该写筛选是这段内容值不值得写编码是写成什么形式、结构化到什么程度融合是和已有的记忆冲突怎么写、重复怎么写存储才是最后一步。前四步都在记进去的那一刻完成也是记忆系统里智力密度最高的部分。很多人以为存储是写入的终点其实存储只是收尾动作。真正的技术难点集中在触发、筛选、编码和融合。下面逐个拆。2.1 触发与筛选为什么 Agent 总是该记的不记先说触发。你可能会想触发不就是对话结束后把上下文存下来吗实际操作中远没有这么简单。我在一个客服知识库 Agent 项目里做过实验每轮对话结束都触发一次记忆写入结果三十分钟的会话产生了 47 条记忆项其中有效信息大概只有 6 条其余全是寒暄、重复表达和无关细节。更麻烦的是这些噪声记忆会被后续检索命中把上下文窗口撑爆还拉低了回答的准确率。触发策略至少要区分三种场景每轮对话结束适合极轻量级的记录比如用户明确表达的需求、偏好、时间承诺。任务里程碑节点比如用户完成了一次订单支付、提交了一份表单、确认了一个方案这类事件值得完整沉淀。异常或显著事件比如用户表达了强烈不满、突然改变计划、提供了一份关键文档这类信息需要立即写甚至要触发一次记忆回顾把它和以前的记忆做关联。筛选则是给触发加上一道闸门。我目前用的是一个两阶段的筛选器先用轻量规则把明显无价值的内容过滤掉比如纯语气词、单轮寒暄、重复追问再对通过规则的内容用一个打分模型判断重要性和时效性。重要性高的进入长期记忆时效性强的进入短期工作记忆两者都低的直接丢弃。一个简单但有效的规则示例保留条件满足其一即可: - 包含明确日期、时间、地点、人名、机构名 - 包含用户对产品或服务的偏好、限制、禁忌 - 包含任务状态变化已完成、已失败、已推迟 - 用户情绪标记为负面或强需求 - 包含数字、金额、订单号等实体化信息这条规则看起来平平无奇但它能把无用记忆的写入量砍掉一大半。我们项目里加上这一层之后单次长对话生成的记忆条数从 47 条降到了 11 条检索命中后被采纳的比例明显提升。2.2 编码方式的选择决定记忆好不好用的分水岭筛选之后是编码。这是记进去的那一刻里最考验设计功力的地方也是我觉得可以称得上真正难的一步的核心原因。什么叫编码简单说就是把原始对话文本转换成一条可复用、可检索、可推理的记忆记录。同样的原始素材编码成什么样直接决定了未来它能不能被用上。我用一个具体例子说明。原始对话是用户我下周二下午三点要参加一个线上评审会需要提前准备一份项目进度汇报的 PPT。低质量编码很多项目的默认做法是直接把这句原文塞进向量库。表面上看没毛病语义检索也能命中但实际用起来问题很多过了两周用户问我最近有什么重要会议这条记忆命中了但 Agent 不知道会议结束没有也不知道这件事和当前任务是否相关。用户改口说评审会改到周四了新的记忆写进来旧的还在。检索时两条都可能命中Agent 直接懵了。内容里没有区分事实陈述和用户意图后续想按用户待办事项维度调用时根本筛不出来。高质量编码则是结构化的三层记忆类型: 日程事件 主体: 用户 动作: 参加线上评审会 时间: 2025-06-17 14:00 前置条件: 需要准备项目进度汇报PPT 状态: 待完成 关联实体: 项目X、线上会议平台 优先级: 高这个格式下召回就变得非常精准可以按时间范围查、按状态查、按实体关联查。而且关键信息全是原子化的后续如果时间变更做合并逻辑也容易得多。我在实际中把编码分为三个抽象层次不同层次适合不同类型的记忆层次形式适用场景优缺点原始层对话原文或轻清洗文本短期内需要引用细节的场景信息无损但冗余度高长期占用空间摘要层LLM 生成的段落摘要中长期的情景记忆压缩比高但过度摘要会丢失关键实体结构化层JSON/表格形式的实体与关系需要精确查询和推理的记忆查询能力强但构建成本最高项目里最终采用的是混合编码原始文本存档摘要层负责语义召回结构化层负责精确过滤和推理。三层之间通过记忆 ID 互相绑定查询时先走结构化过滤再走摘要召回最后回原始文本确认细节。这个方案在召回率和精确率之间做到了比较舒服的平衡。2.3 融合与冲突处理记进去之后不互相打架编码完了还有一个很容易被忽略的步骤——和已有记忆做融合。如果不做这一步记忆库很快就会变成一团相互矛盾、互相重复的杂讯集合。举个真实案例。用户在周一说了我周末一般不喜欢被工作消息打扰周五又发消息说周六上午可以安排一个短会但十点后不行。这时候周五这条新记忆不是简单追加而是要对周一那条偏好记忆做例外补充。如果系统只是机械地把两条都存下来没有任何关联关系未来调用时可能上一次回复周六不安排工作下一次又回复周六上午可以开会前后矛盾。融合要做的事包括三件语义去重识别表达不同但意思相同的新记忆合并成一条避免向量库里出现大量高相似度的重复项。时间修正与版本更新同一事件的记忆发生变化时旧记忆要标记为已过期或被替换不能和新记忆并存。关联链接把新记忆和已有的相关记忆建立关系边的连接比如偏好禁止打扰和周六例外之间建立补充关系。融合操作建议在写入后通过异步任务执行不要阻塞主链路。可以给每条记忆加一个merge_status字段经过融合处理后再进入已确认状态。这样即使合并逻辑偶尔出错也不会影响到当次对话的响应速度。3. 为什么存储与检索解决不了写入端的问题一次失败的调优经历可能你还是会想既然存储和检索有那么多成熟方案写入端做得粗糙一点是不是也能靠检索层的优化兜住我一开始也是这么想的结果项目里一次典型的伪优化经历让我彻底改变了看法。3.1 我试图用检索优化来掩盖写入缺陷某个内部项目里Agent 需要在一段长时间的多轮对话中记住用户提到的多个需求并在后续轮次里准确调用。当时的实现很简单每轮对话原文直接入库不做筛选不做编码不做融合。问题很快就出现了——用户在前面对比了三款产品后面问你刚才说哪个更适合我们Agent 总是答不对。第一反应当然是调检索。我把 TopK 从 5 调到 10把相似度阈值从 0.78 降到 0.7加了关键词加权甚至试了换不同向量模型。效果有一点改善但很不稳定有时候能召回正确信息有时候召回的是用户早期的原始讨论记录——因为原始记录里信息混杂根本分不清哪段才是用户最终的结论。这个阶段我算是彻底理解了一件事检索层的天花板是由写入层决定的。如果写入进去的记忆本身就是一团乱麻召回算法再精确也只是从垃圾堆里找相对不那么垃圾的东西。3.2 写入端的成本实际上更隐蔽另一个让我印象深刻的地方是隐形成本——错误的写入会让系统付出远比记下来更高的代价。比如用户明确说不要推荐含花生成分的零食一个缺乏筛选和结构化编码的记忆系统可能把这句话原封不动存了进去。如果后续用户的提问是有什么坚果推荐吗按语义相关性来看这两句话容易被同时召回。可这个时候 Agent 需要额外判断这条记忆是用户偏好还是这条对话的历史内容这本来应该是写入时就已经区分好的。我在项目里统计过写入端做一次结构化编码大概要花 80-120ms 的 LLM 调用时间。听起来不便宜但它能避免的后续问题包括但不限于检索命中噪声拉低回答准确率、多轮对话内自相矛盾、上下文窗口被无效记忆占满、需要频繁重构向量库中的数据。这些代价叠加起来一次编码的算力开销简直微不足道。所以现在的态度很明确宁可写入端多花一点算力把编码做扎实也不要在检索端反复修补。检索端应该解决的是怎么从高质量记忆里快速找到最相关的那一条而不是怎么从烂数据里捞取可用信息这种别无选择的事情。这段经历后来也成了我们团队的一个共识甚至写进了新人入门文档里先检查写入链路再怀疑检索召回顺序反了就是在浪费时间和算力。4. 一套可落地的「写入优先」记忆管线设计讲完理念和踩坑下面给一套我们项目里实际在用的记忆写入管线设计。它的思路总结成一句话就是把写入端当成一个独立的、有状态的、可观测的服务来设计而不是主流程里顺带做的一步。整体管线长这样对话/事件流 │ ▼ 触发判断模块规则 轻量分类模型 │ ▼ 价值筛选模块重要性打分 时效性标记 │ ▼ 编码模块原始文本存储 LLM 摘要 结构化抽取 │ ▼ 冲突检查与合并模块语义去重 版本更新 关联链接 │ ▼ 存储模块向量库 关系库 缓存 │ ▼ 写入后校验检索自测 衰减调度每一层都有它存在的理由也有对应的具体实现参数和指标下面展开讲。4.1 触发与筛选模块的工程配置触发模块不用太复杂我用的是一条规则加两个信号源。规则层面监听对话事件流中的三个事件类型消息结束、任务状态变更、关键实体出现如时间/地点/金额/邮件地址。信号源层面一个是情感分析结果分数低于 -0.3 触发强提醒另一个是用户显式指令比如记住别忘了这类。筛选模块我用了一个很小的打分模型输入是触发模块传过来的文本和消息上下文输出是三个分数重要性 0-1、时效性 0-1、隐私风险 0-1。决策逻辑如下隐私风险 0.8直接丢弃不写入任何存储层。重要性 0.7进入长期记忆存储。时效性 0.7 且重要性 0.4进入短期工作记忆24 小时后自动清理。两者都低丢弃只保留统计日志。隐私风险这个维度容易被人忽略但在面向个人数据的场景里相当重要。所谓隐私风险主要判断内容包括证件号码、银行卡信息、医疗问诊记录、家庭住址等敏感信息。我们的处理原则是如果信息对任务确实必要比如用户主动提交收件地址用于下单那就把地址归档到独立的加密字段中不让它在自由文本的记忆里出现如果与当前任务无关直接丢弃不进记忆。一套下来单条记忆的整体处理耗时压制在 150ms 以内。如果你们对延时敏感可以把 LLM 编码部分移到异步队列对话主链路只做规则筛选和结构化预解析。4.2 编码模板与元数据设计编码模块是整个管线里最有技术含量的部分。我目前的实现是先调用一次 LLM 做摘要压缩再调用一次 LLM 做结构化信息抽取。两次调用采用不同的 Prompt 模板避免一次调用里角色混杂导致抽取质量不稳定。摘要层的模板我按记忆类型分了几种比较常用的是事件类摘要和偏好类摘要。偏好类摘要的 Prompt 更强调记录用户的稳定倾向比如请将以下对话整理成一条用户偏好记忆要求 1. 只保留用户明确表达的倾向、偏好、限制条件。 2. 忽略单次任务中的临时性信息除非它暗示了一种长期习惯。 3. 输出格式为偏好主体 偏好内容 限制条件如有。结构化抽取层的模板则更像填表要求模型输出固定字段。比如一个任务事件类型的记忆字段包括{ event_type: meeting, subject: 线上评审会, start_time: 2025-06-17T14:00:0008:00, timezone: Asia/Shanghai, location: 线上, participants: [], prerequisites: [准备项目进度汇报PPT], status: pending, source_conversation_id: xxx, importance_score: 0.85, expire_at: null }两个抽取结果加一条原始文本共同构成一条完整记忆。原始文本提供证据链摘要层负责语义召回结构化字段负责精确检索和推理。这个三层结构几乎把记忆的可用性从上到下都覆盖了。关于元数据我强烈建议至少包含这几类字段created_at创建时间、updated_at更新时间、source_id来源会话ID、access_count被调用次数、last_access_at最后调用时间。access_count和last_access_at是后续做记忆衰减和遗忘策略的依据一开始不存后面想补就难了。4.3 去重与合并的实操方法去重和合并在项目里踩过的坑最多这里给几条实打实的经验。第一语义去重不能只靠向量相似度。做过实验两句话我平时喜欢喝美式咖啡和我一般喝美式不喝拿铁向量相似度只有 0.61明显不到常规去重阈值但这两条长期并存就会产生矛盾用户之前说不喝拿铁后面万一改主意了系统该怎么判断更稳妥的做法是向量相似度作为召回候选池具体是否算重复用一个轻量 LLM 判断这两条记忆是否在陈述同一件事、且新记忆是否包含修改意图。第二更新旧记忆时不要物理删除旧版本而是标记为 superseded_by。这样既能保留历史轨迹用于审计也能避免向量库里出现残缺引用。第三合并动作必须可回滚。我在设计里给每条记忆加了一个edit_history字段记录每一次合并前后的 diff。一旦后续对话发现合并后的记忆还是有问题可以直接回滚到上一个版本。第四同类冲突要设定明确优先级。比如用户做过最新显式声明那么它的优先级就高于历史偏好推断如果用户自己前后矛盾今天说喜欢 A明天说喜欢 B我们以最近一次为准同时把之前的偏好标记为已改变。这比让 Agent 自己推断哪个更合理要稳定得多。4.4 写入后校验让记进去变成可验证的过程写入后校验是我特别想强调的一个环节因为这相当于给记进去的那一刻装了一个质检器也正好回应了标题里记进去这个关键词——不是写到库里就算记进去了必须验证未来能不能调得出来、调出来有没有用。具体做法很朴素每条记忆写入完成后异步触发一次自测检索。方法是用这条记忆自己的摘要作为查询词去向量库检索 Top5看看正确记忆是否出现在召回列表里以及排序是不是第一。如果没召回或者排名靠后说明这条记忆的编码方式和索引字段有问题需要重新提取关键词、重新生成摘要。这个自测机制能暴露一个很隐蔽的问题写入内容本身可能是好的但 Embedding 后落到了语义空间的错误区域。比如某个只在特定语境下才有意义的记忆它的向量表示可能离其他常见查询非常远导致后续怎么搜都搜不到。自测不通过的记忆会自动打上needs_reindex标记进入二次编码队列由增强模式再做一次摘要和结构化抽取。这个二次编码通过率通常能到 70% 以上剩下的 30% 基本可以确定是原始信息就模糊或不可用直接丢弃比勉强保留更好。这套自测机制上线后记忆召回准确率从 71% 提升到了 86%效果非常显著而且每次新记忆写入后两秒内就能发现问题不用等到用户真的触发了错误回答才发现。5. 长期演进中的三个方向衰减、分层与主动遗忘写入管线的设计稳定之后下一步要考虑的是长期运行带来的问题。记忆不是存得越多越好一个跑了半年的 Agent记忆库里可能有几十万条记录如果不做衰减和分层即使写入编码做得再好检索质量也会逐渐下降。5.1 记忆衰减让不常用的记忆自动退场衰减策略的目的不是删除记忆而是调整记忆的可访问性。我在系统里实现了一个简单但有效的衰减模型每条记忆有一个retrieval_value计算公式大致为retrieval_value importance_score * decay_factor ^ (days_since_last_access / half_life_days)参数解释importance_score是写入时打的分数half_life_days是半衰期天数不同类型记忆取值不同。常被调用的记忆半衰期设得很长比如 180 天偶尔用到的设 30 天短期记忆设 1 天。公式的计算逻辑很简单——时间越久没用过记忆的检索优先级就越低。衰减到阈值以下的记忆虽然还在库里但不再参与默认召回除非用户显式触发深度检索。这样做的好处是既保留了历史记忆的完整性又保证了日常对话中召回的记忆都是高价值、高时效的。我试过不设衰减直接全量召回上下文的噪声比例大概在 20% 左右加了衰减后降到 7%。5.2 记忆分层工作记忆、情景记忆、语义记忆各司其职分层的思想借用了认知科学里的记忆模型但工程实现上没那么玄乎本质上就是按时效性和抽象程度把记忆分成三个存储区。工作记忆区只保存当前会话内部产生的临时上下文会话结束就清空或归档。它对应的是短期不需要持久化的信息。情景记忆区保存的是具体事件和对话经历保留原始细节和摘要层适合按时间轴回溯调用。语义记忆区保存的是从大量事件中抽象出来的稳定结论比如用户偏好、业务规则、长期目标。三层之间不是孤立的。一条进入语义记忆区的结论通常是从多条情景记忆里提取出来的所以需要在语义记忆上记录关联的情景记忆 ID形成结论可溯源的链路。举个例子用户连续三次在项目汇报前提到我需要一份进度表系统可以在第三次时触发语义抽取生成一条用户每逢项目汇报前需要进度表的语义记忆同时关联三次具体事件。之后用户说我下周要汇报Agent 可以基于语义记忆直接给出进度表不需要再翻历史细节。5.3 主动遗忘比记住更需要勇气的设计决策最后一个想说的话题是主动遗忘。这可能是记忆中讨论最少、但也最实用的一块。遗忘不是删除信息而是基于成本和收益的判断把低价值记忆从活跃集里彻底移除。实现上我用了一个基于access_count和last_access_at的简单策略一条记忆在 90 天内没有被访问、access_count低于 3 次就会被标记为可归档状态从向量库的热索引中迁移到冷存储。冷存储里保留原始结构化数据和文本不参与默认语义召回只在显式审计或深度搜索时可查。这个策略上线后的效果出人意料地好。不只是节省了向量库存储费用更明显的是检索质量的提升——因为活跃集里的记忆不再包含大量可能有用但一直没用上的干扰项。我还发现主动遗忘的机制设计反而让记忆系统更像一个会学习的人而不是一台无限堆积数据的录音机这一点对我的启发很大。如果后续有时间我还会在按需回溯被遗忘记忆的能力上继续打磨——留个入口让用户可以显式地找回某些被归档的历史记录。毕竟遗忘是为了更好的调用不是把过去彻底抹掉。6. 写在最后对记忆系统设计的一点个人体会回看这段做 Agent 记忆系统的经历感触最深的一点就是标题里说的那个判断——记忆真正难的一步确实在记进去的那一刻。存储和取用都有大量成熟工具和方案难的是在写入时做好价值判断、抽象编码、冲突处理和可验证性设计。这一层没有做好后续所有关于检索和增强的努力都像是在坏地基上盖精装房。如果你正准备设计一个 Agent 记忆模块我的建议很直接先花时间把写入管线的每个动作列出来从触发到编码到冲突处理一步一步想清楚然后再去纠结向量库选型、召回策略和排序模型。把记忆编码当成一个和生成回复同等重要的设计对象效果会远超你的预期。最后再分享一个实际测试出来的小数据写完这篇文章时我们线上那个 Agent 的记忆系统单次写入耗时约 180ms检索 Top1 准确率稳定在 88% 左右而线上运行时长已经超过了六个月。比起之前全量记录、检索兜底的版本用户的明显感知是它更懂事了。这大概就是对记进去的那一刻这件事价值的最好验证。