超时必须是拒绝——为什么 HITL 必须 fail-closed

2026-10-05·编排与协作·人机协同
Agent 发出审批请求后用户不响应,60 秒超时了。超时应该被当作'同意'还是'拒绝'?如果超时=同意,攻击者只要不响应就能拿到批准。这篇文章用场景分析讲一个看似简单但在多个框架中做反了的安全原则:超时必须是拒绝。
一个简单的设计问题
Agent 检测到自己可能在循环——连续第二次相同工具调用达到终止阈值。按照两阶段协议,第一次已经自愈过了,这次升级到 HITL:弹出一个审批提示给用户——
"Agent 似乎在重复相同的操作。是否继续?"
用户去开会了。60 秒后超时。
超时应该被当作什么?
- 选项 A:默许(timeout → allow)——用户没说不行,所以继续
- 选项 B:拒绝(timeout → deny)——用户没确认,所以停止
这看起来是一个 UX 决策——"默认同意"更流畅(Agent 不会因为用户暂时离开就停下来),"默认拒绝"更安全(不确定时不做事)。
但这不是 UX 决策——是安全决策。

如果超时=默许
场景一:攻击者不响应
Prompt injection 是 Agent 系统中最普遍的安全威胁之一。攻击者通过在文件、网页、邮件中嵌入恶意指令来劫持 Agent 的行为。
假设一个 Agent 被 prompt injection 引导进入一个破坏性循环——反复执行删除操作。CycleDetector 检测到循环,触发 HITL 问用户"是否继续?"
如果超时=默许(60 秒后自动继续)——攻击者不需要响应。Prompt injection 发起后攻击者不在场(可能在另一个时区的另一台电脑上)。HITL 提示弹出、无人响应、超时、自动继续——破坏性循环继续执行。
HITL 变成了一个"等得起的延迟"——攻击者只要不响应,就能拿到批准。
场景二:用户离开
更日常的场景——用户让 Agent 帮忙重构代码,然后去冲咖啡了。Agent 在执行过程中遇到了一个需要确认的操作("是否删除这个看起来不再使用的测试文件?")。
超时=默许 → 文件被删除。用户回来后发现"那个测试文件为什么不见了?"
超时=拒绝 → Agent 跳过这个操作、标记为"需要用户确认"、继续做其他事。用户回来后看到"有一个操作等你确认"。
场景三:企业合规
在金融/法律合规场景中,某些操作必须有人类审批记录——不是"有审批机制"就行,是"每次操作都有人类的明确同意记录"。
如果超时=默许,审计记录会显示:"操作被批准——原因:超时自动批准。"
任何合规审查员看到"超时自动批准"都会标记为合规违规。 "没人审批"和"有人审批但超时了"在合规角度是一样的——都是没有人类明确同意。

fail-closed 原则
在安全工程中有一个基本原则:fail-closed——当系统状态不确定时,默认选择更安全的选项。
- 门禁系统断电后:门锁上(fail-closed),不是打开(fail-open)
- 防火墙遇到无法分类的流量:丢弃(fail-closed),不是放行(fail-open)
- ATM 机通信中断:拒绝交易(fail-closed),不是允许取款(fail-open)
HITL 超时的 fail-closed 就是:超时 = 拒绝。
三种 HITL 结局的统一处理
我们的引擎把 HITL 的三种结局统一为一个类型:
| 结局 | 含义 | 处理 |
|---|---|---|
| 用户明确同意 | "好的,继续" | 放行 |
| 用户明确拒绝 | "不,停下来" | 拒绝 |
| 超时 | 用户不在/没看到/忘了 | 拒绝(等同于明确拒绝) |
| 取消 | 用户关闭了对话/刷新了页面 | 拒绝 |
| 错误 | 通信失败/渲染错误 | 拒绝 |
"只有明确的 Grant 才是 Grant——其他任何结局都是 Deny。"
调用方的处理逻辑因此极简——不需要区分"超时"和"拒绝"和"取消"。只有一个 nil error 表示"用户同意了",所有非 nil error(包括超时、取消、拒绝、通信错误)都表示"不要继续"。
这消除了一类常见的 Bug——开发者忘了处理某种失败情况。当有五种失败结局需要分别处理时,总有一种会被遗漏。当所有失败统一为"拒绝"时,遗漏不可能发生。
行业实践

