Human-in-the-loop:如何设计用户确认、接管和反馈?
Human-in-the-loop 不是多加一个确认弹窗,而是让 Agent 能够安全暂停、让人看懂并作出决定,再从同一执行状态继续。

文章目录
- 导语
- 一、人应该在哪些地方进入循环
- 动作审批:决定一个动作能否发生
- 接管:改变正在推进的方向
- 反馈:评价结果并帮助后续改进
- 二、确认的对象必须是一项具体动作
- 为什么要保存参数快照
- 三、暂停不是阻塞一个 HTTP 请求
- 四、处理一项待批准动作:批准、编辑、拒绝
- 批准:同意当前快照
- 编辑:创建一项新请求
- 拒绝:产生一个可供 Agent 理解的结果
- 五、恢复执行比暂停更容易出事故
- 同一个批准只消费一次
- 恢复前重查现实世界
- 恢复的是同一个 Run,不是重新猜一遍
- 六、接管:用户如何改变正在运行的 Agent
- 七、反馈不能不加判断地写入 Memory
- 八、前端怎样让用户轻松作决定
- 避免确认疲劳
- 九、后端需要哪些接口和状态约束
- 十、用一个完整例子重建 HITL
- 十一、常见误区
- 把 HITL 做成一句“是否继续”
- 认为点击批准后必须执行
- 用户编辑参数后沿用原批准
- 拒绝后直接把 Run 标成系统错误
- 暂停状态只存进程内存
- 把每条反馈写进长期 Memory
- 为了省事一次批准后永远允许
- 十二、本篇总结
- 参考资料
导语
上一篇把一次动作的策略结果分成三种:
allow / deny / require_approval
前两种比较直接:允许就进入受限执行器,拒绝就返回明确原因。
真正考验产品和工程设计的是第三种。系统不能把请求一直挂在某台服务器的内存里,也不能只弹一句“是否继续”。它需要保存当前进度,让用户知道自己在批准什么,并在几分钟甚至几天后从正确位置恢复。
这就是 Human-in-the-loop,简称 HITL。
HITL 的核心不是“流程里出现了一个人”,而是建立下面这个可恢复协议:
动作即将执行
→ Runtime 暂停并保存状态
→ 人查看充分信息并作出决定
→ Runtime 校验决定和当前环境
→ 执行、改道或结束
这一篇沿用上一篇的通用权限模型,不把 System Prompt 定制成某一种业务流程。具体哪些动作需要确认,仍由应用策略决定。
一、人应该在哪些地方进入循环
Human-in-the-loop 不只等于批准按钮。人至少可能承担三类角色。
动作审批:决定一个动作能否发生
例如生产部署、群发邮件、付款或公开发布。这时 Agent 已经提出了具体动作,人负责放行或拒绝。
接管:改变正在推进的方向
例如 Agent 正在批量迁移文件,用户发现范围不对,要求暂停并改成只处理一个目录。接管改变的是任务输入、计划或控制权。
反馈:评价结果并帮助后续改进
例如“结论正确,但不要修改生成文件”。这条反馈可能只作用于当前 Run,也可能经过确认后成为长期偏好、评测样本或产品规则。
三者不要混为一谈:动作审批是一项有明确对象的决定;接管会改变执行;反馈则可能发生在执行后,也不应自动写入长期 Memory。
这里是按“人为什么进入整个 Run”分类。后文的“批准、编辑、拒绝”,则是用户处理某一项待批准动作时的具体选项。两者不在同一层级:前者描述介入目的,后者描述一次确认界面里的操作。
二、确认的对象必须是一项具体动作
下面两个确认框看起来只差几行字,实际安全性差别很大。
不合格的确认:
Agent 需要你的授权。
[允许] [拒绝]
用户不知道动作、对象、影响和参数,点击只能靠猜。
更有用的确认信息是:
动作:发送邮件
收件人:23 名外部客户
主题:8 月服务升级通知
身份:以 winter@example.com 名义发送
影响:发送后无法撤回
原因:外部通信需要人工批准
[批准发送] [编辑内容] [拒绝]
一个 Pending Action 至少应该保存:
type PendingAction = {
id: string;
runId: string;
actorId: string;
toolName: string;
arguments: unknown;
resourceSnapshot?: {
id: string;
version?: string;
};
reasonCode: string;
createdAt: string;
expiresAt: string;
status: "pending" | "approved" | "rejected" | "expired" | "superseded" | "executed";
};
这不是某个框架规定的固定 Schema,而是为了让确认对象稳定、可恢复、可审计。
为什么要保存参数快照
用户批准的是“给 23 人发送这封邮件”,不是给 send_email 工具一张空白通行证。若批准后模型又把收件人改成 2300 人,原批准必须失效。
工程上可以对规范化后的工具名、参数、资源、主体和有效期计算摘要。执行时只接受与批准对象一致的请求。
三、暂停不是阻塞一个 HTTP 请求
一个不可靠的实现可能这样做:
const approved = await waitForUserClick();
如果等待跨越几小时,进程重启或发布新版本,内存里的 Promise 就会丢失。
更稳妥的做法是把暂停变成持久化状态:

