← 全部文章

Human-in-the-loop:如何设计用户确认、接管和反馈?

Human-in-the-loop 不是多加一个确认弹窗,而是让 Agent 能够安全暂停、让人看懂并作出决定,再从同一执行状态继续。

Human-in-the-loop:如何设计用户确认、接管和反馈?
文章目录
  1. 导语
  2. 一、人应该在哪些地方进入循环
  3. 动作审批:决定一个动作能否发生
  4. 接管:改变正在推进的方向
  5. 反馈:评价结果并帮助后续改进
  6. 二、确认的对象必须是一项具体动作
  7. 为什么要保存参数快照
  8. 三、暂停不是阻塞一个 HTTP 请求
  9. 四、处理一项待批准动作:批准、编辑、拒绝
  10. 批准:同意当前快照
  11. 编辑:创建一项新请求
  12. 拒绝:产生一个可供 Agent 理解的结果
  13. 五、恢复执行比暂停更容易出事故
  14. 同一个批准只消费一次
  15. 恢复前重查现实世界
  16. 恢复的是同一个 Run,不是重新猜一遍
  17. 六、接管:用户如何改变正在运行的 Agent
  18. 七、反馈不能不加判断地写入 Memory
  19. 八、前端怎样让用户轻松作决定
  20. 避免确认疲劳
  21. 九、后端需要哪些接口和状态约束
  22. 十、用一个完整例子重建 HITL
  23. 十一、常见误区
  24. 把 HITL 做成一句“是否继续”
  25. 认为点击批准后必须执行
  26. 用户编辑参数后沿用原批准
  27. 拒绝后直接把 Run 标成系统错误
  28. 暂停状态只存进程内存
  29. 把每条反馈写进长期 Memory
  30. 为了省事一次批准后永远允许
  31. 十二、本篇总结
  32. 参考资料

导语

上一篇把一次动作的策略结果分成三种:

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

用户说“以后都不要自动发邮件”,可能有三种含义:

  1. 只针对这一次任务;
  2. 是该用户的长期偏好;
  3. 应成为整个组织的产品策略。

Agent 不应自己把第三种含义写进全局权限系统。

比较稳妥的分流是:

  • 当前 Run 的修正,写入运行状态;
  • 明确、稳定且经用户确认的个人偏好,进入长期 Memory;
  • 多次出现的质量问题,进入评测数据候选;
  • 组织级安全规则,进入人工维护的策略配置和变更流程。

这也呼应第七篇的原则:Memory 保存的是未来仍有价值的信息,不是所有对话碎片。


八、前端怎样让用户轻松作决定

一个好的确认界面应让用户在几秒内回答四个问题:

要做什么?
作用于哪里?
会产生什么影响?
我有哪些选择?

可以按重要性展示:

  1. 动作和目标;
  2. 关键参数与修改前后差异;
  3. 外部影响、可逆性和费用;
  4. 为什么需要确认;
  5. 批准、编辑、拒绝或接管入口;
  6. 有效期和更详细的审计信息。

对手机端尤其要避免把关键对象藏进折叠面板,也不要让主按钮只写模糊的“继续”。按钮应表达实际结果,例如“向 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

用户要求:

整理本月故障复盘,发给外部合作方。

一个合理流程是:

  1. 模型读取已授权的故障资料并生成邮件草稿;
  2. 模型提出 send_email 调用,参数包含收件人、主题和正文;
  3. Runtime 校验参数,权限策略因“外部通信”返回 require_approval;
  4. Runtime 保存 Run、已生成草稿和 Pending Action,状态转为 waiting_for_approval;
  5. 前端展示收件人、正文、附件和不可撤回提示;
  6. 用户发现其中一位收件人不该接收,于是选择编辑;
  7. 应用创建新参数并重新校验,新动作再次进入待批准状态;
  8. 用户批准,Runtime 确认批准者、参数摘要、有效期和最新权限;
  9. 邮件工具用 Pending Action ID 作为幂等键发送;
  10. Tool Result 写回 Run,模型向用户报告实际发送结果。

这里模型负责起草和提出动作;Runtime 负责暂停、保存、校验和恢复;用户负责对具体外部影响作决定;邮件工具只负责发送并返回结果。

如果你能不看原文复述这十步,并解释为什么“编辑后要重新校验”“批准后还要重查权限”,就抓住了 HITL 的核心。

这个例子仍做了简化。多级审批、会签、职责分离和法规留痕会引入更复杂的工作流引擎,但基础边界不变。


十一、常见误区

把 HITL 做成一句“是否继续”

没有动作、目标和影响信息,用户无法作出有意义的决定。

认为点击批准后必须执行

批准只是必要条件之一。权限失效、资源变化或动作过期时仍应停止。

用户编辑参数后沿用原批准

批准应绑定具体请求;有实质变化就要重新判断。

拒绝后直接把 Run 标成系统错误

拒绝是合法业务结果,Agent 应能解释、改道或结束。

暂停状态只存进程内存

长时间确认必须能够跨进程、跨设备恢复。

把每条反馈写进长期 Memory

当前修正、个人偏好、评测样本和组织策略需要走不同路径。

为了省事一次批准后永远允许

长期授权必须有清楚的作用域、有效期、撤销方式和审计记录。


十二、本篇总结

Human-in-the-loop 不是自动化失败后的补丁,而是 Agent 执行高影响动作时的一种正式控制协议。

一套可靠实现至少做到:

  1. 确认绑定具体动作、参数、资源和主体;
  2. Runtime 能持久化暂停状态,并从同一个 Run 恢复;
  3. 批准、编辑、拒绝和接管具有不同语义;
  4. 恢复前重新校验现实状态,执行保持幂等;
  5. 反馈按当前状态、Memory、Eval 和组织策略正确分流。

到这里,我们已经能让 Agent 计划、受控执行,并在关键节点把决定交还给人。

但新的问题出现了:一次 Run 里可能经过多轮模型调用、工具调用、批准和恢复。当最终结果跑偏时,我们怎样知道偏差从哪里开始?

下一篇进入 Agent 可观测性:如何知道 Agent 为什么跑偏?


参考资料