目录
[1. 为什么需要持久化能力](#1. 为什么需要持久化能力)
[LangGraph 持久化的两大支柱](#LangGraph 持久化的两大支柱)
[2. 线程级持久化(Checkpoint)](#2. 线程级持久化(Checkpoint))
[2.1 核心原理:快照机制](#2.1 核心原理:快照机制)
[2.2 核心概念:Threads 与 Checkpoints](#2.2 核心概念:Threads 与 Checkpoints)
状态快照结构与获取(get_state、get_state_history)
[2.3 生产级实战:从 InMemorySaver 到 PostgresSaver](#2.3 生产级实战:从 InMemorySaver 到 PostgresSaver)
[使用 PostgreSQL 作为 LangGraph 检查点存储库的实施步骤](#使用 PostgreSQL 作为 LangGraph 检查点存储库的实施步骤)
[3. 高级特性:重放与更新状态](#3. 高级特性:重放与更新状态)
[4.1. Checkpoint 的局限性](#4.1. Checkpoint 的局限性)
[4.2 Checkpoint + Store 的双轨并行](#4.2 Checkpoint + Store 的双轨并行)
[4.3 Store 数据模型深度剖析:Namespace-Key-Value](#4.3 Store 数据模型深度剖析:Namespace-Key-Value)
[4.4 生产级实战:从 InMemoryStore 到 PostgresStore](#4.4 生产级实战:从 InMemoryStore 到 PostgresStore)
[在 LangGraph 中使用 Store](#在 LangGraph 中使用 Store)
[4.5 PostgresStore 与 PostgresSaver](#4.5 PostgresStore 与 PostgresSaver)
[5. 实战](#5. 实战)
[6. 坑点](#6. 坑点)
1. 为什么需要持久化能力
状态机(State Machine)是一种计算模型:它描述一个对象在生命周期中,如何根据**"外部事件"** 的触发,在**"不同状态"** 之间进行**"迁移"** ,并执行相应的**"动作"**,它就像一份"游戏规则说明书":告诉系统"现在是什么情况"、"发生了什么事"、"下一步该怎么做"
在 LangGraph 中,工作流(Graph)本质上是状态机。持久化是将这个状态机的运行时快照固化到存储(内存、SQLite 或 PostgreSQL)的能力。它直击两个核心问题:
- 容灾与续跑:程序宕机或重启后,Agent 能从断点继续执行,而非从头开始
- 上下文连续性:在多轮对话中,系统自动加载之前的推理路径和工具调用结果
容灾(Disaster Recovery) 就是系统在遭遇崩溃、断电、进程被杀、网络中断等意外时,能够保全现场数据并在恢复后无缝续跑的能力
LangGraph 持久化的两大支柱
| 能力维度 | 核心机制 | 应用场景 |
|---|---|---|
| 线程级持久化 | Checkpointer 自动保存状态快照 | 维持单次会话的完整上下文 (如多轮对话、工具调用链路) |
| 跨会话持久化 | BaseStore 存储长期数据 | 存储用户画像、偏好设置、跨会话记忆 (如"用户对海鲜过敏") |
"线程持久化"和"操作系统线程"是两个完全独立的概念,具体解释如下:
- 操作系统线程是操作系统层面的概念,是进程内的实际执行单元,是操作系统进行CPU资源调度的最小单位,负责真正运行程序代码
- 线程级持久化是应用层面的概念,与操作系统线程无关。它指的是在聊天系统中,针对每一次独立的聊天会话,将其相关的持久化数据(如会话状态、上下文信息)进行存储和隔离。目的是区分不同用户的聊天会话,确保各会话之间的数据互不干扰。
这里的"状态快照"并非指之前学习过的 State(状态)的快照。
此处的"状态"包含的是运行过程中所需的全部上下文信息,例如:
- 已经调用过哪些工具
- 用户的输入内容
- 完整的聊天历史记录
- 当前流程中下一步要执行的节点信息等
这些信息共同构成了会话的完整"状态快照",用于在需要时恢复或继续该会话的执行
2. 线程级持久化(Checkpoint)
2.1 核心原理:检查点机制
- 当图开始执行时,LangGraph会在每一个超级步结束后自动形成一个检查点,即保存状态快照
- 超级步是指一批可以并行执行的节点集合

这种机制保证了状态与执行路径的解耦。即使下游节点报错,我们也能回滚到任意历史检查点,修改状态后分叉(Fork)执行,原路径保持不变,极大提升了系统的鲁棒性
2.2 核心概念:Threads 与 Checkpoints
- Thread(线程):代表一次独立的对话或任务执行单元,通过 thread_id 唯一标识,用于隔离不同会话间的状态数据
- Checkpoint(检查点):代表某个 Thread 在特定时刻的完整状态快照
一个 会话线程(Thread) 可以拥有多个 检查点(Checkpoints) ,这些检查点按时间顺序形成完整的执行历史。通过这一机制,同一个聊天会话的任意历史状态都可以从任何一个检查点进行追溯和访问,从而支持会话的回滚、恢复或分支等操作
状态快照结构与获取(get_state、get_state_history)
每次获取到的快照(StateSnapshot)包含了丰富的元数据,在生产环境 Debug 时极其重要:
StateSnapshot(
# ========== 1. 核心状态数据 ==========
values={'bar': []},
# 当前图的所有通道(Channel)值。
# 注意:这里只有 {'bar': []},说明 State 中只定义了 'bar' 字段。
# ========== 2. 控制流(下一步去哪儿) ==========
next=('__start__',),
# 当前优先级最高的、待执行的节点名称元组。注意,它是元组,支持并行节点(Send API)。
# '__start__' 是 LangGraph 内置的保留词,代表【图入口】。
# 只要 next 里有东西,调 graph.invoke(None, config) 就会从这里继续跑。
# ========== 3. 持久化寻址配置 ==========
config={'configurable': {
'thread_id': '1', # 会话/线程ID(续跑的唯一主键)
'checkpoint_ns': '', # 命名空间(用于子图隔离,空表示主图)
'checkpoint_id': '1f19076b-6002-6d17-bfff-69c5491efb69' # 当前快照的UUID
}},
# 这个 config 必须原样传给后续的 .invoke() 或 .astream(),LangGraph 靠它去数据库找状态。
# ========== 4. 快照来源元数据 ==========
metadata={'source': 'input', 'step': -1, 'parents': {}},
# source: 生成快照的触发源。
# - 'input' : 用户手动 invoke/astream 新输入(你这里就是初始创建)。
# - 'loop' : 节点内部正常流转产生的。
# - 'update': 手动调用 graph.update_state() 修改状态产生的。
# step: 图执行的步数。-1 表示【初始状态】(还未执行任何逻辑节点)。
# parents: 用于子图父级跟踪(常用于多图嵌套调试)。
# ========== 5. 时间戳 ==========
created_at='2026-08-05T02:38:15.920150+00:00',
# UTC 标准时间。续跑时若涉及实时推理,最好用这个时间判断状态是否"太旧"而需要干预。
# ========== 6. 父快照指针 ==========
parent_config=None,
# 指向此状态之前的 Checkpoint ID。如果为 None,说明这是根节点(初始状态)。
# 你在做"回滚"(replay)时,把这里的值赋给 config 的 checkpoint_id,就能回到过去。
# parent_config 指针的存在使得所有 Checkpoint 构成一个单向链表
# ========== 7. 当前待处理任务详情(最复杂核心) ==========
tasks=(PregelTask(
id='a8728d94-b7d4-22c9-a47c-dac10ad73d16',
name='__start__', # 任务对应的节点名
path=('__pregel_pull', '__start__'), # 内部调度路径(用于分布式追踪)
error=None, # 该任务执行是否报错(None 表示无异常)
interrupts=(), # 任务内部是否有中断(用于 HITL 人工介入)
state=None, # 该任务执行时的临时状态快照
result={'foo': ''} # 该任务节点执行返回的结果(注意:这里 foo 为空字符串)
),),
# ========== 8. 全局中断标志 ==========
interrupts=()
# 顶层图的全局中断列表(非任务级)。如果为空,表示图处于"可继续运行"状态。
)
| 维度 | get_state(config) |
get_state_history(config) |
|---|---|---|
| 核心功能 | 获取指定线程的当前(最新) 状态 | 获取指定线程的完整历史状态列表 |
| 返回内容 | 一个 StateSnapshot 对象,代表该线程最新的检查点 |
一个迭代器 ,按时间顺序(最新在前 )产生多个 StateSnapshot 对象 |
| 关键参数 | - config: 必须包含 thread_id - subgraphs: 是否包含子图状态 |
- config: 必须包含 thread_id - limit: 限制返回的快照数量 - before: 获取此检查点之前的历史 |
| 典型用途 | - 断点续跑 :恢复会话时,获取最新的状态以继续执行 - 外部状态查询:在图表执行外部,检查当前进度 | - 时间旅行(Time Travel) :浏览历史执行记录,找到特定的检查点 ID - 调试与审计:复盘 Agent 的执行路径和状态变化 |
| 对应操作 | "查看当前状态" | "查看历史快照" |
# 代码段 1:使用 get_state() 获取调用前后的状态快照
from langchain.messages import HumanMessage
config = {"configurable": {"thread_id": "1"}}
# 调用前的状态快照
snapshot = agent.get_state(config)
print(snapshot)
result1 = agent.invoke(
{"messages": [HumanMessage(content="你好")]},
config
)
# 调用后的状态快照
snapshot = agent.get_state(config)
print(snapshot)
#代码段 2:使用 get_state_history() 查看状态历史记录
from langchain.messages import HumanMessage
config = {"configurable": {"thread_id": "1"}}
result1 = agent.invoke(
{"messages": [HumanMessage(content="你好")]},
config
)
# 查看状态历史记录
history = list(agent.get_state_history(config))
print(history)
状态快照的存储顺序
| 维度 | 顺序方向 | 示例(按时间发生顺序) |
|---|---|---|
| 存储(写入时) | 过去 → 现在(正向追加) | 存储中:ID 1 → 2 → 3 → 4 |
| 内部指针(链) | 现在 → 过去(反向追溯) | 节点4 的父指针指向 3,3指向2 |
| API返回列表(查询读取时) | 现在 → 过去(逆向展示) | 你看到的数组:[4, 3, 2, 1] |
特殊场景下的顺序细节
- 并行执行(如Send API): Send API 并行分发的所有任务属于同一个超级步,同一超级步内的多个分支结果会被合并归约 后,统一生成一个检查点,而不是按到达先后依次写入多个检查点。不同超级步之间的检查点形成全局线性历史
- "重放"(Replay)时的顺序: 当你从某个旧的检查点恢复执行时,新产生的检查点ID会接续当前的最后一个ID,而不会插入到历史中间。例如:你从检查点_3恢复,执行完后生成的新检查点会是_5(假设之前已经有_4),而不会覆盖_4,这样历史记录就完整保留了"主干"和"分支重放"的路径
检查点的形成时机:基于图的状态变更触发
| 物理顺序 | 形成时机 | 核心特征 | 备注 |
|---|---|---|---|
| ① | 图执行初始化时(Invoke 入口) | 用户输入刚传入,尚未执行任何节点 ,next 指向 _start_ |
整个会话的"出生证明",必然存在 |
| ② | START 内置节点执行完成后 |
START 节点完成路由逻辑,确定了下一步去哪个业务节点,next 字段发生变更,触发持久化 |
START 作为内置任务,执行完成后必然落盘 |
| ③ | 每个业务节点(如 LLM、工具、代码)执行完成后 | 节点运行完毕并返回新的 State,系统自动写入检查点 | 这是最常规的触发点,每个业务节点对应一个检查点 |
| ⑤ | 图执行被中断(Human-in-the-loop)时 | 配置了 interrupt_before 或 interrupt_after,在等待用户输入确认时,保存当前挂起状态的快照 |
依赖检查点来实现"断点续传" |
2.3 生产级实战:从 InMemorySaver 到 PostgresSaver
-
**开发环境:**InMemorySaver
-
最轻量的方式,适合单元测试。注意:进程重启即丢失
==================== 1. 导入内存检查点适配器 ====================
from langgraph.checkpoint.memory import InMemorySaver
InMemorySaver 是 BaseCheckpointSaver 接口的内存实现类。
内部核心数据结构为一个嵌套字典:_storage = {thread_id: {namespace: {checkpoint_id: Checkpoint对象}}}
仅存储于当前 Python 进程的堆内存中,进程终止则数据全失。
适用于:单元测试、本地调试、单线程演示
==================== 2. 实例化存储引擎句柄 ====================
checkpointer = InMemorySaver()
此时仅创建一个空仓库管理员对象,内部 _storage 字典为空。
该对象暴露了标准接口:put()(存快照)、get()(取快照)、put_writes()(存中间任务结果)。
注意:此时还没有绑定任何图,也不涉及任何网络 I/O 或磁盘操作。
==================== 3. 编译图并注入检查点(核心"魔法"发生地) ====================
graph = builder.compile(checkpointer=checkpointer)
compile() 方法一旦传入 checkpointer 参数,Pregel 执行引擎会做 3 件不可逆的底层改造:
① 【引擎绑定】将 checkpointer 实例挂载到 Pregel 对象的 self.checkpointer 属性上。
编译后的 graph 对象(实际是 Pregel 子类实例)从此持有了该存储句柄的强引用。
② 【循环钩子注入】重写 Pregel.run()(即 invoke/astream 的底层循环),强制插入 3 个生命周期钩子:
- 入口钩子(tick 前):执行 self.checkpointer.get() 检索该 thread_id 的历史状态,
实现"续跑时即使传 None 也能恢复现场"。
- 超级步完成钩子(节点返回后):调用 self.checkpointer.put() 序列化当前全量 State 并落盘。
- 任务暂存钩子(节点返回但尚未归约时):调用 self.checkpointer.put_writes() 将节点的原始返回值暂存至 tasks.result 中,用于"两阶段提交"(先存结果,下一拍再执行 Reducer 归约)。
③ 【时间旅行 API 激活】为 graph 动态注入了 .get_state(config) 和 .get_state_history(config) 方法。
若 compile 时不传 checkpointer,调用这两个方法会直接抛出异常(因为没有任何存储层可查询)。
-
**生产环境:**PostgresSaver(强烈推荐)
-
生产环境必须使用持久化数据库
PostgreSQL 配合 langgraph-checkpoint-postgres 提供了事务性保证和卓越的查询性能
在众多持久化存储方案中,PostgreSQL 作为关系型数据库的佼佼者(PostgreSQL 和 MySQL 一样,都是最流行的开源关系型数据库),具备以下显著优势,使其成为持久化的理想选择:
- **LangGraph 原生支持:**LangGraph提供了PostgresSaver,简化了与PostgreSQL的集成过程
- **数据结构化与一致性:**关系型数据库天生适合存储结构化数据。Graph的状态,尤其是消息历史、用户档案、工具使用记录等,都可很好地映射到表格结构中,确保数据的一致性和完整性
- **可靠性与持久性:**PostgreSQL 提供了事务支持、ACID 特性、数据备份与恢复机制,确保数据的持久性和高可用性,即使系统崩溃也能保证数据不丢失
- **强大的查询能力:**SQL 语言提供了灵活且强大的数据查询能力,方便我们对历史行为、用户数据进行分析、统计和审计。结合 pgvector 等扩展,甚至可以直接在数据库中进行向量相似度搜索,实现更高级的知识管理
- **可扩展性:**通过读写分离、分区、集群等技术,PostgreSQL 可以支持大规模的并发访问和数据存储,满足 Agent 在生产环境中的性能需求
- **成熟的生态系统:**拥有庞大的社区支持、丰富的工具和成熟的运维经验,降低了开发维护成本
快速搭建(Docker):
# 1. 拉取 PostgreSQL 官方最新版镜像
docker pull postgres:latest
# 2. 运行 PostgreSQL 容器
# -p 5432:5432 : 将容器的 5432 端口映射到宿主机的 5432 端口(本地连接入口)
# -e POSTGRES_PASSWORD=dfq : 设置超级管理员 postgres 用户的密码为 dfq
# --name postgres-sql : 给容器命名为 postgres-sql(方便后续 stop/start)
# -d : 后台(detach)模式运行,不占用当前终端
docker run --name postgres-sql -e POSTGRES_PASSWORD=dfq -p 5432:5432 -d postgres

使用 PostgreSQL 作为 LangGraph 检查点存储库的实施步骤
-
第 1 步:安装依赖包,安装 psycopg(PostgreSQL 适配器)、langgraph 核心库以及官方提供的 langgraph-checkpoint-postgres 扩展包
pip install -U "psycopg[binary,pool]" langgraph langgraph-checkpoint-postgres
-
第 2 步:配置数据库连接字符串(URI),按照 PostgreSQL 的标准格式拼接连接信息,用于指定数据库地址、端口、账号和库名
格式:postgresql://<用户名>:<密码>@<主机地址>:<端口>/<数据库名>
示例:postgresql://postgres:your_password@localhost:5432/postgres -
第 3 步:初始化检查点存储实例并编译图,利用 PostgresSaver.from_conn_string() 创建存储句柄,并通过上下文管理器(with 语句)管理连接
-
首次使用该数据库作为检查点存储时,必须调用 checkpointer.setup() 方法,它会自动在数据库中创建 checkpoints 和 writes 两张核心表(该操作具有幂等性,重复执行不会报错)
from langgraph.checkpoint.postgres import PostgresSaver
DB_URI = "postgresql://postgres:your_password@localhost:5432/postgres"
使用 with 上下文管理连接生命周期
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
# 首次使用必须执行建表(后续运行可注释或保留,不会重复创建)
checkpointer.setup()# 编译图(将内存版 checkpointer 替换为 Postgres 版) agent = agent_builder.compile(checkpointer=checkpointer) # 后续正常调用 invoke / astream... # agent.invoke(...)
from_conn_string\setup
| 对比维度 | from_conn_string |
setup |
|---|---|---|
| 功能定位 | 创建一个PostgresSaver实例的工厂方法 |
初始化数据库环境的实例方法 |
| 核心作用 | 根据提供的连接字符串,建立与PostgreSQL数据库的连接,并返回一个可用于后续操作的PostgresSaver对象 |
在数据库中创建 LangGraph 运行所需的全部表结构 (如 checkpoints, checkpoint_writes 等)并执行必要的数据库迁移 |
| 参数 | conn_string (str, 必需): PostgreSQL 连接字符串,格式为 postgresql://user:password@host:port/database。 pipeline (bool, 可选): 是否使用 Pipeline 模式,默认为 False |
无参数 |
| 返回值 | 一个配置好的 PostgresSaver 实例对象 |
None |
| 典型使用场景 | 在程序启动时,用于创建检查点管理器 | 仅在首次使用该PostgreSQL数据库作为检查点存储器时调用,用于建表和初始化 |
| 关注点 | 说明与建议 |
|---|---|
setup() 的执行时机 |
该方法涉及 DDL 建表操作。如果在生产环境使用高权限数据库账号,建议在部署脚本 或容器初始化阶段单独执行一次,不要混在业务请求的代码路径里(避免不必要的连接开销) |
| 连接池管理 | 示例中使用 with 是短连接模式。生产环境 建议独立管理连接池(例如 psycopg_pool.ConnectionPool),避免每次请求都重新建立 TCP 连接,造成性能瓶颈 |
| 异步支持 | 如果你的代码是异步的(使用 astream/ainvoke),请使用 AsyncPostgresSaver 并配合 async with 和 await checkpointer.asetup() |

3. 高级特性:重放与更新状态
-
重放: 获取历史上的某个 Checkpoint,并重新执行该节点之后的步骤
-
这对于复现 Bug 或回放推理链极其有用
from langchain.messages import HumanMessage
config = {"configurable": {"thread_id": "1"}}
第一次执行
result1 = agent.invoke(
{"messages": [HumanMessage(content="今天西安的天气如何?")]},
config
)保存调用工具前的状态
print("-" * 80)
print("第一次执行历史:")
to_replay = None
for state in agent.get_state_history(config):
print(
"checkpoint_id: ", state.config["configurable"]["checkpoint_id"],
"消息数: ", len(state.values["messages"]),
"下一节点: ", state.next
)
if len(state.values["messages"]) == 2: # 保存调用工具前的状态
to_replay = stateprint("-" * 80)
print(f"从 {to_replay.next} 节点开始重新执行,重放配置:{to_replay.config}")第二次执行:重放
传入快照配置,invoke(None) 表示不追加新输入,仅重放
result2 = agent.invoke(None, config=to_replay.config)
print("-" * 80)
print("第二次执行历史:重放后")查看新的历史记录
for state in agent.get_state_history(config):
print(
"checkpoint_id: ", state.config["configurable"]["checkpoint_id"],
"消息数: ", len(state.values["messages"]),
"下一节点: ", state.next
)result2['messages'][-1].pretty_print()
-
**更新状态:**直接编辑历史快照的状态
-
例如,用户发现之前提问的"西安"打错了,实际想问"北京"。我们无需重头开始,只需在检查点处修改输入消息,使用 Overwrite 标签替换整个消息列表:
update_state() 方法详解
| 属性 | 详细说明 |
|---|---|
| 核心功能 | 手动更新图的状态,常用于修正错误、实现人机协同或创建新的执行分支 |
| 关键前提 | 编译图时必须 配置了 checkpointer(检查点保存器) |
| 方法签名 | update_state(config, values, as_node=None) |
config |
必需 。包含 thread_id 的配置对象,用于指定要修改哪个会话的状态 |
values |
必需。要更新到状态中的新值,可以是字典或字典序列 |
as_node |
可选。指定此次更新"假装"是由哪个节点执行的。如果未提供,系统会尝试自动推断 |
| 返回值 | 返回一个包含新检查点信息的响应对象 |
| 典型用途 | 1. 修正错误 :在HITL流程中,人工审核并修改工具调用参数 2. 状态注入 :在图执行中断时,手动注入外部信息 3. 时间旅行分支:修改历史状态,创造新的执行路径 |
from langchain.messages import HumanMessage
from langgraph.types import Overwrite
config = {"configurable": {"thread_id": "1"}}
# 第一次执行
result1 = agent.invoke(
{"messages": [HumanMessage(content="今天西安的天气如何?")]},
config
)
# 找到调用LLM前的步骤
print("-" * 80)
print(f"第一次执行历史:")
selected_state = None
for state in agent.get_state_history(config):
print("checkpoint_id: ", state.config["configurable"]["checkpoint_id"],
"消息数: ", len(state.values["messages"]),
"下一节点: ", state.next)
if len(state.values["messages"]) == 1: # 此时消息数为1;下一节点是'llm_call'
selected_state = state
print("-" * 80)
print(f"更新前配置:{selected_state.config}")
# 根据指定的config,更新对应步骤的值
# 更新用户输入
new_config = agent.update_state(
selected_state.config,
{"messages": Overwrite([HumanMessage(content="今天北京的天气如何?")])} # 清空消息,重新写入
)
print("-" * 80)
print(f"更新后配置:{new_config}")
# 第二次执行:更新后的配置
result2 = agent.invoke(None, config=new_config)
for message in result2['messages']:
message.pretty_print()
4.跨会话持久化(Store)
4.1. Checkpoint 的局限性
Checkpoint严格绑定 thread_id,thread_1 和 thread_2 在Checkpointer眼中是完全陌生的两个世界
业务痛点直击:
- 周一用户说:"我爱吃汉堡。"
- 周二用户新开对话问:"我爱吃什么?"
- 没有 Store 的 Agent 只能尴尬回复:"我不知道你具体喜欢吃什么..."
在现实场景中,这意味着:
- 智能客服无法识别上个月刚投诉过、且是 VIP 的客户。
- 健康助手不记得用户有高血压病史,导致每次建议都从零询问。
结论:Checkpoint 解决的是 "过程记忆"(How to do),而无法解决 "事实记忆"(Who/What)
4.2 Checkpoint + Store 的双轨并行
LangGraph 将持久化清晰地拆解为两个正交维度:
| 维度 | Checkpointer(检查点) | Store(存储) |
|---|---|---|
| 核心职责 | 保存执行状态(State) | 保存结构化知识(Knowledge) |
| 数据性质 | 短时/上下文(Session 级) | 长时/事实(User 生命周期级) |
| 数据结构 | 线性历史链表(时间线) | 键值/文档数据库(命名空间) |
| 隔离粒度 | thread_id(会话隔离) |
namespace(业务逻辑划分,如 user_id) |
在 LangGraph 的执行引擎中,两者是两条并行注入的"管线":
- Checkpointer 附着在图的编译期,负责自动快照
- Store 作为上下文对象,由开发者在节点中手动调用,用于读写长期数据
回顾整个 LangGraph 持久化体系,我们完成了三次认知跃迁:
| 阶段 | 特性 |
|---|---|
| 无状态 | 每次对话如初见 |
| 线程级持久化(Checkpoint) | 单次会话内拥有流畅的上下文,扛住程序崩溃 |
| 跨会话持久化(Checkpoint + Store) | 长时记忆:不仅能记住"过程",还能记住"身份"与"偏好" |
LangGraph 的持久化不仅仅是"存数据",而是一套基于不可变状态链 的版本控制系统。它赋予了 Agent 容错性 、可调试性 (时间旅行)和可编辑性(状态分叉)
4.3 Store 数据模型深度剖析:Namespace-Key-Value
Store 是一种键值存储模式,用于在多个独立会话(Thread)之间共享数据
LangGraph 本身不提供 Store 实现,开发者需自行集成 PostgreSQL 等外部存储(见下文)
它的核心设计理念是:
- 命名空间(Namespace):用于组织和隔离数据,通常按用户 ID + 业务类型划分
- 键值对(Key-Value):每条记忆由唯一的 memory_id 标识
- 语义搜索:支持基于向量嵌入的相似性检索
命名空间(Namespace)的设计
# 推荐:使用元组,层次清晰,易于扩展
namespace_user = ("user_123",) # 用户级
namespace_info = ("user_123", "info") # 用户信息
namespace_pref = ("user_123", "preferences") # 用户偏好
namespace_conv = ("user_123", "conversations", "2025-05") # 历史对话
# 不推荐:扁平字符串,难以解析和扩展
namespace_bad = "user_123_preferences_food"
推荐元组是因为元组
- 支持前缀搜索:store.search(("user_123",)) 返回该用户所有命名空间下的数据
- 支持分层管理:可按 ("user_123", "preferences") 精确查找
- 避免字符串解析带来的错误和性能开销
4.4 生产级实战:从 InMemoryStore 到 PostgresStore
| 接口 | 作用 | 关键参数与说明 | 返回值 |
|---|---|---|---|
uuid.uuid4 |
生成一个通用唯一标识符(UUID) ,用于 Store 中每条记忆的 key,确保唯一性 |
无参数 基于随机数生成,重复率极低 | 类型: UUID 对象 说明: 可通过 str() 转为字符串格式(如 "db826e33-c68c-4669-a79a-3579bff02ff1")。 |
store.put |
向指定的命名空间 (namespace) 存入或更新一条键值对记忆 |
- namespace (tuple): 命名空间,如 ("user_123", "prefer"),用于数据隔离 - key (str): 记忆的唯一标识,建议用 uuid4() 生成 - value (dict): 存储的具体内容 - index (bool, 可选): 是否创建向量索引,默认 True(需配置嵌入模型) - ttl (int, 可选): 存活时间(分钟),到期后自动删除 |
类型: None 说明: 执行成功则完成写入/更新;执行失败会抛出异常(如连接断开、键冲突等)。 |
store.search |
在指定的命名空间前缀下搜索记忆,支持前缀匹配 和语义搜索 | - namespace_prefix (tuple): 搜索的命名空间前缀,如 ("user_123",) 匹配该用户下所有数据 - query (str, 可选): 自然语言查询,启用语义搜索(需预先配置嵌入模型) - filter (dict, 可选): 键值对过滤,仅返回匹配的记忆 - limit (int, 可选): 返回数量上限,默认 10 - offset (int, 可选): 分页偏移量,用于翻页 |
类型: List[Item] 说明: 返回 Item 对象列表。每个 Item 包含: - key (str): 记忆的唯一标识 - value (dict): 存储的具体内容 - namespace (tuple): 所属命名空间 - created_at (datetime): 创建时间(UTC) - updated_at (datetime): 最后更新时间(UTC) - score (float 或 None): 向量相似度分数,仅当传入 query 时返回(值越高越相关);普通搜索为 None。 |
InMemoryStore 是 Store 的内存实现,适合开发和测试阶段使用
from langgraph.store.memory import InMemoryStore
import uuid
# 1. 创建存储实例
store = InMemoryStore()
# 2. 定义命名空间
user_id = "user_123"
namespace = (user_id, "preferences")
# 3. 存入记忆
memory_id = str(uuid.uuid4())
memory_value = {"favorite_food": "汉堡", "allergy": "花粉"}
store.put(namespace, memory_id, memory_value)
print("记忆已存入!")
# 4. 搜索记忆
all_memories = store.search(namespace)
for mem in all_memories:
print(mem.dict())
生产环境推荐 PostgresStore
# 安装必要的包
pip install "psycopg[binary,pool]" langgraph
# 同步导入
from langgraph.store.postgres import PostgresStore
# 异步导入
from langgraph.store.postgres import AsyncPostgresStore
| 接口 | 同步方法 | 异步方法 | 说明 |
|---|---|---|---|
| 创建实例 | PostgresStore.from_conn_string() |
AsyncPostgresStore.from_conn_string() |
从连接字符串创建 Store 实例 |
| 初始化建表 | store.setup() |
await store.setup() |
首次使用必须调用,创建表和运行迁移 |
| 存入数据 | store.put(namespace, key, value) |
await store.aput(namespace, key, value) |
存入或更新一条记忆 |
| 获取单条 | store.get(namespace, key) |
await store.aget(namespace, key) |
根据命名空间和键获取单条记忆 |
| 搜索多条 | store.search(namespace_prefix) |
await store.asearch(namespace_prefix) |
搜索命名空间下的多条记忆,支持语义搜索 |
| TTL清理 | store.start_ttl_sweeper() |
await store.start_ttl_sweeper() |
启动后台线程清理过期条目 |
-
同步使用实例
3.1 基础用法
from langgraph.store.postgres import PostgresStoreDB_URI = "postgresql://postgres:dfq@xxx:5432/postgres"
方式一:使用 from_conn_string 配合上下文管理器
with PostgresStore.from_conn_string(DB_URI) as store:
# 【重要】首次使用必须调用 setup() 创建表
store.setup()# 存入数据:namespace 用元组组织,key 用 uuid 保证唯一性 store.put( namespace=("user_123", "preferences"), key="food_pref_001", value={"favourite_food": "汉堡", "allergy": "花粉"} ) # 获取单条数据 item = store.get(("user_123", "preferences"), "food_pref_001") print(item.value) # {'favourite_food': '汉堡', 'allergy': '花粉'} # 搜索命名空间下的所有记忆 results = store.search(("user_123", "preferences")) for mem in results: print(f"Key: {mem.key}, Value: {mem.value}")3.2 启用向量语义搜索
from langchain.embeddings import init_embeddings
from langgraph.store.postgres import PostgresStoreDB_URI = "postgresql://postgres:dfq@xxx:5432/postgres"
with PostgresStore.from_conn_string(
DB_URI,
index={
"dims": 1536, # 向量维度,需与 embedding 模型匹配
"embed": init_embeddings("openai:text-embedding-3-small"),
"fields": ["text"] # 指定对 value 中的哪些字段做嵌入
}
) as store:
store.setup()# 存入文档(会自动对 text 字段生成向量索引) store.put(("docs",), "doc1", {"text": "Python 编程教程"}) store.put(("docs",), "doc2", {"text": "TypeScript 开发指南"}) store.put(("docs",), "doc3", {"text": "其他无关内容"}, index=False) # 跳过索引 # 语义搜索:用自然语言查找最相关的记忆 results = store.search( namespace_prefix=("docs",), query="编程学习资料", limit=2 ) for mem in results: print(f"相似度分数: {mem.score}, 内容: {mem.value}")
注意:语义搜索默认是关闭的,必须通过 index 参数显式配置才能启用。使用向量搜索前,需确保 PostgreSQL 已安装 pgvector 扩展
- 异步使用实例
异步版本适用于高并发场景(如 FastAPI、WebSocket 服务)
from langgraph.store.postgres import AsyncPostgresStore, PoolConfig
DB_URI = "postgresql://postgres:dfq@xxx:5432/postgres"
# 基础异步用法
async with AsyncPostgresStore.from_conn_string(DB_URI) as store:
await store.setup()
# 异步存入
await store.aput(("users", "123"), "prefs", {"theme": "dark"})
# 异步获取
item = await store.aget(("users", "123"), "prefs")
# 异步搜索(带向量)
results = await store.asearch(
("docs",),
query="programming guides",
limit=2
)
4.1 使用连接池(生产环境推荐)
from langgraph.store.postgres import AsyncPostgresStore, PoolConfig
DB_URI = "postgresql://postgres:dfq@xxx:5432/postgres"
async with AsyncPostgresStore.from_conn_string(
DB_URI,
pool_config=PoolConfig(
min_size=5, # 最小连接数
max_size=20 # 最大连接数[reference:14]
)
) as store:
await store.setup()
# ... 业务操作
4.2 启用 TTL(自动过期清理)
from langgraph.store.postgres import AsyncPostgresStore
from langgraph.store.base import TTLConfig
DB_URI = "postgresql://postgres:dfq@115.159.125.195:5432/postgres"
async with AsyncPostgresStore.from_conn_string(
DB_URI,
ttl=TTLConfig(ttl=60) # 60分钟后自动删除[reference:15]
) as store:
await store.setup()
# 启动后台清理任务
await store.start_ttl_sweeper()
# 存入带 TTL 的数据
await store.aput(("temp",), "session_001", {"data": "临时数据"})
# ... 业务操作
# 停止清理任务
await store.stop_ttl_sweeper() # [reference:16]
在 LangGraph 中使用 Store
要在 Graph 中使用 Store,只需在编译时传入 store 参数:
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
agent = builder.compile(checkpointer=checkpointer, store=store)
任何节点函数,如果需要访问 Store,可以通过在参数中声明 store: BaseStore 和 config: RunnableConfig 来获取:
# ==================== 1. 导入依赖抽象层(不是具体实现) ====================
from langgraph.store.base import BaseStore
# 导入 Store 的抽象基类(Abstract Base Class)。
# 注意:这里只导入"接口/协议",而不导入 InMemoryStore 或 PostgresStore。
# 目的:让当前节点函数只依赖抽象,不依赖具体实现。
# LangGraph 引擎会在运行时,将编译时传入的具体实例(如 PostgresStore)注入进来。
from langchain_core.runnables import RunnableConfig
# 导入 LangChain 的运行配置类型。
# 这个 config 对象是 LangGraph 执行上下文的标准载体,包含了:
# - thread_id: 会话线程 ID(给 Checkpointer 用)
# - user_id: 用户 ID(给 Store 用)
# - checkpoint_id: 当前快照 ID(时间旅行用)
# - 以及任何你自定义的 configurable 字段
# ==================== 2. 定义节点函数(注意函数签名的特殊写法) ====================
def my_node(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
# 函数签名的构成要素(按顺序):
# 1. state (位置参数): 当前图的状态对象,LangGraph 强制第一个参数必须接收 State。
# 2. config (位置参数): 运行配置,LangGraph 自动识别 RunnableConfig 类型并注入。
# 3. * (强制关键字分隔符): 这是 Python 的语法,表示 * 后面的所有参数都必须以关键字形式传递。
# 4. store (关键字参数): LangGraph 看到 BaseStore 类型注解,会自动从编译上下文中找到对应的实例注入。
# ==================== 3. 从配置中提取业务身份标识 ====================
user_id = config["configurable"]["user_id"]
# config 是一个字典(或类似字典的对象),内部有一个保留字段 "configurable"。
# 这个字段完全由开发者(你)在调用时定义:
# config = {"configurable": {"thread_id": "1", "user_id": "123"}}
#
# 【设计哲学】:
# - thread_id: 属于"运行态"标识,决定从哪个 Checkpoint 恢复状态。
# - user_id: 属于"业务态"标识,决定从哪个 Store 命名空间读取长期记忆。
# 强烈建议将 thread_id 和 user_id 分离,这样同一个 user_id 跨多个 thread_id 时,
# Store 的数据依然能共享。
# ==================== 4. 构造命名空间(数据隔离的"文件夹") ====================
namespace = (user_id, "info")
# ==================== 5. 调用 Store 接口(完全面向抽象编程) ====================
memories = store.search(namespace)
# ==================== 6. 后续业务逻辑(省略) ====================
除了 store,在节点函数中你还可以声明以下参数(LangGraph 会自动识别并注入):
| 参数名 | 类型 | 用途 |
|---|---|---|
state |
State 类型 | 当前节点的状态(必须存在,且放第一位) |
config |
RunnableConfig |
包含 thread_id、user_id 等运行时配置 |
store |
BaseStore |
跨会话的长期存储库 |
writer |
StreamWriter |
用于流式输出中间结果(给前端推送进度) |
reader |
StreamReader |
用于读取其他任务流(高级并行场景) |
4.5 PostgresStore 与 PostgresSaver
InMemoryStore 重启即失。langgraph-checkpoint-postgres 库同时提供了 PostgresStore,与 PostgresSaver 共享同一连接字符串,自动创建相关数据表,并通过事务保证读写一致性
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore
from langgraph.graph import StateGraph
DB_URI = "postgresql://postgres:dfq@xxx:5432/postgres"
with (
PostgresSaver.from_conn_string(DB_URI) as checkpointer,
PostgresStore.from_conn_string(DB_URI) as store,
):
# 首次使用需初始化(幂等操作)
checkpointer.setup()
store.setup()
# 编译图时同时传入
agent = builder.compile(checkpointer=checkpointer, store=store)
# 在节点函数中通过依赖注入使用 store
# def my_node(state, config, *, store: BaseStore):
# store.put(("user_123", "info"), "name", {"name": "李华"})
- "共享同一连接字符串" 意味着:它们操作同一个数据库,只是存储在不同的表里,不需要开两个数据库连接,节省资源
- 当你首次调用时,PostgresSaver 和 PostgresStore 会自动在数据库中创建所需的表结构,无需手动执行 SQL
- PostgresSaver 创建的表:checkpoints、checkpoint_writes、checkpoint_blobs 等
- PostgresStore 创建的表:store、store_embeddings 等
- 事务保证读写一致性:要么一起成功,要么一起失败(原子性)
- 不会出现:状态保存了但记忆没存上,或反之
5. 实战
改造工作流图
在 llm_call(LLM 决策)之前插入 get_person_by_llm(信息提取)节点
每次工具调用返回后,也重新进入该节点提取最新信息,形成完美的信息循环

核心代码实现
第一步:定义结构化提取器
利用 Pydantic 强制约束 LLM 提取的字段,确保存入 Store 的数据干净整洁
class Person(BaseModel):
name: Optional[str] = Field(default=None, description="这个人的名字")
favourite_food: Optional[list[str]] = Field(default=None, description="最喜欢的食物列表")
model_with_structured = model.with_structured_output(Person)
第二步:提取节点------读取上下文,写入 Store
节点函数通过声明 store: BaseStore,LangGraph 的运行时会自动将全局 Store 注入
def get_person_by_llm(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
# 1. 从最近消息中提取信息
people_info = model_with_structured.invoke(
[SystemMessage(content="提取我的信息,不知道则返回null")] + state["messages"][-3:]
)
# 2. 获取 user_id(与 thread_id 解耦)
user_id = config["configurable"]["user_id"]
# 3. 写入 Store(生产环境建议先 search 后 put,避免冗余)
namespace = (user_id, "profile")
# 1. 搜索现有记录
existing = store.search(namespace, limit=1)
if existing:
# 2. 存在则更新
memory_id = existing[0].key
store.put(namespace, memory_id, new_info)
else:
# 3. 不存在则新增
store.put(namespace, str(uuid.uuid4()), new_info)
return {"llm_calls": state.get('llm_calls', 0) + 1}
第三步:决策节点------读取 Store,增强 Prompt
在调用 LLM 之前,从 Store 中捞出该用户的记忆,动态拼接到 System Prompt 中
def llm_call(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
user_id = config["configurable"]["user_id"]
memories = store.search((user_id, "profile"), limit=1)
memory_context = memories[0].value if memories else "无历史记录"
return {
"messages": [
model_with_tools.invoke([
SystemMessage(content=f"用户档案:{memory_context}。请据此提供个性化服务。")
] + state["messages"])
]
}
第四步:编译与验证
agent = builder.compile(checkpointer=PostgresSaver(), store=InMemoryStore())
# 周一:thread_1, user_1
agent.invoke({"messages": "我叫李华,爱吃汉堡"}, config={"thread_id": "1", "user_id": "1"})
# 周二:thread_2, user_1(新会话,但 user_id 相同)
agent.invoke({"messages": "推荐餐厅"}, config={"thread_id": "2", "user_id": "1"})
# 输出:直接推荐汉堡店,且记得用户叫李华!
最终代码
import uuid
from typing import Optional
from langchain.chat_models import init_chat_model
from langchain.messages import AnyMessage, HumanMessage, SystemMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from langchain_tavily import TavilySearch
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
from pydantic import BaseModel, Field
from typing_extensions import TypedDict, Annotated
import operator
# ============================================================
# 步骤 1: 定义工具和模型
# ============================================================
search = TavilySearch(max_results=4)
tools = [search]
model = init_chat_model("deepseek-v4-flash", temperature=0)
model_with_tools = model.bind_tools(tools)
# ============================================================
# 步骤 2: 定义状态
# ============================================================
class MessagesState(TypedDict):
# 类型: list[AnyMessage] - 任意消息对象的列表
# 合并策略: operator.add - 使用加法操作符进行状态合并
# 效果: 当状态更新时,新的消息会追加到现有列表中,而不是替换
messages: Annotated[list[AnyMessage], operator.add]
# 类型: int - 整数值
# 用途: 跟踪 LLM(大语言模型)的调用次数
llm_calls: int
# ============================================================
# 步骤 3: 新增提取信息节点
# ============================================================
class Person(BaseModel):
"""一个人的信息。"""
# 注意:
# 1. 每个字段都是 Optional "可选的" ------ 允许 LLM 在不知道答案时输出 None。
# 2. 每个字段都有一个 description "描述" ------ LLM 使用这个描述。
name: Optional[str] = Field(default=None, description="这个人的名字")
height_in_meters: Optional[str] = Field(default=None, description="以米为单位的高度")
favourite_food: Optional[list[str]] = Field(default=None, description="最喜欢的食物列表")
model_with_structured = model.with_structured_output(Person)
def get_person_by_llm(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
"""通过 LLM 提取用户信息"""
# 1. 先提取
people_info = model_with_structured.invoke(
[
SystemMessage(
content="你是一个提取信息的专家,只从文本中提取我的相关信息,不能提取别人的信息。"
"如果你不知道要提取的属性的值,属性值返回 null。"
)
]
+ state["messages"][-3:] # 只查看最近 3 条消息
)
# 2. 再保存
user_id = config["configurable"]["user_id"]
# 保存用户基本信息
namespace1 = (user_id, "info")
# 每次 put 前应判断是否存在,再更新。否则会有多条记录被记录。这里简写
store.put(
namespace1,
str(uuid.uuid4()),
{
"name": people_info.name,
"height": people_info.height_in_meters
}
)
# 保存用户偏好
namespace2 = (user_id, "preferences")
store.put(
namespace2,
str(uuid.uuid4()),
{"favourite_food": people_info.favourite_food} # 省略追加逻辑:先搜再更新
)
return {
"llm_calls": state.get('llm_calls', 0) + 1
}
# ============================================================
# 步骤 4: 更新模型调用节点:添加共享用户信息到提示词
# ============================================================
def llm_call(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
"""LLM 决定是否调用工具"""
# 搜索用户信息
user_id = config["configurable"]["user_id"]
namespace1 = (user_id, "info")
namespace2 = (user_id, "preferences")
info_result = store.search(namespace1)
pref_result = store.search(namespace2)
# 构建增强的 System Prompt
system_prompt = (
f"你是一个乐于助人的助手,支持调用工具进行搜索。"
f"查询 LLM 前可参考以下信息:"
f"1. 用户基本情况:{info_result[0].value if info_result else '未知'}"
f"2. 用户偏好情况:{pref_result[0].value if pref_result else '未知'}"
)
return {
"messages": [
model_with_tools.invoke(
[SystemMessage(content=system_prompt)]
+ state["messages"]
)
],
"llm_calls": state.get('llm_calls', 0) + 1
}
# ============================================================
# 步骤 5: 定义工具节点
# ============================================================
tools_by_name = {tool.name: tool for tool in tools}
def tool_node(state: dict):
"""执行工具调用"""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(
ToolMessage(
content=observation,
tool_call_id=tool_call["id"]
)
)
return {"messages": result}
# ============================================================
# 步骤 6: 构建图
# ============================================================
def should_continue(state: MessagesState):
"""根据 LLM 是否调用工具来决定是继续循环(路由到工具节点)还是停止循环(END)"""
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls:
return "tool_node"
return END
# 添加节点并设置边
agent_builder = StateGraph(MessagesState)
agent_builder.add_node(llm_call)
agent_builder.add_node(tool_node)
agent_builder.add_node(get_person_by_llm)
agent_builder.add_edge(START, "get_person_by_llm")
agent_builder.add_edge("get_person_by_llm", "llm_call")
agent_builder.add_conditional_edges(
"llm_call",
should_continue,
["tool_node", END]
)
agent_builder.add_edge("tool_node", "get_person_by_llm")
# ============================================================
# 步骤 7: 编译图(带 Checkpointer 和 Store)
# ============================================================
checkpointer = InMemorySaver()
store = InMemoryStore()
agent = agent_builder.compile(checkpointer=checkpointer, store=store)
# ============================================================
# 运行示例(可选)
# ============================================================
if __name__ == "__main__":
# 第一次聊天
config1 = {"configurable": {"thread_id": "1", "user_id": "1"}}
result1 = agent.invoke(
{"messages": [HumanMessage(content="我叫李华,我最爱吃汉堡。我的好朋友叫小明,他爱吃披萨")]},
config1
)
print(f"\n调用 LLM 总次数:{result1['llm_calls']}次")
for m in result1["messages"]:
m.pretty_print()
# 几天后,同一用户新会话
config2 = {"configurable": {"thread_id": "2", "user_id": "1"}}
result2 = agent.invoke(
{"messages": [HumanMessage(content="给我推荐下餐厅")]},
config2
)
print(f"\n调用 LLM 总次数:{result2['llm_calls']}次")
for m in result2["messages"]:
m.pretty_print()
6. 坑点
- **严格区分 thread_id 与 user_id:**thread_id 归 Checkpointer 管,user_id 归 Store 管。切勿用 thread_id 去隔离 Store,否则跨会话共享失效
- **State 序列化:**Store 存储的值必须可 JSON 序列化。复杂的自定义类在存入前请转换为 dict
- **Store 的写入频率:**切勿在每个微小的状态变更中都写 Store,这会拖垮性能。建议只在关键节点(如用户明确表达偏好、完成支付、注册信息)时触发写入
- **语义搜索的延迟:**开启向量索引后,store.search 会引入额外的计算延迟。建议配合缓存(如 Redis)或仅对最近 N 条记录做重排序
- **Checkpoint 膨胀:**长会话会产生大量快照。建议结合业务逻辑定期清理老旧快照(如仅保留最近 100 步),或使用 LangSmith 进行监控