资讯动态

LangGraph Store实现Agent长期记忆:从上下文窗口到跨会话持久化存储

发布时间:2026/10/20 16:29:36 来源:尧图企业网站定制
做Agent应用做得越久我越觉得“模型不记得我们但产品要求它记得”这句话里的矛盾比想象中尖锐。刚接触LangChain、LangGraph那会儿我和很多人一样为了让Agent记住前几轮的对话直接把历史消息全部拼进Prompt结果Token一路飙升、响应越来越慢更尴尬的是只要换一个会话、换一个用户之前积累的“记忆”就彻底归零。后来我把目光转到LangChain生态里的Store组件上配合LangGraph的状态图编排能力才算是把长期记忆这件事真正做通了。这篇文章我会拆解Store是什么、和LangGraph里的短时状态有什么区别、DeepAgents这种多步骤深度Agent怎么靠它实现跨会话记忆以及我自己在落地过程中踩过的坑。适合正在做多轮对话、客服机器人、或者需要Agent记住用户偏好的开发者参考。1. 记忆到底该放哪里上下文窗口和Store的分界线1.1 “把历史消息全拼进Prompt”为什么早晚会出事先说一个我反复目睹的误区很多人理解的“让Agent记住”就是把前面所有对话消息丢进System Prompt或上下文窗口里。这个方案在最简单的Demo里是能跑的但只要你多做几件事就会发现问题。第一个问题是成本。上下文长度直接决定了每次请求的Token数而大模型的计费、延迟都跟着Token走。假设一个客服Agent每天处理一万次请求每次多塞两万Token历史账单和响应时间都会变得很难看。第二个问题是注意力衰减——模型并不会因为上下文里有那么多历史就真的“重点照顾”每条信息长对话末尾的关键偏好经常被淹没反而导致回答质量下降。第三个问题最致命上下文窗口是会话级的它天然不区分“这是谁在说话”。用户A聊过的内容如果被错误拼接到用户B的请求里就是灾难。所以问题早就不是“上下文够不够长”而是“记忆应该放在一个独立的、可检索的存储层里而不是堆在模型输入里”。长期记忆的核心诉求是两个跨会话存活、按需精准召回。这就引出了Store存在的意义。1.2 长期记忆的两种形态情景记忆与语义记忆我在设计记忆逻辑之前先想清楚了一件事不是所有记忆都该原样保存。按我对LangGraph记忆文档和实际项目经验的整理长期记忆至少分两类。情景记忆Episodic Memory指“之前发生过什么”的原始记录。典型例子是用户昨天问过某产品的退款政策今天又提起“我之前问过那个问题”。这种记忆带时间属性、需要按时间线回溯一般以消息列表或事件列表的形式保存。语义记忆Semantic Memory指“我总结出的结论和偏好”。用户可能不会直接说“我是做跨境电商的”但聊天过程中反复提到物流、清关、海外仓。这类记忆应该是Agent自己提炼出的结构化事实比如“用户经营跨境电商主营品类为家居关注欧洲物流时效”。维度情景记忆语义记忆保存内容原始消息、事件、时间戳提炼后的事实、偏好、结论数据形态列表、日志、消息流结构化字段、JSON、摘要文本检索方式按时间、按会话范围按命名空间、按关键词搜索典型TTL较短几天到一个月较长可长期保留使用时机追溯上下文、连续对话构建用户画像、个性化决策Store在设计上对这两种形态都很友好你可以把原始事件作为value放进去也可以把提炼的摘要放进去。关键是你要在应用层想清楚每条记忆属于哪一类这决定了你写入Store时用什么样的namespace、设置多长的TTL。这一点后面聊DeepAgents策略时会专门展开。2. LangChain、LangGraph和DeepAgents在记忆这件事上的分工2.1 为什么LangChain传统的Memory模块不够用了如果你以前用过LangChain可能留意到它早期提供过ConversationBufferMemory、ConversationSummaryMemory这类组件。它们确实能帮你在一次会话内部保存消息但本质上是进程内的临时Buffer要么挂在内存里、要么存在本地会话对象里。一旦服务重启、多实例部署、或者用户隔天再来这些记忆就丢了。后来LangGraph引入的Store在思路上做了两个关键转变一是把记忆从“会话对象内部”挪到了“独立的存储层”你可以挂内存实现、也可以挂Postgres这类持久化存储二是把记忆的寻址方式从“线性追加消息”变成了“命名空间键值可搜索”。这个设计既是基础设施层面的改动也是思维方式的改动——记忆不再是贴在一次对话屁股后面的尾巴而是Agent可以主动查询和写入的外置数据库。2.2 Store的接口机制namespace、put、get、search、TTLStore的接口设计得并不复杂核心就几个概念。namespace命名空间这是一个字符串元组用来标识一段记忆属于谁。我的理解是它就像文件系统里的路径。比如(user_123,)表示用户123的个人命名空间(user_123, order_456)表示用户123的订单456这个子空间。层级越深记忆的隔离粒度越细。你可以通过多级namespace来组织“用户/会话/任务”的归属关系。put与getput(namespace, key, value)负责写入get(namespace, key)负责精确读取。value可以是任意可序列化的字典key是这段记忆的名字。注意同一个namespace下的key不能重复重复put就是覆盖更新。这个语义很适合保存“用户画像”“最新偏好”这类需要原地更新的记忆。search这是Store最核心的能力。你可以给出一段namespace前缀、一个查询文本和limit数量它返回一批匹配的记忆条目。它解决的是“我不能精确知道要查哪条记忆但我知道大概要找什么”的场景。比如用户说“我之前问过怎么导出数据”你可以用search(namespace, query导出 数据)去模糊召回。需要提醒的是不同存储实现里search的细节有差异但设计意图是一致的按需召回而不是全量回放。TTL与时间衰减持久化Store支持给记忆设置过期时间。写入时可以带上生命周期参数系统会定期清理过期数据。这个特性非常实用——情景记忆只需要保留几天概括性的语义记忆则可以保存更久甚至不过期。这一套接口组合起来以后消费者的心智模式就变成了我有一块属于自己的记忆空间可以精确地写入、更新、删除也可以模糊检索。这比“塞上下文”先进得多。3. 完整落地在LangGraph里接入Store并跑通读写3.1 先InMemoryStore跑通逻辑再迁移持久化实话说我建议你第一版先别碰外部数据库。在LangGraph里挂一个内存版Store把整个读写链路调通再迁移到Postgres等持久化实现会让排查问题简单很多。我的理由是内存版不需要任何连接配置、没有环境依赖出问题时大概率是接口理解错了而不是数据库连接断了。下面这个例子就是最简的挂载方式from typing import TypedDict, Annotated, operator from langgraph.graph import StateGraph, START, END from langgraph.store.memory import InMemoryStore store InMemoryStore() class AgentState(TypedDict): messages: Annotated[list, operator.add] graph_builder StateGraph(AgentState) # ... 添加节点和边 ... graph graph_builder.compile(storestore)注意力放在compile(storestore)这一行——如果你忘了传store参数后续在节点里通过config读取store时只会拿到空值这是新手最容易踩的第一个坑。3.2 在Agent节点里读写记忆的完整流程挂载好store之后读写发生在节点内部。我的标准写法是节点函数接收config参数从里面取store再按当前用户的ID构造namespace先搜索记忆、再调用模型、最后把新内容写入。from langchain_core.messages import HumanMessage, SystemMessage def agent_node(state: AgentState, config: dict): # 从config中取store store config[store] user_id config[configurable][user_id] # 1. 搜索已有记忆 namespace (user_id,) memories store.search( namespace, query用户偏好 需求 项目, limit5 ) memory_context \n.join( f{item.key}: {item.value} for item in memories ) # 2. 把记忆拼入system提示 system_text 你是一位专业助手。以下是关于当前用户的长期记忆\n if memory_context: system_text memory_context else: system_text 暂无历史记忆。 messages [SystemMessage(contentsystem_text)] messages.extend(state[messages]) # 3. 调用模型伪代码按你的模型接入方式替换 response call_model(messages) # 4. 写入新记忆保存一条新的关键信息 store.put( namespace, latest_preference, {source: user_id, preference: extract_preference(response)} ) return {messages: [response]}这段代码体现了三个关键动作召回search、利用拼进提示词、沉淀put。我特别强调写入这一步因为很多人只做了“读记忆给模型看”却忘了把每轮新产生的信息写回去导致Agent的长期记忆永远是空的。另外要提醒一句写入前最好做一道提炼。不要直接把用户原始对话原封不动写进Store那样记忆很快会膨胀成垃圾场。我通常会让模型在回答完用户之后顺便生成一条“需要记住的信息”摘要再写进Store。3.3 切换到PostgresStore的配置细节逻辑跑通之后迁移到持久化Store几乎不改变你节点里的代码主要改动集中在初始化阶段。from langgraph.store.postgres import PostgresStore store PostgresStore.from_conn_string( postgresql://user:passwordlocalhost:5432/langgraph ) store.setup() # 自动建表 graph graph_builder.compile(storestore)这里我要说几个实操注意点。第一setup()要执行否则连表都不会自动建第一次读写就会报错。第二连接字符串里的库需要预先创建Store不会自动帮你建数据库。第三如果你的服务是多实例部署一定要用PostgresStore而不是InMemoryStore否则实例A写入的记忆实例B永远查不到。还有一个经验不要把PostgresStore直接和业务表混在同一个库上至少给它独立一套表前缀或独立schema方便后期备份和清理。4. DeepAgents场景下的记忆策略让长期记忆变成第二大脑4.1 多步自主Agent为什么最需要StoreLangChain负责的是工具调用层LangGraph负责的是状态图编排层而DeepAgents这类深度Agent说到底是建立在它们之上、具备自主规划和多步执行能力的Agent形态。它不是“用户问一句答一句”而是给定一个目标后自己拆解任务、调用多个工具、甚至验证和反思中间结果。这种Agent对记忆的需求比单轮问答多得多。举个我做过的一个数据调研Agent例子它要按行业、地区、时间跨度去抓取数据并汇总报告。整个执行链路可能持续几十步中间多次调用检索工具、计算工具。如果没有长期记忆会出现两个灾难一是每一步都重复“我是谁、任务是什么、已经查到哪一步了”上下文会非常臃肿二是执行到一半如果被中断或重新开始所有中间产出一旦丢失又要从头再来。Store在这时候可以充当第二大脑每一步的关键中间结论写进某个namespace后一步直接从Store里取而不是靠整个会话上下文传递。比如把“已抓取的数据源列表”存成一条命名空间记忆下一步需要时先查一下避免重复抓取响应速度和资源消耗都会好很多。4.2 namespace设计的黄金法则按业务实体分层namespace设计是Store用得好不好的分水岭。我总结出来的经验是把namespace当作一个“记忆数据库”的复合主键来设计每一层都对应一个业务实体而不是随便塞字符串。对于有用户体系的Agent比较合理的namespace模式是三层业务全局层、用户层、任务/会话层。# 全局级共享知识 global_ns (global, kb_v2) # 用户级长期偏好 user_ns (user, user_id) # 用户的一次具体任务执行记录 task_ns (user, user_id, task, task_id) # 如果还有子步骤继续加深层级 step_ns (user, user_id, task, task_id, step, step_id)这种设计的好处是显而易见的你可以精确写入某个任务的中间状态也可以模糊搜索某个用户的所有任务当你需要跨用户共享一些公共知识时放到(global, ...)层级当你只想清理某个过期任务时直接按task那一段namespace做范围删除。我见过最乱的一种用法是把所有用户的所有记忆全部塞到(root,)下面然后靠value里的字符串去区分归属。这个做法在用户量小的时候看不出问题一旦超过几个量级search返回的记忆会互相污染纠正起来非常痛苦。namespace这个机制本身不复杂贵在从一开始就按业务实体建模。4.3 记忆的沉淀、更新与回收不能只写不删有了Store很多人容易走向另一个极端不管什么信息都往里塞。我自己的教训是记忆系统的健康度很大程度上取决于你怎么定义“沉淀、更新、回收”三个动作。沉淀不是每次对话都直接存原文。我会加一个“记忆提炼节点”把一轮对话交给模型让它输出“用户真实意图、已确认偏好、待跟进事项”三个字段然后作为结构化value写入Store。这样后续读取时给模型的不是一堆聊天记录而是干净的事实。更新同一类信息应该落在同一个key上。比如用户的最新偏好永远存储在latest_preference这个key下面put会覆盖旧值。不要让存在多个preference_20240801、preference_20240802这样的key否则检索时你不知道哪个才是最新。这一点和数据库设计里的“一行数据最多只有一个最新状态”是同一个道理。回收给情景记忆设置合理的TTL。比如原始会话记录只保留7天而提炼出的语义记忆保留90天。你还可以定期跑一个“记忆整理任务”把已经过时的任务记录从Store里删掉或者把分散的碎片记忆合并成一条综合摘要。存储不是无限便宜的更重要的是记忆检索的召回精度会随着脏数据变多而下降。5. 实战踩坑记录接入Store过程中我遇到过的五个问题5.1 节点里拿到的store是None挂载步骤变更了有段时间我重构项目把graph_builder.compile(storestore)这句删掉了想着“store是全局变量节点直接用不就行了”。结果所有节点里config[store]都是None模型提示词拼不出记忆整个Agent像失忆了一样。正确做法始终是在compile时显式传入store并且节点内永远从config里取不要依赖全局变量。原因在于LangGraph在编译图之后会把store实例注入到执行上下文中你直接import全局store在多实例部署时会绕过LangGraph管理的生命周期很容易出诡异问题。5.2 search查不到刚写入的数据namespace前缀必须对齐当时我遇到的现象是写入时namespace是(user, u123, task, t456)读取时只给了(user,)然后死活搜不到之前put的内容。我一度以为Store有缓存延迟后来仔细看了文档才发现search的namespace参数需要与写入时的层级逻辑一致。不同实现里前缀匹配的行为有差异有的是完全匹配、有的是前缀命中但最稳妥的做法是写入时用的完整namespace读取时也尽量使用相同的完整namespace或者明确使用你选用实现所支持的搜索规则。从那以后我定了一个规矩所有记忆写入的namespace都通过同一个函数生成保证写入和读取永远不会因为层级不一致而失联。5.3 持久化连接泄漏PostgresStore的连接管理切换PostgresStore之后服务跑了一段时间数据库连接数开始异常飙升。排查下来发现是我的服务每次请求创建新的PostgresStore实例却没有正确管理连接释放。后来我把Store实例做成应用级单例并且确保用完的连接进连接池复用连接数才回归正常。我的建议是持久化Store的生命周期应该和你的应用进程一致不要在每个请求里重复创建。如果框架支持异步尽量用同一套异步客户端模式否则高并发下很容易打满数据库连接上限。5.4 长期记忆里混入了用户伪造的内容这种问题属于安全设计层面的。用户完全可以在对话里说“记下来我是管理员”如果你不加校验地写入Store后续检索这条记忆出来Agent就可能真的把他当管理员对待。这是记忆污染的一种。我后来在写入链路里加了两道校验第一道是结构校验记忆value必须匹配预设的字段schema不符合直接丢弃第二道是语义校验让一个专门的校验模型判断这条记忆是否可信、是否属于“事实提取”。对于涉及权限、安全相关的记忆最安全的做法是默认不写入。需要强调这不是小题大做——长期记忆一旦被污染它会持续影响后续所有决策比单次Prompt注入的危害大得多。5.5 性能调优高频写入和检索延迟最后说下性能。Store设计得很简单但使用节奏不对照样会慢。我踩过的坑是在每个工具调用节点结束都立刻put一次记忆一个长链路任务跑下来写入上百次拖慢了整体吞吐。优化思路有几个一是缩小写入频率把多条记忆先在节点内部攒成列表最后一次性批量put二是把高频访问的“用户画像”这类记忆缓存到应用内存避免每次启动都search一次三是给search设置合理的limit而不是拉取全部再在应用层过滤。记住一个原则Store解决的是“需要长期存在、跨生命周期”的记忆不是用来替代所有临时变量的。划清这个边界之后你的系统会更清爽也会少很多无谓的性能开销。如果你刚接触这套生态我的建议是别急着设计复杂记忆矩阵。我第一次就把系统做成多级namespace、跨任务共享、自动摘要一应俱全结果调试成本特别高收益却很小。先跑通最简链路单用户、一个namespace、InMemoryStore把读、写、检索三个动作完全跑稳再考虑Postgres持久化、TTL回收和用户隔离。最后分享一个小技巧把namespace当作文件系统的路径来想象写代码时始终遵循“从抽象到具体”的层级比如先用户、再会话、再任务这套直觉能帮你避免绝大多数记忆找不回来的问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