跳到内容

人在回路中

人在回路中(或“在回路中”)通过几种常见的用户交互模式增强智能体的能力。

常见的交互模式包括:

(1) 审批 - 我们可以中断智能体,向用户展示当前状态,并允许用户接受某个动作。

(2) 编辑 - 我们可以中断智能体,向用户展示当前状态,并允许用户编辑智能体的状态。

(3) 输入 - 我们可以显式创建一个图节点来收集人类输入,并将该输入直接传递给智能体状态。

这些交互模式的用例包括:

(1) 审查工具调用 - 我们可以中断智能体以审查和编辑工具调用的结果。

(2) 时光旅行 - 我们可以手动回放和/或分叉智能体的过去动作。

持久性

所有这些交互模式都由 LangGraph 内置的持久性层启用,该层将在每个步骤写入图状态的检查点。持久性允许图停止,以便人类可以审查和/或编辑图的当前状态,然后以人类输入恢复。

断点

在图流程的特定位置添加断点是启用人在回路中的一种方式。在这种情况下,开发者知道工作流中哪里需要人类输入,并在特定图节点之前或之后简单地放置一个断点。

在这里,我们用一个检查点和一个断点编译图,断点位于我们想要在其之前中断的节点,即 step_for_human_in_the_loop。然后我们执行上述交互模式之一,如果人类编辑了图状态,这将创建一个新的检查点。新的检查点被保存到线程中,我们可以通过传入 null 作为输入从那里恢复图的执行。

// Compile our graph with a checkpointer and a breakpoint before "step_for_human_in_the_loop"
const graph = builder.compile({ checkpointer, interruptBefore: ["step_for_human_in_the_loop"] });

// Run the graph up to the breakpoint
const threadConfig = { configurable: { thread_id: "1" }, streamMode: "values" as const };
for await (const event of await graph.stream(inputs, threadConfig)) {
    console.log(event);
}

// Perform some action that requires human in the loop

// Continue the graph execution from the current checkpoint 
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

动态断点

或者,开发者可以定义一个必须满足的条件来触发断点。这种动态断点的概念在开发者希望在特定条件下暂停图时非常有用。这使用NodeInterrupt,这是一种特殊的错误类型,可以根据某些条件从节点内部抛出。例如,我们可以定义一个动态断点,当 input 长度大于 5 个字符时触发。

function myNode(state: typeof GraphAnnotation.State): typeof GraphAnnotation.State {
    if (state.input.length > 5) {
        throw new NodeInterrupt(`Received input that is longer than 5 characters: ${state['input']}`);
    }
    return state;
}

假设我们运行图,输入触发动态断点,然后尝试简单地传入 null 作为输入来恢复图执行。

// Attempt to continue the graph execution with no change to state after we hit the dynamic breakpoint 
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

图将再次中断,因为这个节点将使用相同的图状态重新运行。我们需要更改图状态,使得触发动态断点的条件不再满足。所以,我们可以简单地将图状态编辑为一个满足动态断点条件的输入(小于 5 个字符)并重新运行该节点。

// Update the state to pass the dynamic breakpoint
await graph.updateState(threadConfig, { input: "foo" });
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

或者,如果我们想保留当前的输入并跳过执行检查的节点(myNode)怎么办?为此,我们可以简单地执行图更新,将 "myNode" 作为第三个位置参数,并为值传入 null。这不会对图状态进行任何更新,但会将更新作为 myNode 运行,从而有效地跳过该节点并绕过动态断点。

// This update will skip the node `myNode` altogether
await graph.updateState(threadConfig, null, "myNode");
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

请参阅我们的指南,了解如何详细操作!

交互模式

审批

有时我们希望审批智能体执行中的某些步骤。

我们可以在要审批的步骤之前的断点处中断智能体。

这通常建议用于敏感操作(例如,使用外部 API 或写入数据库)。

通过持久性,我们可以向用户展示当前的智能体状态以及下一步,供其审查和审批。

如果获得批准,图将从上次保存的检查点恢复执行,该检查点已保存到线程中。

// Compile our graph with a checkpointer and a breakpoint before the step to approve
const graph = builder.compile({ checkpointer, interruptBefore: ["node_2"] });

// Run the graph up to the breakpoint
for await (const event of await graph.stream(inputs, threadConfig)) {
    console.log(event);
}

// ... Get human approval ...

// If approved, continue the graph execution from the last saved checkpoint
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

请参阅我们的指南,了解如何详细操作!

编辑

有时我们希望审查和编辑智能体的状态。

与审批一样,我们可以在要检查的步骤之前的断点处中断智能体。

我们可以向用户展示当前状态,并允许用户编辑智能体状态。

例如,这可以用于纠正智能体犯的错误(例如,参见下面的工具调用部分)。

我们可以通过分叉当前检查点来编辑图状态,该检查点已保存到线程中。

然后,我们可以像之前一样从分叉的检查点继续运行图。

// Compile our graph with a checkpointer and a breakpoint before the step to review
const graph = builder.compile({ checkpointer, interruptBefore: ["node_2"] });

// Run the graph up to the breakpoint
for await (const event of await graph.stream(inputs, threadConfig)) {
    console.log(event);
}

