【Agent的革命之路——LangGraph】人机交互中的interrupt&Command

1,630 阅读6分钟

在这里插入图片描述LangGraph 中,实现人机交互最核心的就是 Commandinterrupt,就是前面设计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
    }

搭配 invokeainvoke 一起使用

当我们使用 streamastream 运行图 (graph)时,我们将收到一个 Interrupt 事件,让我们知道 interrupt 已触发。但是 invokeainvoke 不会返回中断信息。要访问此信息,我们必须在调用 invokeainvoke 后使用 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 恢复与 Pythoninput() 函数不同,后者从调用 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

注意事项

  1. 假如我们在节点需要 API 调用,则应该放在 interrupt 之后以避免重复,因为每次节点恢复时都会重新触发这些代码。
  2. 当我们将子图作为函数调用时,父图将从调用子图的节点的开头(以及触发 interrupt 的位置)恢复执行。同样,子图 , 将从调用 interrupt() 函数的节点的开头开始恢复。
def node_in_parent_graph(state: State):
    some_code()  # <-- 当子图恢复时这将重新执行。
    # 将子图作为函数调用。子图包含“interrupt”调用。
    subgraph_result = subgraph.invoke(some_input)
    ...
  1. 使用多个中断 ,如果我们在单个节点内使用多个 interrupt 对于验证人工输入等模式很有帮助。但是,如果处理不当,在同一节点中使用多个中断可能会导致问题行为。 当一个节点包含多个 interrupt 调用时,LangGraph 会保留一个特定于执行该节点的任务的恢复值列表。每当执行恢复时,它都会从节点的开头开始。对于遇到的每个中断,LangGraph 都会检查任务的恢复列表中是否存在匹配的值。匹配严格基于索引,因此节点内中断调用的顺序至关重要。 为了避免出现问题,我们需要避免在执行之间动态更改节点的结构。这包括添加、删除或重新排序中断调用,因为这种情况的更改可能会导致索引不匹配。这些问题通常是由非常规模式引起的,例如通过 Command(resume=..., update=SOME_STATE_MUTATION) 改变状态或依赖全局变量动态修改节点的结构。 大家在使用 interrupt 的时候一定要注意顺序,还有节点的返回格式,让我们以后在处理交互逻辑的时候不会出现问题。