Skip to content

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

订单退款说明草稿保存到 Checkpoint,人工审核后用同一 thread_id 恢复处理

图中左边是说明草稿,中间的档案盒代表保存下来的图状态,标签 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"])

按代码顺序跟一遍:

  1. graph.invoke({"order_id": "A123"}, config=config) 先走 prepare,产生草稿;review 调用 interrupt(),把订单号和草稿交给外部,返回值中出现 __interrupt__,图等待审核员回复。
  2. graph.get_state(config) 能看到 order_id 与 draft 已保存,snapshot.next 是 ('review',)。snapshot 是读出的最新状态快照,不是重新执行了一遍图。
  3. 审核员同意后,应用以同一个 config 调用 graph.invoke(Command(resume="approve"), config=config)。review 从开头再跑,随后 interrupt() 得到字符串 approve,返回 DRAFT_APPROVED。演示计数器因而显示审核节点进入 2 次,而前面的 prepare 节点没有重复运行。
  4. T-A124 是另一条独立线程;给它 reject 后得到 DRAFT_REJECTED,不会改变 A123 的状态。DRAFT_APPROVED 只代表说明草稿审核通过,没有发消息给用户,也没有执行退款。

失败情况也要能解释。如果恢复时写成新的 thread_id,应用就找不到原来那条待审核线程;这不是“新建线程后也能继续”。如果用 InMemorySaver 却在审核期间重启进程,内存数据会消失,原线程也无法从该进程恢复。若审核员拒绝,图可按代码记录 DRAFT_REJECTED 并结束;业务程序应告知相关人员修改草稿或另开修订流程,不得把拒绝当成批准。若审核值的格式不在允许范围,真实系统应先校验并拒绝,示例只用两种固定字符串演示。

短期状态、长期 Store 和真正的落盘是三回事 ​

问题CheckpointerStore
保存什么图状态及可恢复执行所需的快照应用主动写入的键值数据,如用户偏好
范围由 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 也不是同一个原子事务。把可重试逻辑设计为幂等,必要时查询外部操作的最终状态,再决定是否补偿或继续。

最后更新2026-09-26
难度P1
频率high
阅读18 min
主题langgraph / persistence / checkpointer
觉得有帮助?把这个链接转给正在求职的朋友 · 用 Ctrl + K 全站搜索其它题