> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# LangGraph人机协作实战
- URL: https://www.hubwiz.com/blog/langgraph-human-in-the-loop-in-practice/
- Published: 2026-09-01T07:49:22.000Z
- Updated: 2026-10-13T16:06:36.000Z
- Description: 在使用LangGraph时，我遇到了一种情况：我不希望代理自行做出最终决定。
- Author: admin
- Tags: AGENT, LANGGRAPH, #Import 2026-10-14 14:41

如果代理想要发送电子邮件、删除文件或审批付款呢？我可能想先审查一下。这就是 **人机协作（HITL）**的基本概念。

在本文中，我将构建一个非常小的示例：代理决定要发送电子邮件，图表**暂停**，人类审查该操作，然后图表根据该决定**恢复**。

我还创建了一个小型交互式演示，您可以在其中**可视化图表、尝试基于终端的批准/拒绝流程，并探索示例背后的完整源代码**。

没有多代理设置或复杂的工作流。

只是：

> **暂停 → 人类决定 → 恢复**

## 1、为什么AI代理需要人类？

当我们乐于让自主代理自行做出决策时，它很有用。

但情况并非总是如此。

有些操作非常重要，我们可能希望人类在操作发生之前先审查。

例如：

- 代表您发送电子邮件或消息
- 修改或删除数据库记录
- 审批付款
- 部署代码
- 删除云资源
- 升级客户支持工单
- 发布AI生成的内容

在这些情况下，我们不一定希望代理拥有最终决定权。

我们希望代理做出决定，向我们展示它想要做什么，然后让人类决定是否应该继续。

这就是人机协作背后的基本思想。

## 2、HITL实际上意味着什么？

这个想法实际上很简单。

**图表暂停执行，等待人类输入，然后根据该输入继续。**

在LangGraph中，我们可以使用`interrupt()`来实现这一点。

当图表到达`interrupt()`时，当前执行将暂停。LangGraph保存状态，以便稍后可以恢复同一执行。

然后，应用程序可以向人类显示中断信息，收集响应，并使用`Command`恢复图表。

因此流程如下：

```
图表开始
    ↓
代理决定发送电子邮件
    ↓
⏸ interrupt()
    ↓
人类审查操作
    ↓
批准 / 拒绝
    ↓
Command({ resume: ... })
    ↓
图表继续

```

这就是我们将要构建的模式。

## 3、我们将要构建的示例

让我们使用一个简单的电子邮件示例。

代理将决定要发送此电子邮件：

```
下午5点与Aman的会议

```

在实际发送电子邮件之前，我们希望先审查它，然后批准或拒绝它。

完整的流程如下：

```
用户
  ↓
代理决定发送电子邮件
  ↓
⏸ 人类批准
  ↓
┌───────────────┐
│ 批准          │ → 发送电子邮件
│ 拒绝          │ → 停止
└───────────────┘

```

对于此示例，我们不会使用真实的电子邮件服务。

`sendEmail`节点将仅在终端打印一条消息。这使示例保持简单，让我们专注于HITL的工作原理。

### 3.1 设置项目

让我们从一个小型Node.js项目开始。

安装LangGraph：

```
npm install @langchain/langgraph

```

稍后我们还将使用Node.js内置的`readline`模块从终端获取人类的响应，因此我们不需要为此安装其他包。

我们的导入如下：

```javascript
import {
  StateGraph,
  Annotation,
  START,
  END,
  interrupt,
  MemorySaver,
  Command,
} from "@langchain/langgraph";

import readline from "node:readline/promises";
import {
  stdin as input,
  stdout as output,
} from "node:process";

```

现在让我们构建图表。

### 3.2 定义状态

首先，我们需要定义图表的状态。

我们的图表只需要两个信息：

- `message` — 代理想要发送的电子邮件
- `decision` — 人类提供的决定

```javascript
const StateAnnotation = Annotation.Root({
  message: Annotation,
  decision: Annotation,
});

```

`Annotation.Root()`定义了在图表中传递的状态形状。

每个节点都可以从状态读取值并返回更新。

