人机交互循环¶
人机交互循环(或“循环中”)通过几种常见的用户交互模式增强代理功能。
常见的交互模式包括
(1) 批准
- 我们可以中断我们的代理,向用户展示当前状态,并允许用户接受操作。
(2) 编辑
- 我们可以中断我们的代理,向用户展示当前状态,并允许用户编辑代理状态。
(3) 输入
- 我们可以显式地创建一个图节点来收集人工输入,并将该输入直接传递给代理状态。
这些交互模式的用例包括
(1) 审查工具调用
- 我们可以中断代理以审查和编辑工具调用的结果。
(2) 时间旅行
- 我们可以手动回放和/或分支代理过去的动作。
持久化¶
所有这些交互模式都由 LangGraph 的内置持久化层启用,该层将在每个步骤写入图状态的检查点。持久化允许图停止,以便人工审查和/或编辑图的当前状态,然后继续执行人工输入。
断点¶
在图流程中的特定位置添加断点是启用人机交互循环的一种方法。在这种情况下,开发人员知道工作流程中需要人工输入的位置,只需在该特定图节点之前或之后放置一个断点。
在这里,我们使用检查点和断点(在我们想要中断之前的节点step_for_human_in_the_loop
)编译我们的图。然后,我们执行上述交互模式之一,如果人工编辑了图状态,则会创建一个新的检查点。新检查点保存到thread
中,我们可以通过传入None
作为输入来从那里恢复图执行。
# Compile our graph with a checkpoitner and a breakpoint before "step_for_human_in_the_loop"
graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["step_for_human_in_the_loop"])
# Run the graph up to the breakpoint
thread_config = {"configurable": {"thread_id": "1"}}
for event in graph.stream(inputs, thread_config, stream_mode="values"):
print(event)
# Perform some action that requires human in the loop
# Continue the graph execution from the current checkpoint
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
动态断点¶
或者,开发人员可以定义一些必须满足的条件才能触发断点。这个动态断点的概念在开发人员希望在特定条件下停止图时非常有用。这使用NodeInterrupt
,这是一种特殊的异常类型,可以根据某些条件从节点内部引发。例如,我们可以定义一个动态断点,当input
长度超过 5 个字符时触发。
def my_node(state: State) -> State:
if len(state['input']) > 5:
raise NodeInterrupt(f"Received input that is longer than 5 characters: {state['input']}")
return state
假设我们使用触发动态断点的输入运行图,然后尝试仅通过传入None
作为输入来恢复图执行。
# Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
图将再次中断,因为此节点将使用相同的图状态重新运行。我们需要更改图状态,以便不再满足触发动态断点的条件。因此,我们可以简单地将图状态编辑为满足动态断点条件(< 5 个字符)的输入,然后重新运行节点。
# Update the state to pass the dynamic breakpoint
graph.update_state(config=thread_config, values={"input": "foo"})
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
或者,如果我们想保留当前输入并跳过执行检查的节点(my_node
)怎么办?为此,我们可以简单地使用as_node="my_node"
执行图更新,并为值传入None
。这将不会更新图状态,但会将更新作为my_node
运行,有效地跳过节点并绕过动态断点。
# This update will skip the node `my_node` altogether
graph.update_state(config=thread_config, values=None, as_node="my_node")
for event in graph.stream(None, thread_config, stream_mode="values"):
print(event)
请参阅我们的指南,了解有关执行此操作的分步指南!
交互模式¶
批准¶
有时我们希望批准代理执行中的某些步骤。
我们可以在要批准的步骤之前的断点处中断我们的代理。
这通常推荐用于敏感操作(例如,使用外部 API 或写入数据库)。
通过持久化,我们可以向用户展示当前代理状态以及下一步,以供其审查和批准。
如果批准,图将从上次保存的检查点(保存到thread
中)恢复执行。
# Compile our graph with a checkpoitner and a breakpoint before the step to approve
graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["node_2"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# ... Get human approval ...
# If approved, continue the graph execution from the last saved checkpoint
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
请参阅我们的指南,了解有关执行此操作的分步指南!
编辑¶
有时我们希望审查和编辑代理的状态。
与批准一样,我们可以在要检查的步骤之前的断点处中断我们的代理。
我们可以向用户显示当前状态,并允许用户编辑代理状态。
例如,这可以用来更正代理的错误(例如,参见下面关于工具调用的部分)。
我们可以通过分支当前检查点(保存到thread
中)来编辑图状态。
然后,我们可以像以前一样从分支检查点继续执行图。
# Compile our graph with a checkpoitner and a breakpoint before the step to review
graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["node_2"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Review the state, decide to edit it, and create a forked checkpoint with the new state
graph.update_state(thread, {"state": "new state"})
# Continue the graph execution from the forked checkpoint
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
请参阅本指南,了解有关执行此操作的分步指南!
输入¶
有时我们希望在图中的特定步骤显式地获取人工输入。
我们可以为此创建一个指定的图节点(例如,我们示例图中的human_input
)。
与批准和编辑一样,我们可以在此节点之前的断点处中断我们的代理。
然后,我们可以执行包含人工输入的状态更新,就像我们对编辑状态所做的那样。
但是,我们添加了一件事
我们可以使用as_node=human_input
和状态更新来指定状态更新应被视为节点。
这很微妙,但很重要
在编辑中,用户决定是否编辑图状态。
在输入中,我们明确地在图中定义了一个用于收集人工输入的节点!
然后,带有人工输入的状态更新将作为此节点运行。
# Compile our graph with a checkpoitner and a breakpoint before the step to to collect human input
graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["human_input"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Update the state with the user input as if it was the human_input node
graph.update_state(thread, {"user_input": user_input}, as_node="human_input")
# Continue the graph execution from the checkpoint created by the human_input node
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
请参阅本指南,了解有关执行此操作的分步指南!
用例¶
审查工具调用¶
一些用户交互模式结合了上述思想。
例如,许多代理使用工具调用来做出决策。
工具调用提出了一个挑战,因为代理必须正确处理两件事
(1) 要调用的工具的名称
(2) 要传递给工具的参数
即使工具调用是正确的,我们也可能希望应用谨慎
(3) 工具调用可能是我们希望批准的敏感操作
考虑到这些要点,我们可以结合上述想法来创建对工具调用的交互循环审查。
# Compile our graph with a checkpoitner and a breakpoint before the step to to review the tool call from the LLM
graph = builder.compile(checkpointer=checkpoitner, interrupt_before=["human_review"])
# Run the graph up to the breakpoint
for event in graph.stream(inputs, thread, stream_mode="values"):
print(event)
# Review the tool call and update it, if needed, as the human_review node
graph.update_state(thread, {"tool_call": "updated tool call"}, as_node="human_review")
# Otherwise, approve the tool call and proceed with the graph execution with no edits
# Continue the graph execution from either:
# (1) the forked checkpoint created by human_review or
# (2) the checkpoint saved when the tool call was originally made (no edits in human_review)
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
请参阅本指南,了解有关执行此操作的分步指南!
时间旅行¶
在使用代理时,我们经常希望仔细检查其决策过程
(1) 即使他们获得了所需的最终结果,导致该结果的推理通常也很重要。
(2) 当代理出错时,了解原因通常很有价值。
(3) 在上述任何一种情况下,手动探索替代决策路径都很有用。
总的来说,我们将这些调试概念称为时间旅行
,它们由回放
和分支
组成。
回放¶
有时我们只想回放代理过去的动作。
上面,我们展示了从图的当前状态(或检查点)执行代理的情况。
我们只需使用thread
传入None
作为输入即可。
thread = {"configurable": {"thread_id": "1"}}
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
现在,我们可以修改它以从特定检查点回放过去的动作,方法是传入检查点 ID。
要获取特定的检查点 ID,我们可以轻松获取线程中的所有检查点并过滤到我们想要的那个。
每个检查点都有一个唯一的 ID,我们可以用它从特定检查点回放。
假设从审查检查点中,我们想从一个xxx
回放。
我们只需在运行图时传入检查点 ID。
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
重要的是,图知道哪些检查点已先前执行。
因此,它将重新播放任何先前执行的节点,而不是重新执行它们。
请参阅本其他概念指南,了解有关回放的相关上下文。
请参阅本指南,了解有关执行时间旅行的分步指南!
分支¶
有时我们希望分支代理过去的动作,并探索图中的不同路径。
如上所述,编辑
正是我们对图的当前状态执行此操作的方式!
但是,如果我们想分支图的过去状态怎么办?
例如,假设我们想编辑特定的检查点xxx
。
我们在更新图的状态时传入此checkpoint_id
。
config = {"configurable": {"thread_id": "1", "checkpoint_id": "xxx"}}
graph.update_state(config, {"state": "updated state"}, )
这将创建一个新的分支检查点xxx-fork
,然后我们可以从中运行图。
config = {'configurable': {'thread_id': '1', 'checkpoint_id': 'xxx-fork'}}
for event in graph.stream(None, config, stream_mode="values"):
print(event)
请参阅本其他概念指南,了解有关分支的相关上下文。
请参阅本指南,了解有关执行时间旅行的分步指南!