LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 2 · State & Memory
1 2-1 State Schema
2 2-2 Reducers
3 2-3 Multi Schemas
4 2-4 Trim & Filter
5 2-5 Chatbot Summary
6 2-6 Ext Memory
SUM Module Summary
HomeLangGraphModule 2 · State & Memory2-6 Ext Memory
📓 Notebook 📖 Explained
LANGGRAPH MODULE 2 · LESSON 6

Chatbot 外部持久化记忆
SqliteSaver · PostgresSaver · Thread 多会话

从内存检查点到真正持久化的数据库存储。
让 Chatbot 在重启后仍能记住所有对话,支持多用户、多会话场景。

1 回顾:我们已经做到了什么,还差什么

在 Module 2 的前几课中,我们逐步构建了一个功能越来越完善的对话型 Chatbot。核心成果包括:

遗留的核心问题

MemorySaver 把所有状态存在内存(RAM)里。一旦 Python 进程退出(重启 Notebook kernel、服务重启、部署更新),所有对话历史全部消失。这对任何真实用户来说都是灾难性的体验。

本课(Lesson 6)的目标:引入外部数据库检查点,让图的状态能够永久写入磁盘或云端数据库,彻底解决"断电即失忆"的问题。

维度 MemorySaver(内存) 外部数据库检查点
存储位置 Python 进程内存(RAM) SQLite 文件 / PostgreSQL / Redis 等
进程重启后 数据全部丢失 数据完整保留,可继续恢复
适用环境 开发、调试、Demo 生产环境、真实用户系统
多进程/多实例 不支持(各自独立内存) 支持(共享同一数据库)
容灾能力 高(服务器崩溃后可完整恢复)
安装依赖 无额外依赖(LangGraph 内置) 需安装对应驱动包

2 MemorySaver vs 外部数据库:两种检查点的根本区别

检查点(Checkpointer)是什么?

LangGraph 的检查点机制是其状态持久化的基础设施层。每次图执行完一个节点后,LangGraph 会把当前完整的 State 序列化,交给检查点器保存。下次调用时,检查点器负责把上次的 State 还原出来,图就能继续上次的上下文。

检查点机制的工作原理

graph.invoke()
节点执行
State 更新
检查点器写入
下次调用时 ↓
graph.invoke()
检查点器读取
还原 State
继续执行

MemorySaver 的内部机制

MemorySaver 用一个 Python 字典(dict)作为存储后端。它将序列化后的状态保存在 self.storage 这个内存字典中,key 是 (thread_id, checkpoint_id) 的组合。这解释了为什么它如此快速但又如此脆弱——数据从未落盘。

from langgraph.checkpoint.memory import MemorySaver

# MemorySaver:数据存在 Python 进程的内存里
memory = MemorySaver()

# 编译时挂上检查点器
graph = workflow.compile(checkpointer=memory)

# 问题:当这个 Python 进程结束,memory 对象销毁,所有数据消失
核心设计:检查点器是可插拔的

LangGraph 的精妙之处在于:检查点器对图的代码完全透明。你只需在 compile(checkpointer=xxx) 时换掉检查点器,图的所有节点、边、逻辑完全不需要改动。这是标准的依赖注入设计模式。

状态如何被序列化?

LangGraph 在写入检查点前,会把整个 State 对象序列化(默认使用 JSON 兼容的格式)。对于 messages 这类 LangChain 消息对象,它们实现了 __json__() 序列化接口,可以完整地被还原为原始类型(HumanMessageAIMessage 等)。

3 SqliteSaver:轻量级持久化方案

SQLite 是世界上部署量最大的数据库引擎,被 Android、iOS、Firefox、Python 标准库等内置使用。它的特点是零配置、单文件、无服务进程,非常适合作为从内存存储升级到持久化的第一步。

安装依赖

# 安装 LangGraph 的 SQLite 检查点支持库
pip install langgraph-checkpoint-sqlite

方式一:内存模式的 SQLite(仍然不持久!)

SQLite 支持一种特殊的 ":memory:" 连接字符串,表示创建一个纯内存中的 SQLite 数据库。这和 MemorySaver 类似,进程退出数据消失,但它使用了 SQLite 的接口:

import sqlite3

# ":memory:" 表示内存 SQLite,进程退出即消失
conn = sqlite3.connect(":memory:", check_same_thread=False)

# check_same_thread=False 允许多线程访问同一连接
(LangGraph 的异步执行需要这个参数)

方式二:文件模式的 SQLite(真正持久化)

