发布于

LangGraph Memory 学习笔记

Authors
  • avatar
    Name
    Charly
    Twitter

学习 LangGraph Memory 时,最容易混淆的是 State、Checkpointer、Store 和消息摘要。它们都与“记忆”有关,但解决的是不同问题。

可以先记住这张表:

概念作用生命周期
AgentStateAgent 当前运行所需的数据单次运行中始终存在
Checkpointerthread_id 保存和恢复 State跨多次 invoke()
Store保存跨线程共享的长期资料跨对话、跨线程
Runtime Context向本次调用提供不可变上下文当前调用
SummarizationMiddleware压缩消息历史,控制上下文长度更新当前线程的 messages

一句话概括:

State         = Agent 当前正在使用的数据
Checkpointer  = 保存和恢复某个对话线程的 State
Store         = 跨对话保存用户的长期资料
Summarization = 压缩 messages,避免上下文不断增长

1. 没有 Checkpointer,也存在 State

State 是 Agent 执行时的数据容器。即使没有配置 Checkpointer,LangGraph 仍然需要在本次调用期间创建和维护 State。

result = agent.invoke({
    "messages": [
        {"role": "user", "content": "Look up user information"}
    ],
    "user_id": "user_123",
})

本次调用内部可以理解为存在下面的状态:

{
    "messages": [...],
    "user_id": "user_123",
}

工具因此可以通过 runtime.state 读取它:

@tool
def get_user_info(runtime: ToolRuntime) -> str:
    """Look up information about the current user."""
    user_id = runtime.state["user_id"]
    return f"Current user ID: {user_id}"

但这只证明 State 在当前运行中存在,并不表示它已经持久化。

State 负责运行
Checkpointer 负责保存

create_agent() 没有传入 Checkpointer 时,可以理解为:

agent = create_agent(
    model=model,
    tools=tools,
    checkpointer=None,
)

此时:

  • 单次 invoke() 中仍然可以读写 State;
  • 不需要提供 thread_id
  • 下一次独立调用不会自动继承上次 State;
  • Python 进程退出后不能恢复。

2. AgentState 是状态结构,不是数据库

AgentState 描述 Agent State 中有哪些字段。默认 Agent State 至少包含 messages,也可以添加业务字段:

from langchain.agents import AgentState


class CustomState(AgentState):
    user_id: str
    user_name: str
    request_count: int

创建 Agent 时注册该结构:

agent = create_agent(
    model=model,
    tools=tools,
    state_schema=CustomState,
)

需要区分:

state_schema = 定义状态的结构
checkpointer = 保存状态字段的值

CustomState 这个 Python 类型仍然位于应用代码中。数据库保存的是 messagesuser_idrequest_count 等字段在某个 checkpoint 中的值。进程重启时,应用需要重新创建相同结构的 Agent,然后再从 Checkpointer 恢复数据。

3. ToolRuntime 让工具读取当前 State

工具声明 runtime: ToolRuntime 后,LangChain 会在执行工具时自动注入 Runtime:

from langchain.tools import tool, ToolRuntime


@tool
def get_user_info(runtime: ToolRuntime) -> str:
    """Get information about the current user."""
    user_id = runtime.state["user_id"]
    return "User is John Smith" if user_id == "user_123" else "Unknown user"

模型看到的工具参数中不会出现 runtime,也不需要由模型生成 user_id。常用 Runtime 信息包括:

runtime.state          # 当前线程的 State
runtime.context        # 本次调用的不可变上下文
runtime.store          # 长期记忆 Store
runtime.tool_call_id   # 当前工具调用 ID
runtime.config         # RunnableConfig
runtime.stream_writer  # 自定义流式事件

4. 工具更新 State 为什么要返回 Command

普通工具返回值默认是“工具结果”,会被包装成 ToolMessage 交给模型,并不会自动成为 State 更新。

@tool
def set_name(name: str) -> dict:
    return {"user_name": name}

这里的字典是给模型读取的工具输出。要明确更新 Agent State,应返回 LangGraph 的 Command

from langgraph.types import Command


@tool
def set_name(name: str) -> Command:
    return Command(
        update={"user_name": name}
    )

如果后续模型还需要看到工具执行结果,可以同时写入 messages

from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime


