[langgraph] Memory

LangGraph 中 PlanState、Checkpointer、Short-term Memory 与 Long-term Memory 的关系

在学习 LangGraph 时,几个概念很容易混在一起: State / PlanStatemessagescheckpointershort-term memorylong-term memoryStore。 它们不是同一个东西,但彼此之间有清晰的层级关系。

核心结论:
  - 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

Short-term Memory
某个 thread_id 下被 checkpointer 保存的 state 历史。是 “保存下来的当前会话 / 当前任务上下文”。

 

4. thread_id 为什么重要?

运行 graph 时,我们通常会传入:

config = {
    "configurable": {
        "thread_id": "smart-plan-demo-001"
    }
}

 

agent.invoke是可以有第二个参数 config的。

如下,有点类似于 index 的效果。

image

 

这个 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() 暂停,让人类确认。

 

整体流程图

用户:帮我在图灵宾馆预定一个房间
LLM 判断:需要调用 book_hotel 工具
ToolNode 开始执行 book_hotel
book_hotel 内部触发 interrupt,暂停等待人工审批
人类选择 OK 或 edit
graph 恢复,工具继续执行
返回最终结果:成功预定酒店

 

一句话总结

LLM 负责提出动作;人类负责审批动作;LangGraph 负责暂停、保存和恢复;工具负责真正执行动作。
posted @ 2026-05-19 17:49  郝壹贰叁  阅读(64)  评论(0)    收藏  举报