人在环中:Propose-Then-Commit 本节摘要:2026 年业界对人在环中(Human-in-the-Loop, HITL)的共识很具体——它不是「Agent 问、用户点批准」那么简单,而是 propose-then-commit(先提议再提交):提议的动作带着幂等键(idempotency key)持久化到耐用存储;连同意图、数据血缘(data lineage)、触及的权限、爆炸半径(blast radius)、回滚计划呈现给评审者;只有在正向确认后才提交;执行后再验证副作用真的发生了。LangGraph 的 配 PostgreSQL 检查点、Microsoft Agent Framework 的 、Cloudflare 的 实现的都是同一个形状——API 名字不同,形状不变。
本节摘要:2026 年业界对人在环中(Human-in-the-Loop, HITL)的共识很具体——它不是「Agent 问、用户点批准」那么简单,而是 propose-then-commit(先提议再提交):提议的动作带着幂等键(idempotency key)持久化到耐用存储;连同意图、数据血缘(data lineage)、触及的权限、爆炸半径(blast radius)、回滚计划呈现给评审者;只有在正向确认后才提交;执行后再验证副作用真的发生了。LangGraph 的
interrupt()配 PostgreSQL 检查点、Microsoft Agent Framework 的RequestInfoEvent、Cloudflare 的waitForApproval()实现的都是同一个形状——API 名字不同,形状不变。典型失败模式是「橡皮图章」式批准:「批准?」被无评审地点下去。已记录的缓解是**质询-应答(challenge-and-response)**配显式清单——评审者必须对三个具体问题正向作答,批准键才亮起。EU AI Act 第 14 条要求高风险 AI 系统的「有效」人工监督,法律语言明确排除橡皮图章,而 propose-then-commit 配质询-应答正是能在第 14 条审查下存活的形状。本节是工程课:把结构化评审变成阻力最小的路径。
对应原课程:Phase 15 · Lesson 15 ·
propose-then-commit(原英文phases/15-autonomous-systems/15-propose-then-commit/docs/en.md)。前置:第 12 节(持久执行)、第 14 节(三探测器)。
阅读完本节,你应当能够:
Agent 要做一个动作,用户要决定:批不批。如果决定是瞬时的,那大概率不是评审;如果决定是结构化的,它慢但可信。工程问题是:怎么让结构化评审成为阻力最小的路径。
2023 年代的 HITL 模式是同步提示:「Agent 想给 X 发邮件、正文 Y —— 批准吗?」用户点批准,所有人都觉得系统很安全。实践中这个界面被严重橡皮图章化:用户批准得很快,批准几乎不预测任何东西;等 Agent 出错时,审计轨迹显示一长串用户自己都记不起来的批准。
2026 年的模式——propose-then-commit——把 HITL 搬到耐用基板上,附上结构化元数据,要求正向提交。每个托管 Agent SDK 都出货一个版本:LangGraph interrupt()、Microsoft Agent Framework RequestInfoEvent、Cloudflare waitForApproval()。API 名字不同,形状不变。
⚠️ 核心张力:橡皮图章让 HITL 沦为仪式。propose-then-commit 的全部价值,在于把「评审这件事」做成结构化、可追溯、不可跳过——而不是给用户一个更花哨的「批准」键。
没有幂等键,瞬时失败后的重试可能让一个已批准动作被执行两次。具体例子:用户批准「从 A 转 100 美元到 B」。网络抖动,工作流重试。用户只批准了一次,但转账执行了两次。幂等键把批准绑定到单个、唯一的副作用;第二次执行是 no-op。
这是 Stripe 和 AWS API 用的同一套幂等模式。把它复用到 Agent 批准上,在 Microsoft Agent Framework 文档里有明确说明。
批准候审室是一块 Agent 不拥有的状态。工作流被暂停(第 12 节)。当批准到达,工作流从精确那一点恢复。这就是为什么 LangGraph 把 interrupt() 配 PostgreSQL 检查点,而不是内存状态——两天后的批准仍能找到完好无损的工作流。
HITL 的默认 UI(「批准/拒绝」按钮)产出的是无真实评审的快速批准。已记录的缓解:质询-应答清单,要求评审者在对三个具体问题正向作答后,批准键才被启用。具体形状:
这不是为官僚而官僚——而是一个强制函数(forcing function)。勾不全的评审者,要么要求澄清(升级),要么拒绝(安全默认)。Anthropic 的 Agent 安全研究明确把清单驱动的 HITL 列为橡皮图章的缓解。
不是每个动作都需要 propose-then-commit。2026 年的指南:
「提交跑完了」不等于「副作用发生了」。网络分区和竞态条件可能产生一个工作流——它以为自己成功了,但后端没持久化。验证步骤在提交后重新读目标资源以确认。这与带 RETURNING 子句的数据库事务、或 PutObject 后再 GetObject 的 AWS 是同一模式。
第 14 条要求高风险 AI 系统在欧盟内有有效的人工监督。「有效」不是装饰性的。监管语言明确排除橡皮图章模式。配质询-应答的 propose-then-commit,是在 Microsoft Agent Governance Toolkit 合规文档中能扛住第 14 条审查的形状。
原课程 code/main.py 用标准库 Python 实现这台状态机。耐用存储是一个 JSON 文件。幂等键是 (thread_id, action_signature) 的哈希。驱动器模拟三个场景:干净的批准流、瞬时失败后的重试(必须不重复执行)、以及橡皮图章默认流对照质询-应答流。下面给出关键骨架。
import hashlib, json def make_proposal(thread_id, action, intent, lineage, perms, blast, rollback): sig = json.dumps(action, sort_keys=True) key = hashlib.sha256(f"{thread_id}:{sig}".encode()).hexdigest() return { "idempotency_key": key, # 重提交同一提议 → 返回同一条记录 "action": action, "intent": intent, # 为什么做 "data_lineage": lineage, # 哪个来源导致 "permissions_touched": perms, "blast_radius": blast, # 最坏情况 "rollback_plan": rollback, "state": "proposed", # proposed → committed → verified }
def commit(proposal, durable_store, approver): rec = durable_store.get(proposal["idempotency_key"]) if rec and rec["state"] == "committed": return rec # 已提交过, 第二次执行是 no-op if not approver.positive_ack(proposal): raise Pending("等待人工正向确认") execute(proposal["action"]) proposal["state"] = "committed" durable_store.put(proposal["idempotency_key"], proposal) return proposal
设计要点:幂等键把「批准」绑定到「唯一副作用」。
execute失败可以重试,但绝不会因为重试而把同一副作用执行两次。
def verify(proposal, read_back): # 「提交跑完」 ≠ 「副作用发生」: 网络分区/竞态可致假成功 if not read_back(proposal["action"]): run_rollback(proposal["rollback_plan"]) # 验证失败 → 自动回滚 + 告警 alert("verify 失败, 已进入已知坏状态") return False proposal["state"] = "verified" return True
CHALLENGE = [ "你理解这个动作触及什么资源吗?", "你确认爆炸半径可接受吗?", "如果失败你有回滚计划吗?", ] def gated_approve(proposal, reviewer): # 批准键默认禁用; 三个问题都正向作答才启用 answered = all(reviewer.acknowledge(q) for q in CHALLENGE) if not answered: return "要求澄清(升级) 或 拒绝(安全默认)" return commit(proposal, durable_store, reviewer)
| 框架 | HITL 原语 | 耐用存储 | 形状是否一致 |
|---|---|---|---|
| LangGraph | interrupt() |
PostgreSQL 检查点 | 是:提议-呈现-提交-验证 |
| Microsoft Agent Framework | RequestInfoEvent |
框架托管状态 | 是:带结构化元数据的耐用 HITL 请求 |
| Cloudflare Agents | waitForApproval() |
Durable Objects | 是:批准候审室在 Agent 之外 |
| 裸 stdlib 实现 | 自建状态机 | JSON 文件(教学) | 同形状, 仅规模不同 |
关键观察:API 名字不同,形状不变——这说明 propose-then-commit 是领域共识,不是某家厂商的发明。选型时看的是耐用存储的可用性、元数据字段的完整度、是否内建质询-应答门控,而不是 API 拼写。
💡 三家都把批准候审室放在 Agent 不拥有的状态上。这是设计层面的硬要求:如果候审室是 Agent 内存的一部分,Agent 重启就丢了;如果是 Agent 能写的资源,Agent 就能自己批准自己。
原课程 outputs/skill-hitl-design.md 评审一条拟议的 HITL 工作流,检查它是否符合 propose-then-commit 形状,并标出缺失项:缺幂等键、缺验证步骤、缺质询-应答层、元数据不全(没有爆炸半径或回滚计划)。它把「枚举后果性动作 → 为每个配提议记录 → 检查幂等与验证 → 检查门控」串成可重复清单。
code/main.py 是零依赖状态机,改耐用存储后端(JSON 换 Redis/PostgreSQL)即可贴近产线;状态机逻辑本身无需改动。
跑 code/main.py:确认已批准提议的重试用的是耐用记录、不重复执行。然后把幂等键改成包含时间戳,展示重试会重复执行——体会幂等键绑定的对象是什么。
加回滚字段:给提议记录扩展 rollback 字段。模拟一次验证失败的执行,展示回滚自动触发。
读 Microsoft Agent Framework RequestInfoEvent 文档:找出一个玩具引擎缺失的元数据字段,加上它,解释它防御什么。
设计质询-应答清单:针对一个具体动作(如「在公开 Twitter 账号发帖」),评审者必须回答哪三个问题?为什么是这三个?
找一个同步「批准?」就够的案例(不需要耐用存储):解释为什么,并说出你接受的风险类别。
下一节,我们把「单步可恢复」升级为「跨多步可恢复」——检查点与回滚(checkpoints and rollback),看长程 Agent 如何在任意一步崩溃后,从已知良好的检查点重启而非从头来过。