只需将 ":memory:" 换成一个文件路径,SQLite 就会在磁盘上创建(或打开)一个 .db 文件。所有数据写入这个文件,进程退出后依然存在:

import sqlite3
import os

# 创建存放数据库的目录
os.makedirs("state_db", exist_ok=True)

# 指定磁盘文件路径——数据将永久保存
db_path = "state_db/example.db"
conn = sqlite3.connect(db_path, check_same_thread=False)

# 用这个连接创建 SqliteSaver 检查点器
from langgraph.checkpoint.sqlite import SqliteSaver
memory = SqliteSaver(conn)
SQLite 数据库里存了什么?

LangGraph 会在你的 .db 文件中自动创建所需的表,主要包括:checkpoints(存储每次状态快照)和 checkpoint_writes(存储待写入的状态更新)。你不需要手动建表,SqliteSaver 会在第一次使用时自动完成初始化。

SqliteSaver 内部如何工作?

SqliteSaver 的读写流程

写入时(invoke 执行后)
  • 1. 将 State 对象 JSON 序列化
  • 2. 生成唯一的 checkpoint_id
  • 3. 以 (thread_id, checkpoint_id) 为 key
  • 4. INSERT INTO checkpoints …
读取时(下次 invoke 前)
  • 1. 按 thread_id 查找最新检查点
  • 2. SELECT … ORDER BY checkpoint_id DESC
  • 3. 反序列化为原始 State 对象
  • 4. 注入图执行上下文继续运行

4 重用消息摘要图:接上外部检查点

这是本课最重要的工程实践要点:图的定义代码完全不变,只需在 compile() 时换一个检查点器。我们把 Lesson 5 的完整消息摘要 Chatbot 图,直接接上 SqliteSaver。

完整的图定义(与 Lesson 5 相同)

from typing_extensions import Literal
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage
from langgraph.graph import END, MessagesState, StateGraph, START

model = ChatDeepSeek(model="deepseek-v4-pro", temperature=0)

# State 继承 MessagesState,额外增加 summary 字段
class State(MessagesState):
    summary: str

# ── Node 1:调用 LLM ──
def call_model(state: State):
    summary = state.get("summary", "")
    if summary:
        # 把历史摘要注入为系统消息
        system_message = f"Summary of conversation earlier: {summary}"
        messages = [SystemMessage(content=system_message)] + state["messages"]
    else:
        messages = state["messages"]
    response = model.invoke(messages)
    return {"messages": response}

# ── Node 2:生成/更新对话摘要,并删除旧消息 ──
def summarize_conversation(state: State):
    summary = state.get("summary", "")
    if summary:
        summary_message = (
            f"This is summary of the conversation to date: {summary}\n\n"
            "Extend the summary by taking into account the new messages above:"
        )
    else:
        summary_message = "Create a summary of the conversation above:"

    messages = state["messages"] + [HumanMessage(content=summary_message)]
    response = model.invoke(messages)

    # 用 RemoveMessage 删除除最近 2 条以外的所有消息
    delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
    return {"summary": response.content, "messages": delete_messages}

# ── 路由函数:消息超过 6 条时触发摘要 ──
def should_continue(state: State) -> Literal["summarize_conversation", END]:
    if len(state["messages"]) > 6:
        return "summarize_conversation"
    return END

# ── 构建图 ──
workflow = StateGraph(State)
workflow.add_node("conversation", call_model)
workflow.add_node(summarize_conversation)
workflow.add_edge(START, "conversation")
workflow.add_conditional_edges("conversation", should_continue)
workflow.add_edge("summarize_conversation", END)

关键一行:换掉检查点器

# 之前用内存检查点:
# graph = workflow.compile(checkpointer=MemorySaver())

# 现在用 SQLite 检查点——图的其他代码零改动!
graph = workflow.compile(checkpointer=memory)  # memory 是 SqliteSaver 实例

带外部检查点的图结构

START
conversation
call_model:调用 LLM
should_continue() 路由
消息 > 6 条
summarize_conversation
生成摘要 + 删除旧消息
消息 ≤ 6 条
END
每次节点执行后,State 自动写入 SQLite 数据库文件

5 Thread(线程)与 thread_id:多用户多会话的核心机制

什么是 Thread?

在 LangGraph 中,Thread(线程)不是操作系统的线程,而是一个对话会话的逻辑隔离单元。每个 Thread 有一个唯一的 thread_id,LangGraph 用它来区分不同用户的对话、或同一用户的不同对话上下文。

