让 Agent 记住这一场对话:LangChain 短期记忆(Short-term Memory)实战
“我叫小明。”“好的小明”“我叫什么名字”“……我不知道。”默认情况下LLM 是无状态的——每次调用都是独立的它根本不记得你上一句说了什么。要让 Agent 在一场对话里前后连贯、记住你刚说过的话需要的是短期记忆Short-term Memory。这篇文章用 6 个完整可运行的例子从记住对话一路讲到对话太长怎么压缩。代码全部内嵌正文复制即可跑。一、短期记忆是什么短期记忆让应用在单个会话thread内记住之前的交互——最常见的形式就是对话历史。它和长期记忆是两回事短期记忆长期记忆范围单个会话内跨多个会话内容完整对话历史提炼的关键信息存储CheckpointerStore生命周期会话结束可丢弃持久保留类比人的工作记忆人的知识库一句话短期记忆维持这一场对话的连贯性让模型记得刚才说过什么。本文讲它——基于Checkpointer实现。二、三个核心机制State Checkpointer thread_id短期记忆建立在三个概念上State状态Agent 把对话历史存在 State 的messages字段里每轮更新。Checkpointer检查点每次状态更新时保存快照让对话可中断、可恢复。thread_id不同 thread_id 的状态完全隔离——用户 A 和用户 B 的对话互不串台。config {configurable: {thread_id: thread_001}} agent.invoke({...}, configconfig) # 同一 thread_id 的多次调用共享记忆Checkpointer 有多种实现决定记忆存在哪、能不能跨重启类型场景持久化InMemorySaver开发/测试❌ 重启丢失SqliteSaver轻量级持久化✅ 落盘到文件无需服务器PostgresSaver生产/多实例✅下文 01/02/03 用SqliteSaver落盘、可跨重启04/05/06 用InMemorySaver这几个演示消息条数变化每次从空开始更清晰。所有例子从.env读取模型配置。三、基础让 Agent 记住对话最简单的短期记忆给create_agent传一个 checkpointer调用时带上固定thread_id。下面用SqliteSaver持久化——连重启都记得。importosimportsqlite3fromdotenvimportload_dotenvfromlangchain.agentsimportcreate_agentfromlangchain.chat_modelsimportinit_chat_modelfromlanggraph.checkpoint.sqliteimportSqliteSaver 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,)# SqliteSaver会话状态持久化到文件connsqlite3.connect(short_memory_01.db,check_same_threadFalse)checkpointerSqliteSaver(conn)checkpointer.setup()agentcreate_agent(modelmodel,checkpointercheckpointer,system_prompt你是一个使用工具帮用户完成任务的有用助手,)config{configurable:{thread_id:thread_001}}if__name____main__:# 第一轮告诉它名字agent.invoke({messages:[{role:user,content:你好我叫张三}]},configconfig)# 第二轮同一 thread_id它能记住上一轮resultagent.invoke({messages:[{role:user,content:我叫什么}]},configconfig)print(result[messages][-1].content)# 应回答你叫张三关键就在config里的thread_id两次 invoke 用同一个 thread_id第二轮才能读到第一轮的历史。换个 thread_id记忆就互相隔离了。把SqliteSaver换成InMemorySaver()逻辑一样只是重启后不记得。四、自定义状态不止存对话默认 State 只有messages。继承AgentState可以加自定义字段用户 ID、偏好等它们同样被 checkpointer 持久化并能在工具里通过runtime.state读取。importosimportsqlite3fromtypingimportAnyfromdotenvimportload_dotenvfromlangchain.agentsimportAgentState,create_agentfromlangchain.chat_modelsimportinit_chat_modelfromlangchain.toolsimporttoolfromlanggraph.checkpoint.sqliteimportSqliteSaverfromlanggraph.prebuiltimportToolRuntime 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,)# 继承 AgentState加自定义字段classCustomAgentState(AgentState):user_id:strpreference:dict[str,Any]tooldefread_state_fields(runtime:ToolRuntime)-str:从状态中读取自定义状态信息user_idruntime.state.get(user_id,missing)preferenceruntime.state.get(preference,{})returnfuser_id{user_id}, preference{preference}connsqlite3.connect(short_memory_02.db,check_same_threadFalse)checkpointerSqliteSaver(conn)checkpointer.setup()agentcreate_agent(modelmodel,tools[read_state_fields],checkpointercheckpointer,state_schemaCustomAgentState,# 声明自定义 Statesystem_prompt你是一个使用工具帮用户完成任务的有用助手,)config{configurable:{thread_id:thread_001}}if__name____main__:# 首轮传入自定义字段resultagent.invoke({messages:[{role:user,content:调用工具 read_state_fields然后告诉我状态里有什么信息}],user_id:user_100,preference:{language:zh-CN,address:北京},},configconfig,)print(result[messages][-1].content)print(**50)# 次轮不再传字段已被 checkpointer 持久化依然读得到resultagent.invoke({messages:[{role:user,content:再读一次当前状态}]},configconfig)print(result[messages][-1].content)要点自定义字段在首轮 invoke 时随输入传入之后被持久化后续轮次不传也能读到。五、在工具里写状态Command工具不仅能读 State还能写State。普通工具return 字符串改不了自定义字段但return Command(update{...})可以。下面update_user_info把user_name写进 Stategreet再读出来用。importosimportsqlite3fromdotenvimportload_dotenvfromlangchain.agentsimportAgentState,create_agentfromlangchain.chat_modelsimportinit_chat_modelfromlangchain.toolsimporttoolfromlangchain_core.messagesimportToolMessagefromlanggraph.checkpoint.sqliteimportSqliteSaverfromlanggraph.prebuiltimportToolRuntimefromlanggraph.typesimportCommandfrompydanticimportBaseModel 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,)classCustomState(AgentState):user_name:strclassCustomContext(BaseModel):user_id:strtooldefupdate_user_info(runtime:ToolRuntime[CustomContext,CustomState])-Command:查询用户信息并持久化到状态中user_idruntime.context.user_id name张三ifuser_iduser_100elseUnknown userreturnCommand(update{user_name:name,# 写入自定义 State 字段messages:[ToolMessage(content成功获取用户信息,tool_call_idruntime.tool_call_id)],})tooldefgreet(runtime:ToolRuntime[CustomContext,CustomState])-str|Command:读取状态中的用户名并问好user_nameruntime.state.get(user_name)ifuser_nameisNone:returnCommand(update{messages:[ToolMessage(content请先调用 update_user_info,tool_call_idruntime.tool_call_id)]})returnfhello{user_name}connsqlite3.connect(short_memory_03.db,check_same_threadFalse)checkpointerSqliteSaver(conn)checkpointer.setup()agentcreate_agent(modelmodel,tools[update_user_info,greet],checkpointercheckpointer,state_schemaCustomState,context_schemaCustomContext,system_prompt你是一个使用工具帮用户完成任务的有用助手,)config{configurable:{thread_id:thread_001}}if__name____main__:resultagent.invoke({messages:[{role:user,content:请先获取我的信息再向我问好}]},configconfig,contextCustomContext(user_iduser_100),)forminresult[messages]:ifhasattr(m,content):print(m.type,m.content)print(**50)# 同一会话user_name 已写入并持久化resultagent.invoke({messages:[{role:user,content:我叫什么}]},configconfig)print(result[messages][-1].content)⚠️ 工具返回Command时update里要带上对应的ToolMessage回应本次 tool_call每个工具调用必须有响应否则消息格式会出错。六、对话太长怎么办上下文窗口管理对话一轮轮累积messages会无限增长带来三个问题超出上下文窗口、成本飙升、模型在长文本里迷失。有三种应对策略都通过before_model中间件在每次模型调用前处理消息。策略一Trim裁剪—— 保留首尾丢中间importosfromtypingimportAnyfromdotenvimportload_dotenvfromlangchain.agentsimportAgentState,create_agentfromlangchain.agents.middlewareimportbefore_modelfromlangchain.chat_modelsimportinit_chat_modelfromlangchain_core.messagesimportRemoveMessagefromlanggraph.checkpoint.memoryimportInMemorySaverfromlanggraph.graph.messageimportREMOVE_ALL_MESSAGESfromlanggraph.runtimeimportRuntime 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,)before_modeldeftrim_message(state:AgentState,_runtime:Runtime)-dict[str,Any]|None:保留第一条和最后几条消息messagesstate[messages]iflen(messages)3:returnNonefirst_messagemessages[0]recent_messagesmessages[-3:]iflen(messages)%20elsemessages[-4:]new_messages[first_message]recent_messages# 清空再放回要保留的RemoveMessage(REMOVE_ALL_MESSAGES) 保留项return{messages:[RemoveMessage(idREMOVE_ALL_MESSAGES),*new_messages]}agentcreate_agent(modelmodel,checkpointerInMemorySaver(),middleware[trim_message])config{configurable:{thread_id:thread_001}}if__name____main__:questions[我叫张三记住我的名字,写一首春天的诗,写一首大海的诗,我叫什么,我叫什么]forquestioninquestions:resultagent.invoke({messages:[{role:user,content:question}]},configconfig)print(消息条数:,len(result[messages]))裁剪用RemoveMessage(REMOVE_ALL_MESSAGES)清空全部再放回要保留的几条。运行能看到消息条数始终被压在很小范围。策略二Delete删除—— 按 id 精确删和裁剪的清空再放回不同删除是按消息 id 逐条移除最早的几条。importosfromtypingimportAnyfromdotenvimportload_dotenvfromlangchain.agentsimportAgentState,create_agentfromlangchain.agents.middlewareimportbefore_modelfromlangchain.chat_modelsimportinit_chat_modelfromlangchain_core.messagesimportRemoveMessagefromlanggraph.checkpoint.memoryimportInMemorySaverfromlanggraph.runtimeimportRuntime 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,)before_modeldefdelete_message(state:AgentState,_runtime:Runtime)-dict[str,Any]|None:删除最早的两条消息messagesstate[messages]iflen(messages)2:return{messages:[RemoveMessage(idm.id)forminmessages[:2]]}returnNoneagentcreate_agent(modelmodel,checkpointerInMemorySaver(),middleware[delete_message])config{configurable:{thread_id:thread_001}}if__name____main__:questions[我叫张三记住我的名字,写一首春天的诗,写一首大海的诗,我叫什么,我叫什么]forquestioninquestions:resultagent.invoke({messages:[{role:user,content:question}]},configconfig)print(消息条数:,len(result[messages]))裁剪和删除都会直接丢信息。如果被删的内容后面还有用就丢了——这是它们的代价。策略三Summarize摘要—— 压缩而非丢弃更聪明的做法用一个模型把早期历史压缩成一段摘要保留最近几条原文。既控制长度又不丢关键信息。LangChain 内置了SummarizationMiddleware开箱即用。importosfromdotenvimportload_dotenvfromlangchain.agentsimportcreate_agentfromlangchain.agents.middlewareimportSummarizationMiddlewarefromlangchain.chat_modelsimportinit_chat_modelfromlanggraph.checkpoint.memoryimportInMemorySaver 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,)summarizerSummarizationMiddleware(modelmodel,trigger(tokens,500),# 超过 500 tokens 触发摘要keep(messages,6),# 保留最近 6 条原文)agentcreate_agent(modelmodel,checkpointerInMemorySaver(),middleware[summarizer])config{configurable:{thread_id:thread_001}}if__name____main__:questions[我叫张三我在学习 langchain,总结一下长期记忆和短期记忆的区别,举两个实际业务中的例子,如果对话很长会出现什么问题,如何使用 SummarizationMiddleware 来处理这些问题,我叫什么,]forquestioninquestions:resultagent.invoke({messages:[{role:user,content:question}]},configconfig)print(消息条数:,len(result[messages]))print(\n最后一轮回复:,result[messages][-1].content)三种策略怎么选场景推荐策略对话过长但需保留上下文Summarize保留关键信息只需要最近的对话Trim简单高效有无关/临时消息要清理Delete精确移除特殊业务逻辑自定义before_model中间件七、避坑裁剪/删除后必须保证消息格式合法这是最容易翻车的地方。删完/裁完剩下的消息必须仍符合 LLM 的格式要求✅ 合法 [HumanMessage, AIMessage, HumanMessage] ← 以 user 消息开头 [AIMessage(tool_calls[...]), ToolMessage, ...] ← 工具调用与 ToolMessage 成对 ❌ 非法 [AIMessage] ← 没以 user 消息开头 [AIMessage(tool_calls[...]), HumanMessage] ← 缺少配对的 ToolMessage比如你删掉了一个带tool_calls的 AIMessage却留下了它对应的 ToolMessage或反之模型调用就会报错。裁剪逻辑要保证成对和开头合法。八、总结概念比喻作用State记事本存当前会话的所有信息messages 自定义字段Checkpointer保存按钮每次更新存快照InMemory / Sqlite / Postgresthread_id笔记本编号隔离不同会话Command写入笔工具里写 StateTrim / Delete撕旧页 / 擦内容直接减少消息会丢信息Summarize做笔记压缩历史、保留关键信息Middleware过滤器在模型调用前后处理消息短期记忆的本质是维持这一场对话的连贯性用 State 存历史、用 Checkpointer 持久化、用 thread_id 隔离会话让模型记得刚才说过什么。对话变长时再用裁剪/删除/摘要把上下文控制在合理范围。至于跨会话、跨天记住你是谁——那是长期记忆long-memory/的 Store的活儿。