DAY 34 / PHASE 4 · HUMAN-IN-THE-LOOP

Human-in-the-Loop 工程

置信度路由 · 异步审批 · 可中断恢复 · 审批审计

2026-06-15 · BigCat

真正的 HITL 不是「弹个框等你点 yes」,是让 agent 学会优雅地暂停、把决定权交还给你、再无缝接回去。

// WHY THIS MATTERS

Day 31 讲过「不可逆动作前加 human gate」——但那只是一行 input()。一旦 agent 跑在后台、跑几百轮、你不在终端前盯着,同步阻塞就崩了:agent 卡死等一个没人的终端;或者你嫌烦,干脆全 auto。生产级 HITL 不是一个对话框,是一套编排工程:agent 何时才值得打扰人(置信度路由)、怎么在不阻塞的前提下问(异步审批 + 状态持久化)、人怎么接管再交还(可中断 checkpoint)、这一切怎么留痕复盘(审批审计)。本期把 require_human 从一行 input(),升级成一个能在你睡觉时挂起、手机点一下就恢复的系统。HITL 的工程难点从来不是「要不要问人」,而是「问的时候 agent 怎么办」。

// 01

置信度路由:决定「什么时候才值得打扰人」

论断:每个动作都问人 → 审批疲劳到全程盲点头;什么都不问 → 把不该自动的自动了。HITL 的第一道工程是路由,不是 gate。

背景与原理

Day 31 按「可逆性」分档(可逆放手、不可逆过人)。这里加第二个轴:置信度 × 影响。两轴交叉出三档——auto(高置信 + 低影响,放手)、ask(低置信 或 中高影响,问人)、deny(越权 / 超预算,直接拒,根本不进人的队列)。关键陷阱:置信度不能让模型自评——它系统性高估。用可验证的代理信号替代:检索命中数、工具是否报错、多次采样是否自洽(self-consistency)、输出是否吻合 schema。模型说「我有把握」不算数,证据说了算。

实战示例

def confidence(ctx):                  # 用可验证信号,不问模型「你多有把握」
    sigs = [ctx.retrieval_hits >= 2,
            not ctx.tool_errored,
            ctx.self_consistency >= 0.8,    # 多次采样是否一致
            ctx.schema_ok]
    return sum(sigs) / len(sigs)

def route(action, ctx):
    if action.over_budget or action.unauthorized:
        return "deny"                     # 越权不是「问人」,是 bug
    c, impact = confidence(ctx), action.impact
    if c >= 0.8 and impact == "low":  return "auto"
    return "ask"                          # 低置信 或 中高影响 → 问人

核心 mental model:HITL 的成本是人的注意力,而注意力是会枯竭的稀缺资源。路由的目标不是「更安全」,是「把人的每一次介入都花在刀刃上」。

失败模式:(1)置信度让模型自评——它在「想完成任务」的压力下高估,等于没路由。(2)阈值拍脑袋定死,从不回归(见 §4)。(3)把 deny 也丢进人的审批队列——越权动作根本不该出现在那里,混进来只会淹没真正需要判断的 ask
进阶资源 · Anthropic Building Effective Agents(人类监督与 agent-computer interface), anthropic.com/engineering/building-effective-agents
// 02

异步审批:让 agent 暂停、通知、恢复

论断:input() 是同步阻塞——后台 agent 用它必死。生产 HITL 的核心是把「等人」从阻塞变成挂起 + 回调

背景与原理

agent 跑到 ask 档时,input() 会冻住整个 worker,干等一个可能几小时没人看的终端。正解是 durable execution 的思路:把当前状态 checkpoint 到进程外(不是留内存)→ 向审批通道(Slack / 手机推送 / 队列)发请求 → 让出资源、退出 → 人决定后通过 callback 或轮询,从 checkpoint 接着跑。两条铁律:状态必须在进程外(进程挂了审批不丢);resume 必须幂等(同一个 approval 被重放,动作不能执行两次)。再给每个审批挂 SLA 超时:挂太久 → 默认 deny 或升级,绝不无限等。