你可以把 thread_id 理解为"对话房间号":不同房间里的对话互不干扰,同一房间里的多次对话共享同一段历史记忆。

# 创建一个对话配置,指定 thread_id
config = {"configurable": {"thread_id": "1"}}

# 第一轮对话
input_message = HumanMessage(content="hi! I'm Lance")
output = graph.invoke({"messages": [input_message]}, config)
for m in output['messages'][-1:]:
    m.pretty_print()

# 第二轮对话(同一 thread_id → 记得 Lance)
input_message = HumanMessage(content="what's my name?")
output = graph.invoke({"messages": [input_message]}, config)
for m in output['messages'][-1:]:
    m.pretty_print()
# 输出:Your name is Lance!

# 第三轮对话
input_message = HumanMessage(content="i like the 49ers!")
output = graph.invoke({"messages": [input_message]}, config)
for m in output['messages'][-1:]:
    m.pretty_print()

多用户场景:不同的 thread_id 完全隔离

# 用户 A 的对话(thread_id="user_alice")
config_alice = {"configurable": {"thread_id": "user_alice"}}
graph.invoke({"messages": [HumanMessage(content="I'm Alice")]}, config_alice)

# 用户 B 的对话(thread_id="user_bob")——与 Alice 完全隔离
config_bob = {"configurable": {"thread_id": "user_bob"}}
graph.invoke({"messages": [HumanMessage(content="I'm Bob")]}, config_bob)

# 用户 A 继续(仍然记得 Alice 的历史)
graph.invoke({"messages": [HumanMessage(content="what's my name?")]}, config_alice)
# → 输出:Your name is Alice!
thread_id 的最佳实践

在真实系统中,thread_id 通常使用用户 ID + 会话 ID 的组合,例如 "user_12345_session_abc"。同一用户可以开启多个独立对话(每个有不同的 session ID),或者复用同一个会话(继续上次的上下文)。推荐使用 UUID 生成会话 ID:str(uuid.uuid4())

查看当前 Thread 的状态

使用 graph.get_state(config) 可以随时查看某个 thread 当前的完整 State,包括 messages 列表和 summary 字段:

# 查看 thread "1" 的当前状态
config = {"configurable": {"thread_id": "1"}}
graph_state = graph.get_state(config)

# graph_state.values 包含完整的 State 字典
print(graph_state.values["messages"])   # 当前保留的消息列表
print(graph_state.values["summary"])    # 当前的对话摘要
messages:
[HumanMessage("i like the 49ers!"),
AIMessage("Great! The 49ers...")]
summary:
"User's name is Lance. He..."
SQLite 中保存的 State(三轮对话后)
messages:
[HumanMessage("new question"),
AIMessage("...")]
summary:
"User's name is Lance. He..."
追加新消息后(summary 得以保留)

6 状态持久化验证:重启内核后恢复会话

这是本课最有说服力的演示。我们来证明:即使 Jupyter Notebook 的 kernel 被完全重启(等同于服务器重启),保存在 SQLite 文件中的对话历史依然存在,可以无缝续接。

验证步骤

# ── 重启 Kernel 后,重新运行以下代码 ──

import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver

# 重新连接同一个 .db 文件
db_path = "state_db/example.db"
conn = sqlite3.connect(db_path, check_same_thread=False)
memory = SqliteSaver(conn)

# 重新编译图(节点/边定义完全不变)
graph = workflow.compile(checkpointer=memory)

# 使用相同的 thread_id——神奇的事情发生了
config = {"configurable": {"thread_id": "1"}}
graph_state = graph.get_state(config)

print("历史消息数量:", len(graph_state.values["messages"]))
print("对话摘要:", graph_state.values.get("summary", "(暂无摘要)"))
# 输出:上次对话的完整历史!
这就是生产级持久化的最小可行形态

SQLite 的这种特性让它非常适合单机部署的应用(个人助手、本地工具、小型 SaaS 等)。对于需要水平扩展(多台服务器同时服务)的场景,则需要升级到 PostgreSQL 等支持并发写入的数据库。

进一步验证:继续这个对话

# 重启后,继续上次的对话——LLM 仍然记得 Lance 喜欢 49ers
input_message = HumanMessage(content="which nfl team do I like?")
output = graph.invoke({"messages": [input_message]}, config)
for m in output['messages'][-1:]:
    m.pretty_print()
# 即使 Kernel 重启过,仍然输出:You like the San Francisco 49ers!

7 生产级方案:PostgresSaver 与部署架构

