你跟一个 AI 助手说我叫小明我喜欢喝拿铁。“它回答好的记住了”。第二天你再打开对话它却问“你好请问怎么称呼”——金鱼记忆。问题在于默认情况下Agent 的记忆只活在一次会话里会话一结束就清空。要让它跨对话、跨天地记住你的偏好和身份需要的是长期记忆Long-term Memory。这篇文章结合 3 个完整可运行的例子讲清长期记忆怎么实现。代码全部内嵌正文复制即可跑。一、长期记忆 vs 短期记忆先分清两个容易混的概念短期记忆Checkpointer长期记忆Store范围单个会话内跨多个会话内容完整对话历史提炼的关键信息偏好、画像、事实生命周期会话结束即丢弃持久保留随时可取访问方式框架自动保存/加载通过runtime.store手动读写类比人的工作记忆人的知识库一句话短期记忆让 Agent 在这一场对话里连贯长期记忆让它跨越所有对话记住你。本文讲的是后者——基于Store实现。二、Store 的数据结构一个 JSON 文件柜长期记忆基于 LangGraph 的Store数据以 JSON 文档存储用命名空间namespace 键key的层级组织。类比文件系统namespace命名空间 ≈ 文件夹路径 key键 ≈ 文件名 value值 ≈ 文件内容JSON (users, user_123) profile → {name: 张三, email: ..., skill: ...} └──── namespace ────┘ └─ key ─┘ └──────────────── value ────────────────┘namespace 的核心作用是隔离——不同用户的数据互不干扰(users, user_123) ← user_123 的数据 (users, user_456) ← user_456 的数据互不可见namespace 的层数是自由的上面用两层最简单。需要按类目细分时可以再加一层比如(users, user_123, preferences)把偏好、画像分开存——第五节就是这么做的。记住这个namespace key → value结构后面所有操作都是围绕它的增删改查。三、最小实现写入 读取实现长期记忆只需三步① 创建 Store → ② 传入create_agent的store→ ③ 工具里用runtime.store读写。下面用InMemoryStore开发用最简单做一个能存能取的助手一个工具负责写、一个负责读。importosfromdataclassesimportdataclassfromdotenvimportload_dotenvfromlangchain.agentsimportcreate_agentfromlangchain.chat_modelsimportinit_chat_modelfromlangchain.toolsimporttoolfromlanggraph.prebuiltimportToolRuntimefromlanggraph.store.memoryimportInMemoryStore load_dotenv()modelinit_chat_model(os.getenv(MODEL_NAME,glm-5.1),model_provideropenai,base_urlos.getenv(OPENAI_API_BASE),api_keyos.getenv(OPENAI_API_KEY),streamingTrue,)dataclassclassCustomContext:user_id:strtooldefsave_user_profile(name:str,email:str,skill:str,runtime:ToolRuntime[CustomContext])-str:保存/更新当前用户的档案信息写入长期记忆。 Args: name: 用户姓名 email: 用户邮箱 skill: 用户技能 namespace(users,runtime.context.user_id)runtime.store.put(namespace,profile,{name:name,email:email,skill:skill})returnf已保存档案name{name}, email{email}, skill{skill}tooldefget_user_profile(runtime:ToolRuntime[CustomContext])-str:读取当前用户的档案信息从长期记忆读取。namespace(users,runtime.context.user_id)itemruntime.store.get(namespace,profile)ifitemisNone:returnNo profilepitem.valuereturnfname:{p[name]}\nemail:{p[email]}\nskill:{p[skill]}\nstoreInMemoryStore()agentcreate_agent(modelmodel,tools[save_user_profile,get_user_profile],storestore,# ② 注入 Storesystem_prompt你是一个使用工具帮用户完成任务的有用助手,)if__name____main__:ctxCustomContext(user_iduser_123)# 1. 写入让 Agent 调用 save_user_profile 把资料存进长期记忆print( 写入 )r1agent.invoke({messages:[{role:user,content:记住我的资料我叫张三邮箱 777qq.com技能 langchain}]},contextctx,)print(r1[messages][-1].content)# 2. 读取新的一轮对话让 Agent 调用 get_user_profile 取回资料print(\n 读取 )r2agent.invoke({messages:[{role:user,content:我叫什么我的技能是什么}]},contextctx,)print(r2[messages][-1].content)两个要点user_id来自runtime.context不要硬编码。命名空间用(users, runtime.context.user_id)动态拼每个用户读写自己的数据。硬编码 user_id 所有人共享一份数据这是经典坑。get返回的是Item对象真正的数据在.value里不存在时返回None。但InMemoryStore有个致命问题数据只在内存里进程一退出就全没了。它只适合开发调试。要真正跨会话、重启还在得换持久化存储。四、持久化换成 SqliteStore把InMemoryStore换成SqliteStore记忆就落盘到一个.db文件里——进程重启后依然存在而且不像 Redis/Postgres 那样需要单独跑一个服务器单机一个文件就搞定。依赖pip install langgraph-checkpoint-sqlite。用法是SqliteStore.from_conn_string(路径)返回一个上下文管理器进去后先调setup()建表。代码几乎和上面一样只换了 Store 的创建方式注意with块importosfromdataclassesimportdataclassfromdotenvimportload_dotenvfromlangchain.agentsimportcreate_agentfromlangchain.chat_modelsimportinit_chat_modelfromlangchain.toolsimporttoolfromlanggraph.prebuiltimportToolRuntimefromlanggraph.store.sqliteimportSqliteStore load_dotenv()modelinit_chat_model(os.getenv(MODEL_NAME,glm-5.1),model_provideropenai,base_urlos.getenv(OPENAI_API_BASE),api_keyos.getenv(OPENAI_API_KEY),streamingTrue,)dataclassclassCustomContext:user_id:strtooldefsave_user_profile(name:str,email:str,skill:str,runtime:ToolRuntime[CustomContext])-str:保存/更新当前用户的档案信息写入长期记忆。 Args: name: 用户姓名 email: 用户邮箱 skill: 用户技能 namespace(users,runtime.context.user_id)runtime.store.put(namespace,profile,{name:name,email:email,skill:skill})returnf已保存档案name{name}, email{email}, skill{skill}tooldefget_user_profile(runtime:ToolRuntime[CustomContext])-str:读取当前用户的档案信息从长期记忆读取。namespace(users,runtime.context.user_id)itemruntime.store.get(namespace,profile)ifitemisNone:returnNo profilepitem.valuereturnfname:{p[name]}\nemail:{p[email]}\nskill:{p[skill]}\nif__name____main__:withSqliteStore.from_conn_string(long_memory.db)asstore:store.setup()# 建表幂等agentcreate_agent(modelmodel,tools[save_user_profile,get_user_profile],storestore,system_prompt你是一个使用工具帮用户完成任务的有用助手,)ctxCustomContext(user_iduser_123)defask(content:str)-str:returnagent.invoke({messages:[{role:user,content:content}]},contextctx)[messages][-1].content# 1. 启动读取第二次起能读到上次写入的持久化证明print( 启动读取 )print(ask(我的档案是什么))# 2. 写入print(\n 写入 )print(ask(记住我的资料我叫张三邮箱 777qq.com技能 langchain))# 3. 写入后读取print(\n 写入后读取 )print(ask(我叫什么我的技能是什么))怎么验证持久化跑两遍这个脚本第一遍启动读取是空的 → 写入 → 读到刚写的。第二遍启动读取直接命中上一遍写到磁盘的档案——这就是长期记忆跨会话、重启还在的铁证。单机/中小规模用 SQLite 足矣如果要多实例共享、海量数据再升级到PostgresStore用法一样from_conn_stringsetup()但绝大多数场景一个 sqlite 文件就够。唯一不能用于生产的是InMemoryStore。五、记忆的完整生命周期增删改查 隔离写入和读取只是开始。Store 还支持更新、搜索、删除、列命名空间。下面这段直接操作 Store不经过模型确定性、可复现把整套生命周期和命名空间隔离一次性演示清楚fromlanggraph.store.memoryimportInMemoryStore storeInMemoryStore()defline(title:str)-None:print(f\n{*50}\n{title})if__name____main__:# ---- Put为两个用户、两个类目写入数据命名空间隔离----line(Put 写入多用户 / 多类目)store.put((users,user_123,profile),info,{name:张三,city:北京})store.put((users,user_123,preferences),coffee,{value:latte})store.put((users,user_123,preferences),language,{value:zh})store.put((users,user_456,preferences),coffee,{value:americano})print(已为 user_123 和 user_456 写入数据)# ---- Get读取单条 ----line(Get 读取)itemstore.get((users,user_123,preferences),coffee)print(user_123 coffee -,item.value)# {value: latte}print(created_at -,item.created_at)# ---- Update相同 namespace key 再 put 即覆盖 ----line(Update 更新覆盖)store.put((users,user_123,preferences),coffee,{value:espresso})print(更新后 -,store.get((users,user_123,preferences),coffee).value)# ---- Search按命名空间前缀搜索可加 filter 精确过滤 ----line(Search 搜索)all_prefsstore.search((users,user_123,preferences))print(user_123 全部偏好:,[(r.key,r.value)forrinall_prefs])filteredstore.search((users,user_123,preferences),filter{value:zh})print(filter valuezh:,[(r.key,r.value)forrinfiltered])# ---- 命名空间隔离user_456 看不到 user_123 的数据 ----line(命名空间隔离)print(user_456 coffee -,store.get((users,user_456,preferences),coffee).value)# ---- list_namespaces列出所有命名空间 ----line(list_namespaces 列出命名空间)fornsinstore.list_namespaces():print( ,ns)# ---- Delete删除单条 ----line(Delete 删除)store.delete((users,user_123,preferences),coffee)print(删除 coffee 后 -,store.get((users,user_123,preferences),coffee))# None运行输出节选Update 更新覆盖 更新后 - {value: espresso} Search 搜索 user_123 全部偏好: [(coffee, {value: espresso}), (language, {value: zh})] filter valuezh: [(language, {value: zh})] 命名空间隔离 user_456 coffee - {value: americano} Delete 删除 删除 coffee 后 - None五个操作对应记忆的生命周期操作方法说明创建store.put(ns, key, value)value 必须是 dict读取store.get(ns, key)返回 Item数据在.value无则 None更新store.put(ns, key, 新value)相同 nskey 直接覆盖搜索store.search(ns_prefix, filter...)按前缀 字段过滤删除store.delete(ns, key)—注意search这里是按字段精确过滤。如果要按语义找相关记忆向量检索那是向量数据库/RAG 的活儿——本系列的rag/目录专门讲不在长期记忆的范畴里硬塞。六、最佳实践与避坑✅ 用 namespace 做隔离别用扁平 key 拼字符串# ✅ 好天然隔离store.put((users,user_id,preferences),coffee,{...})store.put((users,user_id,profile),name,{...})# ❌ 差全堆一层靠拼 key 区分容易冲突store.put((data,),fuser_{user_id}_coffee,{...})✅ user_id 从 context 动态取绝不硬编码硬编码 user_id → 所有用户读写同一份数据彻底失去隔离。永远runtime.context.user_id。✅ 存提炼的关键信息不是整段对话长期记忆该存的是偏好、画像、重要事实“对海鲜过敏”“喜欢拿铁”而不是把几百条聊天记录原样塞进去——那是短期记忆/Checkpointer 的事。⚠️ InMemoryStore 只能用于开发它重启即丢、不能多实例共享。任何要留住数据的场景至少上 SqliteStore。七、总结概念比喻作用Long-term Memory人的知识库跨会话持久记忆Store文件柜存取 JSON 文档namespace文件夹路径隔离不同用户/类目key / value文件名 / 文件内容定位并存储一条记忆InMemoryStore临时便签开发用重启丢失SqliteStore带锁的抽屉单机持久化无需服务器context身份证动态传入 user_id长期记忆的本质是让 Agent 突破单次会话的限制——通过 Store 以namespace key的结构持久化关键信息让它在任意时间、任意对话里都能召回你的偏好、画像和历史从而提供连贯、个性化的体验。从金鱼到大象差的就是这一个store参数和几行runtime.store读写。