┌──────────────── 异步审批循环(不阻塞) ────────────────┐ │ agent loop ──▶ route()==ask │ │ │ │ │ ▼ ① checkpoint 状态到进程外(DB / 文件) │ │ ▼ ② 发审批请求到 Slack / 手机 + approval_id │ │ ▼ ③ 让出 worker,退出 ── 不占 CPU 干等 │ │ · · · (人在手机上点一下) │ │ ▼ ④ callback 带 approval_id 唤醒 │ │ ▼ ⑤ 幂等校验:这个 id 处理过吗?处理过 → 忽略 │ │ ▼ ⑥ resume_from(checkpoint) 接着跑 │ │ 超时未决 ─▶ SLA 触发:默认 deny / 升级给备份审批人 │ └──────────────────────────────────────────────────────────┘

实战示例

async def step(ctx):
    action = plan(ctx)
    if route(action, ctx) == "ask":
        aid = save_checkpoint(ctx)              # ① 状态落进程外,带 approval_id
        notify("slack", action, aid)         # ② 发请求,不等返回
        raise Suspend(aid)                      # ③ 让出 worker,退出
    return execute(action)

def on_approval(aid, decision):               # 人点完后由 callback 触发
    if seen(aid): return                     # ⑤ 幂等:回调重放不重复执行
    mark_seen(aid); ctx = load_checkpoint(aid)
    if decision == "approve": resume_from(ctx)  # ⑥ 从断点接着跑
    else: resume_from(ctx, denied=True)        # 否决也回灌给 agent
失败模式:(1)同步 input() 阻塞整个 worker——一个待审批拖垮所有并发任务。(2)状态只在内存——进程一重启 / 一崩,审批和进度全丢。(3)没幂等——callback 重放或用户连点两下,转账执行两次。(4)审批无超时——一个没人理的 ask 让任务永久挂起。
进阶资源 · Temporal Durable Execution(长流程的状态持久化与重放), temporal.io · LangGraph Human-in-the-loop(interrupt / resume), langchain-ai.github.io
// 03

可中断与人工接管:不止「批 / 否」,还要能「改」

论断:HITL 的高价值不在二选一的批准,而在人能介入修改 agent 的中间产物,再让它接着跑。

背景与原理

把人类介入只想成 approve / reject,是浪费。真实工作流里还有两种更值钱的动作:edit(改它的 plan / 草稿 / 参数再继续)和 takeover(人接手做几步,再还给 agent)。要支持这两种,agent loop 必须满足三个前提:可中断(不是 while True 跑到死)、状态可序列化可从任意 checkpoint 恢复。更关键的一点常被漏掉:人改完之后,必须把修改回灌进 agent 的上下文——否则它下一步又用旧计划覆盖你的修改。所以「计划」「草稿」要做成可被人覆写的显式状态,而不是埋在 prompt 历史里。

实战示例

def loop(state):
    while not state.done:
        if stop_requested(): return snapshot(state)  # 可中断:随时存盘退出
        proposal = plan(state)
        decision = await_human(proposal)        # approve / edit / takeover
        if   decision.kind == "approve":  state = execute(proposal, state)
        elif decision.kind == "edit":
            state.plan = decision.edited         # 人改的计划
            state.ctx += f"[human edited plan: {decision.note}]"  # 回灌!否则被覆盖
        elif decision.kind == "takeover":
            state = decision.human_steps         # 人做几步,再 resume 还给 agent
    return state

一个反直觉但重要的点:edit 比 reject 更省人力。reject 把 agent 打回重来(它可能再犯同样的错);edit 直接把它推到正轨,一次校正胜过十次否决。

