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

Agent 能调工具之后,我开始做一件看起来很基础但很容易踩坑的事:让 Agent 记住上下文

我按照文档加了checkpointer=InMemorySaver(),满心期待 Agent 能像 ChatGPT 一样记得上一句。结果第二次运行脚本,它又问"这是我们的第一次对话吗?"

我一度怀疑InMemorySaver是个假的记忆组件。后来才搞明白:不是没记忆,是我的用法让它没法记忆。


一、我当时想干什么

我想实现最简单的多轮对话:

  1. 第一轮:我问"旧金山天气怎么样?"
  2. 第二轮:我问"那明天呢?"
  3. Agent 应该能理解"明天"指的是"旧金山明天的天气"

我写的代码:

fromlangchain.agentsimportcreate_agentfromlangchain_core.toolsimporttoolfromlangchain_core.utils.uuidimportuuid7fromlangchain_openaiimportChatOpenAIfromlanggraph.checkpoint.memoryimportInMemorySaver@tooldefsearch(query:str)->str:"""搜索工具"""returnf"搜索结果:{query}"llm=ChatOpenAI(model="deepseek-chat",temperature=0,openai_api_key="你的 api key",openai_api_base="https://api.deepseek.com/v1",)agent=create_agent(model=llm,tools=[search],checkpointer=InMemorySaver(),)config={"configurable":{"thread_id":str(uuid7())}}result=agent.invoke({"messages":[{"role":"user","content":"旧金山天气怎么样?"}]},config=config,)result=agent.invoke({"messages":[{"role":"user","content":"那明天呢?"}]},config=config,)

在这个脚本里,记忆是工作的。但当我再次运行脚本,问"上一轮我问了什么",Agent 说"这是我们的第一次对话"。


二、我写的代码

要让记忆跨脚本运行保持,必须保证thread_id不变,并且使用持久化存储。测试时最简单的办法是固定thread_id

# 不要每次都用新的 uuidconfig={"configurable":{"thread_id":"user-chenzc-001"}}

或者把thread_id存到文件里:

importos THREAD_ID_FILE="thread_id.txt"defget_thread_id():ifos.path.exists(THREAD_ID_FILE):withopen(THREAD_ID_FILE,"r")asf:returnf.read().strip()new_id="user-chenzc-001"withopen(THREAD_ID_FILE,"w")asf:f.write(new_id)returnnew_id config={"configurable":{"thread_id":get_thread_id()}}

真正生产环境应该用 SQLite 或 Postgres:

fromlanggraph.checkpoint.sqliteimportSqliteSaverwithSqliteSaver.from_conn_string("checkpoints.sqlite")ascheckpointer:agent=create_agent(model=llm,tools=[search],checkpointer=checkpointer,)

三、我遇到的坑

坑 1:InMemorySaver只存当前进程

我以为InMemorySaver像 Redis 一样会持久化。实际上它就是内存里的一个字典,脚本结束就清空。

fromlanggraph.checkpoint.memoryimportInMemorySaver checkpointer=InMemorySaver()# 这个对象在进程里存数据# 进程结束,数据消失

如果你要跨运行保持记忆,必须换成SqliteSaverPostgresSaver

坑 2:uuid7()每次运行都变

我的代码里:

config={"configurable":{"thread_id":str(uuid7())}}

每次运行脚本,uuid7()都会生成一个新的thread_id。LangGraph 把每个新thread_id当成一次全新对话,自然没有历史。

这就好比你每次打开微信都用一个新微信号,当然看不到昨天的聊天记录。

坑 3:流式输出也忘了传config

我改成stream_events后,又踩了一次同样的坑:

stream=agent.stream_events({"messages":[{"role":"user","content":"搜索 AI 新闻并总结"}]},version="v3",)

报错:

KeyError: 'thread_id'

因为 Agent 配置了checkpointer,所以stream_events也必须传config

stream=agent.stream_events({"messages":[{"role":"user","content":"搜索 AI 新闻并总结"}]},config=config,version="v3",)

坑 4:一次脚本运行只算一轮对话

我最初的测试方式是:运行脚本 → 输入一个问题 → 得到回答 → 脚本退出 → 再次运行脚本。

这种方式下,即使thread_id固定、用了持久化 checkpointer,第二轮运行时 Agent 看到的"历史"也只有第一轮那一条消息。因为脚本一次运行只发了一次invoke

要测试真正的多轮对话,应该把输入放在while循环里:

config={"configurable":{"thread_id":"user-chenzc-001"}}whileTrue:user_input=input("你:")ifuser_input.lower()in["exit","quit"]:breakresult=agent.invoke({"messages":[{"role":"user","content":user_input}]},config=config,)print(result["messages"][-1].content)

四、搞清楚后的结论

LangChain Agent 的记忆机制有三个层级:

每次 invoke 独立

同一进程内多轮

跨进程持久化

无 checkpointer

无记忆

InMemorySaver + 固定 thread_id

进程内记忆

SqliteSaver/PostgresSaver + 固定 thread_id

长期记忆

关键认知:

  1. checkpointer 负责存thread_id负责定位是哪次对话
  2. InMemorySaver只保当前进程,重启就丢
  3. uuid7()适合一次会话内的临时 ID,跨运行要用固定 ID 或持久化生成
  4. invokestream_events都需要config

五、我的收获

记忆不是默认开启的,它需要你显式地告诉系统"这是谁的对话、存在哪里"。

我以前以为加了checkpointer就万事大吉,结果发现它只是基础设施。真正让记忆生效的是thread_id的一致性和存储介质的持久性。

这件事也让我意识到 LangChain 的设计哲学:它把每个能力都拆成显式配置,而不是隐式魔法。记忆如此,工具如此,Agent 也如此。


下一篇我会写 Middleware——怎么给 Agent 加容错、人工审核和文件操作能力。