从内存检查点到真正持久化的数据库存储。
让 Chatbot 在重启后仍能记住所有对话,支持多用户、多会话场景。
在 Module 2 的前几课中,我们逐步构建了一个功能越来越完善的对话型 Chatbot。核心成果包括:
MessagesState 管理对话消息列表,加入 summary 字段保存摘要add_messages Reducer 确保消息是追加而非覆盖MemorySaver 把所有状态存在内存(RAM)里。一旦 Python 进程退出(重启 Notebook kernel、服务重启、部署更新),所有对话历史全部消失。这对任何真实用户来说都是灾难性的体验。
本课(Lesson 6)的目标:引入外部数据库检查点,让图的状态能够永久写入磁盘或云端数据库,彻底解决"断电即失忆"的问题。
| 维度 | MemorySaver(内存) | 外部数据库检查点 |
|---|---|---|
| 存储位置 | Python 进程内存(RAM) | SQLite 文件 / PostgreSQL / Redis 等 |
| 进程重启后 | 数据全部丢失 | 数据完整保留,可继续恢复 |
| 适用环境 | 开发、调试、Demo | 生产环境、真实用户系统 |
| 多进程/多实例 | 不支持(各自独立内存) | 支持(共享同一数据库) |
| 容灾能力 | 无 | 高(服务器崩溃后可完整恢复) |
| 安装依赖 | 无额外依赖(LangGraph 内置) | 需安装对应驱动包 |
LangGraph 的检查点机制是其状态持久化的基础设施层。每次图执行完一个节点后,LangGraph 会把当前完整的 State 序列化,交给检查点器保存。下次调用时,检查点器负责把上次的 State 还原出来,图就能继续上次的上下文。
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__() 序列化接口,可以完整地被还原为原始类型(HumanMessage、AIMessage 等)。
SQLite 是世界上部署量最大的数据库引擎,被 Android、iOS、Firefox、Python 标准库等内置使用。它的特点是零配置、单文件、无服务进程,非常适合作为从内存存储升级到持久化的第一步。
# 安装 LangGraph 的 SQLite 检查点支持库
pip install langgraph-checkpoint-sqlite
SQLite 支持一种特殊的 ":memory:" 连接字符串,表示创建一个纯内存中的 SQLite 数据库。这和 MemorySaver 类似,进程退出数据消失,但它使用了 SQLite 的接口:
import sqlite3
# ":memory:" 表示内存 SQLite,进程退出即消失
conn = sqlite3.connect(":memory:", check_same_thread=False)
# check_same_thread=False 允许多线程访问同一连接
(LangGraph 的异步执行需要这个参数)
只需将 ":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)
LangGraph 会在你的 .db 文件中自动创建所需的表,主要包括:checkpoints(存储每次状态快照)和 checkpoint_writes(存储待写入的状态更新)。你不需要手动建表,SqliteSaver 会在第一次使用时自动完成初始化。
这是本课最重要的工程实践要点:图的定义代码完全不变,只需在 compile() 时换一个检查点器。我们把 Lesson 5 的完整消息摘要 Chatbot 图,直接接上 SqliteSaver。
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 实例
在 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()
# 用户 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 通常使用用户 ID + 会话 ID 的组合,例如 "user_12345_session_abc"。同一用户可以开启多个独立对话(每个有不同的 session ID),或者复用同一个会话(继续上次的上下文)。推荐使用 UUID 生成会话 ID:str(uuid.uuid4())。
使用 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"]) # 当前的对话摘要
这是本课最有说服力的演示。我们来证明:即使 Jupyter Notebook 的 kernel 被完全重启(等同于服务器重启),保存在 SQLite 文件中的对话历史依然存在,可以无缝续接。
在 thread "1" 中与 Chatbot 聊了几轮,状态已写入 state_db/example.db
点击 Jupyter 的 "Restart Kernel" 按钮。Python 进程完全重启,所有内存中的变量(包括 MemorySaver 的内容)全部消失。
重新运行初始化代码,重新连接到同一个 .db 文件,重新编译图(代码完全相同)
调用 graph.get_state(config),你会发现所有历史消息和摘要都完整地从数据库还原出来了
# ── 重启 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!
SQLite 适合单机场景,但当你的 Chatbot 需要服务大量并发用户,或者需要多台服务器横向扩展时,就需要升级到 PostgreSQL 这样的企业级数据库。
# 安装 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 等任意后端 | 需要自己实现接口 |
本课对应的 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
零配置,迭代快,专注图逻辑而非存储细节
只需改一行 compile() 调用,验证持久化逻辑、测试重启恢复、测试多用户隔离
同样只改 compile() 一行,获得完整的生产级并发、事务、备份能力
继承 BaseCheckpointSaver,实现 get()、put() 等接口,即可接入任意后端(Redis、DynamoDB、自研存储等)
LangGraph 把"状态存哪儿"这个问题从图的业务逻辑中彻底解耦。你写的 Node 函数、Edge 路由函数、State 定义,完全不需要知道数据存在内存还是 SQLite 还是 PostgreSQL。这让你的 AI 应用从 Demo 到生产的迁移真正做到零代码改动,只是换一个检查点器实例。
至此,Module 2 完整覆盖了构建一个有完整记忆能力的生产级 Chatbot所需的所有技术:状态设计、消息管理、摘要压缩、外部持久化、多用户隔离。Module 3 将在这个基础上引入人工干预(Breakpoints),让 AI 在关键决策点暂停并征询用户意见,进一步提升系统的可控性和安全性。