// Review the state, decide to edit it, and create a forked checkpoint with the new state
await graph.updateState(threadConfig, { state: "new state" });

// Continue the graph execution from the forked checkpoint
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

请参阅此指南,了解如何详细操作!

输入

有时我们希望在图中的特定步骤显式获取人类输入。

我们可以创建一个为此目的设计的图节点(例如,示例图中的 human_input)。

与审批和编辑一样,我们可以在此节点之前的断点处中断智能体。

然后我们可以执行一个包含人类输入的状态更新,就像我们编辑状态时所做的那样。

但是,我们再添加一件事:

我们可以使用 "human_input" 作为带有状态更新的节点,以指定状态更新应被视为一个节点

这很微妙,但很重要:

在编辑模式下,用户决定是否编辑图状态。

在输入模式下,我们在图中显式定义一个节点来收集人类输入!

带有 B 类输入的 A 类状态更新然后会作为此节点运行。

// Compile our graph with a checkpointer and a breakpoint before the step to collect human input
const graph = builder.compile({ checkpointer, interruptBefore: ["human_input"] });

// Run the graph up to the breakpoint
for await (const event of await graph.stream(inputs, threadConfig)) {
    console.log(event);
}

// Update the state with the user input as if it was the human_input node
await graph.updateState(threadConfig, { user_input: userInput }, "human_input");

// Continue the graph execution from the checkpoint created by the human_input node
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

请参阅此指南,了解如何详细操作!

用例

审查工具调用

一些用户交互模式结合了上述概念。

例如,许多智能体使用工具调用来做出决策。

工具调用带来了挑战,因为智能体必须正确处理两件事:

(1) 要调用的工具名称

(2) 传递给工具的参数

即使工具调用正确,我们也可能希望谨慎行事:

(3) 工具调用可能是一个我们想要批准的敏感操作

考虑到这些要点,我们可以结合上述概念,创建一个对工具调用进行人在回路中审查的流程。

// Compile our graph with a checkpointer and a breakpoint before the step to review the tool call from the LLM 
const graph = builder.compile({ checkpointer, interruptBefore: ["human_review"] });

// Run the graph up to the breakpoint
for await (const event of await graph.stream(inputs, threadConfig)) {
    console.log(event);
}

// Review the tool call and update it, if needed, as the human_review node
await graph.updateState(threadConfig, { tool_call: "updated tool call" }, "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 await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

请参阅此指南,了解如何详细操作!

时光旅行

在使用智能体时,我们经常希望仔细检查其决策过程:

(1) 即使它们达到了期望的最终结果,导向该结果的推理过程通常也很重要,需要检查。

(2) 当代理犯错时,理解其原因常常很有价值。

(3) 在上述任一情况下,手动探索替代的决策路径都很有用。

总而言之,我们将这些调试概念称为 时光旅行,它们由 回放分叉 组成。

回放

有时我们只是想回放智能体的过去动作。

上面我们展示了从图的当前状态(或检查点)执行智能体的情况。

我们只需在 threadConfig 中为输入传入 null 即可实现这一点。

const threadConfig = { configurable: { thread_id: "1" } };
for await (const event of await graph.stream(null, threadConfig)) {
    console.log(event);
}

现在,我们可以修改此操作,通过传入检查点 ID,从特定检查点回放过去的动作。

要获取特定检查点 ID,我们可以轻松地获取线程中的所有检查点并过滤到我们想要的那个。

const allCheckpoints = [];
for await (const state of app.getStateHistory(threadConfig)) {
    allCheckpoints.push(state);
}

每个检查点都有一个唯一的 ID,我们可以使用它来从特定检查点回放。

假设我们审查检查点后,想从检查点 xxx 开始回放。

我们在运行图时只需传入该检查点 ID。

const config = { configurable: { thread_id: '1', checkpoint_id: 'xxx' }, streamMode: "values" as const };
for await (const event of await graph.stream(null, config)) {
    console.log(event);
}

重要的是,图知道哪些检查点已经执行过。

因此,它将回放任何之前执行过的节点,而不是重新执行它们。

请参阅此额外的概念指南,了解有关回放的相关背景信息。

请参阅此指南,了解如何详细操作时光旅行!

分叉

有时我们希望分叉智能体的过去动作,并探索图中的不同路径。

如上所述,编辑 正是我们为图的当前状态进行此操作的方式!

但是,如果我们想分叉图的过去状态怎么办?

例如,假设我们想编辑特定检查点 xxx

我们在更新图的状态时传入此 checkpoint_id

const config = { configurable: { thread_id: "1", checkpoint_id: "xxx" } };
await graph.updateState(config, { state: "updated state" });

这将创建一个新的分叉检查点 xxx-fork,然后我们可以从此检查点运行图。

const config = { configurable: { thread_id: '1', checkpoint_id: 'xxx-fork' }, streamMode: "values" as const };
for await (const event of await graph.stream(null, config)) {
    console.log(event);
}

请参阅此额外的概念指南,了解有关分叉的相关背景信息。

请参阅此指南,了解如何详细操作时光旅行!