深入学LangChain 官方文档(九)Memory 记忆系统首讲

发布时间:2026/7/20 18:06:05
深入学LangChain 官方文档(九)Memory 记忆系统首讲 深入学 LangChain 官方文档九Memory 记忆系统首讲本篇对应的官方文档Short-term memory线程范围 State、checkpointer、thread_id以及长消息的裁剪、删除和摘要。Long-term memory跨线程 Store、JSON 文档、namespace key与runtime.store读写路径。Memory overview长期记忆类型、profile / collection 组织方式以及主链路和后台写入的取舍。本篇讲解范围本篇主要学习建立线程内短期记忆、跨线程长期记忆、两者与业务数据库的边界以及最小代码闭环。记忆抽取模型、向量检索优化、复杂摘要策略和数据库部署细节留给后续专题。我们来看这样一个场景客服 Agent 刚刚帮用户查完退款进度用户过了十分钟又问“那还要多久”它需要知道“那”指的是刚才的退款单商品。几天后用户打开了一个新会话希望系统仍然按自己之前习惯的方式回答要求智能体像之前一样回复简洁、并且要先给结论然后进行详细解释说明。这两个需要“记住”的场景听起来相似但工程上却不是一回事。前者依赖当前会话的连续状态后者要跨越会话边界复用用户偏好。如果在设计中把订单金额、退款状态也混进同一套记忆问题会更严重会出现模型记住的旧值可能覆盖业务系统中的最新事实的严重后果。所以Memory 的起点不是“选哪个数据库”也不是“把历史消息都塞回模型”。真正最需要考虑的的是一条信息以后会在哪个范围被再次读取它由谁维护失效后又由谁负责删除或修正本篇文章将深入学习LangChain官方文档的Memory记忆系统我们将沿着下面这条主线展开先判断记忆范围再看State checkpointer thread_id如何延续当前线程随后进入Store namespace key解释跨线程信息怎样被组织最后把长对话治理、写入时机和生产边界合并成一套判断方法。我们可以像图中一样把同一用户的两次会话拆成两条读取路径当前退款任务沿thread_id恢复新会话中的表达偏好从跨线程 Store 读取。两条路径可以同时服务一个 Agent但它们的生命周期、隔离键和数据责任不同。而要把这两条路径落到系统里第一步不是继续拆存储接口而是先给待处理信息确定去向。一、Memory 不是一个盒子而是一组“再次读取”规则很多记忆方案一开始就列举各种技术名词消息历史、缓存、向量数据库、用户画像、摘要。但名词越多越容易忽略一个基本事实信息被保存不等于它应该在下一次推理中出现。例如用户本轮上传的一次性验证码只对当前请求有效退款任务的中间状态需要在同一会话里延续“偏好中文简洁回答”可能跨会话复用真实退款状态则必须回到订单系统查询。它们都属于“Agent 可能需要的信息”但不应该进入同一个容器。我们可以先用四个问题判断去向这条信息只在本次调用有效还是下次调用还要读取下次读取发生在同一线程还是任意新线程它是辅助模型决策的记忆还是业务系统的权威事实它是否允许被用户查看、修正、遗忘或删除这样的话一次性验证码、线程任务、回答偏好和退款状态会按照这四个问题分别落到 Runtime Context、State、Store 与业务数据库。四个模块对应的是数据职责不是四种可互换的存储产品Runtime Context提供本次运行的稳定依赖State 承接线程内变化Store 保存跨线程可复用记忆业务数据库维护订单等权威记录。判断错位置后续再精细的检索和提示词也只能放大错误。官方文档中这套分法也连接了前两篇内容。第 07 篇讲清了 Runtime Context、State 与 Store 的容器边界第 08 篇讨论哪些上下文应在什么步骤进入模型。Memory 在此基础上进一步一步哪些信息离开当前步骤后仍值得保留并且未来怎样安全地取回来。二、短期记忆让同一条线程能够继续LangChain 官方文档把短期记忆定义在线程范围内。它通常是 Agent State 的一部分其中最常见的字段是messages也可以扩展出当前任务编号、已经确认的参数或会话阶段。只把字段写进 State 还不够进程结束、请求返回或服务重启之后内存里的 Python 对象不会自动存在。checkpointer的职责就是在执行步骤之间保存状态快照并在后续调用时恢复它。调用方再用thread_id指定“这次请求属于哪一条线程”。三者可以这样记State 是内容当前线程已经发生了什么。checkpointer 是持久化机制状态怎样保存、怎样恢复。thread_id是隔离与寻址键本次调用应接到哪条线程上。面对退款追问的问题我们就可以把三者串成了具体执行顺序先按thread_id找线程再由 checkpointer 取回 State最后把新消息接到旧任务之后。恢复链从第一次调用结束时写入状态快照开始第二次请求携带相同thread_idcheckpointer 先取回旧 State再把新消息接入 Agent loop。模型能够理解“那还要多久”依赖的不是模糊的记忆能力而是明确的线程寻址和状态恢复。官方文档还强调State 会在每个执行步骤开始时被读取并在调用或步骤推进过程中更新。这意味着短期记忆并不是只在聊天结束后统一落盘。模型调用、工具执行、Command写回 State都可能形成新的状态版本。如果消息之外还有必须精确延续的字段可以扩展AgentState。例如把refund_id、current_stage、confirmed_email分开保存这要比每次让模型从自然语言历史中重新猜测更稳定。工具读取这些字段时会通过ToolRuntime.state需要更新时返回Command(update...)。这里要注意只有后续步骤确实依赖、并且拥有清晰更新规则的数据才值得进入 State把每个临时变量都持久化只会让恢复语义越来越难解释。这种步骤级持久化会带来一个很实用的能力长任务中途失败后可以从已有状态继续而不必把整个 Agent loop 从头重跑。不过可恢复不等于任意重放都安全。在客服场景中如果某个步骤已经真实提交退款恢复时再次执行同一工具就可能发生重复写入。记忆层保存“发生过什么”业务工具仍要负责幂等键、事务和重复提交检查。三、thread_id决定连续性也决定隔离边界在客服场景中前端通常会为每个会话页生成稳定的线程标识用户在同一会话继续追问时复用这个标识切换到新会话则创建新的thread_id。两次请求是否属于同一用户并不能代替线程边界。如果把user_id直接当成唯一thread_id用户的多个并行任务会被塞进同一条消息历史上午咨询退款下午修改地址模型可能把两个任务的工具结果交叉引用。反过来如果每一次 HTTP 请求都生成新标识短期记忆根本无法恢复“那笔退款”自然失去指代对象。隔离图把“用户身份”和“会话线程”拆开一个user_id可以拥有多条thread_id每条线程维护独立 State。复用同一标识得到连续性错误复用会串线频繁更换则让上下文断裂。最小调用结构并不复杂。关键不是config这几行代码本身而是应用层必须稳定维护线程生命周期。fromlanggraph.checkpoint.memoryimportInMemorySaverfromlangchain.agentsimportcreate_agentfromlangchain_openaiimportChatOpenAI modelChatOpenAI(modelqwen3.7-plus,api_keyYOUR_API_KEY,base_urlYOUR_OPENAI_COMPATIBLE_ENDPOINT,)agentcreate_agent(modelmodel,checkpointerInMemorySaver())thread_config{configurable:{thread_id:refund-thread-20260718}}agent.invoke({messages:[{role:user,content:查询退款单 RF-2048}]},thread_config,)resultagent.invoke({messages:[{role:user,content:那还要多久}]},thread_config,)第二次invoke不需要手工把第一次的全部消息重新拼接进去checkpointer 会依据thread_id恢复线程状态。演示使用InMemorySaver适合本地运行和测试。服务重启后仍要恢复的生产系统应换成数据库支持的 checkpointer并把连接、迁移、备份和保留周期纳入运维。四、长期记忆跨线程保存可复用信息在之前的场景问题中用户几天后新建会话时新的thread_id不应该自动继承旧线程的全部 State。旧会话里可能有临时地址、过期工具结果甚至属于另一个任务的敏感内容。此时如果确实需要复用“偏好中文、回答简洁”就进入长期记忆的范围。LangChain 的长期记忆建立在 LangGraph Store 上。Store 保存 JSON 文档并用namespace和key组织它们namespace像分层目录用来表达用户、团队、应用或记忆类型等作用域。key标识该命名空间内的一条具体记录。value 是 JSON 结构可以保存偏好、事实、摘要或其他可序列化内容。例如(users, u-1024, preferences)可以作为某位用户的偏好命名空间response_style是其中一条记忆的 key值为{language: zh-CN, verbosity: concise}。层级结构先用namespace限定“谁的哪类记忆”再用key定位具体文档。namespace设计同时承担检索范围与权限边界如果所有用户都写入同一平面空间数据串读风险会比检索精度问题更早出现。官方概念文档把长期记忆进一步分成三类Semantic memory 保存事实例如用户偏好episodic memory 保存经历或示例例如过去成功处理某类工单的过程procedural memory 保存行为规则例如系统指令或流程约束。这个分类适合帮助建模但首讲不必为每一类都搭一套数据库。先把作用域、权威来源和更新方式定义清楚通常比先选向量索引更重要。Semantic memory 还可以按 profile 或 collection 组织。profile 把一组字段维护在同一份 JSON 文档里读取简单适合结构稳定的用户画像但每次更新都要正确合并整份结构字段越来越多时更容易覆盖旧值。collection 把事实拆成多条独立记录新增和搜索更灵活也更容易保留每条记忆的来源不过需要处理重复、冲突与过期项。客服偏好的语言和详细程度适合小型 profile大量互不相关的历史兴趣则更接近 collection。选择哪一种取决于更新方式不取决于哪个名字更像“高级记忆”。同时需要注意的是长期记忆也不必在每次模型调用前全量加载Store 支持按 namespace 读取具体 key也可以搜索相关记忆。进入模型的仍应是本次任务所需的最小集合。否则“跨线程可用”很快会变成“每次都把用户全部历史塞进 prompt”成本、隐私和冲突都会一起上升。五、工具通过ToolRuntime访问 Store在Store 被传给 Agent 后工具可以从ToolRuntime的store属性访问它。ToolRuntime参数对模型隐藏不会出现在工具 schema 中模型只需要决定是否调用“记录偏好”或“读取偏好”不需要生成数据库对象、当前用户标识或连接信息。这里仍然需要 Runtime Context 提供当前调用者身份。工具从runtime.context.user_id得到可信的用户 ID再据此构造 namespace。如果把user_id暴露成模型可填写的普通工具参数模型一次错误填参就可能读写到别人的记忆空间。读写链把权限来源和模型参数分开应用注入user_id与 StoreToolRuntime将它们交给工具工具构造 namespace 后执行put、get或search。模型能选择记忆动作却不能自行指定隐藏的身份边界。下面两个工具只处理一类明确记忆回答风格。其中工具名称和 docstring 让模型知道何时调用真正的寻址规则留在应用代码里。fromdataclassesimportdataclassfromlangchain.toolsimportToolRuntime,tooldataclassclassSupportContext:保存当前客服调用中由应用可信注入的用户身份。user_id:strtooldefremember_response_style(language:str,verbosity:str,runtime:ToolRuntime[SupportContext],)-str:记录用户明确要求长期沿用的回答语言和详细程度。namespace(users,runtime.context.user_id,preferences)runtime.store.put(namespace,response_style,{language:language,verbosity:verbosity},)return回答偏好已记录tooldefread_response_style(runtime:ToolRuntime[SupportContext])-dict:读取当前用户已经保存的回答风格偏好。namespace(users,runtime.context.user_id,preferences)itemruntime.store.get(namespace,response_style)returnitem.valueifitemelse{language:zh-CN,verbosity:normal}写入路径是“模型提出记忆动作 → 工具使用可信身份构造 namespace → Store 保存 JSON 文档”读取路径则反向返回 value。工具返回的字符串或字典会成为本轮工具结果但长期记忆本体保存在 Store 中不会因为当前线程结束而消失。六、Memory 不能替代业务数据库到了这里最容易出现的理解误区是既然 Store 可以保存 JSON那订单状态、账户余额和退款结果是不是也可以一起放进去。技术上当然能写但工程上却不应该让它成为权威来源。记忆服务的是 Agent 决策和交互连续性允许经过抽取、摘要、搜索和自然语言更新。业务数据库则要维护事务、一致性、审计、权限和最新状态。用户说“我记得退款金额是 199 元”可以作为对话上下文但最终金额必须查询订单系统过去工具结果里写着“审核中”也不能覆盖业务系统刚更新的“已到账”。上面的边界图中把偏好、对话摘要放在 Memory 一侧把订单、余额、退款状态放在业务数据库一侧。Agent 可以用记忆决定怎样提问和解释但涉及权威事实时必须重新调用业务工具并以实时查询结果为准。我们可以用一个实用思路来做判断如果数据错误会直接改变资金、权限、库存、合规结论或不可逆操作它就不应只存在于模型记忆中。即便为了提速建立缓存也要保留明确的源系统、过期时间和重新校验路径。同样Runtime Context 也不等于长期记忆。当前请求的数据库连接、租户标识和权限对象可以通过 runtime 注入但它们是运行依赖不是让模型在未来会话中自由回忆的内容。七、短期记忆会增长长对话必须治理同一thread_id持续使用State 里的消息会越来越多。checkpointer 能保存它们却不会替你判断每条消息是否仍值得进入模型。这里正好与 Context Engineering 相交持久化解决“能不能找回”上下文策略解决“本次应该让模型看到什么”。官方短期记忆文档给出了三类常见动作trim、delete 和 summarize。裁剪trim模型调用前临时选择一部分消息底层线程记录可以保留适合控制单次上下文窗口。删除delete从 State 中移除消息后续步骤也不再读取适合明确无效或按规则必须清除的内容。摘要summarize把较长历史压缩成短摘要再保留最近的关键消息适合需要延续任务语义的长线程。三种动作最关键的差别是它们分别改变本次可见集合、持久 State 和历史信息的表达形态。三条路径改变的对象不同裁剪主要控制本次模型可见集合删除改变持久 State摘要用压缩后的语义替换一段历史。选择时要同时检查任务连续性、隐私要求和消息合法性不能只按字符数量截断。其中的消息合法性尤其重要包含工具调用的AIMessage与对应ToolMessage需要保持严格配对如果裁剪后只剩工具结果没有发起它的调用信息模型收到的消息序列可能不再合法。System message 的位置、对话起止角色以及供应商对消息序列的约束也要进入裁剪规则。摘要同样不是无损压缩。金额、时间、否定词和未完成动作都可能在摘要中丢失。因此摘要可以保存“用户正在处理退款、已提交材料”却不应该成为退款金额和到账状态的最终依据。需要精确恢复的字段应结构化保存在 State 或业务系统里而不是只留一段自然语言概述。八、记忆什么时候写主链路与后台任务长期记忆并非只能在对话结束时批量生成。官方概念文档给出两种主要写入方式在 hot path也就是用户请求的主执行路径中写或者在后台异步整理。用户明确说“以后都用中文简短回答”主链路写入更合适。意图清楚、结果立即生效工具还能当场返回确认。代价是当前请求多了一次存储操作记忆抽取如果依赖额外模型调用还会增加延迟和费用。如果要从大量历史对话中归纳稳定偏好、合并重复事实或构建经验库后台处理更从容。它不阻塞当前回复也能批量去重但新记忆不会立即可用任务调度失败会造成滞后多条后台任务同时更新同一 profile 时还会出现覆盖冲突。两条写入路径交换的是及时性与解耦程度主链路适合明确、需要立即生效的记忆后台任务适合归纳、合并和低优先级整理。无论走哪条路径都要定义触发条件、冲突策略、版本和删除入口。同时在写入之前还应增加一道价值判断并不是每句用户输入都值得长期保存。一次性情绪、临时地址、验证码和未经确认的推断通常不该进入用户长期画像。比较稳妥的规则是只保存与未来任务稳定相关、获得合理授权、可以解释来源、能够修正或删除的信息。另外同一条记忆还可能被不同请求同时更新。比如主链路刚把verbosity改为concise后台任务却根据旧对话重新写成normal最终结果就会倒退。解决办法不能只靠“最后写入者获胜”至少要保存更新时间或版本合并时识别字段级变化对于用户刚刚明确确认的偏好还应给予比历史推断更高的来源优先级。九、把短期与长期记忆接成一个最小闭环现在把两个层次放进同一个客服 Agent。代码仍采用内存实现便于看清对象关系生产替换存储后调用结构不需要因此改变。fromdataclassesimportdataclassfromlangchain.agentsimportcreate_agentfromlangchain.toolsimportToolRuntime,toolfromlangchain_openaiimportChatOpenAIfromlanggraph.checkpoint.memoryimportInMemorySaverfromlanggraph.store.memoryimportInMemoryStoredataclassclassSupportContext:保存由客服应用注入的当前用户身份。user_id:strtooldefremember_response_style(language:str,verbosity:str,runtime:ToolRuntime[SupportContext],)-str:保存用户明确要求在后续会话继续使用的回答风格。namespace(users,runtime.context.user_id,preferences)runtime.store.put(namespace,response_style,{language:language,verbosity:verbosity},)return偏好已保存tooldefread_response_style(runtime:ToolRuntime[SupportContext])-dict:读取当前用户跨会话保存的回答风格。namespace(users,runtime.context.user_id,preferences)itemruntime.store.get(namespace,response_style)returnitem.valueifitemelse{language:zh-CN,verbosity:normal}modelChatOpenAI(modelqwen3.7-plus,api_keyYOUR_API_KEY,base_urlYOUR_OPENAI_COMPATIBLE_ENDPOINT,)checkpointerInMemorySaver()storeInMemoryStore()agentcreate_agent(modelmodel,tools[remember_response_style,read_response_style],checkpointercheckpointer,storestore,context_schemaSupportContext,system_prompt(你是客服助手。需要延续当前任务时使用线程消息涉及回答风格时读取长期偏好订单与退款事实必须调用业务系统。),)user_contextSupportContext(user_idu-1024)refund_thread{configurable:{thread_id:refund-2048}}agent.invoke({messages:[{role:user,content:退款单 RF-2048 还在审核以后请用中文简短回答}]},refund_thread,contextuser_context,)continuedagent.invoke({messages:[{role:user,content:那现在进展如何}]},refund_thread,contextuser_context,)new_thread{configurable:{thread_id:shipping-7712}}reopenedagent.invoke({messages:[{role:user,content:先读取我的回答偏好再帮我处理新的物流问题}]},new_thread,contextuser_context,)第一次调用同时产生两类可能的变化退款对话进入refund-2048的 State模型若调用偏好工具则“中文、简短”进入u-1024的 Store namespace。第二次调用复用退款线程因此能恢复“那”所指的任务。第三次调用换了新线程旧退款消息不会自动混入但工具仍能按同一用户身份读取长期偏好。代码故意没有实现订单查询工具它用缺口强调一个边界即使短期 State 中留着“还在审核”再次回答真实进展时也应调用业务系统获取最新状态。Memory 提供连续性不能把历史工具结果升级成永久事实。闭环从请求进入开始先按thread_id恢复 State再按用户 namespace 检索必要长期记忆执行结束后分别写回线程状态和经过筛选的长期信息。外圈的权限、遗忘、审计与业务事实校验决定这套记忆能否进入生产而不只是能否跑通示例。十、进入生产前至少补齐六条边界代码跑通之后Memory 才刚刚从概念进入工程。真正上线前至少要补齐以下六点。第一持久化实现。InMemorySaver和InMemoryStore适合本地开发与测试不适合依赖进程重启后恢复的服务。生产环境要选择数据库支持的 checkpointer 和 store并验证 schema migration、备份恢复、连接池与容量上限。第二身份与租户隔离。thread_id、user_id和 tenant 信息分别解决不同问题不能互相替代。namespace 必须由可信应用代码构造所有读取、搜索、更新和删除都要经过同一套鉴权。第三写入质量。模型推断不能悄悄变成用户事实。长期记忆应记录来源、创建时间、最近更新时间和置信边界重要偏好最好由用户明确表达或确认。profile 更新还要处理并发覆盖collection 写入则要处理重复项和过期项。第四遗忘与删除。能写入就必须能删除。线程保留多久、用户能否清除历史、长期偏好如何更正、删除请求是否覆盖索引和备份都应形成可执行策略而不是只在隐私声明里出现。第五可观测与审计。一次回复使用了哪条线程、读取了哪些长期记忆、为什么写入新记忆、最终又调用了哪个权威系统需要留下适度记录。否则出现串线或错误偏好时只能看到模型答错却找不到错误信息从哪里进入。第六失败与重试。checkpointer 写入失败、Store 暂时不可用或后台归纳任务重复执行时系统要有明确降级方式。记忆写入不应让高风险业务动作失去幂等性后台任务也不能覆盖用户刚刚修正的新偏好。验收时不要只测“第二轮能否回答”。至少要覆盖同线程恢复、不同线程隔离、同用户跨线程读取、不同用户无法串读、删除后不再召回、旧业务事实不会压过实时查询以及服务重启后的恢复。这样测到的才是记忆合同而不是一次偶然正确的模型输出。这六条边界共同指向一个结论Memory 不是给模型加一个“更聪明”的开关而是给信息的生命周期建立规则。存储只是其中一环身份、权限、更新、失效、删除和审计同样属于记忆系统。十一、回到开头先判断范围再选择记忆机制现在我们再回过头看最初的客服场景经过本篇对于Memory记忆系统的学习解决问题的处理顺序已经很清楚了“那还要多久”要接续当前退款任务使用相同thread_id由 checkpointer 恢复 State。“以后用中文简短回答”需要跨线程复用经过明确触发后写入 Store并用用户 namespace 隔离。退款金额、审核状态和到账时间属于权威业务事实每次需要时回到业务系统查询。会话过长时根据任务连续性选择裁剪、删除或摘要同时维护工具消息配对和精确字段。记忆写入选择主链路还是后台任务取决于是否要立即生效以及能否承担延迟、一致性和冲突处理。总的来说短期记忆解决的是“这条线程怎样继续”长期记忆解决的是“换一条线程后哪些信息仍值得复用”。把这两句话与业务数据库边界一起记住后续学习 Middleware、Retrieval 和更复杂的 Agent 系统时就不会把所有上下文问题都误归到一个叫 Memory 的盒子里。不过把边界定义清楚还不等于每次执行都会自动守住它。日志、敏感信息过滤、失败重试和高风险操作审批往往同时横跨模型调用与工具调用。全写进提示词不够可靠分散塞进各个工具又很难统一维护。下一篇我们就从这个缺口出发一些看看官方文章中是怎样在执行链路的关键位置集中接住这些规则让它们不再依赖模型“记得遵守”