在
LangGraph 中,实现人机交互最核心的就是 Command 和 interrupt,就是前面设计4种交互模式所使用的共同方法,简单点说就是中断图,获取人类提供的值,再恢复图。我们这次就来详细分析一下这两个语法。
Command
Command是一个类,通过一个或多个命令来更新图的状态并向节点发送消息,它支持4个参数如下:
graph:支持的值包括:不传默认当前图表,传PARENT表示最近的父图update:更新以应用于图形的状态。resume:用来恢复执行的值。与 一起使用interrupt()。goto:后面的值可以是 1.导航到的节点的名称(属于指定的任何节点graph), 2.航到下一个节点名称序列, 3.Send对象(使用提供的输入执行节点) 4.Send对象序列
Command 提供了几个选项来在恢复期间控制和修改图的状态:
1.将值传递给 interrupt :使用 Command(resume=value) 向图 (graph)提供数据,例如用户的响应。从正在使用 interrupt 的节点的开头恢复执行,但是,这次 interrupt() 调用将返回 Command(resume=value) 中传递的值,而不是暂停图(graph)。
graph.invoke(Command(resume={"age": "25"}), thread_config)
2.更新图的状态:使用 Command(update=update) 修改图形状态。这里要注意哦,恢复从使用 interrupt 的节点的开头开始的。从使用 interrupt 的节点的开头恢复执行,并且状态已更新。
graph.invoke(Command(update={"foo": "bar"}, resume="Let's go!!!"), thread_config)
我们就是利用 Command 的这种机制,做到恢复图执行、处理用户输入并动态调整图的状态。
interrupt
interrupt是一个函数,它使用节点内部的可恢复异常来中断图表。我们使用interrupt函数目的是通过暂停图(graph)的执行并向客户端显示一个值来实现人机交互工作流。该值可以传达上下文或请求恢复执行所需的输入。在给定节点中,第一次调用此函数会引发GraphInterrupt异常,从而停止执行。提供的value包含异常并发送给执行图表的客户端。 恢复图(graph)的客户端必须使用Command对象来指定中断和继续执行的值。图(graph)从节点的开头恢复,重新执行所有逻辑。如果节点包含多个interrupt调用,LangGraph会根据节点中的顺序将恢复值与中断进行匹配。此恢复值列表的范围仅限于执行节点的特定任务,不会在任务之间共享。要使用interrupt,您必须启用检查点,因为该功能依赖于持久图的状态。 下面是一个interrupt结合Command的使用方式:
import uuid
from typing import Optional
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.constants import START
from langgraph.graph import StateGraph
from langgraph.types import interrupt
class State(TypedDict):
foo: str
human_value: Optional[str]
def node(state: State):
answer = interrupt(
"你叫什么名字?"
)
print(f"> 从 interrupt 里面接收一个输入值: {answer}")
return {"human_value": answer}
builder = StateGraph(State)
builder.add_node("node", node)
builder.add_edge(START, "node")
# A必须启用检查点
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {
"configurable": {
"thread_id": uuid.uuid4(),
}
}
for chunk in graph.stream({"foo": "abc"}, config):
print(chunk)
# 使用Command恢复
command = Command(resume="来自人类的输入")
for chunk in graph.stream(Command(resume="来自人类的输入"), config):
print(chunk)
了解了上述基本概念,我们下面尝试来添加一个人类节点,可以在图内验证人类提供的输入,我们通过单个节点内使用多个中断调用来实现此目的。
from langgraph.types import interrupt
def human_node(state: State):
"""Human node with validation."""
question = "你的工龄是多少?"
while True:
answer = interrupt(question)
# 验证答案
if not isinstance(answer, int) or answer < 0:
question = f"'{answer} 不是一个有效的工龄。你的工龄是多少?"
answer = None
continue
else:
# 我们继续执行
break
print(f"T这个人的工龄是 {answer} 年.")
return {
"age": answer
}
搭配 invoke 和 ainvoke 一起使用
当我们使用 stream 或 astream 运行图 (graph)时,我们将收到一个 Interrupt 事件,让我们知道 interrupt 已触发。但是 invoke 和 ainvoke 不会返回中断信息。要访问此信息,我们必须在调用 invoke 或 ainvoke 后使用 get_state 方法检索图形状态。
# 运行 graph 收到 interrupt 事件
result = graph.invoke(inputs, thread_config)
# 通过 graph 的状态获得 interrupt 信息
state = graph.get_state(thread_config)
# 打印
print(state.values)
# 打印任务
print(state.tasks)
# 通过人类的输入恢复这个图继续执行
graph.invoke(Command(resume={"age": "25"}), thread_config)
从中断中恢复是如何工作的?
从 interrupt 恢复与 Python 的 input() 函数不同,后者从调用 input() 函数的确切位置恢复执行。
我们来了解下具体的工作原理,当我们在 interrupt 之后恢复执行时,图(graph)执行会从触发最后一个 interrupt 的图节点的开头开始。触发最后一个 interrupt 的图节点开始到 interrupt 的所有代码都会被重新执行。
我们为了更清晰了解上面的情况,简单做个例子:
counter = 0
def node(state: State):
# 当图恢复时从节点开始到中断处的所有代码都会被重新执行
global counter
counter += 1
print(f"> Entered the node: {counter} # of times")
# 暂停这个图,直到有人类输入
answer = interrupt()
print("The value of counter is:", counter)
得到下面结果:
Entered the node: 2 # of times
The value of counter is: 2
注意事项
- 假如我们在节点需要
API调用,则应该放在interrupt之后以避免重复,因为每次节点恢复时都会重新触发这些代码。 - 当我们将子图作为函数调用时,父图将从调用子图的节点的开头(以及触发
interrupt的位置)恢复执行。同样,子图 , 将从调用interrupt()函数的节点的开头开始恢复。
def node_in_parent_graph(state: State):
some_code() # <-- 当子图恢复时这将重新执行。
# 将子图作为函数调用。子图包含“interrupt”调用。
subgraph_result = subgraph.invoke(some_input)
...
- 使用多个中断 ,如果我们在单个节点内使用多个
interrupt对于验证人工输入等模式很有帮助。但是,如果处理不当,在同一节点中使用多个中断可能会导致问题行为。 当一个节点包含多个interrupt调用时,LangGraph会保留一个特定于执行该节点的任务的恢复值列表。每当执行恢复时,它都会从节点的开头开始。对于遇到的每个中断,LangGraph都会检查任务的恢复列表中是否存在匹配的值。匹配严格基于索引,因此节点内中断调用的顺序至关重要。 为了避免出现问题,我们需要避免在执行之间动态更改节点的结构。这包括添加、删除或重新排序中断调用,因为这种情况的更改可能会导致索引不匹配。这些问题通常是由非常规模式引起的,例如通过Command(resume=..., update=SOME_STATE_MUTATION)改变状态或依赖全局变量动态修改节点的结构。 大家在使用interrupt的时候一定要注意顺序,还有节点的返回格式,让我们以后在处理交互逻辑的时候不会出现问题。