持久化执行
持久化执行 是一种技术,流程或工作流在关键点保存其进度,使其能够暂停并在之后从暂停处精确恢复执行。这在需要人机协同的场景中尤为有用------用户可以在继续之前检查、验证或修改流程,同时也适用于可能遇到中断或错误的长时间运行任务(例如,调用大语言模型超时)。通过保留已完成的工作,持久化执行使流程无需重新处理之前的步骤即可恢复------即使在显著延迟后(例如一周后)也能恢复。
LangGraph 内置的持久化层为工作流提供持久化执行能力,确保每个执行步骤的状态都保存到持久化存储中。这一特性保证工作流在被中断时------无论是由于系统故障还是为了进行人机协同交互------都能从最后记录的状态恢复执行。
<提示>
如果您在使用带检查点的 LangGraph,则已启用持久化执行。您可以在任意点暂停和恢复工作流,即使在中断或故障后也能恢复。
要充分发挥持久化执行的优势,请确保您的工作流设计为确定性且幂等,并将任何副作用或非确定性操作封装在任务内。您可以在StateGraph(图API)和函数式API中都使用任务。
</提示>
要求
要在 LangGraph 中利用持久化执行,您需要:
-
在执行工作流时指定一个线程标识符。这将跟踪特定工作流实例的执行历史。
-
将任何非确定性操作(例如,随机数生成)或具有副作用的操作(例如,文件写入、API调用)封装在任务内,以确保当工作流恢复时,这些操作不会为该特定运行重复执行,而是从持久化层检索其结果。更多信息请参见确定性与一致性重放。
确定性与一致性重放
当您恢复一个工作流运行时,代码不会 从执行停止的同一行代码 处恢复;相反,它将识别一个合适的起始点,并从此处继续执行。这意味着工作流将从起始点重放所有步骤,直到达到之前停止的点。
因此,当您为持久化执行编写工作流时,必须将任何非确定性操作(例如,随机数生成)和任何具有副作用的操作(例如,文件写入、API调用)封装在任务或节点内。
为确保您的工作流是确定性的且可以一致地重放,请遵循以下准则:
- 避免重复工作 :如果一个节点包含多个具有副作用的操作(例如,日志记录、文件写入或网络调用),请将每个操作封装在单独的任务中。这确保当工作流恢复时,这些操作不会被重复,而是从持久化层检索其结果。
- 封装非确定性操作 :将任何可能产生非确定性结果的代码(例如,随机数生成)封装在任务 或节点内。这确保在恢复时,工作流遵循完全相同记录的步骤序列,并产生相同的结果。
- 使用幂等操作 :在可能的情况下,确保副作用(例如,API调用、文件写入)是幂等的。这意味着如果在工作流失败后重试操作,它将具有与第一次执行时相同的效果。这对于导致数据写入的操作尤为重要。如果任务 开始但未能成功完成,工作流的恢复将重新运行该任务,依赖记录的结果来保持一致性。使用幂等键或验证现有结果,以避免意外的重复,确保工作流执行平稳且可预测。
有关需要避免的常见问题示例,请参见函数式API中的常见陷阱部分,其中展示了如何使用任务 来构建代码以避免这些问题。同样的原则也适用于StateGraph(图API)。
持久性模式
LangGraph 支持三种持久性模式,允许您根据应用程序的需求在性能和数据一致性之间取得平衡。更高的持久性模式会增加工作流执行的开销。您可以在调用任何图执行方法时指定持久性模式:
python
graph.stream(
{"input": "test"},
durability="sync"
)
持久性模式从最不持久到最持久如下:
"exit":LangGraph 仅在图形执行成功退出、出现错误或由于人机协同中断时保存更改。这为长时间运行的图提供了最佳性能,但也意味着中间状态不会被保存,因此无法从执行过程中发生的系统故障(如进程崩溃)中恢复。"async":LangGraph 在下一步执行时异步保存更改。这提供了良好的性能和持久性,但在执行过程中如果进程崩溃,LangGraph 有可能无法写入检查点。"sync":LangGraph 在下一步开始前同步保存更改。这确保 LangGraph 在继续执行前写入每个检查点,以一定的性能开销为代价提供高持久性。
在节点中使用任务
如果一个节点包含多个操作,您可能会发现将每个操作转换为任务比将这些操作重构为单独的节点更容易。
<选项卡标题="原始">
python
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
import requests
# 定义一个 TypedDict 来表示状态
class State(TypedDict):
url: str
result: NotRequired[str]
def call_api(state: State):
"""执行 API 请求的示例节点。"""
result = requests.get(state['url']).text[:100] # 副作用 # [!code highlight]
return {
"result": result
}
# 创建一个 StateGraph 构建器并添加 call_api 函数的节点
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# 将开始和结束节点连接到 call_api 节点
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# 指定一个检查点库
checkpointer = InMemorySaver()
# 使用检查点库编译图
graph = builder.compile(checkpointer=checkpointer)
# 定义一个带有线程 ID 的配置
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# 调用图
graph.invoke({"url": "https://www.example.com"}, config)
<选项卡标题="使用任务">
python
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.func import task
from langgraph.graph import StateGraph, START, END
import requests
# 定义一个 TypedDict 来表示状态
class State(TypedDict):
urls: list[str]
result: NotRequired[list[str]]
@task
def _make_request(url: str):
"""发起请求。"""
return requests.get(url).text[:100] # [!code highlight]
def call_api(state: State):
"""执行 API 请求的示例节点。"""
requests = [_make_request(url) for url in state['urls']] # [!code highlight]
results = [request.result() for request in requests]
return {
"results": results
}
# 创建一个 StateGraph 构建器并添加 call_api 函数的节点
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# 将开始和结束节点连接到 call_api 节点
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# 指定一个检查点库
checkpointer = InMemorySaver()
# 使用检查点库编译图
graph = builder.compile(checkpointer=checkpointer)
# 定义一个带有线程 ID 的配置
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# 调用图
graph.invoke({"urls": ["https://www.example.com"]}, config)
恢复工作流
一旦在工作流中启用了持久化执行,您可以在以下场景中恢复执行:
- 暂停和恢复工作流 :使用中断函数在特定点暂停工作流,并使用
Command原语以更新后的状态恢复它。详见中断获取更多细节。 - 从故障中恢复 :在异常(例如,LLM 提供商服务中断)后自动从最后一个成功的检查点恢复工作流。这涉及通过提供相同的线程标识符和输入值
None来执行工作流(请参阅函数式API中的此示例)。
恢复工作流的起始点
- 如果您使用的是 StateGraph(图 API),起始点是执行停止的节点的开始处。
- 如果您在节点内部调用子图,起始点将是调用被中断子图的父节点 。在子图内部,起始点将是执行停止的特定节点。
- 如果您使用的是函数式 API,起始点是执行停止的入口点的开始处。
官方文档在示例代码持久化设置部分内容中存在不一致性。让我来解释这种不一致的原因:
实际用法分析
1. 示例代码中确实没有设置 durability 参数
python
# 文档示例中的调用方式:
graph.invoke({"url": "https://www.example.com"}, config)
# ❌ 没有指定 durability 参数
2. 官方文档说明的位置
文档在理论说明部分 提到了 durability 参数:
python
# 这是文档说明部分给出的示例,但前面的代码示例没有使用
graph.stream(
{"input": "test"},
durability="sync" # 这是说明性代码,不是实际示例的一部分
)
为什么会这样?
原因一:默认值的使用
实际上,如果不指定 durability 参数,LangGraph 会使用默认的持久化模式。默认模式通常是:
"async"或"exit",取决于版本和配置
原因二:文档更新不同步
这是技术文档常见的现象:
- 功能先被实现 → 示例代码编写时可能还没有
durability参数 - 文档后续更新 → 添加了理论说明,但示例代码没有同步更新
完整的正确用法应该是:
示例1:使用 durability 参数
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
import uuid
# 构建图...
# ...
# 编译图时指定检查点
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 线程配置
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# 调用时指定持久化模式
graph.invoke(
{"url": "https://www.example.com"},
config,
durability="sync" # ← 这里指定持久化模式
)
示例2:不同调用方式
python
# 1. invoke() 方式
result = graph.invoke(input_data, config, durability="sync")
# 2. stream() 方式
for chunk in graph.stream(input_data, config, durability="async"):
print(chunk)
# 3. astream() 异步方式
async for chunk in graph.astream(input_data, config, durability="sync"):
print(chunk)
三种持久化模式的完整对比
| 模式 | 何时保存 | 性能 | 数据一致性 | 恢复能力 |
|---|---|---|---|---|
"exit" |
只在退出时保存 | 最高 | 最低 | 无法从崩溃中恢复 |
"async" |
异步保存(下一个节点执行时) | 高 | 中等 | 可能丢失少量数据 |
"sync" |
同步保存(下一个节点开始前) | 中等 | 最高 | 完全可恢复 |
实际生产建议
根据场景选择:
python
# 开发/调试场景
graph.invoke(input_data, config, durability="exit") # 性能优先
# 生产环境,关键业务
graph.invoke(input_data, config, durability="sync") # 数据一致性优先
# 生产环境,非关键业务
graph.invoke(input_data, config, durability="async") # 平衡性能与一致性
完整的生产示例:
python
import os
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.graph import StateGraph
import asyncpg
# 1. 创建 PostgreSQL 检查点(生产环境)
async def create_production_checkpointer():
connection = await asyncpg.connect(
os.environ["DATABASE_URL"]
)
return await PostgresSaver.from_conn(connection)
# 2. 构建图
# ...
# 3. 编译时使用生产检查点
checkpointer = create_production_checkpointer()
graph = builder.compile(
checkpointer=checkpointer,
# 可以在这里设置默认持久化模式
# durability="sync" # 作为编译选项
)
# 4. 调用时明确指定持久化模式
result = graph.invoke(
{"task": "重要业务处理"},
{"configurable": {"thread_id": "业务流水号123"}},
durability="sync" # 确保每一步都持久化
)
为什么文档会这样写?
- 教学目的:先展示核心概念,再介绍高级配置
- 渐进式学习:避免初学者一开始就被太多参数吓到
- 文档维护滞后:不同部分的文档由不同人编写/更新
- 版本差异:某些参数可能是新版本添加的
最佳实践建议
- 生产环境 :总是 明确指定
durability参数 - 关键业务 :使用
durability="sync" - 开发环境:可以使用默认值,但理解其含义
- 阅读文档:注意示例代码和理论说明可能不完全一致
python
# 推荐:始终明确指定持久化模式
DURABILITY_MODE = os.getenv("DURABILITY_MODE", "sync") # 从环境变量读取
graph.invoke(
input_data,
config,
durability=DURABILITY_MODE # 明确指定
)
这种文档和示例的不一致性在实际开发中很常见,关键在于理解核心概念和实际用法之间的差异。