失败模式:(1)不可中断的 while True——想介入只能 kill,进度全丢,只能从头再来。(2)人改了草稿,但没回灌 context——agent 下一轮无视你的修改。(3)checkpoint 状态不全——恢复时缺关键变量,agent「失忆」乱跑。
进阶资源 · LangGraph Breakpoints & state editing, langchain-ai.github.io · Microsoft Guidelines for Human-AI Interaction(G9–G11:可纠错 / 可接管), microsoft.com
// 04

审批的可观测与审计:谁批了什么、批得对不对

论断:审批本身是一等数据。不留痕,你既学不到「哪些 ask 是多余的」,也查不清「这个动作当时谁批的」。

背景与原理

每条审批至少要留:动作、agent 的理由、置信度、谁批的、延迟多久、最终结果。这份日志有两个用途。第一,回归路由阈值——某类 ask 你 95% 都批 → 降级为 auto;某类 auto 出过事 → 升级为 ask。§1 的阈值不是一次拍定,是从审计数据里长出来的。第二,升级与委派——高影响动作要求不止一人 / 特定角色批(轻量 RBAC);审批挂太久自动升级给备份审批人。最后,对抗审批疲劳最有效的一招:同类低风险动作批量呈现(一屏批 10 个),而不是弹 10 次对话框。

实战示例

def log_approval(rec):                    # 每条审批落库,供回归 + 追责
    db.insert(action=rec.action, reason=rec.agent_reason,
              conf=rec.confidence, approver=rec.who,
              latency=rec.decided_at - rec.asked_at, outcome=rec.result)

def retune(action_type):                  # 用历史审批回归阈值
    rows = db.query(action_type, last_days=14)
    rate = mean(r.outcome == "approved" for r in rows)
    if rate > 0.95 and no_incidents(rows): suggest("ask→auto")  # 总在批 = 多余的 ask
    if any(r.outcome == "incident" for r in rows): suggest("auto→ask")
失败模式:(1)审批不留痕——事后无法复盘、无法追责、阈值永远拍脑袋。(2)阈值定死不回归——多余的 ask(疲劳)和漏网的 auto(事故)都不修。(3)所有审批一个粒度——高风险和低风险混在一个队列,真正重要的被噪音淹没。
进阶资源 · Anthropic Agentic Misalignment(对敏感动作降自主 / 加监督的建议), anthropic.com/research/agentic-misalignment

// 综合实战 · 给后台 agent 加一层异步 HITL

把四点串成一个周末项目:给你常跑的后台 agent 加一层「能在你睡觉时挂起、手机点一下就恢复」的审批层,并亲手 red-team 它。

  1. 路由:写 (confidence, impact) → {auto, ask, deny} 表,置信度用可验证信号代理(检索命中 / 工具报错 / self-consistency),不问模型。
  2. 异步:把 ask 档从 input() 改成 checkpoint 落库 + 发 Slack/推送 + 挂起退出,别阻塞 worker。
  3. 接管:审批回执支持 approve / edit / takeover 三种,edit 一定回灌 context
  4. 幂等:approval_id 去重,callback 重放 / 用户连点不重复执行。
  5. 审计:每条审批落库,跑一周后用 retune() 回看哪些 ask 该降级、哪些 auto 该升级。
  6. Red-team:故意挂起一个审批 12 小时,验证 SLA 超时触发(默认 deny / 升级)且进程重启后审批不丢。丢了,说明状态还在内存里。

做完这套,你以后看任何「自主 agent 产品」会本能地先问:它问人的时候自己怎么办(阻塞还是挂起)、我能不能改它的中间产物、审批留没留痕——而不是被 demo 里的「一键全自动」唬住。

// ENGLISH GLOSSARY

