[langgraph] Memory
LangGraph 中 PlanState、Checkpointer、Short-term Memory 与 Long-term Memory 的关系
在学习 LangGraph 时,几个概念很容易混在一起: State / PlanState、messages、checkpointer、 short-term memory、long-term memory、Store。 它们不是同一个东西,但彼此之间有清晰的层级关系。
核心结论:
- PlanState 是当前 workflow 运行时的数据结构;
- Checkpointer 把 PlanState 按 thread_id 保存下来,于是形成 short-term memory;
- Store 保存跨 thread、跨任务、长期存在的信息,于是形成 long-term memory。
1. PlanState 是什么?
例如我们定义了一个 PlanState:
class PlanState(TypedDict): messages: Annotated[list[AnyMessage], add_messages] draft_plan: Optional[str] approved: Optional[bool] final_answer: Optional[str]
这个 PlanState 可以理解成:
Graph 当前 运行时携带的数据包。
它里面的 messages 保存当前 workflow 能看到的对话历史,例如:
-
-
- HumanMessage:用户输入
- AIMessage:模型回复
- ToolMessage:工具调用结果
- AIMessage:最终回答
-
PlanState 本身只是定义数据结构,它不是存储系统。
它只是告诉 LangGraph:这个 graph 的 state 里有哪些字段。
2. Checkpointer 是什么?
当我们写:
checkpointer = InMemorySaver() graph = builder.compile(checkpointer=checkpointer)
意思是:
请把 每次 graph 运行过程中 的 state 保存起来。
保存的内容就包括:
state["messages"]
state["draft_plan"]
state["approved"]
state["final_answer"]
Checkpointer 保存的是 PlanState 的快照。
它不只是保存聊天记录,而是保存整个 workflow 当前的运行现场。
3. Short-term Memory
某个 thread_id 下被 checkpointer 保存的 state 历史。是 “保存下来的当前会话 / 当前任务上下文”。
4. thread_id 为什么重要?
运行 graph 时,我们通常会传入:
config = {
"configurable": {
"thread_id": "smart-plan-demo-001"
}
}
agent.invoke是可以有第二个参数 config的。
如下,有点类似于 index 的效果。

