ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AI Agent记忆系统实战:从短期记忆到长期记忆的完整构建方案

AI Agent记忆系统实战:从短期记忆到长期记忆的完整构建方案 做 AI Agent 的人大概率都遇到过同一个尴尬模型能力越来越强工具链也越来越顺但用户只要把对话窗口一关再打开时 Agent 就像彻底失忆了一样连对方叫什么、上次聊到哪、偏好是什么全都不知道。这篇是“走进 AI Agent”系列的第三篇专门把“记忆”这件事掰开揉碎讲清楚——Agent 到底靠什么记住用户怎么在工程上把临时记忆、长期记忆和工作记忆落地以及我踩过的那些坑。这套内容适合正在做 Agent 应用、想给助手加上个性化能力、或者被上下文窗口困扰的开发者。我会从记忆体系分类讲到存储选型再给出一套可以直接抄的代码骨架最后聊聊检索策略和常见故障排查保证看完能直接上手改造自己的 Agent。1. 先把“记忆”这件事拆明白1.1 为什么说没有记忆的 Agent 只是聊天机器人大模型本身是“无状态”的这是很多人容易忽略的事实。你每次调用接口传入的是一段完整的历史聊天文本模型只是根据这段文本生成回复并不会天然记得你三天前说过什么。所谓的“记忆”本质上是我们开发者自己建立起来的一套外部状态管理机制。我见过不少朋友一开始做 Agent把所有希望都寄托在模型身上觉得“我给它指令、给它工具它就能像人一样懂我”。结果一测就发现问题用户问完天气又问穿搭再回头聊天气时Agent 已经忘了用户之前说过“我住杭州偏爱棉麻材质”。这种感觉非常糟糕用户会立刻觉得 AI 不聪明、不贴心最终弃用。所以“让 Agent 记住你”不是锦上添花而是产品能不能留住用户的生死线。我们要做的是把“记忆”从模型的隐式能力中剥离出来变成显式的数据流让记忆可写入、可读取、可更新、可删除这样用户和 Agent 的每一次交互都能沉淀为长期资产。1.2 短期记忆、长期记忆、工作记忆怎么分工在工程上我习惯把 Agent 的记忆拆成三个层次短期记忆、工作记忆和长期记忆。这个分法未必符合学术定义但非常贴近落地实现。短期记忆指的是当前会话内的上下文信息比如这一轮用户说了什么、之前的几轮对话内容。它通常保存在内存或 LangGraph 的 State 中随会话销毁而消失。短期记忆是最容易实现的但也是最容易让上下文窗口爆掉的元凶。工作记忆则是 Agent 在执行一次任务过程中的中间状态。比如 Agent 调用了一个天气 API返回了一堆 JSON它需要暂时记住这个结果生成后续回复或决定下一步工具调用。工作记忆的生命周期通常很短任务结束就释放但它是 Agent 能“边思考边行动”的基础。长期记忆才是“让 Agent 记住你”的核心。它跨会话存在用来保存用户的偏好、历史事实、已完成的业务信息等等。长期记忆通常存储在外部数据库或向量库中可以被随时检索、更新和删除。这是本文的重头戏后面大部分内容都在讲它怎么落地。1.3 一个关键区别记忆不是把聊天记录全塞进提示词有个新手特别容易踩的误区就是把“记忆”简单理解成“把历史聊天记录全部拼进 prompt 里”。这样做表面上像是记住了实际上问题非常大。第一成本不可控。一份 10 万字的聊天记录如果完整进 prompt每次调用的 token 费用会高得吓人延迟也会让人无法接受。第二信息噪音严重。历史里大部分内容对当前回答毫无帮助模型很容易被无关信息干扰反而把关键偏好给淹没了。第三无法跨会话。聊天记录一断记忆就归零。真正可用的记忆系统应该做到“按需提取”。用户问去杭州穿什么Agent 应当回忆起“用户住杭州、偏爱棉麻材质、上次聊过梅雨季”而不是把三个月前的点外卖记录也一起搬出来。这就是检索式记忆存在的意义——它像人脑的海马体一样不把所有信息放在工作台上而是按线索精准调取。2. 记忆的存储方案怎么选2.1 轻量起步ChromaDB 与 FAISS长期记忆最常见的存储形式是向量数据库。原因很简单AI 的记忆线索往往是语义级的用户说“我体寒”很难用精确匹配去找到这句话但用向量检索可以轻松找到语义相似的“我容易手脚冰凉”“我不太能吃凉的东西”。如果你还在做原型验证我推荐先用 ChromaDB。它是本地的、无需部署服务的一个PersistentClient就能落盘配合 OpenAI 的 embedding 接口非常顺手。想更轻快一点FAISS 也能胜任它是纯内存的向量索引库适合数据量不大、不要求复杂过滤的场景。我自己的经验是原型阶段别急着上分布式集群先用单机方案跑通主流程。把记忆写入、检索、更新这条链路跑顺再考虑扩展。很多时候业务逻辑的问题远比存储性能的问题大。2.2 生产环境Milvus 与 pgvector 怎么选等用户量上来了单机方案会碰到瓶颈数据量大导致检索变慢、多用户隔离做起来别扭、重启丢数据或者备份困难。这时候就要换更正规的存储了。Milvus 是专门为向量检索设计的分布式数据库支持几十亿级别的向量规模提供丰富的过滤条件和混合检索能力。适合大量用户、高并发查询的场景。代价是需要额外维护一套集群部署和运维成本都不低。pgvector 则是在 PostgreSQL 里加入了向量类型和相似度检索能力。如果你团队本身就在用 PostgreSQL那 pgvector 是最省心的选择——不需要额外引入新组件事务、备份、权限管理全都复用现有能力。数据量在千万级以内时pgvector 的表现完全够用。我的建议是用户量不大、团队没有专职运维的优先 pgvector数据量大、查询复杂、并发高的在考虑 Milvus。没必要一上来就上重武器。2.3 结构化记忆用图谱存用户偏好不过向量库并非万能的。它擅长“语义相似”检索但在“关系推理”上非常弱。比如你想实现“用户 A 的同事 B 喜欢的餐厅类型”向量库就很难直接回答因为这是关系链上的多跳查询。这时候可以考虑引入图数据库比如 Neo4j。把用户、偏好、实体、事件建模成节点和边就能做非常精细的记忆管理。比如用户节点指向偏好节点“喜欢日料”偏好节点又关联“喜欢寿司”的实例。“邻居推荐这家店因为张阿姨喜欢”这类推理图数据库可以轻松实现。当然图数据库的缺点是建模成本高而且嵌入到 Agent 的开发链路中需要额外写很多逻辑。我的建议是如果产品只是做轻量级的个性化问答向量库足够如果要构建复杂的用户画像和关系推理再考虑引入图存储。2.4 协议层的思考MCP 里的 Memory 服务另外想提一下 MCPModel Context Protocol。它给了 Agent 一个标准化的方式去访问工具和上下文。在 MCP 的生态里Memory 服务已经是被反复讨论的话题了。MCP 的 Memory 服务本质上是一个内存化的知识图谱它内置了实体、关系、观察结果三种类型的数据结构允许 Agent 在运行时创建、更新、查询这些元素。当模型需要记住用户的信息时可以通过 MCP 工具调用写入需要回忆时也可以通过 MCP 工具去检索。这套设计把“记忆”抽象成了模型可以直接操作的“工具”而不是开发者藏在背后的数据逻辑。好处是 Agent 不再被动等待我们喂数据它可以自己决定什么值得记、什么时候去回忆。不过 MCP 的 Memory 目前仍偏简单很多生产级场景还需要自行扩展但它代表了一个正确的方向。3. 实操给 Agent 装上一套记忆系统3.1 环境准备与依赖到实操环节了。我会用一个最小可运行的例子把短期记忆和长期记忆都串起来。先说明一下技术栈用 PythonLangGraph 负责 Agent 流程ChromaDB 做向量库OpenAI 接口做模型和 embedding。你完全可以用自己熟悉的模型和向量库替换。先安装依赖pip install langgraph langchain-openai chromadb openai pydantic如果想把长期记忆落到 PostgreSQL可以再加pip install pgvector psycopg2-binary3.2 短期记忆会话窗口的上下文管理短期记忆最简单本质就是个循环队列。不过还是要做一件事控制窗口长度。我一般用一个ShortTermMemory类最多保留最近 N 轮对话超出部分直接丢弃。from collections import deque class ShortTermMemory: def __init__(self, max_rounds: int 10): self.messages deque(maxlenmax_rounds * 2) def add_user_message(self, text: str): self.messages.append({role: user, content: text}) def add_assistant_message(self, text: str): self.messages.append({role: assistant, content: text}) def to_openai_messages(self, system_prompt: str): return [{role: system, content: system_prompt}] list(self.messages)这个类做的事情非常直白往里丢消息超了长度自动挤掉最老的。实际项目里我还会额外加一个“总 token 数”的预估逻辑因为轮数相同但每轮长度差异可能很大。比如用户某次粘贴了一大段日志光这一条就可能占掉一万 token这时候就要把裁剪策略改为“按累计 token 数 按轮数”双阈值。3.3 长期记忆向量库的写入与检索接下来是核心的长期记忆。我用 ChromaDB 的持久化客户端保存到本地目录。这里有几个设计点一是按用户隔离每条记忆都带user_id元数据二是每条记忆要带时间戳方便后续做衰减或淘汰三是唯一 ID 用于更新和删除。import chromadb import uuid from datetime import datetime class LongTermMemory: def __init__(self, persist_dir./agent_memory): self.client chromadb.PersistentClient(pathpersist_dir) self.collection self.client.get_or_create_collection( namememories, ) def add_memory(self, user_id: str, content: str): memory_id str(uuid.uuid4()) self.collection.add( documents[content], ids[memory_id], metadatas[ { user_id: user_id, created_at: datetime.now().isoformat(), } ], ) return memory_id def search_memory(self, user_id: str, query: str, top_k: int 5): results self.collection.query( query_texts[query], n_resultstop_k, where{user_id: user_id}, ) return results[documents], results[metadatas]这段代码思路很清楚add_memory负责写入search_memory负责按用户隔离检索。这里使用了默认的 embedding 方式生产环境建议显式指定 embedding 模型比如 OpenAI 的text-embedding-3-small或者本地部署的 BGE 系列模型。指定方式只要在初始化 ChromaDB 集合时传入embedding_function...即可。写入的时候还有个小细节不要只存“用户说了什么”最好让模型先把原始对话“提炼”成一条条独立记忆再写入。比如用户说“我上周去了趟青岛那边海鲜真不错”直接存一整句不是不行但更优的做法是拆成两条结构化记忆“用户去过青岛”“用户喜欢青岛的海鲜”。这样检索命中率更高也便于后续做推理。3.4 把记忆接进 LangGraph 流程LangGraph 是编排 Agent 流程的好帮手。它的核心概念是 State——一个状态对象在每次节点执行之间传递这就是天然的“工作记忆”。我们可以把短期记忆和长期记忆都挂进去。from typing import TypedDict, Annotated from langgraph.graph import StateGraph, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] user_id: str short_term ShortTermMemory(max_rounds6) long_term LongTermMemory()这里messages字段用了add_messagesLangGraph 会自动把新消息合并进历史列表这就是工作记忆的流转方式。每次处理新输入之前我会先从长期记忆里检索相关记忆拼进 system promptdef retrieve_memories(state: AgentState): user_id state[user_id] last_user_msg state[messages][-1].content docs, metas long_term.search_memory(user_id, last_user_msg, top_k3) memories [] for doc, meta in zip(docs[0], metas[0]): memories.append(f[{meta.get(created_at, )}] {doc}) return {memories: \n.join(memories)}然后把检索结果拼到 system prompt 里再调用模型生成回复。这样每当用户发来新的问题Agent 会先“想一想”自己记得哪些相关信息而不是盲目把全部历史都翻出来。这也就是检索增强在记忆场景的落地形态。4. 记忆的写入与检索策略比存储更重要4.1 什么该记什么不该记很多开发者把存储搭好后就一股脑把用户所有对话都写入长期记忆。这么做会带来两个大麻烦一是垃圾信息太多检索时总返回无关内容二是日积月累存储成本不可控。我的经验是建立一道“写入闸门”每轮对话结束后用一个轻量级模型或规则去判断这段对话里有没有值得长期记忆的信息。比如用户的明确偏好、长期事实、重要事件、讨厌的东西这些值得记而“好的谢谢”“今天天气不错”这种寒暄直接丢弃。判断方法不复杂可以给模型一个二分类提示词让模型返回worth_saving和memory_items。实测下来准确率能到九成以上。别小看这一步它决定了你整个记忆库的信噪比。脏数据一旦进去想清理就麻烦了。4.2 写入时机对话结束后批量沉淀还是实时写入写入时机的选择也直接影响产品体验。常见的有两种做法实时写入每轮对话结束后立刻把信息写入向量库批量写入多轮对话汇总后统一提炼写入。实时写入的好处是简单逻辑容易串起来而且用户信息能即时更新。缺点是如果用户连续说了很多无关紧要的话写入会很频繁成本和噪音都高。批量写入的好处是能在多轮对话里做交叉验证。例如用户在第三轮说自己住北京第四轮又说“我们这边雾霾严重”这时候批量提炼可以判断出“用户住在北京北京雾霾严重用户可能关注空气健康”。单轮写入的话这种关联性很难捕捉到。我目前更倾向于混合方案每轮先做个轻量判断候选记忆先放队列当对话结束或超过一定轮数再统一做深度提炼和写入。LangGraph 里可以在对话流程末尾挂一个“事后处理”节点干这个活。4.3 检索的质量控制Top-K、阈值与重排长期记忆的检索不是把 top-5 结果丢给模型就完事了这里面有很多细节决定体验好坏。第一相似度阈值。向量检索返回的结果未必都相关。比如用户问“杭州有什么好吃的”检索返回了一条“用户去过杭州出差”语义上沾边但不太有用。我一般会设置一个相关性阈值低于阈值的结果直接丢弃宁可没有记忆也不能用错误记忆误导 Agent。第二Top-K 的数量。太小容易漏掉关键信息太大又容易引入噪音。做通用问答时我用 3 到 5 条如果用户画像涉及维度较多我会分开检索比如“偏好类”查 3 条、“事实类”查 3 条再合并做排序。第三重排。有条件的话可以引入 cross-encoder 重排模型对初步检索的结果再做一次精排把真正贴合当前问题的记忆顶上。这样做一次检索多花几十毫秒但回复的准确率提升明显。特别是那些记忆条目较多的用户效果差距很大。4.4 记忆的更新和遗忘人是会变的。用户上个月还说不爱吃辣这个月突然热衷川菜。如果记忆系统只会积累不会更新长期下来必然产生矛盾。所以“遗忘”和“更新”机制必须从第一天就设计好。我的做法是在向量集合里每个用户的数据按“用户偏好”“事件记录”“事实档案”打标签。当新写入的记忆与旧记忆冲突时不要盲目覆盖而是让模型判断新旧信息是否真的矛盾。如果确实矛盾给旧记忆加一个deprecated标记或者调整它的权重让它不会被优先检索到。另一种机制是时间衰减检索时按时间做加权太久远的记忆降低相关性权重。比如用户五年前喜欢吃烧烤现在未必还喜欢。这个逻辑在加时间戳的那一步就已经埋好了用起来也就多写几行 filter 而已。5. 常见问题与排查实录5.1 记忆不生效先分清是没写入还是没检索遇到“Agent 记不住用户”的情况第一步不是改代码而是先定位问题出在写入链路还是检索链路。我排查时会在系统里加一行日志把每次写入和检索的关键信息都打出来确认写入时到底有没有成功落库、检索时到底有没有查到结果。常见的一个坑是 ChromaDB 的持久化目录写错了。你代码里用的是./agent_memory但实际工作目录在另一个路径导致每次启动都新建了一个空库记忆全丢。这个坑我踩过不止一次现在会在启动阶段加一个校验检查集合里的文档数量是否和上次会话结束时一致。5.2 检索结果太乱控制记忆的细粒度有的朋友跟我说明明存了记忆但检索回来的东西看着就是不对劲——一会儿是“用户喜欢喝咖啡”一会儿是“用户在某次活动上迟到”。这类问题的根源多半是记忆的“细粒度”没有控制好。太细的记忆有价值但很难用太粗的记忆又等于没记。我的习惯是在写入提炼时要求模型遵循“一条记忆只承载一个事实”的原则。如果一条记忆里包含了多个信息点宁可拆成多条。检索命中时相关性才会更精准。5.3 用户画像冲突新旧记忆打架怎么办这类问题在长期运行的产品里几乎必然出现。用户昨天说“我是重度咖啡爱好者”今天又说“我开始戒咖啡了”。如果记忆系统不做冲突处理模型就一会儿说咖啡好一会儿推荐无咖啡因饮品效果非常分裂。我现在的方法是在存储层加一个“事实修正链”新记忆写入时不是直接插入而是先检索旧记忆让模型判断是否存在矛盾。存在矛盾时旧记忆打个“已过时”的标签日期标为旧新记忆带上“自某日起更替”的说明。这样检索时新记忆天然排在前面模型也不会被旧信息误导。5.4 上下文爆窗及时做摘要压缩即使做了检索式长期记忆短期内对话的上下文仍可能超过模型窗口。有些用户会持续聊很久比如连续聊了两三个小时的高强度咨询即便我们只保留最近 10 轮这 10 轮里也可能塞满了大段资料。这时候就需要做摘要压缩。我通常在 LangGraph 中加一个判断节点如果当前消息的总 token 数超过阈值就调用模型把前面的对话提炼成一段摘要然后替换掉旧消息。摘要本身也可以作为一条新的记忆写入向量库这样压缩后的关键信息不会流失。这里有一个小技巧摘要不要只让模型“总结对话”而要明确提示“保留用户偏好、明确需求、关键事实三要素”。否则模型容易写成新闻通稿似的流水账真正有用的信息反而漏掉了。5.5 常见问题速查表症状可能原因解决办法Agent 对用户之前的说法毫无印象长期记忆未写入检查写入链路日志确认对象存储目录检索出的记忆与当前问题无关Top-K 太大或未设阈值降低 Top-K增加相似度阈值过滤用户记忆互相矛盾缺少冲突处理写入时增加新旧记忆矛盾判断上下文窗口超限只做短期记忆未做压缩添加摘要压缩节点压缩后写入长期记忆向量库查询响应越来越慢数据量增长、缺少索引升级为 pgvector按 user_id 建索引多用户之间记忆串台检索时未带用户过滤条件确保where{user_id: user_id}生效6. 记忆的安全与隐私别等到出事再补6.1 敏感信息默认不要进长期记忆记忆系统最容易被忽略的是隐私边界。用户可能无意间向 Agent 透露了电话、地址、身份证号等敏感信息。如果这些信息被写入长期记忆且长时间保留一旦数据库泄露或模型输出被滥用后果非常严重。我的原则是默认不记录敏感信息。在写入提炼节点之前加一道脱敏检查。可以用规则匹配手机号、邮箱、身份证格式也可以用模型判定。发现敏感信息时直接丢弃或者经过用户明确授权后再保存。别想着“先存了再说”数据合规方面底线不能松。6.2 给用户控制权清空记忆、关闭记忆用户应当有权删除自己的记忆数据。产品设计上至少要有“清空我的记忆”或“关闭记忆功能”的选项而且这个功能要真正能落地不只是前端隐藏按钮。实现上删除其实很容易。在向量库里按user_id删除所有记录搞定。真正难的是“关闭记忆”模式下整个会话链路都不能再做写入。我在写这段逻辑时是把记忆模块做成一个可插拔的组件配置开关关闭时写入函数直接变成空操作检索函数返回空列表。这样即便代码里忘了判断开关也不会发生“关了还在记”的问题。6.3 记忆泄露的另一个方向Agent 主动说出他人信息还有个容易被忽视的坑Agent 不仅可能泄露用户自己的信息还可能因为记忆串了用户 ID把 A 的信息说给了 B。这就是多租户隔离没做好的问题。在向量库中所有检索都必须强制带where{user_id: current_user_id}条件这一点我反复强调。同时写入时也要确保用户 ID 来自可信的认证上下文不能来自用户输入。如果 Agent 具备多用户协作功能还要额外设计共享记忆和私有记忆的权限分层。这块逻辑一旦疏忽就是安全事故级别的 bug绝不能不重视。最后分享一点个人体会做记忆系统做到最后我最大的感受是给 Agent 装记忆难点不在于“装”而在于“筛”和“换”。怎么把值得记的筛出来怎么让旧信息优雅地退场这两个问题处理好了Agent 的体验会有质的飞跃。也别指望一个方案解决所有场景——对话机器人、企业知识库、个人助理记忆策略侧重点完全不同。我建议从最小的闭环跑起先解决“用户重复信息不重复问”再逐步加上摘要、冲突处理和时间衰减。这个方向不会走偏。
返回列表