持久执行¶
持久执行是一种技术,工作流或进程会在关键点保存其进度,使其能够暂停并在之后从完全中断的地方恢复。这在需要人在回路的场景中特别有用,用户可以在继续之前检查、验证或修改过程;或者在可能遇到中断或错误(例如,对 LLM 的调用超时)的长时间运行任务中也很有用。通过保留已完成的工作,持久执行使得进程能够在不重新处理之前步骤的情况下恢复,即使经过了显著延迟(例如,一周后)。
LangGraph 内置的持久化层为工作流提供持久执行,确保每个执行步骤的状态保存到持久存储中。这项能力保证了如果工作流中断(无论是系统故障还是人在回路交互),它都可以从上次记录的状态恢复。
提示
如果您正在使用带有检查点 (checkpointer) 的 LangGraph,您已经启用了持久执行。您可以在任何时候暂停和恢复工作流,即使在中断或故障之后。为了最大程度地利用持久执行,请确保您的工作流被设计为确定性和幂等性的,并将任何副作用或非确定性操作包装在任务 (task) 内。您可以从StateGraph (图 API) 和函数式 API 中使用任务 (task)。
要求¶
要在 LangGraph 中利用持久执行,您需要:
- 通过指定用于保存工作流进度的检查点 (checkpointer),在工作流中启用持久化。
- 在执行工作流时指定线程标识符。这将跟踪特定工作流实例的执行历史。
- 将任何非确定性操作(例如,生成随机数)或带有副作用的操作(例如,文件写入、API 调用)包装在任务 (task) 内,以确保在工作流恢复时,这些操作不会在特定运行中重复执行,而是从持久化层检索其结果。更多信息,请参阅确定性与一致性重放。
确定性与一致性重放¶
当您恢复工作流运行时,代码**不会**从执行停止的**同一行代码**恢复;相反,它会识别一个合适的起点,从该点继续执行。这意味着工作流将从该起点重放所有步骤,直到到达停止的点。
因此,当您为持久执行编写工作流时,必须将任何非确定性操作(例如,生成随机数)以及任何带有副作用的操作(例如,文件写入、API 调用)包装在任务 (task) 或节点 (node) 内。
为确保您的工作流具有确定性并能一致地重放,请遵循以下指南:
- 避免重复工作:如果一个节点 (node) 包含多个带有副作用的操作(例如,日志记录、文件写入或网络调用),请将每个操作包装在一个单独的任务 (task) 中。这确保了当工作流恢复时,这些操作不会重复执行,而是从持久化层检索其结果。
- 封装非确定性操作: 将任何可能产生非确定性结果(例如,生成随机数)的代码包装在任务 (task) 或节点 (node) 内。这确保了在恢复时,工作流遵循完全相同的记录步骤序列并产生相同的结果。
- 使用幂等操作:如果可能,请确保副作用(例如,API 调用、文件写入)是幂等的。这意味着如果在工作流中失败后重试某个操作,其效果将与首次执行时相同。这对于导致数据写入的操作尤其重要。如果一个任务 (task) 启动但未能成功完成,工作流恢复时将重新运行该任务 (task),并依赖记录的结果来保持一致性。使用幂等键或验证现有结果,以避免意外重复,从而确保工作流执行顺利且可预测。
对于一些需要避免的常见陷阱示例,请参阅函数式 API 中的常见陷阱部分,该部分展示了如何使用任务 (task) 来构建代码以避免这些问题。同样的原则也适用于StateGraph (图 API)。
在节点中使用任务¶
如果一个节点 (node) 包含多个操作,您可能会发现将每个操作转换为一个任务 (task) 比将这些操作重构为独立的节点更容易。
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
import requests
# Define a TypedDict to represent the state
class State(TypedDict):
url: str
result: NotRequired[str]
def call_api(state: State):
"""Example node that makes an API request."""
result = requests.get(state['url']).text[:100] # Side-effect
return {
"result": result
}
# Create a StateGraph builder and add a node for the call_api function
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# Connect the start and end nodes to the call_api node
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
# Define a config with a thread ID.
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# Invoke the graph
graph.invoke({"url": "https://www.example.com"}, config)
from typing import NotRequired
from typing_extensions import TypedDict
import uuid
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import task
from langgraph.graph import StateGraph, START, END
import requests
# Define a TypedDict to represent the state
class State(TypedDict):
urls: list[str]
result: NotRequired[list[str]]
@task
def _make_request(url: str):
"""Make a request."""
return requests.get(url).text[:100]
def call_api(state: State):
"""Example node that makes an API request."""
requests = [_make_request(url) for url in state['urls']]
results = [request.result() for request in requests]
return {
"results": results
}
# Create a StateGraph builder and add a node for the call_api function
builder = StateGraph(State)
builder.add_node("call_api", call_api)
# Connect the start and end nodes to the call_api node
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)
# Specify a checkpointer
checkpointer = MemorySaver()
# Compile the graph with the checkpointer
graph = builder.compile(checkpointer=checkpointer)
# Define a config with a thread ID.
thread_id = uuid.uuid4()
config = {"configurable": {"thread_id": thread_id}}
# Invoke the graph
graph.invoke({"urls": ["https://www.example.com"]}, config)
恢复工作流¶
在工作流中启用持久执行后,您可以恢复执行以下场景:
- 暂停和恢复工作流:使用 interrupt 函数在特定点暂停工作流,并使用 Command 原语以更新的状态恢复它。更多详细信息请参阅人在回路。
- 从故障中恢复: 在异常(例如,LLM 提供商中断)后,从最后一个成功的检查点自动恢复工作流。这涉及使用相同的线程标识符执行工作流,并为其提供一个
None
作为输入值(请参阅函数式 API 中的此示例)。
恢复工作流的起点¶
- 如果您使用StateGraph (图 API),起点是执行停止的节点 (node) 的开始。
- 如果您在节点内调用子图,起点将是调用已停止子图的父节点。在子图内部,起点将是执行停止的特定节点 (node)。
- 如果您使用函数式 API,起点是执行停止的入口点 (entrypoint) 的开始。