跳至内容

人机协作

人机协作(或“在循环中”)通过几种常见的用户交互模式增强了智能体的能力。

常见的交互模式包括

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

总的来说,我们称这些调试概念为时间旅行,它们由回放分叉组成。

回放

有时我们希望简单地回放智能体过去的操作。

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

我们通过简单地在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);
}

请参阅此附加概念指南,了解与分叉相关的上下文。

请参阅此指南,了解时间旅行的详细操作方法!