Claude Code:auto mode 的硬性上限
Claude Code 的 auto mode 不直接面对"超时=什么"的问题——因为分类器是自动的,不需要等人类。但它有一个等价的机制:
"After 3 consecutive denials or 20 total, we stop the model and escalate to the human. This is the backstop against a compromised or overeager agent repeatedly pushing towards an outcome the user wouldn't want."
3 次连续被阻止或 20 次累计阻止 → 停止 Agent。不是"给更多机会"——是停止。
在 headless 模式(claude -p,无 UI 交互界面)中更极端——没有升级路径。分类器阻止后无法问人(没有 UI),直接停止。
这是 fail-closed 的极致体现——当无法获得人类确认时,停止而非继续。
Cursor:分类器阻止 → Agent 自调整 → 升级
Cursor 的 Auto-review 分类器阻止操作后,不直接弹审批给用户——把解释发回给 Agent。Agent 通常能自己换路。
但这个"自调整"也有限度——如果 Agent 反复被阻止、反复自调整失败:
Cursor 的设计是渐进升级——从"Agent 自调整"到"用户审批"。最终如果用户也不响应——行为上是 Agent 停止等待,不是自动继续。
arXiv:2607.14166 "Stop Means Stop"
这篇 2026 年 7 月的论文专门研究了 Agent 框架中控制原语(stop/cancel/deny)的执行缺口。
核心发现之一:在多个框架中,取消操作不能可靠地传播到最深层的活跃 Agent。
"cancelled runs losing un-checkpointed state" "cancellation failing to propagate to the deepest active agent"
这意味着即使你在 HITL 中选择了"停止",Agent 的子进程或子 Agent 可能仍在运行。"拒绝"不只是设一个标志——是确保所有正在执行的操作都被终止。
论文还发现了一类"重放危害"——某些框架在恢复(resume)时会重放之前被取消的操作。一个被拒绝的工具调用在 Run 恢复后可能被再次执行——因为 checkpoint 记录了工具调用但没有记录拒绝结果。
"Stop Means Stop"——停止意味着停止,不是延迟后继续、不是恢复后重试。
超时时长:60 秒的理由
超时=拒绝确定后,下一个问题是:超时应该设多长?
太短的超时
10 秒——用户可能正在读审批内容、考虑选择。10 秒不够完成一个有质量的决策。短超时会导致频繁的"误拒"——用户本来想同意但来不及操作。
太长的超时
10 分钟——Agent 在等待期间完全阻塞。用户去开会了,Agent 等 10 分钟后才拒绝、才继续做其他事。长超时浪费的是 Agent 的执行时间和用户等待的时间。
60 秒的平衡
60 秒足够一个在场的用户阅读审批内容并做出决策——但不够长到变成"等一个不在的用户"。
关键假设:如果用户在 60 秒内没有响应 HITL 提示,最可能的原因是"用户不在场"而非"用户正在认真思考"。
不同场景可以调整超时时长——CycleDetector 的 HITL 用 60 秒(循环检测需要快速决策),ErrorStreak 的 HITL 也用 60 秒。browser_wait_for_user没有超时——因为物理操作(手动登录、验证码输入)的完成时间不可预测。
超时后的状态保存
超时拒绝后 Agent 不是"直接消失"——它执行优雅终止:
- 当前正在执行的工具调用被取消(通过 context 传播)
- TaskMemory 中的 Plan 状态被持久化(Checkpoint 之前的工作保留)
- 认知结算(CognitiveSettlement)仍然执行——保存会话快照
- 前端收到终止事件(带
reason: hitl_timeout) - 用户回来后可以从断点恢复
超时终止不等于丢弃工作——等于"安全暂停"。
反面案例:默许的代价
$47K 事故的另一面
$47K 事故(LangChain 四 Agent 管线,264 小时循环)之所以烧了那么多钱,一个被忽视的原因是:系统设计中没有 HITL 机制——但即使有,如果超时=默许,结果也一样。
假设那个系统在 Analyzer-Verifier 循环第 100 次时触发了 HITL:"两个 Agent 似乎在无限循环。是否继续?"
如果超时=默许——运行这个管线的工程师可能在下班后离开了。HITL 提示弹出,无人响应,60 秒后默许继续。下一次触发又是 60 秒默许。264 小时里可能触发了几十次 HITL——每次都默许继续。
结果和没有 HITL 一样:$47,000 被烧掉。但更糟——因为有 HITL 机制,事后审计显示"所有操作都经过了审批"。 审计记录中每次 HITL 都是"approved (timeout)"——从合规角度看它"通过了审批",只是审批方式是超时默许。
Kiro 事故的启示
Amazon Kiro 删除生产环境的事故中,没有 HITL 机制——"There was no confirmation prompt, no second pair of eyes, no two-person rule." 如果有一个 HITL 问"是否删除生产环境?",超时=默许会让这个 HITL 毫无意义——工程师离开了工位(或者 Kiro 在夜间自动运行时),超时后默许删除。
HITL 的价值完全建立在"非明确同意就是拒绝"之上。 如果超时可以替代同意,那 HITL 只是一个可绕过的延迟——攻击者等得起、离开的用户等不到。
设计 HITL 超时的五条原则
综合行业实践和安全分析,HITL 超时的设计遵循五条原则:
原则一:超时 = 拒绝,无例外
不管上下文是什么——CycleDetector、ErrorStreak、安全审批——超时都是拒绝。不存在"这个场景下超时可以默许"的例外。
原则二:所有非明确同意都是拒绝
超时、取消、关闭页面、网络断开、渲染错误——统一处理为拒绝。调用方只需要判断"是不是 nil error"。
原则三:拒绝后保存进度
超时拒绝不是"丢弃所有工作"——是"安全暂停并保存"。认知结算仍然执行,TaskMemory 仍然持久化。用户回来后可以恢复。
原则四:物理操作例外
browser_wait_for_user(登录、验证码)没有超时——因为物理操作的完成时间由人类的物理动作速度决定,不受 Agent 控制。
但这个例外很窄——只有"Agent 物理上无法替代人类完成的操作"没有超时。Agent 选择问人类意见(CycleDetector、ErrorStreak)有超时。
原则五:超时时长可配置但有下限
不同场景可以有不同的超时时长。但要有下限——至少 30 秒,给在场的用户足够的阅读和决策时间。太短的超时把"认真审批"变成了"来不及看就被拒了"。
一句话总结
人工审批的价值完全来自"人类明确说了'是'"这个动作。 如果没有这个动作(超时、离开、没看到)也能达到"是"的效果——审批机制就不存在了。
这不是保守——是安全工程的基本原理。在不确定的情况下选择更安全的选项(fail-closed),不是胆小——是负责任。
你现在能做什么
1. 检查你的 Agent 框架——超时后是继续还是停止。 如果超时后 Agent 继续执行——这是一个安全漏洞。攻击者只要不响应就能获得默许。立即修改为超时 = 停止。
2. 所有 HITL 回调统一处理为"只有明确 Grant 才是 Grant"。 超时、取消、关闭页面、网络错误——全部等同于拒绝。不要分别处理五种失败情况——只判断"是不是明确的 yes",其他全是 no。
3. 超时后保存进度——不是丢弃工作。 超时拒绝 ≠ 删除所有成果。已完成的步骤保留,TaskMemory 持久化,用户回来后可以恢复。"安全暂停"而非"紧急删除"。
4. 物理操作(登录、验证码)不设超时。 browser_wait_for_user 这类操作的超时应该很长或没有——物理操作的完成时间由人类决定。但其他类型的 HITL(循环检测、错误审批)必须有超时。
5. 超时时长至少 30 秒。 太短的超时让在场用户来不及做有质量的决策——变成"又超时了"的烦躁体验。60 秒是大多数场景的好选择。
引用来源
- Anthropic Engineering Blog, "How we built Claude Code auto mode" — 3 consecutive / 20 total denials → stop; headless 模式无升级路径
- Cursor Blog, "Governing agent autonomy with Auto-review" — 分类器 → Agent 自调整 → 升级
- arXiv:2607.14166, "Stop Means Stop" — 取消传播缺口、重放危害
- vectara/awesome-agent-failures, "$47K infinite loop" — 无 HITL 的 264 小时循环
- Docker Blog, "AI Coding Agent Horror Stories: The Agent That Deleted Production" — Kiro 事故
- AI Failure Index, "Amazon's Kiro coding agent deleted a production environment"
更多推荐

所有评论(0)