Agent记忆系统从零到实战:短期与长期记忆实现与LangGraph落地

发布时间:2026/9/12 13:50:03
Agent记忆系统从零到实战:短期与长期记忆实现与LangGraph落地 前面两篇把Agent的框架和工具调用捋完了今天这篇聊聊我踩坑最多、也最能拉开体验差距的部分——记忆。如果一个Agent聊完就忘那它本质上就是个带流畅话术的搜索引擎称不上真正的“助手”。只有让Agent记住你的偏好、习惯、项目背景、上次聊到一半的结论它才从玩具变成生产工具。这篇我直接把记忆系统拆开讲清楚短期记忆与长期记忆的实现思路、方案选型再用LangGraph搭一个可持续跨会话记忆的Agent实例最后把我在生产环境里踩过的坑和排查经验一并交代。1. 为什么“记住了”才是合格的Agent1.1 无状态Agent的尴尬聊过就忘先说不加记忆的Agent是什么体验。你用同一个Prompt或者同一个API Key拉起一个Agent跟它说“我喜欢简洁的回答风格不要铺陈”它当场答应得好好的第二天再打开同一个对话页面问它“按我昨天说的风格给我写个邮件”它一脸茫然好像昨天的话从来没发生过。这不是模型不行是因为每次请求都是“无状态”的模型看到的内容只有你这次发出去的消息加上系统提示词它没有任何“昨天”的概念。无状态在API调用层面是天然属性大模型本身不对持久化数据负责。所有的“记忆”本质上是应用层帮它补的——把该记的存下来在合适的时候重新塞回它的上下文里。理解这一点特别关键很多人以为换个贵的模型记忆就解决了其实记忆跟模型能力是两回事再强的模型你不给它历史信息它也猜不出你上周的偏好。在生产项目里无状态带来的问题更具体。用户连续问了三个问题第一个问题说“我在用Spring Boot 3.2JDK 21”第二个问题说“帮我看看这个报错”第三个问题说“我的环境还需要什么配置”如果Agent不能把三个问题串起来第二个问题它得让用户重新贴一遍报错第三个问题它得再问一遍用户的技术栈。用户不是来伺候你的这种体验基本等于劝退。加了记忆之后Agent的行为表现会发生质变多轮对话能承上启下跨会话能延续偏好甚至能根据历史行为主动做推测。这个质变背后其实就是一个简单的闭环——写入记忆、存储记忆、检索记忆、注入上下文。难的不是概念而是怎么把这个闭环在企业级场景里做得不炸、不贵、不泄露隐私。1.2 记忆的分层模型短期记忆、长期记忆与情景记忆我在设计记忆系统时喜欢先把记忆拆成三层。这个分层不是学术概念自嗨而是直接对应不同的存储介质和访问策略架构上分清楚了后面写代码才不拧巴。第一层是短期记忆说白了就是当前会话里模型能看到的那部分上下文包括用户最近几轮发言、Agent最近的回复、正在执行的任务中间态。这一层通常放在上下文窗口里实现也最简单——历史消息按时间顺序拼进Prompt。但它的上限被上下文长度锁死正常不会真把几万条历史全塞进去所以需要窗口化策略只保留最近的N轮再往前的就压缩或移交到下一层。第二层是长期记忆跨会话持久化的信息都归它管。典型内容有用户的固定偏好称呼、语言风格、信息详略程度、项目背景技术栈、业务领域、关键约束、历史结论上次讨论的方案、已经确认的需求。这一层适合放到外部存储关系数据库存结构化偏好向量数据库存非结构化的语义记忆或者干脆两者结合。长期记忆的核心问题是“怎么挑出值得记的”以及“检索时怎么把最相关的记忆找回来”。第三层是情景记忆记录的是某个具体时间点发生过的完整事件。比如用户上周五在工单里提到“订单模块经常超时”这条信息包含时间、事件、情绪和上下文它既不是一条偏好也不是一条稳定的知识而是一段历史。情景记忆的价值在于做推理时能引用具体事件比如Agent可以说“按照您上周五反馈的订单超时问题我调了数据库连接池参数”。这层往往用日志型存储或时间序列数据库承载检索时按事件语义和时间范围双维度匹配。三层之间不是孤立的实际运行时有一条数据流会话中的短期记忆快塞不下时触发提炼逻辑把关键信息抽取到长期记忆和情景记忆下一次会话开始时再根据用户的新输入检索长期记忆把相关片段注入到Prompt里作为背景。这套“写—存—检—注”的循环就是Agent记忆系统的完整骨架。2. 记忆系统实现方案与选型思路2.1 最朴素的记忆把历史对话塞进上下文先说一种几乎所有做过ChatBot的人都会上手的方案——直接把历史对话拼进上下文。实现成本极低用一个列表接收历史消息请求模型时按角色前缀拼接比如把用户消息标成HumanAgent消息标成AI然后整体传给模型。这套方案在对话轮次少、单轮内容短的场景下完全够用尤其是客服问答类的临时对话用户问一句答一句不需要跨会话记忆短期记忆用上下文拼接就够了。但把历史对话塞进上下文的坑很快会暴露。第一个坑是Token成本爆炸。假设每轮对话平均消耗800个Token50轮下来就是4万个Token按主流模型的价格算一次请求的成本已经从几分钱跳到几块钱业务量一大成本直接失控。第二个坑是模型“注意力稀释”——上下文里塞了太多早期内容模型对最近关键信息的关注度反而下降尤其是中间混杂了一些无关闲谈时效果会明显变差。第三个坑是历史消息不做结构化区分所有信息一视同仁用户临时说了一句“这个颜色我不喜欢”跟用户半年前说“我偏好深色模式”在拼接方案里都变成了同样权重的消息模型根本分不清哪个是长期偏好哪个是随口一提。所以这个方案我给出的结论是适合原型验证和低并发内部工具不适合生产级助手。真正要上生产你的记忆系统至少要能回答两个问题——哪些信息值得跨会话保留以及下一次用户提问时怎么把最相关的记忆捞出来。这就引出了下一节说的向量检索方案。我在实际项目中还见过一种中间态优化不拼接全部历史而是用LLM定期把历史对话“总结压缩”成一段摘要然后只拼摘要和最近几轮全文。这个思路比全量拼接聪明得多摘要保留了大方向信息最近几轮保留细节上下文。实现并不复杂但摘要有一个天然缺陷——它是一次性压缩细节会丢用户半年前提到的一个关键项目名在摘要里可能被一句话带过而检索时你又不能像向量库那样按语义命中它。所以摘要方案适合做短期记忆的延伸不适合做长期记忆的主体。2.2 向量检索型长期记忆让Agent“想得起来”长期记忆想要在跨会话场景中“想得起来”主流做法是向量化存储加语义检索本质跟RAG一样把记忆内容切成片段用Embedding模型转成向量存入向量数据库用户提问时把问题也转成向量在库里做相似度检索取回最相关的记忆片段注入Prompt。这套方案的好处是打破了“必须精确匹配关键词”的限制用户说“我想起了之前你帮我配置的链路追踪”即使历史里没有“链路追踪”这个词但向量相似度能把“SkyWalking采集日志链路”这条记忆捞出来这是关键词搜不到的。具体到落地有几个环节必须仔细打磨。第一是记忆片段的粒度切太粗一段记忆里混了多个主题检索命中时会把无关内容一起带进来切太细又会被打断成碎片语义不完整。我在实践中一般按“一个自然段落讲清一件事”为标准切分长度控制在100到300字之间。第二是Embedding模型的选择通用Embedding模型对技术术语和业务黑话的效果比较弱尤其在企业内部场景最好在专用语料上做微调否则检索Top 5里可能有两三条完全无关。第三是元数据打标每条记忆入库时至少带上用户ID、存入时间、来源会话ID、记忆类型这样检索时可以按用户ID过滤避免A用户的记忆污染B用户。向量数据库的选型我分别趟过几类。如果只是想快速验证Chroma或者FAISS本地跑都行数据量在十万条以下时响应还可以。要上生产我建议直接用具备服务化能力的向量库比如Milvus或Qdrant它们对数据的持久化、索引构建、并发查询处理得更成熟。还有一条路是直接用云厂商的向量检索服务省去运维适合团队规模不大的场景。选型时除了检索性能记得重点看过滤能力大多数场景不是全库检索而是先按用户维度过滤再在子集里做相似度这个能力很多轻量级库并不完善。另外我想强调一点向量检索是“模糊回想”不是“精确读档”。它适合把相关背景捞出来但不适合做精确的事实查询。比如用户问“我上个月买的套餐多少钱”如果这个价格信息在向量库里以长文本存储检索可能命中了一条相关的但价格已经被修改的旧记录。所以在做记忆系统时最好把“固定事实型记忆”用户ID、套餐、缴费日期用结构化字段存关系库把“语义型记忆”偏好、项目背景、讨论结论存向量库两类配合各干各擅长的事。2.3 主流Agent框架的记忆方案对比最近这个方向框架乱得很LangGraph、Spring AI、LlamaIndex、AutoGen都有各自的记忆实现选型时容易眼花。我实际用下来把它们放在一起对比更有感觉。LangGraph是目前我用得最多的它的核心思路是把Agent定义成一个图节点是处理逻辑边是流转关系状态在节点间传递。记忆功能基于两个机制一个是Checkpointer负责把每一步的状态快照持久化这样Agent执行一半宕机可以恢复也能让同一个会话的上下文跨请求续上另一个是外部存储你自己接向量库或数据库来保存跨会话的长期记忆。LangGraph上手的门槛确实高一些但灵活度最高适合复杂流程和深度定制的生产项目。Spring AI对Java生态很友好如果你的团队全是Java开发者那它是自然选择。它在记忆方面提供了ChatMemory接口内置了MessageWindowChatMemory这种基于滑动窗口的实现直接把最近N轮消息管理好长期记忆则可以配合Spring的VectorStore抽象接各类向量库。用Spring AI搭一个带短期记忆的Agent非常快几行配置就能跑起来但要做复杂的记忆提炼、多级记忆融合需要自己再写不少胶水代码。LlamaIndex本身侧重知识库和检索增强它天然适合做“给Agent外挂记忆”的场景。它的ChatMemoryBuffer管短期记忆VectorIndex管长期检索如果你已经有文档知识库想把知识库和会话记忆打通LlamaIndex会比较顺手。AutoGen是多Agent协作框架记忆的侧重点不在单Agent的个性化记住而在多Agent之间共享上下文和任务状态。选型建议用一句话收束先看团队技术栈再看业务复杂度最后看记忆的核心诉求是“续上当前会话”还是“跨会话个性化”。如果只是续会话用Spring AI的内置方案就够了如果要做企业级的长期记忆和人设一致性LangGraph的可控性会更强。3. 实操用LangGraph搭一个带永久记忆的Agent3.1 环境准备与基础代码骨架这一节我把前面讲的概念全部落到代码里。直接上LangGraph搭一个带长期记忆的Agent流程是先实现短期记忆跨请求续会话再实现长期记忆从向量库存取用户偏好最后加一条记忆提炼链路。环境准备先列一下我用的是Python 3.11框架依赖如下pip install langgraph langchain langchain-openai langchain-community chromadb一个最小的LangGraph Agent通常包含状态定义、节点函数和编译三部分。状态用MessagesState表示它内部维护了一个消息列表每次节点执行后会把新产生的消息追加进去。先搭一个最简单的问答节点from typing import TypedDict, Annotated from langgraph.graph import StateGraph, MessagesState, START, END from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage, SystemMessage model ChatOpenAI(modelgpt-4o-mini, temperature0.7) def call_model(state: MessagesState): response model.invoke(state[messages]) return {messages: [response]} # 构图 graph StateGraph(MessagesState) graph.add_node(assistant, call_model) graph.add_edge(START, assistant) graph.add_edge(assistant, END) agent graph.compile()现在这个Agent还是无状态的你调用一次agent.invoke()聊完就结束。要让它的短期记忆跨请求生效需要引入Checkpointer。3.2 用Checkpointer实现跨会话状态持久化Checkpointer是LangGraph里管短期记忆的核心组件。它会在每个节点执行后把整个状态保存下来下次推进图时能从保存的状态继续走。最直观的体现是你传入一个thread_id它就能恢复属于这个线程的历史对话相当于给Agent开了“内存档案”。我用SQLite作为持久化存储先创建一个连接并传入编译参数from langgraph.checkpoint.sqlite import SqliteSaver # 使用内存型SQLite生产环境建议换成Postgres或MySQL的checkpointer with SqliteSaver.from_conn_string(:memory:) as saver: agent graph.compile(checkpointersaver) config {configurable: {thread_id: user-123}} # 第一轮对话 result1 agent.invoke( {messages: [HumanMessage(content请记住我叫陈晨偏好用简洁的列表回答技术问题)]}, configconfig ) # 第二轮对话Agent已经能看到第一条历史 result2 agent.invoke( {messages: [HumanMessage(content我叫什么名字)]}, configconfig ) print(result2[messages][-1].content)运行之后能看到第二轮模型会正确回答“陈晨”。机制是LangGraph把整个MessagesState按thread_id存进了SQLite第二轮invoke时图先从库中加载历史消息再拼接新消息传给模型。这里有个容易被忽略的细节checkpointer传入的SqliteSaver必须是同一个连接实例才能保证状态共享。如果你在每次请求里都新建一个SqliteSaver那thread_id变成了一串没有记忆库的钥匙什么也查不到。生产环境里我会创建一个模块级单例连接或者在FastAPI应用启动时初始化一次避免反复开关数据库连接。短期记忆到这里工作正常但问题在于它记得太多了。这个Agent会把所有消息原封不动存进SQLite时间一长单次推理时传给模型的消息列表会无止境膨胀。更麻烦的是它只是记住了“全部”并没有甄别哪些信息值得长期保留用户随口一句“今天下雨了”也会被永久存着没有任何价值。所以下一节要解决的关键问题就是——从大量历史状态里提取值得长期记住的信息并把它放进向量库。3.3 增加记忆检索与注入从向量库召回用户偏好长期记忆我选择Chroma做向量库存储两类信息一是从历史对话中提炼出的用户偏好二是关键事件记录。先把基础检索链路搭出来from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma memory_store Chroma( collection_nameagent_long_term_memory, embedding_functionOpenAIEmbeddings(modeltext-embedding-3-small) )接着在Agent的图里增加两个节点retrieve_memory负责从向量库检索与当前输入相关的记忆extract_memory负责把本轮对话中的关键信息提炼并写入向量库。先实现检索节点def get_last_user_message(messages): for m in reversed(messages): if isinstance(m, HumanMessage): return m.content return def retrieve_memory(state: MessagesState, config) - dict: # 取出最后一次用户输入 query get_last_user_message(state[messages]) if not query: return {} user_id config[configurable].get(user_id, default_user) # 先按用户过滤再语义检索避免跨用户记忆串场 docs memory_store.similarity_search( query, k5, filter{user_id: user_id} ) memory_text \n---\n.join(doc.page_content for doc in docs) return {memory_context: memory_text}状态里加上memory_context字段后模型调用节点需要把它拼进SystemMessage。这里我强烈建议不要直接把检索结果塞进用户消息而是塞进系统提示词。原因在于系统提示词在模型眼里是权威级别的信息用户消息则可能被当成可忽略或可质疑的输入塞进系统提示词能让模型更重视这些回忆片段。接着实现记忆提炼节点。这一步是长期记忆能不能“进化”的核心。我用一轮独立的LLM调用做信息抽取把用户输入里值得长期记住的内容挑出来结构化后写入向量库import json def extract_memory(state: MessagesState, config) - dict: user_id config[configurable].get(user_id, default_user) # 取本轮新增的对话 messages state[messages] if len(messages) 2: return {} conversation_pair f 用户说{messages[-2].content if isinstance(messages[-2], HumanMessage) else } Agent说{messages[-1].content if isinstance(messages[-1], AIMessage) else } extraction_prompt f 从下面的对话中提取需要长期记住的用户信息包括但不限于 - 用户固定偏好称呼、风格、语言习惯 - 用户的项目背景技术栈、业务领域、团队规模 - 用户明确表达的需求和结论 - 重要的事件记录时间、对象、结果 只输出JSON列表每项包含type和content两个字段。 对话内容{conversation_pair} llm_output model.invoke([HumanMessage(contentextraction_prompt)]).content try: memories json.loads(llm_output) except json.JSONDecodeError: memories [{type: unknown, content: conversation_pair}] for mem in memories: memory_store.add_texts( texts[mem[content]], metadatas[{ type: mem.get(type, unknown), user_id: user_id, created_at: datetime.now().isoformat() }] ) return {}最后重构图把两个新节点加入执行链路graph StateGraph(AgentState) graph.add_node(assistant, call_model) graph.add_node(retrieve_memory, retrieve_memory) graph.add_node(extract_memory, extract_memory) graph.add_edge(START, retrieve_memory) graph.add_edge(retrieve_memory, assistant) graph.add_edge(assistant, extract_memory) graph.add_edge(extract_memory, END) agent graph.compile(checkpointersaver)AgentState需要增加memory_context字段模型调用节点改为读取它并拼进SystemMessagedef call_model(state: AgentState): memory_ctx state.get(memory_context, ) system_prompt f你是用户的专属助手。 以下是关于用户的长期记忆请在实际答复中主动参考 {memory_ctx} messages [SystemMessage(contentsystem_prompt)] state[messages] response model.invoke(messages) return {messages: [response]}这一套跑下来效果和之前的无记忆Agent完全不同。第一轮用户说“我叫陈晨负责公司的支付系统偏好列表式回答”第二轮用户隔天再问“帮我写一封给技术部同事的邮件催一下支付网关的联调进度”系统会从向量库检索到陈晨的姓名、业务领域和表达偏好模型会直接以“陈晨”的身份落款并自动采用列表式、干脆利落的表达风格。这就是长期记忆带来的体验跃迁。注意一个性能细节extract_memory每一次对话都要调用一次LLM如果对话频率高Token成本会翻倍。我在生产里常用两个降本手段一是限制触发频率比如每五轮对话或每三分钟才做一次提炼二是增加“信息价值过滤”让提炼Prompt只输出有长期价值的信息对那些日常闲聊直接返回空列表。4. 记忆工程的坑与经验4.1 记忆污染的后果与清洗策略记忆系统最隐蔽、最难受的问题不是“记不住”而是“记错”。我在一个客服项目里吃过一次大亏用户反馈“订单总是超时”当时上下文里有另一条关于“网络抖动”的闲聊提炼节点无差别地把两条信息混合成一条“订单超时与网络不稳定有关”的记忆写进了向量库。之后Agent在回答所有订单相关问题时都把这条不准确的归因当事实引用输出结果开始出现幻觉式的错误推理而且因为每次都会检索到这条记忆错误被一遍遍强化。这个问题的根源在于记忆提炼节点缺少校验。提炼模块本质上也是LLM它会把推理和事实混在一起。我的解决方案分两层。第一层是让提炼Prompt尽量“事实化”——只抽取用户明确说出的陈述性内容禁止模型自行推断因果拿不准的信息放弃写入。第二层是建立记忆“可信度”机制每条记忆带上数据来源和置信度分数模型引用记忆时能根据来源判断权重同时定期用一版独立LLM对存量记忆做审计把与近期对话明显矛盾的记忆标记为过期。另外我建议大家给长期记忆加一个“删除接口”千万不要把记忆库做成只写不删的黑洞。我遇到过团队把记忆库跑了一年里面堆了数万条冗余记忆检索Top 5经常被陈旧记忆占据。后来我在管理后台单独做了一个“记忆管理”页面支持按用户查看记忆内容、手动删除单条、一键清空实时性和准确性都好了很多。4.2 上下文膨胀Token成本与性能平衡记忆系统的初衷是让Agent更聪明但设计不好反而让推理变慢变贵。短期记忆里存了太多历史消息每次请求造Prompt时把这堆消息全部塞进去模型处理时间拉长Token费用直线上升。我见过一个团队因为短期记忆不设上限每次请求的Prompt居然有十多万Token一次对话下来成本比人工客服还贵。成本控制的做法我总结成“三限”限轮数短期记忆默认只保留最近10到20轮消息超过的移出上下文触发一次摘要后归档到长期记忆。限长度单条记忆入库前先做裁剪超过300字的内容通过LLM压缩成一句话摘要再存储。限召回量向量检索Top K控制在3到5条多了不仅费Token还容易灌入无关信息。这三个限制看着简单但对稳定性和成本的改善非常明显。我优化过的一个项目把Prompt从平均4万Token压到了8000Token以下响应时间从8秒降到2秒左右模型输出质量反而更稳定因为干扰信息少了。这里还有一个容易被忽略的平衡点记忆不是越多越好召回不是越准越好。记忆的目的是帮助模型“聚焦”一旦Prompt里塞入了过多背景模型会陷入选择困难反而忽略用户当前的核心问题。所以在做检索排序时我不仅看相似度分数还会加上时间衰减因子——距离当前时间太久的记忆相似度分数做一下打折这样近期信息优先避免老记忆抢占新信息的位置。4.3 多Agent场景下的记忆隔离与共享稍微复杂一点的系统不会只有一个Agent。我的一个项目里跑了三个Agent一个负责售前咨询一个负责售后工单一个负责内部知识问答。如果它们共享同一个记忆库会出现很尴尬的场景用户在售前Agent那聊了“预算有限希望找性价比高的方案”到了售后Agent那边这个信息被检索出来售后Agent开始推荐低价产品但用户此时关注的是故障处理这种错位很让人抓狂。解决方案是给记忆加“命名空间”。我在所有记忆元数据里固定维护三个字段user_id表示这条记忆属于哪个用户agent_id表示这条记忆在哪个场景下产生scope_type表示这条记忆的可见范围。可见范围有三种取值private只对当前Agent可见shared在多个Agent之间可见team在用户所属团队内可见。默认创立的所有记忆都是private只有用户明确表示“这个偏好对所有服务都生效”或者“告诉我的专属客服经理”时才通过一个显式的转换流程升级为shared或team。这个设计既保住了多Agent协作时的信息共享效率也防止了各场景信息乱窜导致的体验劣化。做好隔离还有一个额外好处调试时能按Agent维度单独查记忆定位问题快很多。4.4 隐私边界该记住什么该忘掉什么最后说一个容易被技术团队忽视的话题——记忆的隐私和合规边界。Agent能够记住用户本身就意味着它掌握了大量个人数据如果记忆库不做隐私控制一旦泄露就是安全事故。我在设计记忆系统时有几个硬性原则所有业务不管大小都套用。第一是“最小必要”只记录支撑服务所必需的信息用户没提的、跟服务无关的敏感信息身份证号、银行卡号、完整家庭住址一律不进记忆库提炼节点在写入前会自动做敏感信息脱敏把数字替换成占位符。第二是“用户可控”给用户提供查看自己记忆内容的入口支持一键导出和一键清除这既是合规需要也让用户对Agent产生信任感。第三是“存储隔离”记忆库在物理或逻辑上与主业务数据库隔离访问凭证分权只有Agent服务本身有读写权限内部后台查看也需要单独授权。有些记忆是需要主动遗忘的。比如用户请求删除历史数据、用户注销账号、或者某条记忆内容与后续事实矛盾这些场景都应该有一条定时清理任务把对应记忆抹掉。我踩过的一个坑是用户要求删除账号我把关系库的数据清了但向量库里还留着几千条该用户的记忆导致几个月后新用户注册时偶发检索到前一个人的信息这类事故在合规审计里非常麻烦。所以清理逻辑一定得同步覆盖所有存储介质——关系库、向量库、对象存储少一个都不行。5. 写在后面记忆是Agent的人设底座这套记忆系统的雏形是我在一个企业内部助手项目里一点一点打磨出来的。最开始只是为了让Agent记住用户的称呼后来发现记住偏好比记住称呼重要再后来发现会“遗忘”比会“记住”更重要。走到现在我对记忆系统的理解变成了一个非常朴素的总结记忆不是为了堆砌数据而是为了让Agent在恰当的时机展现出“它真的懂你”的那一面。如果这篇对你有一点启发我建议你先从最简单的一步开始——给现有Agent加一个线程级的短期记忆让同一个用户的多轮对话能承上启下。这一步做完再去想向量库、记忆提炼、多Agent共享。记忆系统不是一个一次性的功能而是一套持续演的机制它需要你随着业务反馈不断调整提炼规则、检索策略和过期清理策略。最后再分享一个小技巧调试记忆Agent时多打印“这次检索到了什么记忆”亲眼看到召回内容是否合理比只看最终回答更有效。