Appearance
Q50 · LangGraph 中的持久化与 Checkpointer 是什么?
客服智能体已经为订单 A123 写好一段“能否申请退款”的说明,但公司要求人工审核后才能发给用户。审核员可能几小时后才回复;这段时间程序可能重启。若图只把草稿放在运行中的 Python 变量里,审核回来时就找不到前面的工作。LangGraph 的 **Checkpointer(检查点保存器)**解决的是“这条执行流程停在哪里、当时的状态是什么、以后怎样接着走”。
LangGraph 把工作拆成节点,例如“拟草稿”“等审核”;节点读取并更新一份图状态。编译图时接入 checkpointer,它会为指定的执行线程保存checkpoint(状态快照)。下一次带相同 thread_id(线程标识)调用图,就能找到这条线程的已保存状态。官方把 checkpointer 用于线程内的短期记忆、人工中断、故障恢复与历史回看;把长期、跨线程的数据交给另一种 Store(存储)。LangGraph:Persistence · LangGraph:Checkpointers
术语解释:这几个名字各是什么意思
| 术语或符号 | 它是什么 | 订单 A123 的例子 |
|---|---|---|
| 图 / graph | 由节点和连接关系组成的执行流程 | 先拟草稿,再到审核节点 |
| 节点 / node | 图中一项具体工作,读取图状态并返回更新 | prepare 写草稿;review 等审核 |
| 图状态 / state | 这条流程当前需要保存的字段 | order_id、draft、审核状态 |
| checkpoint | 某个执行阶段结束后保存的图状态快照,还包含继续运行所需的信息 | A123 的草稿已经写好,下一步是审核 |
| Checkpointer | 负责写入、读取这些快照的保存器 | InMemorySaver 或数据库保存器 |
| thread / 线程 | 按 thread_id 串起来的一系列图运行和快照;这里不是操作系统线程 | A123 这次待审核流程 |
thread_id | 查找同一线程状态的标识,由应用稳定传入 | 演示值 T-A123 |
config | 调用图时传入的配置字典 | {"configurable": {"thread_id": "T-A123"}} |
interrupt() | 在节点中暂停,向外部交出待处理信息,等待恢复值 | 把草稿交给审核员 |
Command(resume=...) | 把外部回复作为中断点的返回值,继续同一线程 | 审核员回复 approve |
get_state(config) | 读取该线程最新保存的状态快照 | 查看 A123 草稿及下一节点 |
| Store | 在图状态之外按命名空间和键保存应用数据,可跨线程使用 | 用户长期偏好的客服用语 |
| 幂等 | 同一操作重复执行,外部结果仍等同于执行一次 | 同一退款请求重试不会产生两笔退款 |
这个 thread_id 只是检索状态的键,不是登录用户身份,也不能直接当授权依据。系统应先核对当前操作者是否能访问这条线程;多租户环境还要把租户或业务范围纳入应用自己的隔离设计。A123 是本例的订单号,T-A123 是本例的图线程号,它们长得相似只是为了便于跟踪,并不要求真实系统这样命名。
Checkpoint 存下了什么,何时存
可以把 checkpoint 看成“图走到一个可继续的位置时留下的记录”。LangGraph 官方说明:checkpointer 按 **super-step(超级步)**边界保存状态;一个超级步是同一轮被调度执行的节点集合。简单顺序图 START → prepare → review → END 会在输入和各节点阶段形成可回看的快照;并不是 Python 代码每执行一行都自动保存一次。快照中有当前字段值、下一步要执行的节点等信息,graph.get_state(config) 可读取最新的 StateSnapshot。LangGraph:Checkpoints 与 super-steps · LangGraph:Get state