这个 thread_id 就像一个会话 ID / 任务 ID。
| thread_id | 保存内容 |
|---|---|
| smart-plan-demo-001 | 这个任务的 PlanState 快照 |
| smart-plan-demo-002 | 另一个任务的 PlanState 快照 |
因此 short-term memory 一般是:
某一个 thread 内部的记忆。
例如本轮对话历史、工具调用结果、当前流程停在哪一步、approval_node 是否暂停、draft_plan 是什么。
第二次调用时,Agent 能看到同一个 thread 里之前的对话历史。
checkpointer = InMemorySaver() def get_weather(city: str) -> str: """获取某个城市天气""" return f"城市:{city},天气一直都是晴天!" agent = create_react_agent( model=llm, tools=[get_weather], checkpointer=checkpointer, # 注意通常叫 checkpointer,不是 checkpoint ) config = { "configurable": { "thread_id": "1" } } cs_response = agent.invoke( {"messages": [{"role": "user", "content": "长沙天气怎么样?"}]}, config, ) bj_response = agent.invoke( {"messages": [{"role": "user", "content": "北京呢?"}]}, # 因为 config中的thread_id,所以知道这是在“问什么” config, )
5. Long-term Memory / Store 是什么?
Long-term memory 和 PlanState 不一样。
Long-term memory 保存的是更长期、更稳定的信息,例如:
- 用户偏好中文回答
- 用户喜欢代码给完整文件
- 某个项目的长期背景知识
这些信息不应该只属于某一次 graph run,也不应该只放在某个 thread 的 PlanState 里。 它们应该放在更长期的存储中,比如:
Store / Database / Vector Store / User Profile Memory
Store 不是说:graph 运行完以后,自动把整个 PlanState 塞进 long-term memory
而是:从 PlanState 里提取值得长期保存的信息。然后 store.put(...) as followings.
def memory_update_node(state):
# 从 state 中挑选值得长期保存的信息
preference = extract_preference_from_messages(state["messages"])
# 写入 long-term store
store.put(
namespace=("users", user_id, "preferences"),
key="planning_preference",
value={"text": preference},
)
return {}
6. Short-term Memory vs Long-term Memory
| 概念 | 保存什么 | 生命周期 | 例子 |
|---|---|---|---|
| PlanState | 当前 graph 正在用的数据结构 | 当前运行中 | messages, draft_plan, approved |
| Checkpointer | 当前 thread 的 state 快照 | 当前会话 / 当前任务 | 流程暂停在 approval_node |
| Short-term Memory | checkpointer 保存下来的 thread 状态 | 一个 thread 内 | 本轮对话历史、工具结果、当前步骤 |
| Long-term Memory / Store | 跨 thread 的长期信息 | 长期 | 用户偏好、项目背景、长期知识 |
7. 为什么 messages 既在 PlanState,又像 memory?
因为 messages 是一种数据字段。
它可以存在于 PlanState 里:
messages: Annotated[list[AnyMessage], add_messages]
但它是否真正成为“记忆”,取决于有没有被保存。
| 情况 | 结果 |
|---|---|
| 没有 checkpointer | messages 只在本次 graph.invoke 运行中存在,运行结束就没了 |
| 有 checkpointer | messages 会被保存到 thread_id 对应的 checkpoint,下一次同一个 thread_id 可以继续用 |
一句话:
messages 是内容;checkpointer 是保存机制;short-term memory 是保存后的效果。
PlanState:当前数据结构。
Checkpointer:保存当前数据结构。
Short-term Memory:被 checkpointer 保存的当前 thread 状态。
Store / Long-term Memory:跨 thread 的长期知识和用户信息。
补充一个特殊的 state情况:
当工具方法中调用一些类似id的数据。
from typing import Annotated from langgraph.prebuilt import InjectedState, create_react_agent from langgraph.prebuilt.chat_agent_executor import AgentState from langchain_core.tools import tool class CustomState(AgentState): user_id: str @tool(return_direct=True) # 工具返回结果后,可以直接作为最终结果返回,不一定再交给 LLM 重新组织语言。 def get_user_info( state: Annotated[CustomState, InjectedState] ) -> str: """查询用户信息。""" user_id = state["user_id"] return "user_123 用户的姓名是张三。" if user_id == "user_123" else "未知用户" agent = create_react_agent( model=llm, tools=[get_user_info], state_schema=CustomState, ) agent.invoke( { "messages": "查询用户信息", "user_id": "user_123", } )
工具本质上是一个函数。当 LLM 决定调用工具时,它不会直接执行函数,而是生成一个结构化的 tool call,包括工具名称和参数。
{ "name": "get_weather", "args": { "city": "悉尼" } }
普通参数通常由 LLM 根据用户问题、历史消息和工具 schema 自动生成。
然后 LangGraph / ToolNode / runtime 接收这个 tool call,在真实 Python 环境中执行对应函数。
函数返回结果后,结果会作为 ToolMessage 放回对话历史。
LLM 再根据这个结果继续推理或生成最终答案。
但有些参数,比如 user_id、权限、当前 state,不应该由 LLM 生成,而应该由 LangGraph 从系统内部 state 自动注入。也就是通过 InjectedState 告诉大模型:你不用操心参数的事儿了~
人机交互
from langgraph.checkpoint.memory import InMemorySaver from langgraph.types import interrupt, Command from langgraph.prebuilt import create_react_agent from langchain_core.tools import tool # ============================================================ # 1. 定义一个“敏感工具” # ============================================================ # 这个工具不是普通查询工具。 # # 普通工具: # 查天气、算数学、查资料 # 一般可以直接执行 # # 敏感工具: # 订酒店、发邮件、下订单、删文件、改数据库 # 这些动作会产生真实后果 # # 所以: # LLM 可以“提出要调用这个工具” # 但工具真正执行前,需要人类确认 # ============================================================ @tool(return_direct=True) def book_hotel(hotel_name: str) -> str: """ 预定酒店房间。 Args: hotel_name: 酒店名称。 Returns: str: 预定结果。 注意: hotel_name 是普通工具参数。 这个参数由 LLM 根据用户输入生成。 例如用户说: 帮我在图灵宾馆订一个房间 LLM 可能生成 tool call: book_hotel(hotel_name="图灵宾馆") """ # ======================================================== # 2. interrupt:在真正执行敏感动作前暂停 # ======================================================== # 这一步非常关键。 # # 代码执行到 interrupt(...) 时: # 第一次执行: # graph 会暂停 # 当前状态会被 checkpointer 保存 # 外部程序会收到一个“需要人工审批”的信号 # # resume 之后: # interrupt(...) 会返回人类传进来的审批结果 # # 所以 interrupt 不是普通 input()。 # 它是 LangGraph workflow 级别的“暂停点”。 # ======================================================== response = interrupt( { "message": "即将执行敏感工具:book_hotel", "tool_name": "book_hotel", "current_args": { "hotel_name": hotel_name, }, "instruction": ( "请选择:\n" "1. {'type': 'ok'} 表示同意使用当前参数继续执行。\n" "2. {'type': 'edit', 'args': {'hotel_name': '新的酒店名'}} " "表示先修改参数,再继续执行。" ), } ) # ======================================================== # 3. 处理人类审批结果 # ======================================================== # response 是 Command(resume=...) 传回来的内容。 # # 如果人类传: # {"type": "ok"} # # 表示: # 使用 LLM 原来生成的 hotel_name,不修改。 # # 如果人类传: # { # "type": "edit", # "args": { # "hotel_name": "希尔顿酒店" # } # } # # 表示: # 人类觉得 LLM 选的酒店不对, # 所以把 hotel_name 改成新的值。 # ======================================================== response_type = response["type"].lower() if response_type == "ok": # 人类批准了。 # 什么都不用改,继续使用原来的 hotel_name。 pass elif response_type == "edit": # 人类没有直接批准,而是修改了工具参数。 # 这里用人类修改后的酒店名覆盖原来的 hotel_name。 hotel_name = response["args"]["hotel_name"] else: # 如果人类返回了不认识的 type,说明审批结果格式不对。 # 这种情况应该直接报错,避免执行错误动作。 raise ValueError(f"Unknown response type: {response['type']}") # ======================================================== # 4. 真正执行敏感动作 # ======================================================== # 注意: # 只有通过 interrupt 的人工审批后, # 代码才会执行到这里。 # # 也就是说: # LLM 只能提出“我要订酒店” # 人类确认后,工具才真正执行 # ======================================================== return f"成功在 {hotel_name} 预定了一个房间。" # ============================================================ # 5. 创建 checkpointer # ============================================================ # interrupt 必须依赖 checkpointer。 # # 为什么? # 因为 graph 暂停后,需要记住: # - 当前执行到哪个工具 # - 工具参数是什么 # - messages 是什么 # - 当前 thread_id 是什么 # # InMemorySaver 只是 demo 用。 # 生产环境一般要换成数据库型 checkpointer,例如 PostgresSaver。 # ============================================================ checkpointer = InMemorySaver() # ============================================================ # 6. 创建 ReAct Agent # ============================================================ # create_react_agent 会帮你创建一个典型 ReAct loop: # # LLM # ↓ # 判断是否需要工具 # ↓ # ToolNode 执行工具 # ↓ # 工具结果返回给 LLM # ↓ # LLM 生成最终答案 # # 这里 book_hotel 是一个工具。 # LLM 可以决定调用它。 # 但 book_hotel 内部有 interrupt, # 所以真正执行前会暂停并等待人工审批。 # ============================================================ agent = create_react_agent( model=llm, tools=[book_hotel], checkpointer=checkpointer, ) # ============================================================ # 7. thread_id # ============================================================ # thread_id 用来标识当前 workflow / 对话 / 任务。 # # 第一次运行和后面 resume 恢复时, # 必须使用同一个 thread_id。 # # 否则 LangGraph 不知道你要恢复哪一个暂停中的任务。 # ============================================================ config = { "configurable": { "thread_id": "hotel-booking-demo-001" } } # ============================================================ # 8. 第一次运行 agent # ============================================================ # 用户提出: # 帮我在图灵宾馆预定一个房间 # # LLM 看到工具 book_hotel 后,可能生成 tool call: # book_hotel(hotel_name="图灵宾馆") # # ToolNode 开始执行 book_hotel。 # # 但 book_hotel 内部遇到 interrupt(...)。 # # 所以 graph 会暂停, # 并返回一个需要人工审批的信息。 # ============================================================ for chunk in agent.stream( { "messages": [ { "role": "user", "content": "帮我在图灵宾馆预定一个房间", } ] }, config=config, ): print(chunk) print("\n") # ============================================================ # 9. 人类选择 OK:批准原参数 # ============================================================ # 如果人类同意: # 就用 Command(resume={"type": "ok"}) 恢复 graph。 # # 恢复后: # interrupt(...) 会返回 {"type": "ok"} # book_hotel 会继续执行 # 最终返回: # 成功在 图灵宾馆 预定了一个房间。 # ============================================================ for chunk in agent.stream( Command( resume={ "type": "ok" } ), config=config, ): print(chunk) print("\n") # ============================================================ # 10. 如果人类想修改参数,可以用下面这种方式 # ============================================================ # 注意: # 这段和上面的 OK 方式二选一。 # # 如果你已经执行了 OK 恢复, # 就不要再执行下面这个 edit 恢复。 # # 这个 edit 的意思是: # 不要订“图灵宾馆” # 改成订“希尔顿酒店” # ============================================================ # for chunk in agent.stream( # Command( # resume={ # "type": "edit", # "args": { # "hotel_name": "希尔顿酒店" # } # } # ), # config=config, # ): # print(chunk) # print("\n")
LangGraph Human Approval 示例:敏感工具调用前先暂停
LLM 可以自动选择工具,但如果这个工具会产生真实后果,例如订酒店、发邮件、写数据库,就应该在真正执行前用
interrupt() 暂停,让人类确认。

浙公网安备 33010602011771号