> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 人工介入（HITL）：停轮、回答、继续

> 什么是 Human-in-the-loop，Claude Code / Codex / AI SDK 应用里它长什么样；NiceEval 如何把它变成可评的路径——waiting 停轮、input.requested 说明在等什么、t.respond 替人回答后同一轮继续。

HITL（human-in-the-loop，人工介入）指 agent 在执行中间停下来，把决定权交回给人：拿到回答之前不继续。它不是异常，而是 agent 产品刻意设计的安全门与信息门——敏感操作前要人批准，信息不够时要人补充，多个方案时要人拍板。

你大概率已经每天在用它：

* **Claude Code**：跑一条有风险的命令、改一个权限外的文件之前，会停下来问"允许吗？"——批准了才执行，拒绝了就换路。
* **Codex**：审批模式（suggest / auto-edit 等）决定哪些动作要先经人确认，本质是同一扇门。
* **AI SDK 应用**：工具不带 `execute` 时，工具调用会浮到前端，由界面上的人确认后把结果交回（`addToolResult`），模型再继续——这是自研应用里最常见的审批门写法。

对被测对象来说，这条"停下来等人"的分支和别的行为一样需要回归：批准后该发生的事发生了吗？拒绝后真的没做吗？问错了人、停错了地方呢？但 eval 是自动跑的，里面没有人。[NiceEval](https://niceeval.com/) 的做法是把"人"折叠成三个可编排的动作：看到 agent 停了（`t.parked()`）、检查它在等什么（`t.requireInputRequest()`）、替人回答（`t.respond()`）。批准、拒绝、补充信息，从此都是能写进 eval 的路径。

## 停轮如何表达：waiting + input.requested

`send` 返回的 `Turn.status` 有三个值：`"completed"`（跑完了）、`"failed"`（出错了）、`"waiting"`——**这一轮没跑完，agent 停在等人输入上**。waiting 不是失败，是挂起：回答到达后同一轮接着跑，不重发请求、不开新对话。

停下的轮次还要同时说明"在等什么"：每个待回答的问题吐一条 `input.requested` 事件，带一个稳定的请求 `id`、停在哪个动作上（`action`）、可选的裁决项（`options`，如 approve / deny）。状态和事件缺一不可——`waiting` 让 eval 知道该回答了，`input.requested` 让回答有的放矢。

## 一次完整握手

`t.respond` 底层没有专用通道——它只是**又一次普通的 `send`**，人的裁决以结构化形式随输入到达：

<img src="https://mintcdn.com/fasteval/5NO1lb4w1roXJkwo/images/hitl-handshake-zh.svg?fit=max&auto=format&n=5NO1lb4w1roXJkwo&q=85&s=f79c9789b53e32bf62a072eb1a9a2d8e" alt="HITL 一次完整握手：t.send 后应用停在审批上，本轮以 waiting 返回并带 input.requested；t.parked 与 t.requireInputRequest 检查停轮；t.respond 作为第二次普通 send 把结构化裁决交回，同一轮继续执行到 completed，断言照常。" width="1240" height="668" data-path="images/hitl-handshake-zh.svg" />

两个容易想歪的地方：

* **回答轮不是新对话。** 它发生在同一条会话线上，`ctx.session` 还是同一个——Adapter 靠它找回上一轮挂起的现场。
* **回答不用从文本里猜。** `input.responses` 逐请求带着 `{ requestId, optionId }`（自由文本回答则是 `{ requestId, text }`），命中选项的 `optionId` 在 eval 侧已校验过存在，打错的字直接抛错而不是静默传给应用。每种回答到 Adapter 长什么样，见[不同回答的入参](/zh/concepts/adapter#不同回答的入参)。

## eval 侧：三个动作

```ts theme={null}
const draft = await t.send("给老板发一封周报邮件。");
draft.parked();                                          // ① 断言这一轮停下了

const req = t.requireInputRequest({ action: "send_email" });   // ② 取出待答请求

await t.respond({ request: req, optionId: "approve" });  // ③ 替人回答,发出下一轮
t.calledTool("send_email", { status: "completed" });
```

* `t.parked()` 断言这一轮确实以 waiting 停下；反过来 `t.succeeded()` 在 waiting 轮上会失败——停轮是显式状态，不会被当成"跑完了"。
* `t.requireInputRequest(filter)` 从待处理请求里精确取一个（按 `id` / `prompt` / `action` / `optionIds` 等字段匹配），匹配到 0 个或超过 1 个都会抛，多个请求并停时靠它消歧。
* `t.respond(...)` 回答并发出下一轮；同类请求要给同一个答案时（比如逐个批准一批改动），`t.respondAll(optionId)` 一次处理完。

批准和拒绝是同一扇门的两个分支，各写一条 eval 才算评完：批准后该发生的发生了，拒绝后该发生的没发生。逐个 API 的完整用法见 [Drive](/zh/concepts/drive#人工介入-hitl)。

## Adapter 侧：两条义务，一次续跑

HITL 对 Adapter 的全部要求就三件事——前两件发生在停下的那一轮，第三件发生在回答到达的下一轮：

1. **停轮时如实返回 `status: "waiting"`**，不要把挂起报成 `"completed"`，否则 `parked()` 失效、`succeeded()` 假通过；
2. **每个待回答的问题吐一条 `input.requested`**，`id` 稳定、字段尽量填全——eval 侧的检查和对位全靠它们；
3. **下一次 `send` 先交裁决、再续跑**：从 `input.responses` 按 `requestId` 把裁决交回应用（不要按顺序猜），然后接着上一轮挂起的地方继续，而不是重发请求。

"挂起的现场"（比如读了一半的 SSE 流）存在本会话线的 `ctx.session` 上——停轮时 `ctx.session.hold(现场)`，回答轮 `ctx.session.take()` 取回，取到即清除。怎么和流式驱动拼起来，见[写 send](/zh/guides/write-send#第五步：hitl) 第五步的完整骨架。

和 [NiceEval](https://niceeval.com/) 的其它能力一样，HITL 没有布尔声明：**做到了就是有**。send 返回过 `"waiting"` 并吐了 `input.requested`，`t.respond` 就能工作；没做到，eval 会在第一个 `parked()` 或 `requireInputRequest()` 上明确失败，而不是静默假通过。能力如何从构造中来，见 [Adapter](/zh/concepts/adapter#能力从哪来：构造证明，不是问卷)。

## 拒绝不是故障

人否决和工具报错是两回事，事件流里用不同的 `status` 区分：被人拒绝的调用，`action.result` 的 `status` 是 `"rejected"` 而不是 `"failed"`。这样 `t.noFailedActions()` 不会被一次正当的人工否决误伤，而 `t.calledTool("deploy", { status: "rejected" })` 可以精确断言"发起了调用、被人拦下了"——这正是拒绝分支的 eval 要写的断言。

## 哪些 agent 用不到 HITL

不是每个 agent 都有这条分支，没有就整个跳过——Adapter 不用管 `waiting`，eval 里也不会出现 `t.respond`：

* **每轮一口气跑完的 agent**：问答、检索、翻译这类单发即回的服务，执行中没有等人的环节。
* **审批门被关掉的运行形态**：Claude Code、Codex 产品本身是 HITL 的，但作为被测对象在沙箱里通常全自动跑（跳过权限确认）——内置的 `claude-code` / `codex` / `bub` sandbox agent 就是这样，所以它们都不做 HITL。
* **审批发生在执行循环之外**：比如工单系统里人工复核 agent 的产出。那是另一个系统的流程，不经过 `send`，eval 评不到也不该评。

判断标准只有一条：**你的 agent 执行循环里存在"停下来等人、拿到回答再继续"的分支，而你想把批准和拒绝写进 eval。**

## 相关阅读

* [Drive](/zh/concepts/drive#人工介入-hitl) — eval 侧的完整用法：`t.parked()`、`t.requireInputRequest()`、`t.respond()` / `t.respondAll()`。
* [Adapter](/zh/concepts/adapter#不同回答的入参) — 回答到 Adapter 的结构化入参，四种典型形态。
* [写 send](/zh/guides/write-send) — Adapter 侧实操：`ctx.session.hold` / `take` 与流式 + HITL 的完整骨架。
* [接入你的 Agent](/zh/guides/connect-your-agent) — 接入全景：最小接入、参数通道与增量地图。
