我加了 `InMemorySaver`,为什么 Agent 还是没记忆?

发布时间:2026/7/7 18:05:42
我加了 `InMemorySaver`,为什么 Agent 还是没记忆? Agent 能调工具之后我开始做一件看起来很基础但很容易踩坑的事让 Agent 记住上下文。我按照文档加了checkpointerInMemorySaver()满心期待 Agent 能像 ChatGPT 一样记得上一句。结果第二次运行脚本它又问这是我们的第一次对话吗我一度怀疑InMemorySaver是个假的记忆组件。后来才搞明白不是没记忆是我的用法让它没法记忆。一、我当时想干什么我想实现最简单的多轮对话第一轮我问旧金山天气怎么样第二轮我问那明天呢Agent 应该能理解明天指的是旧金山明天的天气我写的代码fromlangchain.agentsimportcreate_agentfromlangchain_core.toolsimporttoolfromlangchain_core.utils.uuidimportuuid7fromlangchain_openaiimportChatOpenAIfromlanggraph.checkpoint.memoryimportInMemorySavertooldefsearch(query:str)-str:搜索工具returnf搜索结果{query}llmChatOpenAI(modeldeepseek-chat,temperature0,openai_api_key你的 api key,openai_api_basehttps://api.deepseek.com/v1,)agentcreate_agent(modelllm,tools[search],checkpointerInMemorySaver(),)config{configurable:{thread_id:str(uuid7())}}resultagent.invoke({messages:[{role:user,content:旧金山天气怎么样}]},configconfig,)resultagent.invoke({messages:[{role:user,content:那明天呢}]},configconfig,)在这个脚本里记忆是工作的。但当我再次运行脚本问上一轮我问了什么Agent 说这是我们的第一次对话。二、我写的代码要让记忆跨脚本运行保持必须保证thread_id不变并且使用持久化存储。测试时最简单的办法是固定thread_id# 不要每次都用新的 uuidconfig{configurable:{thread_id:user-chenzc-001}}或者把thread_id存到文件里importos THREAD_ID_FILEthread_id.txtdefget_thread_id():ifos.path.exists(THREAD_ID_FILE):withopen(THREAD_ID_FILE,r)asf:returnf.read().strip()new_iduser-chenzc-001withopen(THREAD_ID_FILE,w)asf:f.write(new_id)returnnew_id config{configurable:{thread_id:get_thread_id()}}真正生产环境应该用 SQLite 或 Postgresfromlanggraph.checkpoint.sqliteimportSqliteSaverwithSqliteSaver.from_conn_string(checkpoints.sqlite)ascheckpointer:agentcreate_agent(modelllm,tools[search],checkpointercheckpointer,)三、我遇到的坑坑 1InMemorySaver只存当前进程我以为InMemorySaver像 Redis 一样会持久化。实际上它就是内存里的一个字典脚本结束就清空。fromlanggraph.checkpoint.memoryimportInMemorySaver checkpointerInMemorySaver()# 这个对象在进程里存数据# 进程结束数据消失如果你要跨运行保持记忆必须换成SqliteSaver或PostgresSaver。坑 2uuid7()每次运行都变我的代码里config{configurable:{thread_id:str(uuid7())}}每次运行脚本uuid7()都会生成一个新的thread_id。LangGraph 把每个新thread_id当成一次全新对话自然没有历史。这就好比你每次打开微信都用一个新微信号当然看不到昨天的聊天记录。坑 3流式输出也忘了传config我改成stream_events后又踩了一次同样的坑streamagent.stream_events({messages:[{role:user,content:搜索 AI 新闻并总结}]},versionv3,)报错KeyError: thread_id因为 Agent 配置了checkpointer所以stream_events也必须传configstreamagent.stream_events({messages:[{role:user,content:搜索 AI 新闻并总结}]},configconfig,versionv3,)坑 4一次脚本运行只算一轮对话我最初的测试方式是运行脚本 → 输入一个问题 → 得到回答 → 脚本退出 → 再次运行脚本。这种方式下即使thread_id固定、用了持久化 checkpointer第二轮运行时 Agent 看到的历史也只有第一轮那一条消息。因为脚本一次运行只发了一次invoke。要测试真正的多轮对话应该把输入放在while循环里config{configurable:{thread_id:user-chenzc-001}}whileTrue:user_inputinput(你)ifuser_input.lower()in[exit,quit]:breakresultagent.invoke({messages:[{role:user,content:user_input}]},configconfig,)print(result[messages][-1].content)四、搞清楚后的结论LangChain Agent 的记忆机制有三个层级每次 invoke 独立同一进程内多轮跨进程持久化无 checkpointer无记忆InMemorySaver 固定 thread_id进程内记忆SqliteSaver/PostgresSaver 固定 thread_id长期记忆关键认知checkpointer 负责存thread_id负责定位是哪次对话InMemorySaver只保当前进程重启就丢uuid7()适合一次会话内的临时 ID跨运行要用固定 ID 或持久化生成invoke和stream_events都需要config五、我的收获记忆不是默认开启的它需要你显式地告诉系统这是谁的对话、存在哪里。我以前以为加了checkpointer就万事大吉结果发现它只是基础设施。真正让记忆生效的是thread_id的一致性和存储介质的持久性。这件事也让我意识到 LangChain 的设计哲学它把每个能力都拆成显式配置而不是隐式魔法。记忆如此工具如此Agent 也如此。下一篇我会写 Middleware——怎么给 Agent 加容错、人工审核和文件操作能力。