SQLite 适合单机场景,但当你的 Chatbot 需要服务大量并发用户,或者需要多台服务器横向扩展时,就需要升级到 PostgreSQL 这样的企业级数据库。

PostgresSaver 的使用方式

# 安装 PostgreSQL 检查点支持库
# pip install langgraph-checkpoint-postgres psycopg

from psycopg import connect
from langgraph.checkpoint.postgres import PostgresSaver

# 连接到 PostgreSQL 数据库
conn_string = "postgresql://user:password@localhost:5432/chatbot_db"
conn = connect(conn_string, autocommit=True)

# 初始化检查点表(首次使用时调用)
checkpointer = PostgresSaver(conn)
checkpointer.setup()  # 自动创建所需的数据库表

# 编译图时挂上 PostgreSQL 检查点——其余代码零改动!
graph = workflow.compile(checkpointer=checkpointer)

生产部署架构对比

方案 适用规模 优点 限制
MemorySaver 开发 / Demo 零配置,速度极快 进程重启即失,不支持多进程
SqliteSaver 单机小型应用
个人工具
零服务进程,单文件,部署简单 不支持高并发写入,不适合多实例
PostgresSaver 中大型 SaaS
企业应用
高并发、横向扩展、事务安全 需要维护 PG 服务,配置相对复杂
自定义 Checkpointer 特殊需求 可接入 Redis、DynamoDB 等任意后端 需要自己实现接口

典型生产部署架构(PostgreSQL 方案)

用户 A
用户 B
用户 C
↓ HTTP/WebSocket 请求
LangGraph App
实例 1
LangGraph App
实例 2
LangGraph App
实例 3
↓ PostgresSaver 读写
PostgreSQL 数据库
所有用户的 Thread 状态集中存储

LangGraph Studio 中的集成

本课对应的 Studio 配置在 module-2/studio/chatbot.py 中。使用 langgraph dev 命令启动本地开发服务器后,可以在 LangSmith Studio 的 UI 界面中可视化地查看每个 Thread 的完整状态历史、在任意检查点重放对话、甚至直接编辑 State。这是调试外部记忆问题的强大工具。

# 在 module-2/studio 目录下运行:
langgraph dev

# 访问 Studio UI:
# https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024

8 完整流程回顾与最佳实践

本课的知识图谱

检查点器(Checkpointer)
  • • 负责序列化 / 反序列化 State
  • • 在 compile() 时注入,图代码透明
  • • MemorySaver / SqliteSaver / PostgresSaver
  • • 可自定义接入任意存储后端
Thread / thread_id
  • • 逻辑对话会话的隔离单元
  • • 通过 config 字典传递给 invoke()
  • • 不同 thread_id → 完全独立的历史
  • • 生产中用 userId + sessionId 组合
SqliteSaver 使用要点
  • • 连接字符串用文件路径(非 :memory:)
  • • check_same_thread=False(多线程安全)
  • • 自动创建检查点表,无需手动建表
  • • 重连同一文件即可恢复所有历史
State 序列化
  • • 消息对象实现了 JSON 序列化接口
  • • 还原后类型完整(HumanMessage 等)
  • • summary 字段也随 State 一起持久化
  • • get_state() 可随时查看当前快照

完整的迁移路径

核心工程价值

LangGraph 把"状态存哪儿"这个问题从图的业务逻辑中彻底解耦。你写的 Node 函数、Edge 路由函数、State 定义,完全不需要知道数据存在内存还是 SQLite 还是 PostgreSQL。这让你的 AI 应用从 Demo 到生产的迁移真正做到零代码改动,只是换一个检查点器实例。

学习路线上的位置

Module 2-1 State Schema 自定义:TypedDict、Pydantic、MessagesState
Module 2-2 Reducer 机制:add_messages 追加而非覆盖,自定义 Reducer
Module 2-3/4 消息修剪与过滤:控制 context window 大小
Module 2-5 消息摘要 Chatbot:summary 字段 + RemoveMessage + MemorySaver
Module 2-6 ← 外部持久化记忆:SqliteSaver + Thread + 重启恢复
Module 3 Human-in-the-loop:在图执行中断,等待人工审批
Module 5 跨 Thread 的长期记忆 Store(超越 Checkpointer 的更高层记忆)
下一步

至此,Module 2 完整覆盖了构建一个有完整记忆能力的生产级 Chatbot所需的所有技术:状态设计、消息管理、摘要压缩、外部持久化、多用户隔离。Module 3 将在这个基础上引入人工干预(Breakpoints),让 AI 在关键决策点暂停并征询用户意见,进一步提升系统的可控性和安全性。