个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [LangGraph Checkpointer:持久化、断点续跑与多轮记忆](#LangGraph Checkpointer:持久化、断点续跑与多轮记忆)
-
- [一、Checkpointer 做了什么](#一、Checkpointer 做了什么)
- 二、最小可用:内存版
- [三、生产:SQLite / Postgres](#三、生产:SQLite / Postgres)
-
- [3.1 SQLite(单机、轻量)](#3.1 SQLite(单机、轻量))
- [3.2 Postgres(生产推荐)](#3.2 Postgres(生产推荐))
- [四、断点续跑:interrupt + resume](#四、断点续跑:interrupt + resume)
- 五、查看和管理历史
-
- [5.1 拿到当前状态](#5.1 拿到当前状态)
- [5.2 遍历历史](#5.2 遍历历史)
- [5.3 从历史快照分叉(时间旅行)](#5.3 从历史快照分叉(时间旅行))
- [5.4 手动改状态](#5.4 手动改状态)
- [六、Checkpointer 与消息裁剪](#六、Checkpointer 与消息裁剪)
- 七、常见坑
- 八、小结
LangGraph Checkpointer:持久化、断点续跑与多轮记忆
图跑完状态就没了------这在 demo 里没问题,在生产里是硬伤:
用户刷新页面对话断了、人工审批要等一天、服务重启任务丢了。
Checkpointer 解决的就是"让图的状态活下来 "。
这篇讲清楚它怎么用,以及
thread_id这个关键概念。
一、Checkpointer 做了什么
简单说:每执行完一个节点,就把当前 State 快照存一份。
节点 A 执行完 → 存快照 1
节点 B 执行完 → 存快照 2
节点 C 执行完 → 存快照 3
有了这些快照,你就能:
- 继续:同一个会话的下一次输入,从最新快照继续;
- 回溯:拿到历史上任意一个快照;
- 恢复 :
interrupt暂停后,隔多久都能接着跑; - 分叉:从某个历史快照复制出一条新分支(做"如果当时......"的实验)。
二、最小可用:内存版
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
checkpointer = InMemorySaver()
agent = create_react_agent(llm, tools, checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-123"}}
agent.invoke({"messages": [("user", "我叫小明")]}, config=config)
agent.invoke({"messages": [("user", "我叫什么?")]}, config=config)
# → "你叫小明"
关键就在 thread_id。
- 同一个
thread_id→ 共享同一份状态,多轮对话有记忆; - 不同
thread_id→ 完全独立的会话。
InMemorySaver 只存在进程内存里,重启即丢。
只适合开发调试。生产必须换成持久化的实现。
三、生产:SQLite / Postgres
3.1 SQLite(单机、轻量)
bash
pip install langgraph-checkpoint-sqlite
python
from langgraph.checkpoint.sqlite import SqliteSaver
# 方式一:文件路径,需手动管理连接
saver = SqliteSaver.from_conn_string("checkpoints.db")
# 方式二:配合 sqlite3 连接(推荐,可设 check_same_thread=False)
import sqlite3
conn = sqlite3.connect("checkpoints.db", check_same_thread=False)
saver = SqliteSaver(conn)
agent = create_react_agent(llm, tools, checkpointer=saver)
SQLite 适合单机部署、中小规模 。优点是不需要额外服务,
缺点是写操作串行,并发高了会争锁。
3.2 Postgres(生产推荐)
bash
pip install langgraph-checkpoint-postgres
python
from langgraph.checkpoint.postgres import PostgresSaver
DB_URI = "postgresql://user:pwd@localhost:5432/agentdb"
with PostgresSaver.from_conn_string(DB_URI) as saver:
saver.setup() # 第一次要建表
agent = create_react_agent(llm, tools, checkpointer=saver)
...
生产选 Postgres 的理由和向量库那篇一样:
备份、监控、扩容都复用现有体系,不用再维护一个新组件。
四、断点续跑:interrupt + resume
这是 Checkpointer 最有价值的用法。结合上一篇讲的人工介入:
python
from langgraph.types import interrupt, Command
def sensitive_node(state):
# 暂停,等待外部决策
decision = interrupt({"action": "refund", "amount": 299.0})
if decision == "approve":
return {"result": do_refund()}
return {"result": "用户已拒绝"}
调用流程:
python
config = {"configurable": {"thread_id": "order-888"}}
# 第一次调用:跑到 interrupt 就停
result = agent.invoke({"messages": [("user", "我要退款")]}, config=config)
print(result.get("__interrupt__"))
# [Interrupt(value={'action': 'refund', 'amount': 299.0})]
# ------ 此时可以关掉服务、过一天再回来 ------
# 恢复执行
result = agent.invoke(Command(resume="approve"), config=config)
注意第二次 invoke 传的不是新的用户输入,而是 Command(resume=...)。
只要 thread_id 一致,LangGraph 就能从快照里恢复出暂停时的状态继续跑。
这个能力支撑了非常实际的场景:
退款审批、合同确认、大额下单 ------图可以挂起几天,
人工在后台点完"同意",流程自动往下走。
五、查看和管理历史
5.1 拿到当前状态
python
snapshot = agent.get_state(config)
print(snapshot.values) # 当前 State 内容
print(snapshot.next) # 下一步会执行哪些节点
print(snapshot.tasks) # 待办(含 interrupt 信息)
5.2 遍历历史
python
for snap in agent.get_state_history(config):
print(snap.config["configurable"]["checkpoint_id"], snap.next)
5.3 从历史快照分叉(时间旅行)
python
history = list(agent.get_state_history(config))
old = history[2] # 回到第 3 个快照
# 用旧的 checkpoint_id 构造新 config
forked_config = {
"configurable": {
"thread_id": "user-123",
"checkpoint_id": old.config["configurable"]["checkpoint_id"],
}
}
# 在新分支上继续跑,不影响原分支
agent.invoke({"messages": [("user", "换个思路重新分析")]}, config=forked_config)
"分叉"在调试时特别有用 :跑到一半发现走错了,
退回去改个输入重新跑,原分支还留着可以对比。
5.4 手动改状态
python
agent.update_state(config, {"answer": "手动修正后的答案"})
直接覆写某个字段,然后继续跑。适合人工修正 Agent 的错误输出。
六、Checkpointer 与消息裁剪
有个实际问题:状态存久了,消息列表会越来越长 ,
每次加载都要把所有历史消息塞进 prompt,成本和延迟都会涨。
解法是定期裁剪,只保留最近 N 条:
python
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages, RemoveMessage
MAX_MESSAGES = 20
def trim_messages(state):
msgs = state["messages"]
if len(msgs) <= MAX_MESSAGES:
return {}
# 删掉最老的,保留最近 MAX_MESSAGES 条
to_remove = [RemoveMessage(id=m.id) for m in msgs[:-MAX_MESSAGES]]
return {"messages": to_remove}
在图里加一个节点做这件事:
python
graph.add_node("trim", trim_messages)
graph.add_edge(START, "trim")
graph.add_edge("trim", "agent")
注意别把系统消息和用户的最新输入裁掉,否则会出奇怪的行为。
另一个思路是摘要 :历史太长时用 LLM 压成一段摘要,
替换掉原始消息。LangChain 有现成的 summarize_messages。
七、常见坑
| 现象 | 原因 | 解法 |
|---|---|---|
| 多轮对话没记忆 | 每次 thread_id 不一样 |
用同一个稳定 id(如用户 ID) |
| 服务重启后记忆丢失 | 用了 InMemorySaver |
换 Sqlite / Postgres |
resume 报找不到线程 |
thread_id 变了 |
两次调用必须一致 |
| 状态越来越大、越来越慢 | 消息无限累积 | 定期裁剪或摘要 |
| Postgres 报缺表 | 没调 setup() |
首次运行执行 saver.setup() |
| SQLite 报线程错误 | 多线程共享连接 | check_same_thread=False |
八、小结
- Checkpointer 每节点存一份 State 快照,支撑继续 / 回溯 / 恢复 / 分叉;
thread_id是唯一关键:相同 id 共享会话,不同 id 完全隔离;InMemorySaver只用于调试,生产用 SqliteSaver 或 PostgresSaver;- 断点续跑 =
interrupt()暂停 +Command(resume=...)恢复,
配合持久化可以挂起几天; get_state/get_state_history/update_state三个 API 用于查看和管理;- 消息会无限累积,务必做裁剪或摘要,否则成本和延迟会失控。
下一篇讲部署上线------把链或 Agent 变成一个 HTTP 服务。