@tool
def set_name(name: str, runtime: ToolRuntime) -> Command:
    """Set the current user's name."""
    return Command(
        update={
            "user_name": name,
            "messages": [
                ToolMessage(
                    content=f"Name set to {name}",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )

这样,user_name 会成为正式的 State channel write,经过 reducer 合并,并在配置 Checkpointer 时进入 checkpoint。

不同位置更新 State 的方式不同:

更新位置推荐方式
普通 LangGraph 节点return {"user_name": "Bob"}
Node 风格 Middlewarereturn {"user_name": "Bob"}
工具函数内部return Command(update={...})
图外部手动修改agent.update_state(config, {...})
State 更新并动态跳转Command(update={...}, goto="node")
从 interrupt 恢复Command(resume=...)

不要依赖直接修改:

runtime.state["user_name"] = name

这类修改未必会被识别为正式的 channel write,也无法可靠经过 reducer 和 checkpoint 机制。

5. State 字段、Channel 和 Reducer

LangGraph 可以把 State 的每个字段理解成一个 channel:

messages
user_id
user_name
request_count

节点返回 State 更新后,Reducer 决定新值如何与旧值合并。普通字段通常采用新值覆盖旧值;messages 则使用消息 reducer,把新消息加入消息历史。

自定义累加字段可以声明 Reducer:

import operator
from typing import Annotated


class CustomState(AgentState):
    request_count: Annotated[int, operator.add]

节点每次返回:

return {"request_count": 1}

最终结果会累加。Reducer 在并行节点或多个工具同时更新同一字段时尤其重要,否则 LangGraph 无法确定如何解决写入冲突。

6. Checkpointer 是线程级短期记忆

Checkpointer 按 thread_id 保存 checkpoint:

config = {
    "configurable": {
        "thread_id": "thread-001"
    }
}

相同 thread_id 表示继续同一段对话:

agent.invoke(
    {
        "messages": [
            {"role": "user", "content": "My name is Bob"}
        ]
    },
    config,
)

agent.invoke(
    {
        "messages": [
            {"role": "user", "content": "What is my name?"}
        ]
    },
    config,
)

换成另一个 thread_id 就会成为一个新的对话线程。

常见实现的区别:

Checkpointerinvoke()跨进程重启适用场景
None无状态调用
InMemorySaver学习、测试
SqliteSaver本地开发
PostgresSaver生产环境

7. PostgresSaver 的使用与恢复

from langgraph.checkpoint.postgres import PostgresSaver

DB_URI = "postgresql://postgres:password@localhost:5432/langgraph"

config = {
    "configurable": {
        "thread_id": "thread-001"
    }
}

with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    checkpointer.setup()

    agent = create_agent(
        model=model,
        tools=tools,
        state_schema=CustomState,
        checkpointer=checkpointer,
    )

    result = agent.invoke(
        {
            "messages": [
                {"role": "user", "content": "Hello"}
            ],
            "user_id": "user_123",
        },
        config,
    )

setup() 用于创建或升级数据库表,通常在首次部署或数据库迁移阶段执行,不需要在每个业务请求中重复执行。

进程退出后,只要满足下面几个条件,就可以恢复:

  • PostgreSQL 数据仍然存在;
  • 连接到同一个数据库;
  • 使用相同 thread_id
  • 应用重新创建兼容的 Agent 和 State schema;
  • 自定义 State 值能够被序列化和反序列化。

Checkpointer 恢复的是 LangGraph State 和执行信息,不会恢复 Python 局部变量、模型实例、数据库连接或未写入 State 的对象。

8. PostgreSQL 中保存了什么

PostgresSaver 主要使用下面几张表:

checkpoint_migrations
checkpoints
checkpoint_blobs
checkpoint_writes

checkpoints

保存 checkpoint 的整体结构,包括:

  • thread_id
  • checkpoint_id
  • 父 checkpoint;
  • channel 版本;
  • 图执行结构;
  • metadata。

checkpoint_blobs

保存各 State channel 的序列化值。messages 包含 HumanMessageAIMessageToolMessage 等复杂对象,通常会以二进制数据保存。

checkpoint_writes

保存节点已经产生、但尚未合并进下一个完整 checkpoint 的写入,包括:

  • 普通 State 字段更新;
  • 模型生成的 AIMessage
  • 工具生成的 ToolMessage
  • 节点错误;
  • interrupt 和 resume 数据;
  • 内部任务调度信息。

它对故障恢复非常重要。假设同一个 superstep 中有两个并行节点,一个成功、一个失败,LangGraph 可以保留成功节点的 pending writes,恢复时只重试失败节点。

9. Checkpointer 不等于长期记忆 Store

Checkpointer 保存的是某个对话线程的状态,Store 保存的是多个对话可以共享的长期资料。

user_123
├── thread_001
├── thread_002
└── thread_003

三个线程拥有各自的 checkpoint,但可以从 Store 读取同一位用户的长期偏好。

适合放进 Checkpointer 的数据:

  • 当前对话消息;
  • 当前任务进度;
  • 临时选择;
  • 工作流状态;
  • interrupt 状态。

适合放进 Store 的数据:

  • 用户长期偏好;
  • 用户档案;
  • 跨会话共享事实;
  • 可检索的语义记忆;
  • 多个 Agent 或多个线程共享的数据。

10. State、Context 和 Store 如何选择

稳定且不应该被 Agent 修改的身份信息,通常更适合放在 Runtime Context 中。例如登录用户 ID:

from dataclasses import dataclass


@dataclass
class AppContext:
    user_id: str

工具通过下面的方式读取:

@tool
def get_user_info(runtime: ToolRuntime[AppContext]) -> str:
    user_id = runtime.context.user_id
    return f"User ID: {user_id}"

可以按照下面的规则选择:

数据推荐位置
登录用户 ID、权限信息Context
当前对话消息State
当前任务进度State
Agent 可以修改的用户称呼State
跨会话用户偏好Store
数据库连接、服务客户端Runtime 或依赖注入

11. Summarization 是上下文压缩

SummarizationMiddleware 负责控制消息上下文长度:

SummarizationMiddleware(
    model=summarization_model,
    trigger=("tokens", 4000),
    keep=("messages", 20),
)

它的逻辑可以理解为:

消息达到触发阈值
将较早消息生成摘要
保留最近 20 条消息
使用摘要替换更早的消息

需要注意:

  • 中间件通常在下一次模型调用前检查阈值;
  • 消息数量不超过 keep 时,没有旧消息可供摘要;
  • 摘要会成为当前 messages State 的一部分;
  • 摘要属于当前 thread 的短期记忆;
  • 它不能代替跨线程的 Store。

调试当前 LangChain 实现时,可以检查摘要消息是否包含内部来源标记:

message.additional_kwargs.get("lc_source") == "summarization"

这个字段适合调试,但属于实现细节,不应作为核心业务协议。

12. 裁剪消息不等于删除数据库历史

从当前 State 删除旧消息,只表示后续模型不再接收这些消息。PostgresSaver 仍可能在历史 checkpoint 中保留原始数据。

因此,真正的数据删除还需要考虑:

  • 删除整个 thread;
  • 清理过期 checkpoint;
  • 设置数据保留策略;
  • 根据隐私要求删除关联的 blobs 和 writes。

上下文管理和数据库数据保留是两个不同问题。

13. 查看和调试 State

查看最新状态:

snapshot = agent.get_state(config)

print(snapshot.values)
print(snapshot.next)
print(snapshot.tasks)
print(snapshot.metadata)

查看所有字段:

for key, value in snapshot.values.items():
    print(key, value)

查看 State 历史:

for snapshot in agent.get_state_history(config):
    print(snapshot.config)
    print(snapshot.values)
    print(snapshot.next)

查看反序列化后的 pending writes:

checkpoint_tuple = checkpointer.get_tuple(config)

if checkpoint_tuple:
    for task_id, channel, value in checkpoint_tuple.pending_writes:
        print(task_id, channel, value)

不要只查询 PostgreSQL 的一张表来手动重建 State。完整状态可能分布在 checkpointscheckpoint_blobscheckpoint_writes 中,让 LangGraph 负责反序列化通常更可靠。

14. 最需要记住的十句话

  1. AgentState 是运行状态,Checkpointer 是状态持久化。
  2. 没有 Checkpointer,单次 invoke() 仍然有 State。
  3. create_agent() 不传 Checkpointer 时,默认没有跨调用记忆。
  4. 相同 thread_id 才会恢复相同对话。
  5. InMemorySaver 不能跨进程,PostgresSaver 可以。
  6. 工具通过 runtime.state 读取当前 State。
  7. 工具要更新 State,应返回 Command(update=...)
  8. 普通图节点返回字典即可更新 State。
  9. Checkpointer 是线程内记忆,Store 是跨线程长期记忆。
  10. Summarization 只是压缩消息上下文,不等于长期记忆。

参考资料