---
title: "人工介入：挂起、审核与继续执行"
---



Agent Graph 在转账、发布、删除、审批等关键动作前，经常需要等待人工决定。人工介入不是让线程持续阻塞，而是：保存状态、结束本次执行，收到决定后再恢复。

```text
proposal_agent
  -> approval
       ├─ 未决定：pending
       ├─ APPROVE：executor_agent
       └─ REJECT：reject_finish
```

### 1、审核节点

```java
spec.addActivity("approval")
        .title("人工审核")
        .task((ctx, node) -> {
            String decision = ctx.getAs("approval.decision");
            if (decision == null) {
                AgentSession session = ctx.getAs(Agent.KEY_SESSION);
                ctx.put("approval.request", ctx.get("change.proposal"));
                session.pending(true, "变更方案等待人工审核");
            }
        })
        .linkAdd("approval_route");
```

`session.pending(true, reason)` 已经会：

- 停止当前 Flow 执行；
- 将 Context 标记为 stopped；
- 保存 pending 原因。

不需要再额外调用 `ctx.stop()`。

不要依赖首次 `TeamResponse.getContent()` 返回待审核原因。审核原因应从：

```java
session.getPendingReason();
```

读取。若产品需要明确响应文本，可在挂起前显式设置 `TeamTrace.finalAnswer`，但它不能替代 pending 状态。

### 2、审核路由

```java
spec.addExclusive("approval_route")
        .linkAdd(executor.name(), link -> link.when(ctx ->
                "APPROVE".equals(ctx.getAs("approval.decision"))))
        .linkAdd("reject_finish");
```

拒绝节点明确收敛结果：

```java
spec.addActivity("reject_finish")
        .task((ctx, node) -> {
            String result = "变更已拒绝："
                    + ctx.getAs("approval.reason");
            ctx.put("change.result", result);

            TeamTrace trace = TeamTrace.getCurrent(ctx);
            if (trace != null) {
                trace.setFinalAnswer(result);
            }
        })
        .linkAdd(Agent.ID_END);
```

### 3、首次执行

```java
AgentSession session = InMemoryAgentSession.of("change-001");

changeGraph.prompt("升级订单服务并执行数据库变更")
        .session(session)
        .call();

if (session.isPending()) {
    System.out.println(session.getPendingReason());
    System.out.println(session.getContext().get("approval.request"));
}
```

此时应持久化快照并释放当前请求线程，而不是原地等待人工操作。

### 4、批准后继续

```java
session.getContext().put("approval.decision", "APPROVE");
session.getContext().put("approval.operator", "alice");
session.getContext().put("approval.time", System.currentTimeMillis());

TeamResponse resumed = changeGraph.prompt()
        .session(session)
        .call();
```

`TeamAgent` 恢复调用会在本轮准备阶段清理 pending 标志，因此显式调用 `session.pending(false, null)` 不是继续执行的必要条件。业务层可以调用它表达“审核任务已解除”，但不能替代写入审核决定。

空参数 `prompt()` 表示使用 Trace 中的原始 Prompt 续跑，不是发起新的空任务。

### 5、stop、interrupt 与 pending

| API | 作用范围 | 持久化语义 |
|---|---|---|
| `ctx.interrupt()` | 截断当前分支 | 不代表 Session pending |
| `ctx.stop()` | 停止整个本次 Flow 执行 | 不保存 pending 原因 |
| `session.pending(true, reason)` | 停止整个 Flow，并保存 Session 挂起状态 | 推荐用于可恢复人工等待 |

并行图中，`interrupt()` 不等于停止其他分支；高风险审批通常应使用 Session pending。

### 6、审核节点会重新执行

FlowTrace 在进入节点时记录位置。恢复到挂起 Activity 后，该 Activity 的 task 会再次运行。因此审核节点应该只做：

- 检查决定；
- 生成待办；
- 保存待审核数据；
- 切换 pending 状态。

不要这样设计：

```text
扣款 -> pending -> 等审批
```

应拆成：

```text
准备扣款 -> 等待审批 -> 幂等扣款 -> 记录结果
```

外部动作还应使用业务幂等键，例如 `change-001/execute`。

### 7、Graph HITL 与 ReAct HITL

| 机制 | 适用范围 |
|---|---|
| Graph HITL | 决定整个业务阶段是否继续 |
| ReAct 工具 HITL | 决定某一次工具调用是否允许 |
| Solon Flow Workflow | 正式人员任务、认领、审核、退回等工作流 |

ReAct 工具审核应按调用 UUID 关联：

```java
HITLTask task = HITL.getPendingTaskByCallUuid(session, callUuid);
HITL.submit(session, task,
        HITLDecision.approve().comment("管理员已确认"));

agent.prompt().session(session).call();
```

同名工具可能同时产生多个待审核调用，不要只按工具名称匹配。