在我们的例子中，代理会将电子邮件消息写入`message`。

稍后，人类批准节点会将人类的答案写入`decision`。

### 3.3 创建操作

接下来，我们需要一个代表我们要保护的操作的东西。

在实际应用中，这可能是调用电子邮件API。

对于我们的示例，我们将保持简单：

```javascript
function sendEmail(state) {
  console.log(`\n📧 已发送电子邮件："${state.message}"`);

  return {};
}

```

这里重要的不是函数做什么。

重要的是**它何时运行**。

我们不希望此节点在人类批准操作之前运行。

### 3.4 创建代理

通常，这是LLM出场的地方。

模型可以查看用户的请求并决定需要发送电子邮件。

但添加LLM会使此示例不必要地复杂化。

因此，现在我们将使用一个普通函数来表示代理的决定：

```javascript
function agent() {
  return {
    message: "下午5点与Aman的会议",
  };
}

```

可以将此节点理解为：

> *"我已决定要发送此电子邮件。"*

在实际应用中，可以将此节点替换为LLM和工具调用逻辑。HITL部分将保持基本相同。

### 3.5 添加人类批准步骤

现在我们进入重要部分。

我们需要图表在发送电子邮件之前停止，并等待人类决定。

我们可以使用`interrupt()`来实现：

```javascript
function humanApproval(state) {
  const decision = interrupt({
    message: state.message,
    question: "您要发送此电子邮件吗？",
  });

  return {
    decision,
  };
}

```

当LangGraph到达：

```javascript
interrupt(...)

```

时，图表暂停。

我们传递给`interrupt()`的值成为应用程序可以接收的中断负载。

在我们的示例中，该负载包含：

```json
{
  "message": "下午5点与Aman的会议",
  "question": "您要发送此电子邮件吗？"
}

```

然后，我们的应用程序可以向人类显示该信息。

人类做出决定后，我们恢复图表。

恢复时提供的响应成为`interrupt()`的返回值。

因此，如果人类批准，那么：

```javascript
const decision = interrupt(...);

```

最终将给我们：

```javascript
const decision = "approve";

```

我们将该值存储在状态中。

现在我们需要根据该答案决定图表的去向：

```javascript
function routeAfterApproval(state) {
  if (state.decision === "approve") {
    return "sendEmail";
  }

  return END;
}

```

如果人类批准，我们将转到`sendEmail`。

如果他们拒绝，图表结束。

### 3.6 添加检查点

在编译图表之前，我们还有一个重要部分：检查点。

为什么？

因为我们的图表将暂停并在稍后恢复。

当`interrupt()`暂停图表时，LangGraph需要保存执行状态，以便稍后可以继续该执行。

对于这个小示例，我们将使用`MemorySaver`：

```javascript
const checkpointer = new MemorySaver();

```

现在让我们将所有内容连接在一起：

```javascript
const graph = new StateGraph(StateAnnotation)
  .addNode("agent", agent)
  .addNode("humanApproval", humanApproval)
  .addNode("sendEmail", sendEmail)

  .addEdge(START, "agent")
  .addEdge("agent", "humanApproval")

  .addConditionalEdges(
    "humanApproval",
    routeAfterApproval,
    {
      sendEmail: "sendEmail",
      [END]: END,
    }
  )

  .compile({
    checkpointer,
  });

```

图表现在如下所示：

```
START
  ↓
agent
  ↓
humanApproval
  ↓
 ┌──────────────┐
 │              │
approve       reject
 │              │
 ↓              ↓
sendEmail      END
 │
 ↓
END

```

`MemorySaver`将检查点保存在内存中，这对于这个小示例来说是完美的。

在生产应用中，您通常会使用持久性检查点，以便状态可以在进程重启等情况下幸存。

还有一个重要细节：`thread_id`。

`thread_id`告诉LangGraph我们正在处理哪个检查点执行。

当我们暂停并在稍后恢复图表时，我们使用相同的`thread_id`。

### 3.7 运行图表

现在让我们实际运行它。

首先，我们将创建一个小型readline接口，以便终端可以充当我们的人类审查者：

