培歌行学LangGraph(15):持久化模式——何时保存检查点?

0 阅读6分钟
  1. 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
  2. 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
  3. 培歌行学LangGraph(3):编译、执行与可视化
  4. 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
  5. 培歌行学LangGraph(5):状态管理与graph.invoke入参深度解析
  6. 培歌行学LangGraph(6):Multi Schema多状态管理详解
  7. 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
  8. 培歌行学LangGraph(8):控制流详解
  9. 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
  10. 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
  11. 培歌行学LangGraph(11):用循环结构实现 ReAct Agent
  12. 培歌行学LangGraph(12):递归限制——别让你的图无限跑下去
  13. 培歌行学LangGraph(13):节点执行与容错机制
  14. 培歌行学LangGraph(14):启用可恢复执行(Checkpoint)

一、为什么需要不同的持久化模式?

启用检查点后,LangGraph 会在执行过程中保存状态快照。但这里有一个权衡:

保存越频繁,容灾能力越强,但性能开销也越大。

  • 每执行一步都保存 → 崩溃后恢复最完整,但速度慢
  • 只在结束时保存 → 速度快,但崩溃后无法恢复

LangGraph 提供了三种持久化模式,让你在容灾能力性能开销之间做选择。


二、三种模式详解

2.1 exit 模式(退出模式)

写入时机:只在图正常结束、异常退出或被中断时保存。

graph.invoke(input_data, config=config, durability="exit")

特点

  • ✅ 性能开销最小
  • ✅ 响应延迟最低
  • ❌ 容灾能力最弱
  • ❌ 进程中途崩溃,所有中间状态丢失

适用场景

  • 短时间运行的任务
  • 不需要中途恢复的简单流程
  • 对性能要求极高的场景

2.2 async 模式(异步模式)⭐ 默认模式

写入时机:每个超步结束后,在后台异步写入检查点。

graph.invoke(input_data, config=config, durability="async")

特点

  • ✅ 容灾能力较强
  • ✅ 写入在后台进行,不阻塞主流程
  • ❌ 性能开销较高(但用户感知不明显)

适用场景

  • 大多数生产环境
  • 多轮对话系统
  • 需要平衡性能和可靠性的场景

2.3 sync 模式(同步模式)

写入时机:每个超步结束后,等待检查点写入完成,再进入下一个超步。

graph.invoke(input_data, config=config, durability="sync")

特点

  • ✅ 容灾能力最强
  • ❌ 性能开销最大
  • ❌ 响应延迟最高(等待磁盘写入)

适用场景

  • 金融交易系统
  • 不允许丢失任何中间状态的场景
  • 对可靠性要求极高、对延迟不敏感的场景

三、三种模式对比

模式写入时机性能开销响应延迟容灾能力推荐场景
exit运行结束时最低最弱简单任务、批处理
async每个超步后异步写入较强⭐ 大多数场景
sync每个超步后同步写入最高最强金融、关键业务

四、三种模式的实际表现

假设你的图有 3 个节点:A → B → C,每个节点执行 1 秒。

exit 模式

执行 A1秒)
执行 B1秒)
执行 C(1秒)
→ 保存检查点
总耗时:3

如果在执行 B 时进程崩溃 → 所有状态丢失,无法恢复。

async 模式

执行 A1秒)→ 后台异步保存
执行 B1秒)→ 后台异步保存
执行 C(1秒)→ 后台异步保存
总耗时:≈3秒(异步保存几乎不增加耗时)

如果在执行 B 时进程崩溃 → 可以从 A 的检查点恢复。

sync 模式

执行 A1秒)→ 等待保存完成(额外耗时)
执行 B1秒)→ 等待保存完成(额外耗时)
执行 C(1秒)→ 等待保存完成(额外耗时)
总耗时:>3秒(每次都要等待写入)

如果在执行 B 时进程崩溃 → 可以从 A 的检查点恢复,且保证 A 的状态已持久化


五、代码示例

from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "test_session"}}

# 使用 exit 模式
result = graph.invoke(
    {"messages": [HumanMessage("你好")]},
    config=config,
    durability="exit"
)

# 使用 async 模式(默认)
result = graph.invoke(
    {"messages": [HumanMessage("你好")]},
    config=config,
    durability="async"
)

# 使用 sync 模式
result = graph.invoke(
    {"messages": [HumanMessage("你好")]},
    config=config,
    durability="sync"
)

注意:三种模式下,正常执行完成时返回的结果是一样的。区别只体现在异常中断后的恢复能力上。


六、如何选择?

你的应用场景是什么?
│
├── 简单任务,不需要恢复 → exit
│
├── 多轮对话、常规 Agent → async(默认,推荐)
│
└── 金融、医疗、关键业务 → sync

经验法则

  • 90% 的场景用 async就够了
  • 开发调试时可以用 exit提高速度
  • 只有对数据一致性要求极高的场景才用 sync

七、面试题

面试题1:三种持久化模式的核心区别是什么?

:核心区别在于检查点的写入时机

  • exit:只在运行结束时写入
  • async:每个超步结束后异步写入
  • sync:每个超步结束后同步写入,等待完成

写入越及时,容灾能力越强,但性能开销也越大。

面试题2:为什么 async是默认模式?

:因为 async容灾能力性能开销之间取得了最好的平衡。它在每个超步后保存检查点,保证了较强的容灾能力;同时写入操作在后台异步进行,不会阻塞主流程,用户感知不到明显的性能下降。

面试题3:exit模式真的有用吗?什么时候用它?

:有用。在以下场景中 exit模式是合适的选择:

  1. 批处理任务:一次性执行,不需要中途恢复
  2. 开发调试:追求最快的迭代速度
  3. 短流程:整个图几毫秒就跑完了,崩溃概率极低

面试题4:sync模式为什么会导致响应延迟增加?

:因为 sync模式在每个超步结束后,会等待检查点写入完成才进入下一个超步。如果检查点后端是数据库(如 PostgreSQL),每次写入都需要网络 I/O 和磁盘 I/O,这会增加额外的等待时间。

面试题5:三种模式会影响正常的业务输出吗?

不会。三种模式只影响检查点的写入时机和容灾能力,不影响图本身的执行逻辑。在正常执行完成的情况下,三种模式返回的结果是一样的。


持久化模式的选择,本质上是在问自己:我能接受丢失多少状态?

如果答案是"一点都不能丢",选 sync;

如果答案是"丢了也无所谓",选 exit;

大多数时候,答案是"尽量别丢,但别太慢",那就选 async。