在langgraph很多的人把Durable Execution译做持久化执行,容易与前面将的持久化混淆,其本意是当程序在某个点中断,可能是有意为之,比如让用户介入,也可能是无意为之,比如调用远程大模型超时,langgraph提供了机制,保证可以从中断点进行执行,更像下载时的断点续传,所以感觉翻译成接续运行更贴切。
1.接续一致性问题
为了理解接续运行机理,首先必须弄明白从中断中恢复运行时接续点在哪里?在langgraph中,并非从中断执行的那行代码开始,针对以下三种不同的情况,接续点定义如下:
1)当使用Graph API时,接续点为发生中断的节点的第一行代码
2)当在一个节点内部调用一个子图发生中断时,接续点为调用子图节点的父节点,而在子图内部恢复运行时从发生中断的子图节点第一行代码开始
3)当使用函数式API时,接续点为发生中断的主函数的第一行代码
从以上分析可知,在恢复运行时的接续点与发生中断的代码之间有其他的操作步骤,工作流恢复运行时会重放起始点到中断代码之间的所有步骤,因此必须保证重放的步骤的执行结果与中断前的执行结果完全一致。
以下三种情况会导致接续的不一致性,
1)非确定性操作,比如生成随机数,每次调用会生成不同的随机数;
2)有副作用的操作,比如写文件;
3)第三就是非幂等操作,
这三类操作如果已经执行了一次,当从中断中恢复运行时,如果再次执行,则产生不可预料的后果。
2.实现接续一致性
为实现接续一致性,针对上面所说的导致接续不一致性的三种情况采取不同的处理策略:
1)针对非确定性操作,封装到一个任务或节点内,从而借助langgraph的持久化层保存运行的结果
2)针对有副作用的操作,封装到单独的任务中,从而在接续运行时不再重复执行这些任务
3)针对非幂等的操作,通过使用幂等的键并根据已有数据进行验证,实现操作幂等
2.1非确定性操作封装
以下代码为包含非确定性操作的示例。如果发生中断后,再次进入时random会是一个与中断前执行不同的值,执行的分支与第一次可能是不同的。
from langgraph.func import entrypoint
import random@entrypoint(checkpointer=checkpointer)
def workflow(inputs: dict) -> int:
input_value = inputs["input_value"]
random = random.randint(1, 10)if random > input_value:
result = slow_task(1).result()
value = interrupt("question")
else:
result = slow_task(2).result()
value = interrupt("question")return {
"result": result,
"value": value
}
以下代码把获取随机数非确定性操作封装到一个任务内,因为任务的执行结果会被持久化,所以如果传入的参数中input_value值不变,random从持久化层获取,也是不变的,所以执行路径也是不变的,具体如下:
from langgraph.func import entrypoint
import random
@task
def get_random():#该任务用于获取随机数
return random.randint(1, 10)
@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
input_value = inputs["input_value"]
random = get_random().result()if random > input_value:
result = slow_task(1).result()
value = interrupt("question")
else:
result = slow_task(2).result()
value = interrupt("question")return {
"result": result,
"value": value
}
2.2有副作用操作封装
以下代码访问一个网络地址并把结果保存在result中,如果发生中断后恢复执行,还需要再次请求同一个地址:
#只有一个节点的图,在节点中访问一个网络地址
from typing_extensions import TypedDict, NotRequired
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
import requestsclass State(TypedDict):
url: str
result: NotRequired[str]
def my_node(state: State):
result = requests.get(state['url']).text[:1000]
return {
"result": result
}
checkpointer = InMemorySaver()
graph_builder = StateGraph(State)
graph_builder.add_node("my_node", my_node)
graph_builder.add_edge(START, "my_node")
graph_builder.add_edge("my_node", END)
graph = graph_builder.compile(checkpointer=checkpointer)config = {"configurable": {"thread_id": 1}}
result = graph.invoke({'url': 'https://www.baidu.com'}, config=config)
print(result)
以下代码把网络请求封装到一个任务内,从而接续运行时不需再次进行网络请求:
from typing_extensions import TypedDict, NotRequired
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
import requestsclass State(TypedDict):
url: str
result: NotRequired[str]
@task
def http_request(url: str)->str:
return requests.get(url).text[:1000]
def my_node(state: State):
result = http_request(state['url']).result()
return {
"result": result
}
checkpointer = InMemorySaver()
graph_builder = StateGraph(State)
graph_builder.add_node("my_node", my_node)
graph_builder.add_edge(START, "my_node")
graph_builder.add_edge("my_node", END)
graph = graph_builder.compile(checkpointer=checkpointer)config = {"configurable": {"thread_id": 1}}
result = graph.invoke({'url': 'https://www.baidu.com'}, config=config)
print(result)
2.3非幂等操作处理
把非幂等操作转换为幂等操作的思路与传统编程思路一致,此处不再举例。
3.恢复工作流
针对实现了继续一致性的工作流,可以从中断或者错误中恢复工作流的运行。
1)从中断中恢复
在工作流中调用interrupt暂停工作流的执行,并等待用户的输入。用户输入后,使用携带更新后状态的Command继续执行工作流,并且保证与预期的一致
2)从错误中恢复
如果在执行工作流时报错了,重新执行工作流时(调用invoke、stream或者astrem),调用的输入传入None,并且携带上次执行工作流相同的thread_id
4.持久化模式
langgraph基于持久化实现了接续运行中的数据一致性,langgraph同时提供了三种持久化模式供程序员选择,三种持久化模式是性能和一致性的权衡,程序员可根据具体的需求做选择。
三种持久化模式为:
1)exit:整个工作流执行全部完成后才做持久化,中间状态并不保存。该模式适合需要长时间运行的任务,但不能支持终端,也不支持从错误中恢复
2)async:执行完一个超步后,在执行下一个超步时,对本超步状态进行持久化。采该改模式时,如果在执行一个超步时程序崩溃,前一超步的状态可能未持久化
3)sync:执行完一个超步后,在执行下一个超步前,对本超步的状态进行持久化。该模式提供了最高级别的持久化,但带来很大的性能消耗
390

被折叠的 条评论
为什么被折叠?