```javascript
const rl = readline.createInterface({
  input,
  output,
});

```

接下来，我们将创建配置：

```javascript
const config = {
  configurable: {
    thread_id: "thread-1",
  },
};

```

现在我们可以启动图表：

```javascript
const stream = await graph.stream(
  {
    message: "",
  },
  config
);

```

图表开始运行。

它通过`agent`节点，然后到达`humanApproval`。

此时，`interrupt()`暂停图表。

我们可以在流中检查中断：

```javascript
for await (const chunk of stream) {
  if (chunk.__interrupt__) {
    const interruptValue =
      chunk.__interrupt__[0].value;

    console.log(
      "\n⏸ 等待人类批准...\n"
    );

    console.log(
      "代理想要发送此电子邮件："
    );

    console.log(`"${interruptValue.message}"`);

    console.log(
      `\n${interruptValue.question}`
    );
  }
}

```

现在终端将显示类似以下内容：

```
用户：给Aman发送关于下午5点会议的电子邮件

⏸ 等待人类批准...

代理想要发送此电子邮件：
"下午5点与Aman的会议"

您要发送此电子邮件吗？

```

此时，**图表已暂停**。

尚未发送任何内容。

现在让我们实际询问人类他们想要做什么：

```javascript
let humanAnswer;

while (true) {
  humanAnswer = (
    await rl.question("\n批准或拒绝：")
  )
    .trim()
    .toLowerCase();

  if (
    humanAnswer === "approve" ||
    humanAnswer === "reject"
  ) {
    break;
  }

  console.log(
    '请输入"approve"或"reject"。'
  );
}

```

终端现在将等待我们：`批准或拒绝：`

如果我们输入：`approve`

我们可以恢复图表。

### 3.8 恢复图表

要恢复中断的图表，我们使用`Command`：

```javascript
await graph.invoke(
  new Command({
    resume: humanAnswer,
  }),
  config
);

```

注意我们使用的是相同的`config`，这意味着我们使用的是相同的`thread_id`。

这就是LangGraph知道我们想要恢复哪个中断执行的方式。

我们通过`resume`提供的值成为`interrupt()`的返回值。

因此，如果我们输入：`approve`

那么在`humanApproval`内部：

```javascript
const decision = interrupt(...);

```

将返回：`approve`

然后图表到达我们的路由函数：

```javascript
function routeAfterApproval(state) {
  if (state.decision === "approve") {
    return "sendEmail";
  }

  return END;
}

```

因为决定是`"approve"`，所以图表移动到`sendEmail`。

我们应该看到：

```
📧 已发送电子邮件："下午5点与Aman的会议"

```

就这样。电子邮件操作仅在人类批准后才发生。

## 4、当我们拒绝时会发生什么？

让我们再次尝试相同的操作。

这次，当终端询问：

```
批准或拒绝：

```

我们输入：

```
reject

```

图表通过以下方式恢复：

```javascript
await graph.invoke(
  new Command({
    resume: "reject",
  }),
  config
);

```

现在`state.decision`包含`"reject"`。

我们的路由函数将图表直接发送到`END`。

因此`sendEmail`节点永远不会运行。

流程是：

```
代理想要发送电子邮件
        ↓
   ⏸ 已暂停
        ↓
人类：拒绝
        ↓
      END

```

这里重要的是图表不仅仅是返回不同的消息。

实际的操作节点从未执行。

这就是人类批准有用的原因。

## 5、将示例整合在一起

至此，完整的流程如下：

```
                 ┌─────────────┐
                 │    START    │
                 └──────┬──────┘
                        ↓
                 ┌─────────────┐
                 │    Agent    │
                 └──────┬──────┘
                        ↓
              ┌───────────────────┐
              │  人类批准          │
              │                   │
              │   ⏸ interrupt()   │
              └─────────┬─────────┘
                        ↓
                 人类决定
                   /       \
                  /         \
             approve       reject
                ↓             ↓
         ┌────────────┐      END
         │ sendEmail  │
         └──────┬─────┘
                ↓
               END

```

整个HITL工作流基本上是：