图中左边是说明草稿,中间的档案盒代表保存下来的图状态,标签 thread_id: T-A123 表示它归属哪个线程。审核发生在暂停期间;收到回复后,应用用同一 thread_id 找回保存的状态并继续。最右侧“恢复处理”表示草稿审核流程继续,不表示退款已打款。图省略了状态快照的元数据和存储后端选择,它们在下文说明。
人工中断有一个容易误解的细节:interrupt() 表面上让流程停在节点中间,但恢复时该节点会从开头重新运行,interrupt() 接收到的恢复值才让它越过暂停点。它不是把 Python 函数的调用栈原样冻结后从下一行继续。因此在中断之前调用外部写接口,恢复时很可能再调用一次。官方中断文档明确提醒:中断前的副作用应是幂等的,或把副作用放在中断后、拆到独立节点。LangGraph:Interrupts
用一个无模型密钥的程序暂停和恢复
下面的程序使用 Python 3.10+ 与 langgraph 1.x;安装 pip install 'langgraph>=1,<2' 后可独立运行,不需要模型 API 密钥或数据库。代码只审核客服说明草稿,不调用真实退款接口。它用 InMemorySaver 把快照暂存在进程内,便于看清机制;进程一结束,这些快照就丢失。生产环境若要跨重启恢复,应使用持久存储后端。LangGraph:InMemorySaver · LangGraph:存储选择
代码里的业务输入只有订单号 A123。RefundState 规定图状态可含哪些字段;total=False 表示创建状态字典时这些字段可以逐步补上。prepare 根据订单号生成 draft;review 把草稿交给人审并读取恢复值。review_calls 只是演示用的本地计数器,不属于图状态,也不会由 checkpointer 保存。builder 是建图器,graph 是编译后可运行的图,checkpointer 是快照保存器,config 指定本次要操作的线程。START 和 END 是图的入口、出口标记。
python
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
class RefundState(TypedDict, total=False):
order_id: str
draft: str
approved: bool
review_status: str
def prepare(state: RefundState):
return {"draft": f"订单 {state['order_id']} 的退款说明草稿,等待人工审核。"}
review_calls = 0
def review(state: RefundState):
global review_calls
review_calls += 1 # 只为演示节点重跑,不是外部业务写入
decision = interrupt({"order_id": state["order_id"], "draft": state["draft"]})
approved = decision == "approve"
return {
"approved": approved,
"review_status": "DRAFT_APPROVED" if approved else "DRAFT_REJECTED",
}
builder = StateGraph(RefundState)
builder.add_node("prepare", prepare)
builder.add_node("review", review)
builder.add_edge(START, "prepare")
builder.add_edge("prepare", "review")
builder.add_edge("review", END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "T-A123"}}
paused = graph.invoke({"order_id": "A123"}, config=config)
print("待审核内容:", paused["__interrupt__"][0].value)
snapshot = graph.get_state(config)
print("已保存状态:", snapshot.values)
print("下一节点:", snapshot.next)
resumed = graph.invoke(Command(resume="approve"), config=config)
print("恢复结果:", resumed["review_status"])
print("审核节点进入次数:", review_calls)
other_config = {"configurable": {"thread_id": "T-A124"}}
graph.invoke({"order_id": "A124"}, config=other_config)
rejected = graph.invoke(Command(resume="reject"), config=other_config)
print("另一线程结果:", rejected["review_status"])按代码顺序跟一遍:
graph.invoke({"order_id": "A123"}, config=config)先走prepare,产生草稿;review调用interrupt(),把订单号和草稿交给外部,返回值中出现__interrupt__,图等待审核员回复。graph.get_state(config)能看到order_id与draft已保存,snapshot.next是('review',)。snapshot是读出的最新状态快照,不是重新执行了一遍图。- 审核员同意后,应用以同一个
config调用graph.invoke(Command(resume="approve"), config=config)。review从开头再跑,随后interrupt()得到字符串approve,返回DRAFT_APPROVED。演示计数器因而显示审核节点进入 2 次,而前面的prepare节点没有重复运行。 T-A124是另一条独立线程;给它reject后得到DRAFT_REJECTED,不会改变 A123 的状态。DRAFT_APPROVED只代表说明草稿审核通过,没有发消息给用户,也没有执行退款。
失败情况也要能解释。如果恢复时写成新的 thread_id,应用就找不到原来那条待审核线程;这不是“新建线程后也能继续”。如果用 InMemorySaver 却在审核期间重启进程,内存数据会消失,原线程也无法从该进程恢复。若审核员拒绝,图可按代码记录 DRAFT_REJECTED 并结束;业务程序应告知相关人员修改草稿或另开修订流程,不得把拒绝当成批准。若审核值的格式不在允许范围,真实系统应先校验并拒绝,示例只用两种固定字符串演示。
短期状态、长期 Store 和真正的落盘是三回事
| 问题 | Checkpointer | Store |
|---|---|---|
| 保存什么 | 图状态及可恢复执行所需的快照 | 应用主动写入的键值数据,如用户偏好 |
| 范围 | 由 thread_id 定位,主要服务一条线程的前后运行 | 可跨多个线程,通常按用户或业务命名空间访问 |
| 本例 | A123 的草稿、待审位置和审核结果 | 用户 U9 偏好“客服说明简短” |
| 写入方式 | 图运行时由 checkpointer 记录状态 | 业务代码明确调用 Store 的读写方法 |
| 是否跨进程重启 | 取决于具体保存器;InMemorySaver 不行 | 也取决于具体实现;InMemoryStore 同样只在内存里 |
LangGraph 官方文档把前者称作线程内的短期记忆,把后者用于长期、跨线程数据;两者可以同时编译进同一张图。“持久化接口”不等于“必然落盘”:演示所用 InMemorySaver 和 InMemoryStore 都只在内存;生产中需要数据库后端,例如官方列出的 PostgresSaver 或相应的持久 Store。轻量本地演示可用 SQLite 保存器,但官方把内存保存器定位为调试/测试,Postgres 保存器用于生产负载。业务还要设计保留期限、清理、访问控制和敏感数据保护。LangGraph:Checkpointer vs Store · LangGraph:Checkpoint saver 选型
把“上一轮对话”放进图状态时,checkpointer 可以让同一 thread_id 的下一轮看到它;若把用户偏好存进 Store,则同一用户的新线程也可读取。它们不是自动同步的:一条线程的草稿不会因为存在 checkpoint 就自动成为全用户可用的偏好。反过来,Store 里的一条偏好也不会自动决定图从哪个节点恢复。Store 的命名空间和键需要应用设计,并且要从可信的用户身份确定,不能照单全收模型生成的用户 ID。LangGraph:Stores
恢复时怎样避免重复外部动作
若把 send_refund(order_id) 或“发邮件”写在 review 的 interrupt() 之前,首次进入节点会执行一次;恢复时节点从开头重跑,就可能再执行一次。这个位置应只做纯计算、读操作或可安全重试的幂等动作。更稳妥的是先暂停、取得批准,再到单独节点执行外部动作;执行时仍由后端用固定业务键(例如订单号加审批请求号)去重,并核验订单当前状态。原因是即使动作放在中断之后,进程崩溃、超时、重试或历史回放仍可能让外部调用再次发生;checkpoint 不能给任意外部系统保证“恰好一次”。LangGraph:中断与副作用
还要分清普通恢复与历史回放。get_state_history(config) 可查看同一线程的旧快照;指定过去的 checkpoint_id 可从旧状态重放,用于排错或比较方案。重放时旧快照之前已完成的阶段可复用,后面的节点可能重新执行,所以不能把“时间旅行”当成对真实退款的安全回滚。对于同一超级步中并行节点的失败,官方说明已成功节点的**pending writes(待提交写入)**可被保存,恢复时不必重跑这些成功节点;这进一步说明恢复粒度取决于图步骤和已保存记录,而非一律从头或一律从故障那一行继续。LangGraph:Replay 与 pending writes
面试时可以这样回答
“LangGraph 的 checkpointer 会按 thread_id 保存某条图执行的状态快照,让同一线程在下一轮对话、人工中断或故障后找回状态。比如退款说明草稿写完后,审核节点用 interrupt() 暂停;审核员回来时应用用相同 thread_id 和 Command(resume=...) 继续。恢复不是冻结 Python 栈,发生中断的节点会从开头重跑,所以中断前不能放不幂等的外部写入,后续退款等写操作也要靠业务幂等键和状态校验。Checkpointer 保存线程内的短期图状态;Store 保存主动写入、可跨线程使用的长期数据。最后要看具体后端:InMemorySaver 适合演示,跨进程重启要用数据库型保存器。”
如果面试官追问“有 checkpointer 是否就不会重复调用工具”,可以回答:不能这样保证。它记录图状态和已完成步骤,部分成功的并行节点还有待提交写入恢复机制;但中断节点会重跑,外部 API 与 checkpoint 也不是同一个原子事务。把可重试逻辑设计为幂等,必要时查询外部操作的最终状态,再决定是否补偿或继续。