
“Agent 记忆”项目登顶——这几个月你只要刷技术社区大概率能看到这个词反复出现。作为一个从去年就开始折腾 AI Agent 的人我和很多同行一样一开始对“给 Agent 加记忆”这事是持怀疑态度的。毕竟市面上大多数所谓“记忆功能”无非就是把对话历史塞进上下文窗口堆多了要么烧钱要么让模型犯迷糊。但这次这个项目能在热度榜上待这么久靠的显然不是这种初级玩法。它本质上是把人类记忆机制里的分层、遗忘、强化逻辑搬进了大模型系统里让 Agent 能真正“记住该记住的忘掉该忘的”。对正在做 Agent 开发、想把产品从“演示级”推向“生产级”的朋友来说这套思路非常有参考价值。这篇文章我就把项目的核心设计、技术选型、关键参数调优和我在实际复现中踩过的坑一次性讲清楚。1. 整体设计核心先想清楚“记忆”到底要解决什么问题1.1 问题原点大模型天生没有记忆先说一个最基础但也最容易被忽视的事实大型语言模型本身是无状态的。你每一次调用接口模型看到的都是干干净净的上下文它不记得十分钟前和你聊了什么更不会在没有提示词的情况下去主动关联你上周提过的某个需求。这在闲聊场景里无所谓但在真正的业务系统里就是致命伤。举个例子我做过一个客服 Agent用户第一次进来报了自己的设备型号和故障码第二次又来问“那我现在该怎么办”如果 Agent 没有记忆它会把用户当陌生人要求重新描述问题。这种体验用户一次就想卸载。所以我们要做的“记忆”本质上是给无状态的模型外挂一个带有持久化、检索、更新能力的存储系统。这和给程序加缓存、加数据库是一个逻辑只不过存储的粒度不是结构化的用户ID而是语义化的“对这个用户的理解”。1.2 方案选型为什么记忆必须分等级做 Agent 记忆最忌讳的事情就是“一锅烩”。把短期对话、长期偏好、任务进度全部塞进同一个向量库里检索的时候要么噪声太大要么重要信息被淹没。这次项目里采用的是一个很经典的“三级记忆”架构工作记忆、情景记忆、语义记忆。工作记忆对应的是当前会话上下文相当于人类的“脑子里的暂存区”容量小、时效短但读取速度必须最快。通常在工程实现上用缓存Redis 或者干脆就是进程内缓存来存配合 token 窗口管理确保每次请求的上下文里只有当前任务最相关的信息。情景记忆对应的是“某时某地发生了什么事”比如用户上次投诉了哪个功能、某次对话里用户透露了自己的职业背景。这类记忆以事件为单位存储带有时间戳属于最原始的记忆素材。语义记忆则是从情景里抽象出来的、关于用户长期不变的画像结论比如“用户是后端工程师倾向于简洁的技术回答反感过度营销”。为什么这么分因为它们的生命周期和检索策略完全不同。情景记忆会快速过期、需要定期压缩语义记忆则要长期保存、优先级最高工作记忆则是实时变化的。如果你不区分这些差异用一套统一逻辑去处理最后一定是短期的干扰长期、长期的淹没短期。1.3 项目登顶的关键判断记忆不是越大越好这个项目能在热榜上站稳我私下复盘了很久觉得最核心的判断是那句在很多相关讨论里反复出现的话记忆信息 × 有效时间。它没有痴迷于无限延长上下文而是把力气花在“如何高效遗忘”和“如何精准唤起”上。这其实扭转了很多人的直觉。过去大家总觉得能让 Agent 记住更多细节就是更强的能力。但真实情况是拖垮 Agent 的不是记忆太少而是记忆太杂。模型在生成时面对一堆历史片段需要自己分辨哪些重要哪些次要这既浪费 token又容易把注意力引到无关细节上。所以这个项目的核心架构很早就从“记忆存储”转向了“记忆治理”把遗忘机制、权重衰减、信息压缩当成一等公民来设计。这一点在后面的实操部分我会展开讲因为难度最大也最容易踩坑。2. 记忆的写入与存储怎么让 Agent “真正吃掉”每一次对话2.1 写入触发点不是所有对话都值得记我做第一版记忆系统的时候犯过一个很蠢的错误把所有对话历史全部做向量化然后统统丢进向量数据库。结果就是用户随口说的一句“今天天气不错”都被当成长期记忆存了下来三个月后还在干扰检索。后来我学乖了写入记忆前必须先过一道“重要性过滤”。这个过滤有两条线一条是显式规则比如用户明确表达了偏好“我更喜欢xxx”“以后不要yyy”或者包含关键实体项目名、版本号、日期、金额另一条是模型打分每次对话结束后用一个小模型比如 GPT-4o-mini 或本地 Qwen对当轮对话做一个摘要并打一个 0-10 的重要性分数低于 5 分的直接丢弃不进入记忆库。这听起来会多一次模型调用但实测下来非常值。因为真正进入长期记忆库的数据量会缩小一个数量级后续检索的准确率会显著提升综合成本反而是下降的。要算账的话一次摘要调用可能花费 0.01 元但它省掉的是每次对话时多拉取几百条无关记忆所浪费的 token那个费用才是大头。2.2 存储结构向量 元数据的双轨制记忆写进库里光有向量是不够的。很多人在初学时容易陷入一个误区embedding 了一把梭检索全靠向量相似度。但在实际生产里你迟早会发现需要按时间过滤、按用户过滤、按类型过滤。这些需求纯靠向量是做不到的。这个项目里用的存储结构就是“向量 结构化元数据”的组合。向量负责语义检索元数据负责结构化筛选。每条记忆记录大致长这样记忆IDUUID用户ID记忆类型情景/语义/偏好内容摘要纯文本内容向量embedding重要性分数0-10时间戳写入时间最后访问时间用于衰减计算访问次数用于强化计算实际查询时先按元数据粗筛比如“只查这个用户最近30天的情景记忆”再在筛选结果里做向量检索最后按一个综合评分排序。这一步看似简单但它把向量检索的召回质量提升了一大截。我亲眼见过很多项目上了向量库之后效果还不如用倒排索引多半就是没做元数据这一层。2.3 双网络记忆模型的启发短期通道和长期通道要分开写热搜词里出现了“双网络记忆模型”这个思路值得展开讲讲。传统的单库记忆把所有信息写入同一个地方读取时一把抓。但如果你研究过认知科学就会发现人类大脑的短期记忆和长期记忆走的根本不是同一条通路。受这个启发这次项目在实现上把写入也分成了两条通道短期通道直接记录原始对话片段轻量存储过期时间短长期通道则需要经过“提炼-抽象-再编码”的流程把多条相关短期记忆合并成一条更凝练的语义记忆。举一个我在复现时用得最多的场景。用户连续五天下班后都会问“今天收盘了吗”“我那只基金怎么样了”这五条短期记忆如果单独存每条都价值有限。但把它们放一起压缩就能生成一条非常有价值的语义记忆“用户持有基金关注每日收盘动态”。有了这条记忆Agent 甚至可以在用户开口之前就主动推送相关信息。长期通道的写入尽量不要同步执行而是放到异步任务里做。因为提炼合并需要调模型、可能需要跨会话检索耗时几百毫秒到几秒不等如果放在用户请求路径上TTS 切口就能直接感受到延迟。3. 记忆的提取与遗忘机制核心的“score 时间半衰期”模型3.1 为什么分数必须随时间衰减这是整个项目技术含量最高的部分也是它能在社区引发大量讨论的原因。直接用向量相似度来做记忆召回会有一个明显的问题任何被检索到的记忆只要是语义相近的就会每次都命中而那些虽然重要但不常被提起的记忆会越来越难浮出水面。这其实是“最热记忆陷阱”。比如用户三个月前提过一次自己在筹备婚礼当时那条记忆非常重要但随着时间推移它对当前任务的关联度会快速下降。如果不做衰减这条记忆会永远以高权重出现在检索结果里反而把真正近期重要的信息挤下去。解决方案就是引入时间半衰期。具体的评分公式推导如下先定义记忆的综合召回分 R R S × γ^t B × f(N)S 是记忆写入时的重要性分数0-10γ 是衰减系数范围 0.9~0.99γ 越小衰减越快t 是距离上次访问的时间单位可以是天也可以是小时取决于你的业务节奏B 是基础分通常设为 1 或 2保证记忆即使衰减到很低仍然有被召回的可能N 是该记忆被访问的次数f(N) 是强化函数常见取法是对数函数实际落地时不必在实时查询时动态算指数直接用一个定时任务定期批量更新每条记忆的当前分数存回数据库。查询时直接把分数排序和向量相似度做一个加权和作为最终的召回排序依据。这里有一个关键经验γ 的选择一定要结合业务节奏来测试。如果是 To B 的 SaaS 产品用户信息相对稳定γ 可以设得高一点0.98左右让历史画像能长期生效如果是电商客服这类会话响应及时性要求极高的场景γ 就要低一些0.93~0.95否则三天前的一次咨询记录还占据高权重会严重干扰当下的意图识别。3.2 强化机制被反复使用的记忆权重更高半衰期解决的是“遗忘”但光有遗忘还不够。你会发现有些记忆虽然时间久了但用户每次会话都会涉及这种记忆如果因为时间被衰减掉就非常可惜。所以需要配套一个“强化机制”。每次检索命中一条记忆后就给这条记忆的访问次数 N 加一。配合对数强化函数高频访问的记忆会维持在一个稳定的高分数上只有真正长时间不再被触及的记忆才会逐步沉底。这个设计模仿的是人类记忆的“间隔重复”原理一个知识点被反复提取提取路径就越来越牢固最终变成科隆肌肉记忆。在 Agent 侧被反复引用的用户画像信息就应该长期钉在高层不被时间冲刷掉。实际表现是一个用户反复表现出来的技术栈偏好会越来越容易召回而一次性的偶然事件会快速退出舞台。3.3 记忆冲突与更新旧记忆怎么死还有一个容易被忽视的细节当新写入的记忆和旧记忆发生矛盾时怎么办比如用户之前说“我不用 Java”半年后却说“最近在写 Java 项目”。如果系统不做任何处理两条互相矛盾的记忆会同时存在Agent 生成时就会精神分裂。这个项目里采用的策略是“写时冲突检测 旧记忆降权”。每次新记忆写入前先在已有的语义记忆里搜索语义相似的记录。如果发现冲突就把旧记忆的重要性分数直接打个五折同时给新记忆打一个略高的初始分确保下一次检索时新信息优先。这个策略比较朴素但胜在稳定可控不会出现你辛辛苦苦建好的用户画像被一条新信息连根拔起的极端情况。如果你用的是向量数据库冲突检测就是一个普通的向量相似度查询走索引消耗不大。唯一需要注意的是不要把冲突检测做成同步强依赖建议做成异步。我踩过的坑是刚开始把它放同步链路结果每次写入记忆都要先查一遍库高峰期直接拖慢了响应。4. 实操过程从零落地一个带记忆的 Agent4.1 工具链选型和环境准备如果只是做技术验证完全没有必要一开始就上全套生产级组件。我自己的倾向是本地单机先跑通全链路确认设计合理再考虑加固。基础环境建议这样搭Python 3.103.11、3.12 也行别用 3.9 以下某些新库不支持LLM 接口用 OpenAI 兼容格式的这样可以随时切换不同模型Embedding 模型用 text-embedding-3-small 级别就够不需要上 large向量数据库根据规模决定千万条量级以上选 Qdrant 或 Milvus百万条以内用 Chroma 或者干脆用轻量的 FAISS 先顶着如果元数据筛选压力大建议直接上 PostgreSQL pgvector一张表解决向量和结构化查询我实际复现的时候用的是 PostgreSQL 16 pgvector原因很简单我就一张 mem_table 表里面既存元数据又存向量查询时一条 SQL 搞定不需要维护两个系统的一致性。4.2 落实记忆写入的完整流程这一步我把整个流程拆成五个步骤每一步都有对应的伪代码方便大家直接改造成自己的项目。第一步生成摘要。每轮对话结束后把对话切片发给摘要模型返回一段 100 字以内的结构化摘要。def generate_summary(dialogue_turns: list[dict]) - dict: prompt f 你是一个对话摘要生成器。请提炼以下对话中的关键事实、用户偏好和任务进度。 只输出 JSON格式 {{summary: ..., importance: 0-10}} 对话内容 {dialogue_turns} resp llm.chat(prompt) return json.loads(resp)第二步实体与类型判定。从摘要里识别是否包含用户ID、时间、偏好信号决定这条记忆进入哪个类型通道。第三步生成向量并写入库。用 Embedding 模型把摘要文本转成向量连同元数据一并插入 PostgreSQL。def save_memory(user_id, mem_type, summary, importance, time): vec embedding_model.encode(summary) sql INSERT INTO mem_table (user_id, mem_type, summary, vec, importance, timestamp, last_access, access_count) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) cur.execute(sql, (user_id, mem_type, summary, vec, importance, time, time, 0))第四步触发遗忘任务。写一个定时任务建议 cron 每 10 分钟跑一次对所有记忆统一做一次时间衰减将 score 值更新回数据库。这个任务的核心就是那条衰减公式。def decay_scores(): sql UPDATE mem_table SET score importance * POWER(%s, EXTRACT(EPOCH FROM (now() - last_access)) / 86400.0) WHERE timestamp now() - interval 1 hour cur.execute(sql, (decay_factor,))第五步异步冲突检测。新写入的记忆和已有记忆计算相似度超过阈值时执行旧记忆降权。4.3 查询时的召回排序策略到了查询阶段很多初学者的第一反应直接把用户问题向量化去库里找 top K 相似向量就行了。这个方案在数据量小几百条的时候确实可用但一旦超过几千条效果就会急剧变差。原因很简单向量相似度捕捉的是“语义相近”而不是“对当前任务真正重要”。我现在的做法是双路召回加打分融合。第一路走向量相似度取前 50 条候选第二路走结构化筛选把当前时间窗口内的高分记忆也拉出来即使它们和当前问题语义不那么相近。两路结果合并后去重再用一个轻量的重排序模型cross-encoder综合记忆分、时效性、类型权重重新打分排序取前 5 条放入上下文。这里分享一个我个人的经验值Agent 上下文里真正需要的历史记忆3-5 条足够。超过这个数模型会开始困惑哪些是背景、哪些是当前任务本身。很多时候不是记忆越多越好而是越准越好。4.4 效果评测方法怎么衡量记忆系统好不好很多团队做记忆系统做完了不知道怎么评估就说“感觉变聪明了”。这种玄学验收迟早会在某个刁钻场景里翻车。我自己习惯用一套标准化的评测集来做回归测试。评测集的构建方式是找一个真实场景比如“用户报修设备 后续追问进度”人工构造 20-30 条带“记忆依赖”的任务。每条任务的标准答案是在没有记忆时Agent 无法完成或需要用户重复信息在有记忆时Agent 能直接利用历史信息完成。评测指标用两个记忆召回率Agent 是否成功从记忆库中提取到了那条关键记忆响应准确率Agent 的最终回答是否与人工标注的正确答案一致这个评测集我会长期维护每当调整衰减系数、重排序策略时都要跑一遍回归确保没有把某个场景修坏了。5. 踩坑记录那些文档里不会教你的实战问题5.1 向量 ID 冲突与一致性维护刚开始用 PostgreSQL pgvector 时我直接用了自增 ID 做记忆主键。看似稳妥但后来遇到一个场景两条相同文本的记忆用户隔几天说了同样的话向量一致但语义上其实是重复记录。结果检索时它们相似度 1.0互相纠缠导致召回结果被同一语义占据。解决方式很简单写入前再做一次“近似查重”。如果检索到相似度超过 0.98 的记忆说明是重复内容不再写入新记录而是给已有记录做一次强化访问次数加一。这样不仅省了存储还让系统自动记住了“这是用户反复强调的偏好”。5.2 弹幕播放器里碰到的并发怪象做 Web 端 Agent 的时候我一度遇到了一个让人抓狂的问题用户连续发送多条消息Agent 的记忆写入发生了乱序。A 消息先到B 消息后到但 B 的记忆先写入了库A 的记忆后写入。结果就是记忆的时间戳和实际对话顺序不一致半衰期算法直接错乱近期记忆被错误地判定为旧记忆。排查了半天发现元凶是异步任务的多线程并发。解决方案是给每轮对话打上一个全局递增的 seq 序号写入记忆时以 seq 为准而不是以系统当前时间为准。所有记忆的排序、衰减计算都基于这个 seq而不是 wall clock time。这是非常关键的细节但在很多热门框架里并没有默认处理需要自己留意。5.3 弹性伸缩时候的温度差异最后提一个现象可能会让不少做生产环境的同行感同身受。同一套记忆系统在测试环境跑得很稳但一上生产压测效果就开始抖动。排查后发现问题不是记忆本身而是不同并发度下模型温度的波动带来的输出不稳定。这个听起来和记忆无关但实际影响非常大。记忆系统做的事是“给模型提供更精准的上下文”但最终输出质量还取决于生成模型自身的确定性。如果你在乎 Agent 的稳定性在生产环境一定把 temperature 固定在 0.2 以下或者关闭采样top_p 1。不然你调试半天的记忆召回策略会被随机性掩盖成“玄学失灵”。还有一个比较隐蔽的坑是 Embedding 模型在并发场景下的 batch size 设置。如果你用的是什么本地部署的 Embedding 服务batch size 开太大反而会导致响应时间飙升。建议实测不同 batch size 的 P95 延迟找到你硬件配置下的最优值。这块没有通用参数纯粹属于“你自己的机器只有你自己知道”的那类问题。就我个人的体会而言Agent 记忆这个方向最容易犯的错不是技术实现太难而是很多人一上来就想着做一个“无限记忆”的庞然大物。真正聪明的做法是先想清楚哪些该记、哪些该忘、哪些该强化。这套“score 时间半衰期”的组合拳在绝大多数业务场景里都能用不到五百行代码解决掉百分之八十的记忆需求。最后再分享一个小技巧如果你的 Agent 目前用的是单轮提示词、还没有任何记忆能力不用急着上全套记忆系统。先把用户的偏好、项目背景、历史进度手动写进系统提示词里观察一轮效果。你会发现仅仅是一个简单的记忆注入就已经能让 Agent 的表现上一个台阶。从这一步开始你才会真正理解记忆对于 Agent 来说不只是功能而是灵魂。