上周我自己的一个项目在内部 Agent 应用挑战赛的技术赛道拿了第一。名字就叫Agent 记忆。说是项目其实是一整套给 AI Agent 补记忆的方案短期工作记忆、长期持久记忆、跨会话调用、跨环境迁移全部揉在一起。名次宣布之后私信问技术细节的人比过去三个月加起来的都多大家最关心的问题高度一致你的 Agent 到底是怎么记住我的这个项目能登顶核心不是模型强也不是某个框架用得花哨而是把记忆这件事真正拆开做了。我指的并不是深度学习里那个 LSTM、长短期记忆网络而是 AI Agent 场景下的一套记忆分层架构。简单说我用双网络记忆模型把短期记忆和长期记忆分开管理再用记忆score时间半衰期的设计给每条记忆打分、衰减、遗忘。这套组合拳看起来不复杂但落地时涉及的细节非常多。这篇文章我把自己从 0 到 1 的实现过程完整复盘一遍重点讲设计思路、核心公式、可复现的代码以及一堆只有踩过坑才会知道的注意事项。适合正在做 AI Agent 应用、或者被对话一多就失忆折磨的开发者参考。1. 项目为什么能登顶Agent 记忆是 AI Agent 的刚需1.1 Agent 的失忆问题到底有多痛任何一个做过 Agent 应用的人都体会过这种尴尬用户前面聊了十分钟说清楚了自己是做什么的、有什么偏好、正在跟进什么项目结果新开一个会话Agent 立刻变成陌生人连你刚才说过喜欢简洁回答这种话都接不住。这不是模型能力问题是架构问题。传统的 Agent 通常是无状态地调用大语言模型每次请求把当前对话窗口里的内容全部塞给模型。一旦上下文窗口塞满或者用户开启新会话之前的信息就全没了。用户嘴上不说心里想的是这也太傻了。我做过好几个 Agent 项目最后都栽在这一步功能都能跑但体验永远差一口气。Agent 记忆登顶的原因某种意义上就是这个痛点被彻底解决了。用户在对话中产生的实体、偏好、任务进度会被自动抽取成结构化的记忆条目写入长期记忆库新会话启动时系统会按相关性把这些记忆重新检回来注入到上下文中。整个过程是自动的Agent 不再只是能聊而是记得你。1.2 双网络记忆模型短期记忆与长期记忆的分工我采用的方案不是自己硬拍脑袋想出来的它借用了认知科学里一个非常经典的理论互补学习系统Complementary Learning Systems, CLS。这个理论本来是用来说明人类大脑为什么既有快速遗忘的短期记忆又有几乎永久保存的长期记忆。换成 Agent 的视角就是双网络记忆模型。短期记忆网络负责快速编码当前会话里的信息存储在当前上下文窗口里速度快但容量小会话结束或窗口滚动后就会被覆盖。长期记忆网络负责慢速整合重要的信息写入持久化存储容量大读取时需要经过检索。这两个网络不是完全割裂的。短期记忆里那些反复出现、重要程度高的信息会被定期转写成长期记忆长期记忆被检索出来后会再次进入短期记忆窗口变成当前对话的一部分。这样既避免了什么问题都往长期记忆里塞导致的检索噪音也避免了所有信息都只停留在当前窗口导致的一丢就忘。1.3 项目要解决的三个核心指标需求理清之后我给自己定了三个硬指标后面所有设计都围绕它们展开。第一是命中率。用户说过的关键信息在后续会话中能不能被正确想起来。这不只是检索准不准的问题还涉及记忆抽取时有没有把信息抽全。第二是时效性。用户的偏好是会变的。上个月用户说我喜欢详细的技术文档这个月他改口说直接给我结论。如果记忆系统只记得上个月的话反而帮倒忙。所以记忆必须有时间衰减机制旧信息权重随时间下降。第三是可控性。用户应该知道 Agent 记住了什么也应该能删掉某些记忆。没有这个能力再聪明的记忆系统也会让人没有安全感。实际做下来这三点里最难的反而不是第一个而是第二个。时间衰减的参数设置不好要么记忆永远不更新要么重要信息没几天就被遗忘了。2. 记忆系统的核心设计score、时间半衰期与双网络协同2.1 记忆条目的数据结构所有记忆功能的起点是一条结构化的记忆记录。我最初试过直接用纯文本存记忆比如在向量库里存一长段用户喜欢xxx讨厌xxx。后来发现这种方案检索时太粗糙更新时更痛苦因为没法单独控制某一条偏好的生命周期。最终我设计了一个包含核心字段的结构{ memory_id: mem_8f3a2c1e, user_id: user_12345, content: 用户偏好回报项目进展时先说结论再给细节, memory_type: preference, importance: 0.85, access_count: 12, created_at: 1716537600, last_accessed_at: 1716624000, last_updated_at: 1716624000, half_life_days: 30, embedding: [0.012, -0.034, ...] }字段看着多但每一个都有用。memory_type用于区分偏好、事实、任务进度、用户身份等不同类别的记忆不同类型在检索时的权重不一样。importance是抽取时模型对这条信息重要性的判断。access_count记录被命中的次数一个信息如果经常被用权重应该更高。half_life_days是这条记忆的半衰期决定它衰减的速度。最重要的设计是用memory_id做记忆的唯一标识而不是把内容本身当主键。这样当用户修正自己说过的话时我们可以定位到具体某一条记忆去更新而不是新增一条互相矛盾的内容。2.2 记忆score时间半衰期衰减公式的取舍网上流传的记忆score时间半衰期这句话其实就是我项目里记忆权重计算的概括。完整公式可以拆成两部分。基础权重反映这条记忆有多重要、多常用weight importance * log(2 access_count)log的作用是让访问次数的影响递减访问 10 次和访问 100 次的差距不会太大防止高频出现的琐碎信息把真正重要但低频的关键记忆挤下去。时效权重则是一条随时间衰减的系数decay 0.5 ^ (age_days / half_life_days)age_days是当前时间到last_updated_at的天数。半衰期表示这条记忆的时效权重降到一半需要多少天。最终一条记忆的实时 score 就是两者相乘score importance * log(2 access_count) * 0.5 ^ (age_days / half_life_days)这个公式看起来简单参数却调了很多轮。最初我把所有记忆的半衰期都设成一样的 7 天结果用户身份信息这种应该长期保留的内容没过几天权重就掉了导致 Agent 经常忘了用户是谁。后来改成按memory_type配置不同半衰期用户身份类 90 天项目事实类 30 天偏好类 15 天临时类 3 天。提示半衰期不是越大越好。用户偏好变化很快半衰期太长会让 Agent 坚持用过时的偏好回答问题这比没有记忆更糟糕。2.3 记忆的写入、更新与遗忘机制记忆抽取是写入环节最重要的一步。我在每次 Agent 回复结束后会把用户最近几轮对话发给模型让模型抽取值得记录的信息要求只抽取实体、偏好、任务进展、明确承诺这类内容不抽取寒暄和与用户无关的信息。返回格式用 JSON{ memories: [ { content: 用户负责电商平台的后端架构, memory_type: identity, importance: 0.9 }, { content: 用户当前在优化订单接口的响应时间, memory_type: task, importance: 0.7 } ] }拿到抽取结果后不是直接插入而是先做一轮去重和冲突处理。我会用 embedding 算一下新记忆和已有记忆的相似度。相似度超过 0.92就认为是同一条记忆此时只更新现有条目的last_updated_at、content和access_count。如果新记忆和旧记忆在内容上冲突比如用户之前喜欢详细回答现在说以后别写那么长我会把新内容覆盖旧内容同时把旧记忆的 score 大幅调低而不是立刻删除。遗忘机制很多人忽略但它恰恰是防止记忆库膨胀的关键。我设了一个阈值如果一条记忆的 score 低于 0.05且超过 60 天没有被访问就进入候选清理列表。清理前会先备份到单独的归档表而不是物理删除。这样做的好处是如果未来某个时刻用户突然又问了相关话题系统还能尝试从归档里捞回来。3. 实操落地从零搭建一套可用的 Agent 记忆模块3.1 环境与技术选型这个项目我最终选择了轻量方案Python SQLite sentence-transformers。很多朋友一上来就想上 Milvus、Weaviate 这类专业向量数据库但实际做下来单机应用使用 SQLite 加 numpy 暴力算余弦相似度完全够用。原因很简单个人 Agent 的记忆量级通常在几万条以内暴力计算几万维向量的余弦相似度在这个量级下耗时只有几十毫秒。Embedding 模型我用了开源的BAAI/bge-small-zh中文效果不错向量维度是 512 维计算压力小。整套依赖非常简单pip install sqlite-utils sentence-transformers numpy openai其中openai仅用于调用 LLM 做记忆抽取不参与检索。整个记忆模块不依赖任何重量级框架方便移植。存储层我用的是 SQLite 单表加 JSON 字段。SQLite 的好处是文件级迁移极其方便后面要换电脑或者备份直接拷贝一个.db文件。向量单独放在一个 numpy 数组文件里每次启动时加载进内存。数据量小的场景下这种数据库管元数据内存管向量的混合方案是最省事的。3.2 记忆写入流程的实现记忆写入核心分三步抽取、更新、向量化。我直接贴核心代码这是整个项目中我调试最久的部分。import json import time import numpy as np class MemoryStore: def __init__(self, embedder): self.embedder embedder self.memories [] self.embeddings [] def add_memory(self, content, memory_type, importance, user_id): now time.time() mem { memory_id: fmem_{len(self.memories):06x}, user_id: user_id, content: content, memory_type: memory_type, importance: importance, access_count: 0, created_at: now, last_accessed_at: now, last_updated_at: now, half_life_days: self._half_life_for(memory_type), } embedded self.embedder.encode(content) # 去重 for i, old_emb in enumerate(self.embeddings): sim np.dot(embedded, old_emb) / (np.linalg.norm(embedded) * np.linalg.norm(old_emb)) if sim 0.92: self.memories[i][last_updated_at] now self.memories[i][importance] max( self.memories[i][importance], importance ) self.memories[i][access_count] 1 return self.memories[i][memory_id] self.memories.append(mem) self.embeddings.append(embedded) return mem[memory_id] def _half_life_for(self, memory_type): return { identity: 90, task: 30, preference: 15, temporary: 3, }.get(memory_type, 7)这里有个细节特别提一下去重判断时我把access_count加一了理由是如果模型反复抽取出同一个内容说明它在对话中确实频繁出现这条记忆的权重理应更高。但千万不要因为去重就把created_at重置否则一条老记忆永远都不会被遗忘。3.3 记忆检索与上下文注入的实现检索我用的是混合召回而不是单一向量相似度。只有向量相似度是不够的因为有些重要记忆可能表达方式和当前问题不同向量相似度不高但实际价值很高。混合召回的分数由三部分构成def recall_memories(self, query, top_k5): query_emb self.embedder.encode(query) now time.time() results [] for i, mem in enumerate(self.memories): emb self.embeddings[i] relevance np.dot(query_emb, emb) / ( np.linalg.norm(query_emb) * np.linalg.norm(emb) ) age_days (now - mem[last_updated_at]) / 86400 decay 0.5 ** (age_days / mem[half_life_days]) score ( mem[importance] * np.log(2 mem[access_count]) * decay ) # 综合分数相关性 50%记忆重要性 30%时效性 20% final_score 0.5 * relevance 0.3 * score 0.2 * decay results.append((final_score, mem)) results.sort(keylambda x: x[0], reverseTrue) return [mem for _, mem in results[:top_k]]召回之后还需要把这些记忆组织成对 LLM 友好的文本注入到 system prompt。我使用的模板大概是这个风格以下是关于用户的长期记忆请在与用户对话时遵守 - 用户负责电商平台的后端架构重要度高最近确认于 2 天前 - 用户偏好回报项目进展时先说结论重要度高最近确认于 1 天前 - 当前任务优化订单接口响应时间进行中记忆注入不是越多越好。我实测下来超过 8 条记忆塞进上下文时模型反而开始混乱甚至会把记忆里过时的内容当成当前最新事实。最终我固定为 top 5并对每条记忆用一句话标注最近确认时间。这个细节可以提醒模型优先采信时间新的记忆。3.4 记忆管理、查看与权限控制记忆管理面板是这个项目能登顶的另一个加分项。我提供了一个极简的本地 Web 管理页面用 Flask 写的路由只有三个查看全部记忆、按关键词搜索、删除指定记忆。用户访问后可以直观看到Agent 记住了我什么然后一键删除不想保留的条目。app.route(/memories, methods[GET]) def list_memories(): store current_app.config[store] memories store.list_all_memories() return jsonify(memories) app.route(/memories/memory_id, methods[DELETE]) def delete_memory(memory_id): store current_app.config[store] store.delete_memory(memory_id) return {status: deleted}我一直认为记忆功能必须有可见性。如果一个系统在后台偷偷记录用户信息又不让用户查看和删除这迟早会变成事故。项目评审时我专门演示了管理页面很多评委当场给分的原因就是我看到了完整闭环。4. 踩坑实录记忆丢失、检索不准与跨环境迁移4.1 典型问题速查表开发过程中踩过的坑我整理成了一份速查表基本上是这一周被私信问得最多的问题集合。现象可能原因解决方案Agent 完全想不起用户说过的话记忆抽取没生效对话结束后没有调用抽取接口检查是否在每次 Agent 回复后触发了异步记忆写入记忆内容对但检索不出来只用了向量相似度召回语义距离远改用混合召回加入 importance 和时间衰减检索出来了但回答用不上注入位置不对记忆被 system prompt 其他内容淹没把记忆放在 system prompt 最顶部并明确标注优先遵守重要信息几天后消失了半衰期设置太短统一用了默认值按记忆类型区分半衰期同一个信息存储了多条去重阈值太宽松将相似度阈值提高到 0.92并增加 content 归一化记忆库膨胀得很快没有遗忘机制增加 score 阈值和最终访问时间双条件清理4.2 跨电脑、跨账号迁移记忆的正确姿势被问最多的问题之一是把一套 Agent 的记忆迁移到另一台电脑或者另一个账号类似换账号如何获得原来账号的记忆这样的场景。思路本身不难但有几个坑。第一步导出记忆库文件和环境配置包括 SQLite 数据库、向量数组文件、半衰期配置 JSON。因为这些全部是本地文件迁移起来非常轻。第二步在新环境里重新计算向量索引。很多人直接把旧的 embedding 数组拷贝过去就完事结果发现如果新旧环境的 embedding 模型版本不一致向量维度或者语义空间可能会变导致检索效果下降。正确做法是加载原始记忆库用当前环境的 embedding 模型重新批量向量化重建索引。第三步处理 user_id 的映射。记忆库里的每条记忆都会绑定 user_id。换账号时如果直接把旧账号的 user_id 改成新账号的会出现一种情况新账号一启动就把旧身份记忆带进来用户完全蒙了。我建议迁移时对新账号显示一个导入的历史记忆清单让用户决定哪些保留、哪些删除。这在多用户场景下尤其重要。4.3 关于记忆安全的一个提醒记忆功能的本质是长期保存用户数据所以安全风险要提前想清楚。我在项目里做了三件事记忆库文件不落磁盘明文至少做一层系统级目录加密管理页面要求 token 认证不能裸跑在公网删除操作走软删除而不是物理覆盖防止误删后无法恢复。额外提一点不要把用户的敏感信息例如密码、完整身份证号、支付信息抽进记忆库。我在记忆抽取的系统提示词里明确写了遇到这些信息不要记录。你不主动防模型真的会把它们当成普通记忆记下来之后每次对话都会出现在上下文中泄露风险极大。5. 登顶之后的思考Agent 记忆还能怎么演进5.1 记忆压缩与摘要分层第一版方案把所有长期记忆都平铺在库里检索时靠 score 排序。但超过两个月后记忆条数会膨胀到几万条检索噪音变多。我下一步计划加入会话级摘要每个会话结束之后不仅抽取单条记忆还为整个会话生成一段摘要。几天前的会话摘要再进一步压缩成周摘要。检索时先定位摘要再根据摘要线索召回具体记忆条目。这套分层逻辑很像人脑的事件记忆和语义记忆具体某天发生的事会随时间模糊但逐渐沉淀下来的规律性认知反而更稳定。对 Agent 来说分层摘要还有一个额外好处上下文注入时可以先注入摘要用户追细节时再精确召回避免每轮对话都塞一堆碎片化记忆。5.2 多 Agent 共享记忆与记忆冲突现在很多项目是多个 Agent 协作。分布式记忆就成了新的问题。我在评审会上被问得最多的问题是两个 Agent 同时更新同一条记忆怎么办我的方案是给每条记忆增加一个version字段更新时走乐观锁先读取版本号更新时比对版本号不一致就放弃并重新读取合并。这套机制很朴素但足够应对多数场景。更大的隐患是记忆冲突。Agent A 记住用户偏好 AAgent B 记住用户偏好 B两边冲突时必须以最近确认时间和用户明确表达为准不能让模型自己猜。我准备在下一版中加入记忆冲突自动检测当新记忆与旧记忆语义相反时触发复制到人工确认队列而不是自动覆盖。5.3 未来方向项目登顶只是里程碑不是终点。我自己对 Agent 记忆后续有三个方向的规划。第一是记忆跨模态化用户不只在聊天还在上传文档、画图、语音记忆系统需要支持这些非文本形态的索引。第二是记忆控制从规则走向自适应半衰期、重要性阈值这些参数目前靠人工调长期来看应该让模型根据用户反馈自动调整。第三是记忆可解释性让用户不只看到记住了什么还能看到为什么记住这条、它影响了哪个回答。这个方向目前几乎没有成熟方案但我觉得它才是记忆功能进入企业级应用的前提。做Agent 记忆之前我一直以为最难的会是模型怎么判断该记什么做下来才发现真正的难度都在工程细节里怎么去重、怎么衰减、怎么召回、怎么让用户觉得安全。这也是这个项目能登顶的底层原因——不是想了一个大概念而是把一个概念拆成了无数个可以被验证的小决策。最后分享一个我个人的小习惯每次调完一套记忆策略我会用同一个测试语料库反复跑三遍看第一遍和第二遍的结果差异。记忆系统是状态系统最怕的就是状态不稳定。如果你也在做 Agent 记忆建议你的第一步不是写代码而是先定义一套你自己的测试语料里面至少要包含用户身份、偏好变化、任务进度、临时闲聊四类场景。有了这个底子后面所有优化才有基准。