Human-in-the-Loop (HITL)
把人类判断嵌入 agent 执行回路的工程,而非事后审查。
Confidence Routing
按(置信度 × 影响)把动作路由到 auto / ask / deny,决定是否打扰人。
Durable Execution
状态持久化在进程外,进程崩溃 / 重启后能从断点恢复的执行模型。
Checkpoint
可序列化的 agent 状态快照,挂起与恢复的载体。
Idempotent Resume
同一审批被重放,恢复逻辑只执行一次、不产生重复副作用。
Human Takeover
人接手执行若干步后再把控制权交还 agent。
Trajectory Editing
人修改 agent 的中间计划 / 草稿,并回灌其上下文后继续。
Approval Queue
待人裁决的 ask 动作队列,支持批量、超时、升级。
Escalation / SLA
审批超时后自动升级给备份审批人或按默认策略裁决。
Approval Fatigue
审批过多导致盲目点批,使 HITL 退化为橡皮图章。

// 深入思考

§1 用「可验证信号」代理置信度。但很多开放任务(写一段策略建议)根本没有可验证信号,这种怎么定 auto / ask?
没有可验证信号,就退回按影响分档(回到 Day 31 的可逆性轴):开放任务的输出若只是「草稿给你看」,影响低 → auto 出草稿但默认进 review;若会自动发出去(发邮件 / 发推),影响高 → 强制 ask。换句话说,置信度路由和可逆性 gate 是互补的两个轴,不是二选一:有可验证信号时用置信度,没有时退回影响 / 可逆性。两者都不强时,fail-safe 默认 ask。真正危险的是「既无信号又默认 auto」。
异步审批要 durable execution、状态落库、幂等回调——这对个人项目是不是过度工程?什么时候 input() 就够?
有个清晰的分界:agent 是否在你的注意力之外运行。你坐在终端前盯着它一步步跑,input() 完全够,别上队列。一旦它(a)跑在后台 / cron、(b)单次运行跨越你的注意力窗口(几十分钟以上)、或(c)多个实例并发——同步阻塞就会咬你:worker 卡死、进程一崩进度全丢。判据不是「项目大小」,是「出问题时你在不在场」。不在场,才需要 durable。在场,过度工程反而是负担。
edit / takeover 让人改 agent 的中间产物。但如果人改得比 agent 还糟,或人自己也不确定?HITL 是不是只是把责任甩给疲惫的人?
这是真问题。HITL 不创造判断力,它只放置判断力——放错地方就是甩锅。两条原则避免:(1)只在人确实比模型强的点上要人介入(领域知识、价值取舍、对你私人语境的了解),纯算力 / 检索类别让 agent 自己来。(2)给人决策所需的上下文,而不是光丢个 yes/no——附上 agent 的理由、置信度、备选项。如果人只能盲批,那不是 HITL,是责任转移的仪式。当人也不确定时,正确动作往往是 escalate(升级)或 abstain(搁置),而非硬批。
§4 用历史审批回归阈值=用过去决定未来。如果你过去图省事乱批,回归出的 auto 阈值会不会把坏习惯固化成自动化?
会,这是 HITL 审计的核心陷阱:它会放大你的批准偏差retune() 里那个 no_incidents 条件就是防线——只在「总批 没出过事」时才建议降级,光「总批」不够。但更稳的做法是别让回归自动改阈值,只让它提议suggest() 而非 apply()),由你定期 review。把人的偏差喂回系统、系统再自动化这个偏差,是 HITL 最隐蔽的失败——所以审计本身也要被审计。
这套 confidence routing 和 Day 31 的 reversibility gate 是什么关系?什么时候用哪个?会打架吗?
不打架,是纵深的两层。Day 31 的 reversibility gate 是底线安全:不可逆动作无论置信度多高都过 gate——这是不能被路由绕过的硬约束。本期的 confidence routing 是上层效率:在「该不该问人」的灰区里,用置信度决定打扰频率,省人的注意力。组合方式:先过 reversibility(不可逆 → 必 ask,置信度再高也不放行),可逆动作再交给 confidence routing 决定 auto / ask。一句话:可逆性决定「能不能自动」,置信度决定「值不值得问」。前者管安全下限,后者管效率上限。

// 延伸阅读