我加了 `InMemorySaver`,为什么 Agent 还是没记忆?
Agent 能调工具之后,我开始做一件看起来很基础但很容易踩坑的事:让 Agent 记住上下文。
我按照文档加了checkpointer=InMemorySaver(),满心期待 Agent 能像 ChatGPT 一样记得上一句。结果第二次运行脚本,它又问"这是我们的第一次对话吗?"
我一度怀疑InMemorySaver是个假的记忆组件。后来才搞明白:不是没记忆,是我的用法让它没法记忆。
一、我当时想干什么
我想实现最简单的多轮对话:
- 第一轮:我问"旧金山天气怎么样?"
- 第二轮:我问"那明天呢?"
- 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()# 这个对象在进程里存数据# 进程结束,数据消失如果你要跨运行保持记忆,必须换成SqliteSaver或PostgresSaver。
坑 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 的记忆机制有三个层级:
关键认知:
- checkpointer 负责存,
thread_id负责定位是哪次对话 InMemorySaver只保当前进程,重启就丢uuid7()适合一次会话内的临时 ID,跨运行要用固定 ID 或持久化生成invoke和stream_events都需要config
五、我的收获
记忆不是默认开启的,它需要你显式地告诉系统"这是谁的对话、存在哪里"。
我以前以为加了checkpointer就万事大吉,结果发现它只是基础设施。真正让记忆生效的是thread_id的一致性和存储介质的持久性。
这件事也让我意识到 LangChain 的设计哲学:它把每个能力都拆成显式配置,而不是隐式魔法。记忆如此,工具如此,Agent 也如此。
下一篇我会写 Middleware——怎么给 Agent 加容错、人工审核和文件操作能力。