**代理决定 → 暂停 → 人类审查 → 恢复 → 操作**

## 6、关于interrupt()的一个重要事项

关于中断有一个值得了解的细节。

当图表恢复时，LangGraph会从头重新进入包含`interrupt()`的节点。

它不会简单地从`interrupt()`之后的确切JavaScript行继续。

然而，在恢复的执行上，`interrupt()`调用通过`Command({ resume: ... })`提供的值返回。

因此，我们需要小心在`interrupt()`之前放置副作用。

例如，避免这样做：

```javascript
function humanApproval(state) {
  saveSomethingToDatabase();

  const decision = interrupt("批准？");

  return { decision };
}

```

当图表恢复时，该代码可以再次运行。

对于具有真实副作用的操作，最好将它们保留在人类批准步骤之后。

这正是我们在这里所做的：

```
humanApproval
      ↓
interrupt()
      ↓
人类响应
      ↓
sendEmail

```

实际的电子邮件操作仅在批准后发生。

## 7、底层发生了什么？

如果我们剥离代码，过程实际上非常简单。

1. 图表开始执行。
2. 代理决定要发送电子邮件。
3. 图表到达`interrupt()`。
4. LangGraph暂停执行。
5. 当前状态被检查点保存。
6. 应用程序接收中断信息。
7. 人类审查操作。
8. 人类提供决定。
9. 应用程序使用`Command`恢复图表。
10. `interrupt()`返回人类的响应。
11. 图表根据该响应继续。

因此重要的部分不是代码。

而是暂停和恢复模式。

```
        图表
          │
          ▼
    代理决定
          │
          ▼
      interrupt()
          │
          │
      ┌───┴───┐
      │ 人类  │
      └───┬───┘
          │
     approve/reject
          │
          ▼
       恢复
          │
          ▼
      继续

```

一旦您理解了这个流程，HITL就不再神秘了。

## 8、自己尝试HITL流程

我还创建了一个小型交互式演示，以使HITL流程更易于理解。

它包括：

- **图表可视化** — 查看工作流在人类批准处暂停并跟踪批准/拒绝路径。
- **终端模拟器** — 通过选择`approve`或`reject`与工作流交互。
- **完整源代码** — 探索演示背后的完整实现。

您可以在此处尝试演示：

**\[尝试交互式HITL演示 →** [https://nayan-kunwar.github.io/hitl-langGraph-interactive-demo/](https://nayan-kunwar.github.io/hitl-langGraph-interactive-demo/?ref=hubwiz.com) \]

该演示是一个托管在GitHub Pages上的独立HTML页面，旨在补充本文讨论的LangGraph实现。

## 9、我们可以在哪里使用HITL？

电子邮件示例只是演示该模式的一种简单方式。

当代理即将执行需要人类监督的操作时，可以使用相同的方法。

例如：

- 发送电子邮件或消息
- 更新或删除数据库记录
- 审批付款
- 部署代码
- 删除云资源
- 发布AI生成的内容
- 升级客户支持请求

实际操作会改变，但模式几乎保持不变：

**代理决定 → 暂停 → 人类审查 → 恢复 → 操作**

## 10、结束语

重要的部分基本上是：

- `interrupt()`暂停图表
- `Command({ resume: ... })`提供人类的响应
- 检查点保存图表状态
- `thread_id`让LangGraph知道要恢复哪个执行

这就是真正的思想。

您不需要庞大的工作流来为代理添加人类控制。有时您只需要在重要操作之前进行一次适当放置的暂停。

代理仍然可以完成大部分工作，但当重要事情即将发生时，人类可以拥有最终决定权。

---

原文链接：[Human-in-the-Loop in LangGraph: A Practical Example](https://medium.com/@nayankunwar678/human-in-the-loop-in-langgraph-a-small-practical-example-0e3f455e7d8b?ref=hubwiz.com)

汇智网翻译整理，转载请标明出处

> **这是「Agent / MCP」专题的一部分。**完整地图见：[Agent / MCP / 自动化完全指南](https://www.hubwiz.com/blog/agent-stack/)