ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent记忆系统实战:短期记忆、长期记忆与完整工程落地

AI Agent记忆系统实战:短期记忆、长期记忆与完整工程落地 做AI Agent开发的人基本都会遇到同一个坎模型能力再强只要不带记忆每次对话都像第一天上班的新同事。你上周刚跟它说过的偏好、约定好的称呼、上个月定的方案细节它第二天就忘得干干净净。这个痛点我在这套“走进AI Agent”系列里反复提过到了第三篇专门把“记忆”这件事摊开讲。这篇文章要解决的就是怎么让Agent真正“记住你”。我会从记忆体系的整体设计讲起再拆短期记忆、长期记忆的工程实现最后给出一套可以直接落地的最小闭环方案同时把常见坑位都标出来。适合已经用LangChain、LangGraph这类工具搭过基础Agent、但觉得交互体验还差点意思的开发者也适合准备系统学习AI Agent的读者拿来当路线参考。1. 记忆体系整体设计与思路拆解1.1 没有记忆的Agent和有记忆的Agent差在哪里先说一个最直观的差异同样是“帮我推荐咖啡豆”无记忆Agent会问一遍“你喜欢什么口味”而有记忆Agent会直接说“这次还要偏深烘焙、带坚果调的哥伦比亚豆吗上次你说过不喜欢酸感重的”。后者给人感觉完全不一样——前者是工具后者才像搭档。从产品角度看记忆能力直接影响留存。用户愿意回来用你的Agent很多时候不只是因为模型聪明而是因为“它懂我”。这种懂就是靠记忆系统堆出来的。从技术角度看记忆不是单一模块而是横跨提示词工程、检索系统、存储设计、状态管理等多个环节的一套体系。很多开发者的误区是记忆不就是把对话历史存起来下次再塞给模型吗早期我也是这么干的后来发现完全不是这么回事。上下文窗口有限token费用摆在那里用户历史可能很长陈旧信息还会干扰当前判断。真正可用的记忆系统需要分层设计、按需存取、定期更新。1.2 三层记忆模型短期、长期、程序记忆我在实操中比较习惯把Agent记忆拆成三层这个框架便于理解也便于做工程拆分。短期记忆工作记忆对应当前会话内的上下文。用户这轮说了什么、上轮问题是什么、Agent已经给出了哪些回复这些信息都在短期记忆里。它天然就是对话历史本身实现上最简单但管理起来有讲究——窗口怎么截、压缩怎么做、关键信息怎么保留都不像表面那么简单。长期记忆跨会话记忆负责把用户真正“记住”。包括用户偏好、历史事实、过去讨论过的方案、约定过的规则等。它需要从每轮对话中提炼出值得存的东西写进存储并在后续会话中按需召回。这一层是“让Agent记住你”的核心战场。程序记忆对应Agent“会做什么”的技能型和流程型记忆。比如用户经常让Agent用某种格式输出或者你封装了一个工具、一套执行流程Agent会记住在什么场景下该怎么调用。这部分跟Agent的工具链、技能库耦合比较深。这一层相对进阶我在这篇文章里会重点讲前两层第三层只做思路提示。理解这三层之后你就能意识到“让Agent记住你”不只是一个“存起来再取出来”的存储问题而是一套从感知、筛选、存储到召回、更新、遗忘的完整闭环。1.3 为什么不能简单把对话历史全塞给模型最朴素的记忆方案确实就是“把历史消息全部拼起来一股脑丢给模型”。这个方案在小规模、短会话场景下能用但规模一上来就会出三个问题。第一个问题是上下文窗口物理瓶颈。主流模型的上下文窗口从几十K到两百K不等看起来很大但对话历史是累积的几十轮后就可能占掉大半窗口。更麻烦的是你还要留出空间给系统提示词、工具定义、检索回来的参考资料、模型输出真正留给“用户历史”的空间远没有想象中充足。第二个问题是注意力稀释。这一条很多人会忽略。模型对上下文不同位置的关注度并不均匀历史太长时越靠前的信息越容易被忽略或者被大量无关信息淹没。用户三个月前说过“我住在南山区”这种关键信息如果被夹在500条对话里一起送进去模型很可能“看不见”。这样既花了tokens又没有真正记住。第三个问题是成本。每次请求的token消耗直接跟上下文长度正相关生产环境日均上万次请求时每多塞一千token都是一笔肉眼可见的账单。我见过一个团队就因为没有做上下文裁剪一个月token账单翻了三倍。所以记忆系统的本质不是“存很多”而是“存得准、取得到、塞得少”。后面每个环节的设计都要围绕这句话来转。2. 短期记忆让当前会话自然连贯2.1 对话历史管理的常规做法与窗口策略短期记忆在工程上其实就是消息列表管理。每次请求时把user消息、assistant回复、函数调用结果按时间顺序拼成一个message数组跟当前用户输入一起发给模型这就是基础形态。但消息列表不能无限增长所以要定一个窗口策略。我常用的是按轮次窗口加按token阈值双条件控制保留最近N轮对话同时统计消息总token数超过阈值就触发压缩或丢弃。N和阈值的取值没有绝对标准取决于你用的模型上下文大小和业务复杂度。以32K上下文模型为例我一般会把system prompt控制在1000~2000 token历史消息控制在8000~12000 token以内剩下留给检索结果和输出。实现的时候要特别注意一个细节不要只按“轮”截断因为用户和Agent的单轮消息长度可能差异很大。有人习惯按字符数切但中英混合场景下用token更准确。实际项目里我会把消息逐条加入统计token数直到接近阈值再停下。这样截断行为更平滑不会出现“明明还剩一大半窗口却因为某几轮特别长把后面全挤掉了”的情况。2.2 上下文压缩让Agent在有限窗口里记住关键信息纯截断的问题是被丢掉的部分可能包含关键偏好、待办事项或用户刚交代过的约束。只留最近几轮一旦中间隔了比较长的查询前面的关键信息就丢了。解决办法是做上下文压缩或改写。原理不复杂用一次独立的LLM调用把旧对话里的核心信息提炼成一段摘要然后把摘要作为一条特殊消息放在历史开头再附上最近几轮完整消息。这样既保留了关键信息又不占太多窗口。我实际用过的压缩Prompt大概是这样的风格请把以下对话历史压缩成一份摘要要求 1. 保留用户的偏好、任务约束、已确定的事实 2. 保留尚未完成的待办事项 3. 去掉寒暄、可省略的客套、重复表达 4. 摘要控制在200字以内 对话历史 {history}这个方案我跑通之后效果立竿见影。之前经常出现“用户上午说过不要辣下午点单时Agent又推荐辣味菜品”的尴尬情况加了压缩后基本根治了。不要小看这一步它承担的是“艾宾浩斯遗忘曲线”里把关键记忆从短期转巩固的那个角色。另外要提醒一点不是每次请求都要做压缩。触发条件可以用“历史超过每轮窗口的80%”这类阈值。否则每个请求都额外调一次LLM成本和延迟都会上升。2.3 实操心得工具调用消息与系统提示词的处理短期记忆里最容易翻车的其实是工具调用相关的消息。现在很多Agent都有工具调用能力函数调用过程中的tool/function消息会穿插在对话里。这类消息对模型理解“前面发生了什么”很重要比如“搜索到了3条结果”“数据库查到了5行记录”。但如果全部保留会占大量窗口。我建议的策略是完整保留最近两轮工具消息更早的只保留摘要并把工具调用的中间结果做瘦身——只保留结论字段不保留大段原始返回。另一个容易忽略的点是system prompt的处理。很多人把系统提示词当成“写一次就不动”的东西但在带记忆的Agent里系统提示词需要动态生成开头是固定的人格设定中间插入压缩后的历史摘要再插入检索回来的长期记忆最后是当前对话状态。这样每次请求的system prompt都是拼装出来的而不是静态字符串。我踩过的一个坑是把长期记忆也塞在system prompt里导致system prompt越来越长最终挤占了对话历史的空间。后来我把记忆注入跟system prompt解耦记忆内容单独作为一条memory消息放在历史最前面效果明显更好。3. 长期记忆跨会话记住用户的关键工程3.1 记忆写入从对话里提炼“该记的事”长期记忆的核心问题不是“怎么存”而是“什么值得存”。不是每一句用户话都值得进长期记忆否则存储里全是噪音。我常用的方案是“总结—抽取—清洗”三步走。对话到达某个结束点比如一个完整任务完成、或用户明显切换了话题时触发一次记忆更新流程。先让LLM把本轮对话的关键信息总结出来再从中抽取结构化条目最后做一次冲突检测和去重。抽取环节可以用一个返回JSON的Prompt。比如从对话中抽取值得长期记忆的信息返回JSON数组。 字段说明 - type: user_preference(用户偏好) / personal_info(个人信息) / task_status(任务进度) / project_fact(项目事实) - content: 记忆内容一句话描述 - keywords: 关键词数组用于检索 - importance: 1~5重要程度 - timestamp: 当前时间 对话内容{conversation}这个流程跑起来之后你手里就有结构化、可检索、带重要度的记忆条目了。importance字段很有用它决定这一条记忆在检索时的权重也决定系统会不会主动把它放进长期存储。需要注意的是记忆写入不要每轮都做否则成本高、噪音大。我通常设定触发条件用户明确表达了偏好任务发生状态变化出现新的个人信息用户与Agent约定后续要做的事。这些条件可以用简单的规则判断也可以让LLM在返回结果时附带一个should_update_memory标记。3.2 记忆存储结构化字段与向量语义双通道长期记忆存哪里我见过很多新手的处理方式是存MySQL或者直接丢进JSON文件这本身上没问题但要做好设计。我比较推荐双通道存储结构化通道用关系型数据库或Redis存用户偏好、个人信息这类强结构化条目语义通道用向量数据库存更偏语义描述的记忆比如“用户喜欢在周末晚上研究咖啡拉花技巧”。前者适合精确查询后者适合模糊检索。为什么不能只靠结构化通道因为很多记忆本质是语义性的你很难用字段精确描述。比如“用户对最近的某个项目很有热情”这个信息怎么建表建一个“项目热情度”字段既不现实也不通用。但在向量库里存一句自然语言关联上相似度检索就可以被正确召回。向量库选型上轻量项目可以用Chroma这类本地库生产环境可以考虑Weaviate、Qdrant或者直接用pgvector挂在PostgreSQL上。嵌入模型的选择也有讲究中文场景下bge系列、m3e这类模型效果通常比纯英文模型更好。向量维度不用刻意选很大768或1024都够用关键是检索阈值要调准。我经历的一个事故是向量库相似度阈值设太高导致语义相关但字面不同的记忆召回不出来。比如用户说过“我不吃香菜”过了两个月问“推荐一下麻辣烫店”Agent因为没召回“不吃香菜”这条记忆兴致勃勃地给出一堆放香菜的方案。这个问题的根子不是模型不强而是检索召回环节做不好。3.3 记忆读取检索、排序与注入记忆读取决定Agent“想得起什么”。我常用的读取流程是先把当前用户query和最近上下文拼成一个检索请求从向量库召回topK条相关记忆再从结构化库里按用户ID和场景做精确查询最后按重要度、时间衰减、相关性综合排序取前N条注入上下文。召回数量要克制。我一般向量召回5~10条结构化查询取2~5条最终注入3~6条。注入太多会挤占窗口、分散注意力太少又可能漏掉关键信息。这个数量要结合模型能力和业务形态调没有固定值。注入时最好给记忆加上时间信息比如“2024年3月5日记录”。这样模型能区分新记忆和旧记忆如果旧记忆跟当前事实冲突它更倾向采信更新的内容。还有一个容易踩的坑记忆检索请求里一定要带用户身份过滤。否则你查到的可能是另一个用户的记忆。我见过一个demo产品上线后用户A的偏好被注入到用户B的对话里直接导致产品被打差评。这个bug排查很久最后发现是检索接口漏了user_id过滤。3.4 更新与遗忘用户改主意了怎么办长期记忆不是写了就完事。用户可能改主意偏好可能过期记忆也需要更新和遗忘机制。最经典的情况是用户说“我以后不喝深烘了改喝浅烘”。如果系统只往库里加新记忆而不处理旧记忆检索时旧记忆和新记忆同时命中Agent就会行为矛盾。我的处理方式是抽取环节如果发现新记忆跟旧记忆存在冲突走“覆盖更新”逻辑——把旧记忆标记为expired新记忆写入生效。遗忘机制也必须有。一条记忆如果不被召回、不被确认时间久了就应当降权或清除。我常用的是时间衰减公式recall_score base_score * exp(-λ * age_days)。λ根据业务调比如λ0.01表示大约70天内记忆权重衰减到36%。这样能保证记忆系统不会越积越臃肿也不会因为陈年旧记忆干扰当前判断。顺便提一句让用户主动管理记忆也是一个很好的产品设计。给Agent加一个“我记错了”的反馈入口让用户能看到Agent记住了什么、能手动删除某条记忆这在C端产品里非常加分。这一条我强烈建议做因为它既解决遗忘问题又让用户感到“系统被自己掌控”。4. 实操案例搭建“记住你”的最小闭环4.1 场景定义与整体流程这一节我用一个完整的小案例串起来。场景很简单用户第一次告诉Agent“我喜欢喝深烘焙的咖啡不喜欢酸味重的”然后过了一段时间用户问“推荐一款适合我的咖啡豆”。目标就是Agent能结合长记忆给出个性化推荐而不是从头问一遍。整体流程设计为5步对话采集、记忆抽取、记忆存储、记忆检索、上下文注入。每一步都能对应用前面讲到的机制。先用基础的消息循环跑对话当检测到用户说出偏好类信息时触发记忆抽取。抽取结果写入结构化存储和向量库。下一次会话开始时检索模块根据用户身份拉取相关记忆注入到系统提示词中再让模型生成回复。4.2 记忆抽取环节的Prompt与结果这是整个流程里最能看到“工程感”的一步。抽取Prompt长这样你是记忆管理助手负责从对话中提取值得长期保存的信息。 对话 {conversation} 任务 1. 判断是否存在值得保存的长期记忆 2. 如果有输出JSON数组每个元素包含 - type: user_preference / personal_info / task_status / project_fact - content: 记忆内容 - keywords: 检索用关键词 - importance: 1~5 - conflict_with: 如果有冲突的旧记忆列出旧记忆ID否则为null 如果没有值得保存的内容返回空数组。假设用户说了“我喜欢喝深烘焙的咖啡不喜欢酸味重的”LLM应该返回类似[ { type: user_preference, content: 用户偏好深烘焙咖啡不喜欢酸味重的咖啡, keywords: [咖啡, 深烘焙, 酸味], importance: 4, conflict_with: null } ]如果库里已经有一条“用户喜欢浅烘焙咖啡”这一步就应该检测到conflict_with字段走更新逻辑而不是简单追加。4.3 检索注入与回复生成到了用户再次提问“推荐适合我的咖啡豆”时检索模块工作。我用一个简单的语义检索函数来说明思路import requests def search_memory(user_id, query, top_k5): # 1. 先按user_id过滤避免串号 # 2. 对query做embedding # 3. 在向量库召回复合条件的记忆 # 4. 按重要度和时间衰减排序 resp requests.post( http://localhost:8000/search, json{user_id: user_id, query: query, top_k: top_k} ) return resp.json()[results]召回结果可能是“用户偏好深烘焙咖啡不喜欢酸味重的咖啡”。注入后系统提示词变成你是用户的咖啡助手。关于用户你有以下长期记忆 - (2024年6月2日) 用户偏好深烘焙咖啡不喜欢酸味重的咖啡 请基于这些信息结合用户当前问题回答。模型看到这条信息再结合商品库里的描述推荐结果自然就会避开浅烘焙和酸感强的豆子优先推深烘焙、坚果/巧克力调的款型。这个最小闭环跑通之后你就能直观理解“记住你”是怎么发生作用的了。4.4 参数选择与成本控制建议在这个闭环里有几个关键参数需要调向量维度、召回数量、相似度阈值、触发记忆抽取的频率。我建议起步配置向量维度用768或1024召回topK设为5~8条相似度阈值从0.7起调根据实际命中情况上下浮动记忆抽取只触发在用户表达明确偏好或任务状态变化的时机不要每个请求都跑。这样在个人项目或小团队产品里记忆抽取带来的额外LLM调用量是可控的。成本控制上还有一个技巧不要把整段历史都交给抽取模型而是先把对话做简单截取只把跟潜在记忆相关的部分发过去。比如检测到用户说“我喜欢”“我讨厌”“以后别”这类句式时再触发抽取能省下大量无关调用的花费。5. 常见问题与排查技巧实录5.1 五大高频问题速查表长期跟Agent记忆系统打交道我总结了几类高频问题列成表格方便对照排查。现象可能原因解决方案记忆一直不生效相似度阈值太高召回为空降低阈值查看实际召回结果用户A的记忆出现在用户B对话里检索时漏了user_id过滤在所有查询路径加用户身份过滤用户改主意了Agent还是按旧偏好回答缺少记忆覆盖更新机制抽取时检测conflict标记旧记忆过期上下文被大量旧记忆挤占回复跑偏注入记忆条数过多未按相关度排序限制注入条数增加时间衰减和重要度排序记忆抽取成本过高每轮对话都触发抽取只在关键句式或状态变化时触发这五类问题里前两个属于系统性bug后三个属于设计缺陷。系统性bug要修代码设计缺陷要调策略。5.2 排查思路与避坑经验如果记忆不生效我建议先做“可视化回溯”把每次请求的完整上下文打印出来看看检索模块到底返回了什么、注入了什么。这一招比任何调试工具都管用。很多看起来玄乎的“模型不听话”其实都是上下文里根本没有该有的记忆或者记忆被放在了太靠后的位置。另一个经验是关于embedding的“跨语言陷阱”。如果你用的embedding模型中文能力弱用户的“深烘焙”和“深焙”可能被映射到完全不同的向量区域导致召回失败。解决办法一个是换中文效果好一点的embedding模型另一个是在存储时主动添加同义词关键词在抽取阶段就顺手做一次扩展。还有个细节记忆抽取用的LLM跟对话用的LLM要分开。对话模型可以追求智能和自然抽取模型则要稳定、结构化输出能力强而且通常用小一点的模型就够了。在LangGraph这类框架里不同节点配置不同模型是很自然的事这也能显著降低成本。最后提一下隐私合规。涉及到用户的个人信息要设计留存期限、用户可查看可删除的界面。很多时候产品被下架或收到用户投诉不是模型没用对而是记忆系统触碰了用户数据红线。这一条做在前面后面能省掉无数麻烦。我在摸爬滚打中形成的习惯是给每条长期记忆都带一个created_at和source字段既方便做时间衰减也方便未来做数据审计。这套习惯从个人项目一路带到生产环境一直没出过问题。
返回列表