FDE W4D4 学习手册 · HITL 与中断恢复
W4 Day4 · A 级(必须掌握,面试核心)· 4h · 学会用 LangGraph 把"人有最终否决权"落到可恢复、可重试、可审计的工程系统里
本日定位:Agent 在真实业务(尤其医疗理赔)里不能"自助决策高风险动作"。本日聚焦 Human-in-the-Loop:用 Checkpointer + Interrupt 让流程在关键节点停下等人拍板,并能从中断点精确恢复。
学完能回答:① LangGraph 里 HITL 怎么用 Checkpointer+Interrupt 实现;② 中断恢复、幂等键、最大执行时间如何共同保证可靠性。
使用方法:通读原理 → 重点背「面试话术」「易错点」→ 做自测清单 → 配合《FDE-W4D4-评测题.md》。选中不熟的词可标注(左下★重要 / 右下📌待查)。
候选人视角:36 岁,Java + 大数据 + 医疗保险背景。全流程围绕"特药理赔 Agent"——涉及大额赔付、用药合规,必须有人工审批兜底。
一、HITL 的本质:为什么 Agent 需要"人兜底"
Human-in-the-Loop(人在回路)指在 Agent 自主执行流程中,把部分高风险决策节点交回给人确认/修改/驳回,人拥有最终否决权。它和"纯人工流程""纯自动流程"都不同:Agent 跑大部分步骤,只在关键闸门等人。
1.1 为什么必须 HITL
- 高风险动作不可逆:特药理赔一旦放款,追回成本极高;用药建议错误可能危害生命健康。
- 模型不确定性:LLM 有幻觉、边界模糊,对"是否合规"这类判断需要人类专家拍板。
- 合规与审计:医保/保险监管要求"谁决策、谁负责",必须有可审计的人工节点。
- 异常处理兜底:低置信度、规则冲突、知识库缺失时转人工,而非硬猜。
1.2 HITL 的三种形态
| 形态 | 触发方式 | 典型场景 |
| 审批闸(Approval) | 执行高风险工具前停下等人确认 | 放款、调用外部写接口、外发消息 |
| 修改/纠正(Edit) | 人修改 Agent 的中间状态或输出 | 人工修正理赔金额、补充材料 |
| 降级转人工(Escalate) | 低置信度/异常时直接转人工坐席 | 材料存疑、规则冲突、拒赔争议 |
工程上 HITL 不是"让模型问一句'可以吗'",而是系统级的暂停-恢复机制:状态要落库、流程要能从中断点继续、人的决定要可回写。否则"等一下"就变成"从头重跑",既浪费钱又可能二次出错。
HITL 是"Agent 跑流程、人在关键闸门拍板"。它既不同于纯人工(慢),也不同于纯自动(危险)。在医疗理赔里,放款、用药建议这类高风险动作必须由人最终确认,所以 HITL 是刚需而非可选项。
① 把 HITL 理解成"模型在聊天里问用户一句"是常见误解——那是交互,不是可恢复的工程化 HITL。② HITL 不等于"所有步骤都等人",那样就退化成纯人工,丧失 Agent 提效价值。③ 只在高风险 + 不可逆 + 模型不确定的节点设闸,否则审批疲劳反而降低质量。
二、LangGraph Checkpointer:状态持久化基础
LangGraph 的图是有状态的(StateGraph),每执行一个节点都会产生状态变更。Checkpointer 就是把这个状态快照持久化到外部存储(如 PostgreSQL)的机制,让图可以"暂停、断线、再恢复"。
2.1 核心概念
- Thread:一次对话/任务的唯一标识(thread_id),所有状态快照按它组织。
- Checkpoint:某一时刻的完整状态快照(包含 messages、自定义 state 字段、待执行节点等)。
- Checkpointer 后端:MemorySaver(内存,开发用)、PostgresSaver(生产用)、SqliteSaver 等。
- pending writes:中断时"已决定要做、但尚未提交"的写操作,恢复时重放。
2.2 为什么 HITL 必须依赖 Checkpointer
graph.invoke(input, { thread_id }) → 执行到中断点 → 状态快照落库(含 pending)→ 释放资源 → 人审批 → graph.invoke(None, { thread_id }) → 读快照从中断点继续
没有 Checkpointer,进程一停状态就没了,无法恢复;有了它,中断只是"不继续执行下个节点",状态已安全落库。
PostgreSQL Checkpointer 选型理由:① 事务 + 持久化,状态不丢;② 与生产业务库同生态,运维成本低;③ 支持并发线程隔离;④ 可作为后续 Trace/审计的数据源。医疗理赔对可靠性要求高,生产必须用 PostgresSaver,绝不能用 MemorySaver。
Checkpointer 把图的每一步状态快照存到外部(如 PostgreSQL)。它就是 HITL 能"暂停后恢复"的基础:中断时状态已落库,人审批后再用同一个 thread_id 调 invoke,图从快照接着跑。
① MemorySaver 只存在进程内存,重启即丢,只适合本地调试,生产用它是重大事故。② thread_id 必须稳定且唯一,混淆不同用户/任务的 thread 会导致状态串台。③ Checkpointer 存的是完整状态快照,state 设计过大(如塞整篇 PDF)会拖慢落库与恢复。
三、Interrupt:精确暂停与恢复
LangGraph 提供 interrupt()(配合 @node 使用)让节点在执行到某行时主动暂停并把控制权交还调用方,同时保存上下文。恢复时从断点继续,不会重跑已完成的节点。
3.1 工作机制
from langgraph.types import interrupt, Command
def approval_node(state):
# ... 模型已生成建议赔付金额 ...
decision = interrupt({
"type": "human_approval",
"payload": {"claim_id": state["claim_id"],
"amount": state["suggested_amount"],
"reason": state["reason"]}
})
# 恢复后,decision 就是人返回的结果(通过/驳回/修改)
state["approval"] = decision
return state
3.2 暂停 vs 恢复
| 阶段 | 调用方式 | 行为 |
| 首次运行 | graph.invoke(input, {"configurable":{"thread_id":T}}) | 跑到 interrupt 暂停,返回 interrupt 信息,状态落库 |
| 恢复运行 | graph.invoke(Command(resume=human_decision), {"configurable":{"thread_id":T}}) | 从断点继续,interrupt() 返回 resume 值,后续节点执行 |
3.3 与"普通异常"的区别
- interrupt:受控暂停,状态已保存,可恢复,是设计内的流程控制。
- 异常/崩溃:未受控,状态可能不一致,需靠重试/幂等恢复。
interrupt 是 LangGraph 实现 HITL 的核心原语。它的价值在于状态在暂停点被完整保留,恢复时不是"重跑整个图",而是"从断点续跑",既省 token 又避免重复副作用(如重复查库)。
interrupt() 让节点在中途主动暂停,把要人确认的信息交出去,状态自动落库。人审批后,用 Command(resume=决定) + 同一 thread_id 再 invoke,图从断点继续,interrupt() 返回人的决定。
① 恢复时必须传同一个 thread_id,否则读不到快照,等于新任务从头跑。② interrupt 里返回给模型的 resume 值会在断点处作为 interrupt() 的返回值,不要在恢复时又重新调模型生成。③ 不要在 interrupt 之前节点里做"有副作用且不幂等"的事(如已放款),否则暂停前就产生了不可恢复动作。
四、人工审批触发:什么时机、什么条件下停下
触发人工审批是 HITL 的"闸门设计",目标是只在必要且高价值处拦截,而非无差别拦截。
4.1 触发策略(按风险分层)
| 层 | 条件 | 动作 | 示例 |
| 免审 | 低风险 + 高置信度 | 自动通过 | 金额≤阈值、规则明确匹配 |
| 预警 | 中风险/中置信度 | 记录 + 抽样复核 | 模型置信度 0.6~0.8 |
| 强审批 | 高风险/低置信度 | 中断等人 | 大额赔付、用药冲突、材料缺失 |
4.2 触发信号来源
- 模型自评置信度:logits/概率或显式
confidence 字段。
- 规则引擎:命中"黑名单药品""超额赔付"等业务规则直接拦截。
- 知识库缺失:RAG 检索无高相关结果,无法支撑结论。
- 工具调用风险等级:调用"放款""外发"类工具前必拦截。
闸门设计是工程权衡:拦截太多→审批疲劳、丧失提效;拦截太少→漏判高风险。医疗理赔建议以"金额 + 合规风险 + 模型置信度"三维作为触发条件,并把触发逻辑做成可配置规则,方便策略和监管对齐。
审批触发讲究"该拦才拦"。我们用金额阈值、合规规则命中、模型置信度、知识库缺失几个信号综合判断;只有高风险(大额、用药冲突、材料缺失)才真正 interrupt 等人,低风险自动过。
① 不要把"所有调用都人工审批"当 HITL 的正确姿势——那是伪自动化。② 触发条件硬编码在节点里难维护,应抽成独立规则/配置。③ 若触发信号本身依赖模型输出(如 confidence),要防模型"自信地错",规则层不能完全信模型自评。
五、审批通过与驳回:状态如何回流
人给出决定后,通过 Command(resume=...) 把结果送回图,断点节点拿到决定继续。
5.1 通过的回流
- 审批字段写入 state:
approval = "approved"、审批人、时间戳。
- 流程继续到"执行动作"节点(如调用赔付接口、生成通知书)。
- 下游节点读取审批结果,确保只在 approved 时产生副作用。
5.2 驳回的处理
- 审批结果
approval = "rejected" + 驳回原因。
- 不走"执行动作"节点,转入驳回分支:通知用户、记录原因、可选转人工坐席。
- 不得重复触发同一审批(用幂等键/状态标记防重)。
5.3 代码结构示意
def execute_node(state):
if state["approval"]["decision"] != "approved":
return {"status": "rejected", "reason": state["approval"]["reason"]}
# 仅在 approved 且幂等键未消费时执行副作用
if not ledger.mark_consumed(state["claim_id"], state["idempotency_key"]):
return {"status": "already_done"}
payout.call(claim_id=state["claim_id"], amount=state["amount"])
return {"status": "paid"}
通过/驳回本质是对 state 中审批字段的分支。关键是:副作用(放款/外发)必须放在"确认 approved 且幂等键未消费"之后执行,从根上避免驳回后误放款或重复放款。
人点通过,state 写 approved,流程继续去执行放款;人点驳回,走驳回分支(通知+记录+转人工),绝不执行放款。分支判断紧盯审批字段,保证"没批就不动钱"。
① 常见 bug:节点没检查 approval 状态就直接放款,导致驳回后仍执行。② 驳回分支若再次触发审批会形成死循环,需设最大轮次/转人工上限。③ 审批结果的"原因"必须落库,既为审计也为后续模型复盘。
六、人工状态修改与回写
HITL 不只是"同意/拒绝",人常需修改中间结果(如把模型算的赔付额从 8 万改成 5 万,并补充材料)。这要求状态可被安全地"回写后继续"。
6.1 修改的两种路径
| 路径 | 做法 | 适用 |
| resume 直接带新值 | Command(resume={"decision":"approved","amount":50000}) | 一次性修改少量字段 |
| update_state 显式改 | 恢复前调用 graph.update_state(thread_id, {"amount":50000}) | 需多字段/多次编辑、人工工作台 |
6.2 回写后的继续
- 修改写回 Checkpoint 后,再 invoke 即可从当前状态继续。
- 下游节点应读取最新 state,而非缓存旧值。
- 每次修改都应记录"谁、何时、改了什么",形成审计轨迹。
状态回写的可控性是 HITL 比"纯对话问答"高级的地方:人不是给一句自然语言意见,而是在结构化状态上做确定性修改,下游确定性地受影响。这对医疗理赔的金额、用药方案这类数值型结论尤为重要。
人工不仅能批/拒,还能直接改状态——比如把模型算的赔付额改小,用 update_state 或 resume 带新值写回,图继续跑时下游用最新值。所有修改留痕可审计。
① 回写要防止"字段覆盖丢失":用浅合并而非整对象替换,避免误清其他字段。② 人工改完金额后,若下游依赖"模型原始输出"做对账,需保留 original 与 edited 双版本。③ 回写时机必须在执行副作用之前,否则改了也救不回已放款。
七、异常重试与恢复一致性
恢复执行时可能发生网络超时、工具失败、模型报错。需要分层重试策略,且每次重试都基于已落库的状态,避免状态漂移。
7.1 重试分层
| 失败点 | 策略 | 说明 |
| LLM 调用超时 | 指数退避重试 ≤3 次 | 临时网络抖动 |
| 工具调用失败 | 重试 + 降级(备用接口/转人工) | 外部依赖不稳 |
| 恢复时 checkpoint 损坏 | 读最近可用快照 / 告警 | 极端情况 |
| 人长时间不审批 | 超时转自动驳回/升级 | 防止任务挂死 |
7.2 一致性要点
- 重试必须在同一 thread_id 下,复用同一快照。
- 所有重试要记 Trace(次数、原因),用于成本归因。
- 重试不能放大副作用——靠幂等键保证"重试 N 次也只生效一次"。
重试的核心是"可重入":无论失败在第几步,重新执行都应收敛到同一结果。Checkpointer 提供状态锚点,幂等键保证副作用只发生一次,二者配合才谈得上"可靠恢复"。
恢复执行也可能失败(超时/工具挂)。我们按失败点分层重试:LLM 退避重试、工具重试+降级、人不审批则超时升级。所有重试都基于同一个落库状态,靠幂等键保证不重复产生副作用。
① 重试若不带 thread_id,会新起任务、状态错乱。② 退避重试要设上限,否则雪崩。③ 重试仍失败必须降级而非卡死,医疗场景"挂起不处理"本身也是事故。
八、幂等键:保证副作用"只发生一次"
幂等(Idempotency)指同一操作执行一次和多次效果相同。在 HITL 恢复里,放款、外发、写库这类副作用必须幂等,否则"重试/重复恢复"会导致二次放款。
8.1 幂等键设计
# 每次"提交审批结果 + 执行动作"用全局唯一 idempotency_key
key = f"{claim_id}:{approval_id}" # 或由客户端/审批系统生成
if ledger.mark_consumed(claim_id, key):
payout.call(claim_id, amount) # 首次:执行
else:
return {"status": "already_done"} # 重复:直接返回成功,不重复执行
8.2 关键点
- 键的粒度:到"一次具体动作"(如某次审批对应的放款),而非整个任务,避免误吞合法二次操作。
- 存储:幂等表用唯一索引(claim_id, key),并发抢注,只一个成功。
- 有效期:键可设 TTL,过期允许重新发起(但需谨慎,避免绕过审批)。
幂等键是金融/医疗系统的生命线。它把"恢复重试"从"危险操作"变成"安全操作":即使因网络/崩溃重试多次,放款只发生一次。这是面试官最爱追问的可靠性细节。
幂等键保证同一动作重复执行只生效一次。我们给每次审批+执行生成唯一键,落幂等表用唯一索引抢注;只有首次执行放款,重复调用直接返回成功。这样中断恢复、网络重试都不会二次放款。
① 不要用"随机每次都新的 key"——那就失去幂等意义;key 必须能标识"同一个动作"。② 幂等检查与执行要在同一事务,否则并发下仍有竞态。③ 只做"去重"不做"状态校验"不够——还要确认上次是成功还是失败,避免把失败的当成功跳过。
九、最大执行时间:防止挂起与失控
Agent 可能因循环、等待人工过久、工具卡死而"永远跑不完"。最大执行时间(超时/步数上限)是兜底安全网。
9.1 多个时间维度
| 维度 | 含义 | 典型值 |
| 单步 LLM 超时 | 一次模型调用的最长等待 | 30~60s |
| 单步工具超时 | 一次工具调用的最长等待 | 10~30s |
| 单轮 Agent 步数上限 | 防止无限循环(max steps) | 10~20 步 |
| 人工审批超时 | 等人拍板的最长时限 | 数小时/可配置 |
| 整体任务 SLA | 端到端最长耗时 | 按业务定 |
9.2 超时的处理
- 单步超时 → 退避重试或降级。
- 步数超限 → 判定异常,转人工/告警,记录原因。
- 审批超时 → 自动驳回或升级到更高权限/值班人。
最大执行时间是对"失控"的最后防线。它的设计哲学是:宁可失败得可控(超时转人工/告警),也不要"永远挂着"造成业务阻塞。在理赔 SLA 里,挂起比拒赔更糟糕。
我们设多层超时:单步 LLM/工具超时、Agent 总步数上限防循环、人工审批超时(超时自动升级或驳回)。原则是把"失控"变成"可控的失败",而不是让任务永远挂着。
① 只设一个全局超时不够,要分层;单步卡死和整体 SLA 是不同问题。② 步数上限设太小会误杀正常长流程,设太大又防不住循环,需按真实 case 标定。③ 审批超时若"默认通过"是重大合规风险,医疗场景一般默认驳回或升级而非放行。
十、特药理赔 Agent 的 HITL 落地示例
场景:用户提交特药(如某抗癌药)理赔申请,Agent 负责材料审核、用药合规性判断、赔付金额计算,最终放款需人工审批。
10.1 流程图(关键节点)
接收申请 → 材料抽取(LLM) → 知识库校验(用药合规/医保目录)
→ 规则引擎(金额/黑名单) → [高风险?] --是--> interrupt(人工审批)
--否--> 自动通过
人工审批: 通过/驳回/修改金额
→ 执行放款(幂等键保护) → 生成通知书 → 结束
10.2 一次完整 HITL 会话
- thread_id=
claim_10086 首次 invoke,跑到"人工审批"节点 interrupt,返回待审信息(金额 8 万、用药合规、置信度 0.92)。
- 状态快照落 PostgreSQL,释放计算资源。
- 理赔专家在后台看到待审,把金额改为 5 万(超出目录限额部分不赔)并点"通过"。
- 系统用
Command(resume={"decision":"approved","amount":50000}) + 同 thread_id 恢复。
- 执行节点校验幂等键
claim_10086:approval_77 未消费 → 放款 5 万 → 标记已消费。
- 若此时因网络重试了一次恢复调用,幂等键已消费,直接返回"已处理",不会二次放款。
这个示例把当天所有概念串起来了:Checkpointer 落库保证可恢复、interrupt 实现受控暂停、人工修改回写状态、幂等键防重复放款、超时/步数上限防失控。这是面试时最该能"讲成一段故事"的例子。
特药理赔 Agent:材料抽取→合规校验→规则判断,高风险就 interrupt 等人审批,人可改金额再批。恢复用同 thread_id,放款前校验幂等键,重复调用也不会二次放款。这段完整故事就是 HITL 的答卷。
十一、可靠性保证四件套总结
Checkpointer(状态不丢) + Interrupt(受控暂停) + 幂等键(副作用一次) + 最大执行时间(失控可控)
| 风险 | 对应机制 | 失效后果 |
| 进程崩溃状态丢失 | Checkpointer 落库 | 任务从头重跑、重复副作用 |
| 高风险动作无人确认 | Interrupt + HITL | 误放款/误建议 |
| 重试/重复恢复放大副作用 | 幂等键 | 二次放款、重复外发 |
| 循环/挂起/久等 | 最大执行时间 | 任务失控、SLA 违约 |
保证可靠性就四件事:状态用 Checkpointer 落库不丢;高风险用 Interrupt 暂停等人;副作用用幂等键保证只一次;用最大执行时间兜底失控。四者缺一个,医疗理赔场景都可能出事故。
十二、面试达标线①:HITL 在 LangGraph 里怎么用 Checkpointer + Interrupt 实现
| 步骤 | 动作 | 关键代码/概念 |
| 1. 配 Checkpointer | 用 PostgresSaver 给图挂持久化 | graph = builder.compile(checkpointer=PostgresSaver(...)) |
| 2. 设计 state | 把待确认信息放进 state | messages + claim 字段 |
| 3. 中断节点 | 在关键闸门调用 interrupt() | 返回待审 payload |
| 4. 首次运行 | 带 thread_id invoke,跑到中断点暂停落库 | 返回 interrupt 信息 |
| 5. 人审批 | 人看信息,做通过/驳回/修改 | 前端/工作台 |
| 6. 恢复 | Command(resume=决定) + 同 thread_id invoke | 从断点续跑 |
一句话:给图挂 PostgresSaver,在关键节点 interrupt() 暂停并把待确认信息交出去;人审批后用 Command(resume=结果) 配合同一 thread_id 恢复,图从断点继续。Checkpointer 负责把状态落库,Interrupt 负责受控暂停。
十三、面试达标线②:中断恢复 / 幂等键 / 最大执行时间如何保证可靠性
- 中断恢复:Checkpointer 把状态快照落库,interrupt 在断点暂停;恢复时读快照从断点续跑,不重跑已完成节点,省 token 且避免重复副作用。
- 幂等键:每次"审批+执行"用唯一键,落幂等表唯一索引抢注;只有首次执行放款,重复恢复/重试直接返回已处理,杜绝二次放款。
- 最大执行时间:分层超时(单步 LLM/工具、总步数上限、审批超时),把"失控"转为"可控失败(转人工/告警/升级)",防循环与挂起。
- 三者配合:Checkpointer 给状态锚点,Interrupt 给流程控制,幂等键给副作用安全,超时给兜底,共同构成医疗级可靠 Agent。
可靠性=中断恢复(状态可续)+幂等键(副作用只一次)+最大执行时间(失控可控)。中断恢复靠 Checkpointer+Interrupt,幂等键防重复放款,超时把失控变可控失败。三者缺一不可。
十四、W4D4 自测清单
- 能说清 HITL 三种形态(审批/修改/转人工)及医疗理赔为何必须 HITL。
- 能解释 Checkpointer 的作用、thread_id 与 checkpoint 的关系,以及为何生产必须用 PostgresSaver 而非 MemorySaver。
- 能讲清 interrupt() 的暂停-恢复机制,以及 resume 值如何在断点返回。
- 能区分 interrupt(受控暂停)与异常崩溃(未受控)。
- 能设计审批触发策略(金额/规则/置信度/知识缺失三维)。
- 能说清审批通过/驳回时状态如何回流,副作用为何要放在 approved 校验之后。
- 能解释人工状态修改的两种回写路径(resume 带值 vs update_state)。
- 能讲清异常重试的分层策略与"同一 thread_id 复用快照"原则。
- 能设计幂等键(粒度、唯一索引、事务、失效处理),说清它如何防二次放款。
- 能列出最大执行时间的多个维度,并说明超时为何要"可控失败"而非挂起。
- 能把"特药理赔 Agent"的 HITL 完整流程讲成一段故事(达标线①②)。
十五、高频面试题速记卡
Q:LangGraph 里 HITL 靠什么实现?
Checkpointer(状态落库,如 PostgresSaver)+ interrupt()(受控暂停)。人审批后用 Command(resume=决定)+同 thread_id 恢复,从断点续跑。
Q:为什么生产不能用 MemorySaver?
它只在进程内存,重启即丢状态,无法恢复。医疗/金融必须用 PostgresSaver 持久化。
Q:恢复时必须传什么?
同一个 thread_id,否则读不到快照,等于新任务从头跑。
Q:幂等键解决什么问题?
保证副作用(放款/外发)只发生一次。重复恢复或网络重试都不会二次放款。
Q:驳回后为什么不能再次触发审批?
会形成死循环且无意义,应设最大轮次/转人工上限,且用状态标记防重。
Q:人工修改状态用哪两种方式?
resume 直接带新值(一次性少量字段);恢复前 update_state(多字段/工作台编辑)。
Q:最大执行时间为什么要分层?
单步卡死和整体 SLA 是不同问题;分层(LLM/工具/步数/审批超时)才能精准兜底。
Q:审批超时默认通过安全吗?
不安全,医疗场景一般默认驳回或升级,绝不能"超时自动放行"。
Q:Interrupt 和异常崩溃区别?
interrupt 是设计内受控暂停、状态已落库、可恢复;崩溃是未受控、状态可能不一致。
Q:特药理赔 HITL 流程一句话?
材料抽取→合规校验→规则判断,高风险 interrupt 等人审批(可改额),恢复后幂等键保护放款。
FDE W4D4 学习手册 · HITL 与中断恢复(面试级)· 配合《FDE-W4D4-评测题.md》自测
延伸学习资源(中文 / 非 OpenAI):
① B站 LangGraph 系列 · Checkpoint / Interrupt / HITL 章节(BV1zKx4zWEZP)——建议倍速看 interrupt 实战那集。
② LangGraph Interrupts 官方文档:docs.langchain.com/oss/python/langgraph/interrupts——官方对 interrupt/resume 的机制说明与多轮审批示例。
③ LangGraph Checkpointer 文档(PostgresSaver):docs.langchain.com/oss/python/langgraph/persistence——生产级持久化配置。
📌 待查★ 重要