这里“拒绝后回到 Running”不是说继续执行原动作,而是把拒绝作为观察结果交回循环。模型可以提出低风险替代方案,也可以向用户解释任务无法继续。
暂停时通常要持久化:
- 当前 Agent Run 和所在 Step;
- 已完成的工具结果;
- 待批准动作及其参数;
- 下一次恢复所需的模型上下文或框架状态;
- 用于防止重复处理和并发覆盖的状态版本号、过期时间和审计关联信息。
不同 SDK 的字段和恢复方式并不相同。例如 OpenAI Agents SDK 的一种 HITL 流程会返回 interruptions,在 RunState 上记录批准或拒绝,再用同一状态恢复运行。本文的 PendingAction 是更通用的应用层表达,不是对该 SDK 内部类型的复刻。
四、处理一项待批准动作:批准、编辑、拒绝
批准:同意当前快照
批准记录应包含批准人、时间、对象和范围。恢复后仍要检查:
- 批准人是否有资格批准;
- Pending Action 是否已过期或已处理;
- 请求参数是否仍与批准快照一致;
- 用户权限和资源状态是否发生变化;
- 是否满足执行时的其他硬规则。
批准不是绕过后续校验的万能票据。
编辑:创建一项新请求
这里的编辑,是修改 Agent 即将执行的工具参数。例如用户把金额从 5000 改成 500,把收件人从 23 名外部客户改成 20 名,或从邮件中移除不该发送的附件。它不是修改 System Prompt,也不是让 Agent 重新规划整个任务。
一次完整交互可以是:
Agent 展示待发送邮件
→ 用户点击“编辑”
→ 表单展示收件人、主题、正文和附件
→ 用户修改并提交
→ 原待批准动作失效
→ 系统用新参数创建待批准动作并重新校验
→ 界面展示修改差异
→ 用户再次批准、继续编辑或拒绝
用户编辑后,动作的对象、范围或影响已经变化。比较清晰的实现是:让原 Pending Action 标记为 superseded,基于编辑后的参数创建新请求,再重新进行 Schema、权限和策略判断。不要在已批准对象上静默改参数,也不要沿用原来的批准结果。
编辑和接管的边界也要明确:“把收件人从 23 人改成 20 人”仍是在调整这次发送动作;“不要发送,只保存草稿”已经改变任务方向,属于接管。
拒绝:产生一个可供 Agent 理解的结果
拒绝不应只把 Run 置为失败。可以向下一轮模型返回:
{
"ok": false,
"error": {
"code": "ACTION_REJECTED_BY_USER",
"message": "用户拒绝向外部收件人发送邮件",
"retryable": false
}
}
模型随后可以删除该步骤、生成草稿供用户自行发送,或说明目标无法在当前约束下完成。
五、恢复执行比暂停更容易出事故
很多 HITL 原型能停下来,却不能可靠地继续。
同一个批准只消费一次
用户连点按钮、消息队列重复投递、服务重试,都可能让恢复接口被调用多次。状态更新需要条件约束:
UPDATE pending_actions
SET status = 'approved', decided_by = :user_id
WHERE id = :id AND status = 'pending';
只有成功更新一行的请求获得处理权。真正产生外部副作用的工具还需要幂等键,例如使用 pendingAction.id 作为付款或发送请求的幂等标识。
恢复前重查现实世界
等待期间,订单可能已取消,文件版本可能已变化,用户角色也可能被收回。执行前需要重新读取关键资源并重跑授权。
如果当前状态与用户看到的快照不一致,应让旧批准失效,展示差异后重新确认。
恢复的是同一个 Run,不是重新猜一遍
如果每次批准后把原始用户问题重新发给模型,它可能重新规划并生成不同参数。正确恢复点应保留已完成步骤和待执行调用,避免重复执行前面的副作用。
六、接管:用户如何改变正在运行的 Agent
接管比批准更开放,因为用户可能提交新的自然语言要求:
先停一下,不要改数据库,只整理迁移方案。
应用需要先确定新输入影响什么:
- 只是补充当前步骤的信息;
- 修改后续 Plan;
- 取消尚未开始的动作;
- 请求终止整个 Run;
- 由用户转为手工处理。
已经发生的外部副作用不能靠“取消”抹掉。界面要明确区分:
已完成:创建备份
正在执行:无
已取消:数据库迁移、生产发布
对正在执行且不可中断的工具,系统可以记录取消意图,并在工具返回后停止后续步骤;不要假装远端动作已经被撤销。
新的用户输入应作为一条可追踪事件加入 Run,而不是偷偷改写旧消息。这样后续才能解释计划为什么改变。
七、反馈不能不加判断地写入 Memory
用户说“以后都不要自动发邮件”,可能有三种含义:
- 只针对这一次任务;
- 是该用户的长期偏好;
- 应成为整个组织的产品策略。
Agent 不应自己把第三种含义写进全局权限系统。
比较稳妥的分流是:
- 当前 Run 的修正,写入运行状态;
- 明确、稳定且经用户确认的个人偏好,进入长期 Memory;
- 多次出现的质量问题,进入评测数据候选;
- 组织级安全规则,进入人工维护的策略配置和变更流程。
这也呼应第七篇的原则:Memory 保存的是未来仍有价值的信息,不是所有对话碎片。
八、前端怎样让用户轻松作决定
一个好的确认界面应让用户在几秒内回答四个问题:
要做什么?
作用于哪里?
会产生什么影响?
我有哪些选择?
可以按重要性展示:
- 动作和目标;
- 关键参数与修改前后差异;
- 外部影响、可逆性和费用;
- 为什么需要确认;
- 批准、编辑、拒绝或接管入口;
- 有效期和更详细的审计信息。
对手机端尤其要避免把关键对象藏进折叠面板,也不要让主按钮只写模糊的“继续”。按钮应表达实际结果,例如“向 23 人发送”。
避免确认疲劳
可以减少无价值中断,但不能用模糊的“全部允许”换取流畅:
- 通过更细的策略自动放行低风险动作;
- 将同一目标、同一风险、同一事务内的动作合理组合;
- 对批量批准展示完整范围和例外项;
- 把长期授权限定在工具、资源、参数范围和时间窗口内;
- 一旦参数越界,重新触发确认。
九、后端需要哪些接口和状态约束
一种简化的接口设计如下:
GET /runs/:runId/pending-actions
POST /pending-actions/:id/approve
POST /pending-actions/:id/reject
POST /pending-actions/:id/revise
POST /runs/:runId/cancel
后端的重点不在路由名字,而在这些约束:
- 决策者身份由服务端认证;
- 只能处理
pending且未过期的动作; - 批准者必须满足审批策略;
- 参数摘要、资源版本和授权需要复核;
- 状态转换具备原子性;
- 恢复任务可重试,外部副作用保持幂等;
- 每次提议、查看、决定、执行都写入审计事件。
前端刷新或换设备后,应能从服务端恢复 Pending Action,而不是依赖本地组件状态。
十、用一个完整例子重建 HITL
用户要求:
整理本月故障复盘,发给外部合作方。
一个合理流程是:
- 模型读取已授权的故障资料并生成邮件草稿;
- 模型提出
send_email调用,参数包含收件人、主题和正文; - Runtime 校验参数,权限策略因“外部通信”返回
require_approval; - Runtime 保存 Run、已生成草稿和 Pending Action,状态转为
waiting_for_approval; - 前端展示收件人、正文、附件和不可撤回提示;
- 用户发现其中一位收件人不该接收,于是选择编辑;
- 应用创建新参数并重新校验,新动作再次进入待批准状态;
- 用户批准,Runtime 确认批准者、参数摘要、有效期和最新权限;
- 邮件工具用 Pending Action ID 作为幂等键发送;
- Tool Result 写回 Run,模型向用户报告实际发送结果。
这里模型负责起草和提出动作;Runtime 负责暂停、保存、校验和恢复;用户负责对具体外部影响作决定;邮件工具只负责发送并返回结果。
如果你能不看原文复述这十步,并解释为什么“编辑后要重新校验”“批准后还要重查权限”,就抓住了 HITL 的核心。
这个例子仍做了简化。多级审批、会签、职责分离和法规留痕会引入更复杂的工作流引擎,但基础边界不变。
十一、常见误区
把 HITL 做成一句“是否继续”
没有动作、目标和影响信息,用户无法作出有意义的决定。
认为点击批准后必须执行
批准只是必要条件之一。权限失效、资源变化或动作过期时仍应停止。
用户编辑参数后沿用原批准
批准应绑定具体请求;有实质变化就要重新判断。
拒绝后直接把 Run 标成系统错误
拒绝是合法业务结果,Agent 应能解释、改道或结束。
暂停状态只存进程内存
长时间确认必须能够跨进程、跨设备恢复。
把每条反馈写进长期 Memory
当前修正、个人偏好、评测样本和组织策略需要走不同路径。
为了省事一次批准后永远允许
长期授权必须有清楚的作用域、有效期、撤销方式和审计记录。
十二、本篇总结
Human-in-the-loop 不是自动化失败后的补丁,而是 Agent 执行高影响动作时的一种正式控制协议。
一套可靠实现至少做到:
- 确认绑定具体动作、参数、资源和主体;
- Runtime 能持久化暂停状态,并从同一个 Run 恢复;
- 批准、编辑、拒绝和接管具有不同语义;
- 恢复前重新校验现实状态,执行保持幂等;
- 反馈按当前状态、Memory、Eval 和组织策略正确分流。
到这里,我们已经能让 Agent 计划、受控执行,并在关键节点把决定交还给人。
但新的问题出现了:一次 Run 里可能经过多轮模型调用、工具调用、批准和恢复。当最终结果跑偏时,我们怎样知道偏差从哪里开始?
下一篇进入 Agent 可观测性:如何知道 Agent 为什么跑偏?
参考资料
- OpenAI Agents SDK:Human-in-the-loop https://openai.github.io/openai-agents-js/guides/human-in-the-loop/
- Model Context Protocol:Tools https://modelcontextprotocol.io/specification/2025-06-18/server/tools
- OWASP:Transaction Authorization Cheat Sheet https://cheatsheetseries.owasp.org/cheatsheets/Transaction_Authorization_Cheat_Sheet.html