"我刚才不是说了吗?"这大概是我在开发某AI Agent助手项目时最常听到的测试反馈。用户在对话里补充了自己的工作习惯,过了半小时再问关联问题,助手已经把那段信息忘得干干净净。大语言模型本身是无状态的,它不记得上一个请求说过什么,这是所有Agent类应用绕不开的一道坎,也是记忆组件存在的根本理由。
于是记忆组件就成了Agent系统里最基础也最容易忽略的一块拼图。这篇内容我想聊聊我在Agent项目里做记忆组件设计的完整思路:记忆怎么分类、用什么存储、什么时候写入、怎么检索召回,以及真实项目中踩过的几个典型坑。适合正在搭Agent大脑架构、准备给聊天型应用加"记性"的开发者参考。
1. 没有记忆的Agent,能力再强也是"一次性工具"
1.1 失忆的三种典型表现
先说最直观的现象。一个没有记忆组件的Agent,通常会表现出三种失忆状态:
- 会话内失忆:对话超过上下文窗口长度后,开头提到的信息被截断,Agent开始答非所问。
- 跨会话失忆:用户开了一个新会话,之前的对话历史一条都没有延续。用户必须重新说一遍自己的背景和需求。
- 知识沉淀缺失:用户多次强调的偏好、关键事实、正在进行的任务进度,完全无法积累成"越来越懂用户"的能力。
这三种失忆里,第一种最容易让用户烦躁,第二种最常见,第三种最伤产品体验。我自己在给某模拟项目X设计客服型Agent时,最痛的其实是第三种——用户已经第三次问"我上次说的那个地址变更办理到哪一步了",Agent每次都像第一次听到这个问题一样。
1.2 记忆组件的本质:给Agent装一个"外置大脑"
要理解记忆组件,先要理解大语言模型的特性。模型本身是一个函数:输入一段文本,输出一段文本。它没有状态,没有持久化的概念。每次调用模型接口,它都像第一次见到这段文字一样去理解它。
所以Agent记忆组件的本质,就是把"状态"从模型内部挪到模型外部,在模型之外构建一个持久化的信息管理系统。这个系统负责两件事:
- 把需要记住的信息,在合适的时机写下来,存到合适的存储介质中;
- 把当前任务需要的信息,在合适的时机取出来,拼进模型的上下文里。
这个过程有点像给人配了一个私人秘书:用户说什么,秘书在旁边记笔记;用户问问题之前,秘书先把相关的那几页笔记翻开放在桌面上。模型本身不需要变,变的是它每次"看到"的上下文素材。
1.3 什么类型的Agent最需要记忆组件
我接触过的Agent项目里,有几类应用对记忆组件的需求最迫切:
| Agent类型 | 典型场景 | 没有记忆的后果 |
|---|---|---|
| 对话助手 | 闲聊、日常咨询、陪伴型问答 | 用户感觉在和失忆者聊天 |
| 客服机器人 | 工单查询、进度跟踪、售后处理 | 用户反复描述问题,体验极差 |
| 个人知识管家 | 日程管理、文件整理、偏好记录 | 无法形成个性化服务 |
| 任务执行Agent | 多步骤任务规划、定时执行 | 任务中途无法续接,前功尽弃 |
如果项目里做的AI应用属于上述类型,记忆组件就是上线前的必做项。如果只是单次问答型工具(比如一次性文案生成、单轮翻译),则不需要花力气做记忆,没必要为不需要的场景增加复杂度。
2. 记忆分层:工作记忆、短期记忆、长期记忆不能混为一谈
在设计记忆组件时,最容易犯的错误就是"一把抓"——把所有信息都塞进同一个存储里。实际上,不同类型的记忆生命周期和访问模式完全不同,必须分开设计和处理。我的习惯是把Agent记忆分成三层:工作记忆、短期记忆和长期记忆。
2.1 工作记忆:上下文窗口的调度问题
工作记忆对应的是大语言模型当前的上下文窗口。它本质上不是一个存储系统,而是一个"调度决策"过程:在有限的token预算里,往窗口里放哪些内容、不放哪些内容。
举个例子。假设模型上下文窗口是128K token,一次用户对话加上系统提示、工具返回结果可能占了30K,剩下的空间怎么分配给对话历史、检索到的记忆、外部资料?我的通用优先级是:
- 当前用户输入(绝对不可省略)
- 与当前任务直接相关的检索记忆
- 系统提示与角色设定
- 最近的对话轮次(滑窗保留最近N轮)
- 早期的对话摘要(压缩后的内容)
在实际项目里,我通常把上下文划分为:系统提示固定占一段比例、用户当前输入原样保留、工作记忆区放检索出的记忆内容、对话历史滑动窗口。这样模型每次看到的上下文是结构化的,而不是一锅粥。
2.2 短期记忆:会话内的"便利贴"
短期记忆对应的是单个会话(session)内的临时状态。它不需要跨会话持久化,生命周期和会话一致。典型内容包括:用户刚刚选择了一个选项、正在填写表单的中间状态、临时切换的配置项。
实现方式最简单,就是一组以会话ID为键的KV存储,设置一个过期时间(TTL)。比如会话结束或30分钟无活动后清空。不需要上向量库,也不需要复杂的关系型设计,复杂了反而带来维护负担。
2.3 长期记忆:真正意义上的"记性"
长期记忆是跨会话、跨时间的持久化记忆,是记忆组件价值的核心体现。长期记忆内部又可以细分为:
- 语义记忆:用户画像类信息,比如"用户偏好简洁回复""用户主要从事后端开发工作"。这类信息一旦建立就相对稳定,更新频率低。
- 情景记忆:具体发生过的事件,比如"用户上周五提交了域名备案申请""用户此刻正在跟进项目X的第三轮迭代"。这类信息有明确的时间属性,时效性最重要。
- 任务状态记忆:正在进行的任务进度,比如"代码迁移任务已完成60%,剩余部分是基于消息队列的改造"。它介于语义和情景之间,既涉及事实也涉及时间。
在设计表结构或存储结构时,给每条长期记忆打上type标签是很必要的,因为不同类型的检索方式和衰减策略完全不同。语义记忆需要长期稳定保留,情景记忆则要按时间衰减。
3. 存储选型:向量库不是银弹,混合存储才是常态
3.1 三种存储方案的本质差异
做记忆组件,第一个绕不开的选择是"用什么存储"。很多人一提到记忆就想到向量数据库,其实这是误解。向量库擅长的是"语义相似度检索",但它不擅长精确查询,也不擅长表达结构化关系。一个成熟的记忆组件,通常需要三类存储协同工作。
| 存储类型 | 擅长内容 | 访问方式 | 主要短板 |
|---|---|---|---|
| 向量存储 | 非结构化的语义记忆、长期记忆的模糊召回 | 相似度检索 TopK | 检索结果相关性问题 |
| KV缓存(Redis类) | 短期记忆、高频精确访问的会话状态 | 按key精确读取 | 不支持语义查询 |
| 关系型数据库 | 用户画像、记忆时间线、结构化事件 | 条件查询、聚合统计 | 处理非结构化内容困难 |
3.2 三类记忆分别用什么存储
我的推荐组合是:
- 工作记忆:不落存储,直接用上下文窗口配合调度策略管理。
- 短期记忆:用KV缓存,TTL过期自动清理。
- 长期记忆:主存储在关系型数据库,同时把非结构化内容做向量化后同步进向量存储。
长期记忆这样做是有道理的:关系型数据库负责"全量存证",保证每条记忆都有结构化元数据(创建时间、来源会话、类型标签、重要度),方便后续做筛选和审计;向量存储负责"语义召回",在需要时通过相似度检索找到相关记忆。两者不是替代关系,而是"全量管理 + 高效检索"的分工关系。
3.3 混合架构下的数据流向
写入路径:Agent从对话中提取信息 → 结构化处理后先写入关系型数据库(保存全量)→ 同时将需要做语义检索的内容向量化写入向量存储 → 高频热点数据再放入KV缓存。
读取路径:用户发起请求 → 先检查KV缓存中是否有会话状态 → 触发长期记忆检索时,从向量存储做语义召回,再从关系型数据库补全记忆条目详情 → 组装进上下文。
这样设计的收益是:每一类存储都只做自己最擅长的事,不会出现"把结构化数据塞进向量库导致检索不准"或"把海量非结构化记忆塞进关系型数据库导致查询复杂"的问题。
4. 记什么、何时记、怎么更新:写入链路的三道闸门
4.1 第一道闸门:什么时候值得写入
记忆写入最怕的就是"什么都记"。如果用户说了一句"今天天气不错"就被写进长期记忆,那记忆库很快就会堆满噪音。我在项目里总结了三类值得写入的时机:
- 用户显式告知的信息:用户主动说过"提醒我周五下午开会""我不吃辣",这类信息具有明确的持久价值。
- 跨轮次被引用的信息:对话中反复被提到的内容,比如用户在多个轮次里强调的某个约束条件,说明它很重要。
- 任务关键节点:Agent完成了一个步骤、用户确认了一个方案,这类事件是后续任务续接的重要锚点。
一个实用技巧:在Agent系统中加一道"记忆写入判断"规则,只有满足上述条件之一的片段才触发写入流程。实测下来能有效过滤掉约七成无意义的信息。
4.2 第二道闸门:写入前必须做提炼
原始对话文本不适合直接写入。对话里有很多口语化、冗余的表达,直接存进去不仅浪费存储空间,检索时也会被大量无关词干扰。我的做法是:用模型做一次"记忆提炼",把原始对话转成简洁的记忆条目。
一个结构化的记忆条目大致长这样:
{ "id": "mem_8f3a2", "type": "user_preference", "content": "用户偏好代码示例中的注释使用中文", "importance": 0.8, "created_at": "2024-05-11T09:30:00Z", "last_access_at": "2024-06-01T14:20:00Z", "access_count": 4, "source_session_id": "session_210923", "status": "active" }type字段决定这条记忆的衰减策略,importance决定它被检索时的排序权重,status字段用来处理冲突——当用户后来表达了与之相反的信息时,旧条目被标记为superseded,而不是直接删除,这样可以保留历史轨迹,方便回溯。
4.3 第三道闸门:冲突与遗忘
用户是善变的。今天说"我更喜欢用Python写脚本",两周后说"我现在主要写Go了"。这种新旧记忆冲突如果不处理,Agent就会频繁给出自相矛盾的回复。
冲突处理策略我总结为三步:
- 新信息写入时,检索是否存在语义上相似但内容相反的旧记忆;
- 若有冲突,将旧记忆的
status标记为superseded,新记忆以active状态写入; - 模型在读取记忆时,优先读取
active状态的条目,superseded条目仅在用户明确追问历史时才拿出来。
遗忘机制同样重要。没有遗忘的记忆系统会越来越笨重。我目前用的策略是三重衰减:时间衰减(超过30天未访问的重要度自动降权)、访问衰减(高频被读取的记忆降级为"已知信息")、容量淘汰(单用户记忆条数超过上限时,优先淘汰低重要度且长期未访问的条目)。
5. 让Agent"想起来":读取、检索与上下文组装的配合
记忆写入得再好,读取得不到位也是白搭。读取链路的核心目标只有一个:在正确的时间,把正确的记忆片段,用正确的形式放进模型的上下文里。
5.1 什么时候触发记忆读取
我总结的触发时机有三类:
- 会话初始化时:用户建立新会话,系统自动拉取该用户的高活跃度长期记忆,作为背景信息拼入上下文。
- 对话中检测到指代时:用户说"我之前提到过的那个方案""你上次说的那个问题",这里明显指涉历史信息,需要触发语义检索。
- Agent规划行动前:任务执行类Agent在拆解步骤前,先检查是否有相关的历史任务状态或知识。
5.2 多路召回与重排
向量相似度检索单独用常常不够。原因很简单:向量相似度高不代表逻辑相关。比如用户问"上次你说那个方案有几个备选",向量库里最高相似度的记忆可能是"方案B的部署成本较高",而不是用户真正想要的"方案备选清单"。
我现在的做法是多路召回 + 重排。多路召回同时做三件事:
- 向量相似度召回:从向量存储按语义相似度取TopK;
- 结构化标签召回:按记忆条目的
type、用户ID、时间范围做精确筛选; - 活跃度召回:按
access_count和last_access_at取该用户最近活跃的记忆。
三路结果合并去重后,用一个打分公式重排:
score = 0.5 * importance + 0.3 * freshness + 0.2 * relevance其中relevance是向量检索的相似度得分,freshness由last_access_at距当前时间的间隔计算得出。这个公式是我根据实测效果调的,不一定是通用最优解,但实践证明比单靠向量相似度排序稳定得多。
5.3 上下文组装:给记忆加上"包装"
检索出来的原始记忆不能直接拼进上下文,需要的是一条格式化的"记忆块"。我在项目里用的是这种格式:
【记忆】类型:用户偏好 | 更新时间:2024-05-11 用户希望代码注释使用中文解释关键逻辑 【记忆】类型:任务状态 | 更新时间:2024-06-01 项目X第二轮迭代已发布V0.9,剩余消息队列改造约40%这样做的原因有两个:一是给模型提供元信息,让它能判断这条记忆的新旧和有效范围;二是用明确的"记忆"标记把检索内容与当前对话区分开,避免模型把历史记忆误当成当前用户输入。
还有一个非常容易被忽略的问题:token预算。检索出来的记忆再准确,也不能无限制地挤占上下文窗口。我给每条记忆块设置了最大长度(比如不超过150字),单次召回的条数上限也做了硬限制(默认5条)。等模型上下文窗口够大之后可以放宽,但在当前阶段,"精而少"比"多而杂"更可靠。
6. 实战复盘:记忆组件设计里的四类典型翻车现场
最后聊几个我在实际项目里踩过的坑。这些坑在设计文档阶段几乎看不出来,都是在实测和上线后才暴露的,写出来帮大家省点时间。
6.1 向量检索的"伪相关"问题
第一次做记忆检索时,我天真地以为向量检索能解决一切。实测后发现问题很大:用户问"他平时什么时候有空",向量召回的第一条记忆却是"用户提到过喜欢靠窗的座位"。语义层面两者有部分重合,但逻辑上完全不相关。
后来加了一层结构化标签过滤,记忆写入时就打上type和关键标签,检索时先按标签粗筛,再在粗筛结果里做向量相似度排序。"伪相关"问题的发生率下降了很多。
6.2 记忆写得太碎,检索时一团乱麻
早期为了"不遗漏信息",我把用户说的每一句带一点信息量的话都写成记忆条目。结果用户问一个简单问题,召回了一堆碎片化记忆,上下文里全是"用户昨天说想换键盘""用户上周提到椅子有点矮"这类信息,核心事实反而被淹没。
解决办法是增加提炼粒度——把同一次对话中的多条相关信息合并成一条综合记忆,比如"用户近期在调整办公环境:想换机械键盘、关注人体工学椅、喜欢靠窗位置"。宁可少而整,不要多而碎。
6.3 上下文被记忆挤爆
有段时间测试反馈说"Agent好像变笨了"。排查后发现,是检索的触发条件太宽松,每次对话都往上下文里塞大量记忆块,模型被无关信息干扰,反而忽略了当前请求。
我当时用的办法是统计模型回答错误时的上下文构成,发现记忆块占比超过40%时准确率明显下降。于是把单次召回的条数从20条砍到5条,每条长度控制在150字以内,准确率立刻回升。这个数字可以作为调优的起点参考,不一定照抄,但方向是对的。
6.4 记忆污染:过时信息比没有信息更危险
最严重的一个坑是记忆更新不及时。用户的某个需求已经完成了,但我没有在任务完成后自动更新对应的记忆条目状态,导致Agent在下一次对话里还在引用"任务进行中"的旧状态,给了用户完全错误的答复。
后来我在任务执行链路的末端加了一步"任务结果回写":任务完成后,不仅要更新任务状态,还要把相关的长期记忆条目同步刷新,该置为完成的置为完成,该撤回的撤回。这一步听起来简单,但遗忘它会让整个记忆系统的可信度大打折扣。
我现在的使用体会是,记忆组件设计里最难的其实不是存储技术选型,也不是检索算法调优,而是"边界控制"——知道什么该记、什么不该记,什么该拿出来、什么该藏起来。写了一段时间之后我才意识到,好的记忆组件不是什么都记得,而是总能在对的时间想起对的事。这个原则贯穿了记忆分类、存储选型、写入链路、读取链路的所有设计决策。如果你的Agent应用也正在被"失忆"困扰,不妨从这道边界开始重新审视自己的方案。