跳到内容

人机环路

人机环路(或“环内”)通过几种常见的用户交互模式增强了代理的功能。

常见的交互模式包括

(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" 作为具有状态更新的节点,以指定状态更新应被视为节点

这很微妙,但很重要

通过编辑,用户可以决定是否编辑图状态。

通过输入,我们在图中显式定义一个节点来收集人工输入!

然后,包含人工输入的状态更新作为此节点运行。

// 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) 在上述任何一种情况下,手动探索替代决策路径都很有用。

总的来说,我们将这些调试概念称为 时间旅行,它们由 重放派生 组成。

重放

有时我们希望简单地重放代理的过去操作。

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

我们通过简单地传入 null 作为带有 threadConfig 的输入来做到这一点。

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);
}

有关派生的相关背景信息,请参阅此额外的概念指南

请参阅本指南,了解有关如何执行时间旅行的详细说明!