📌 本文是「AI Agent 的人类控制权」系列的第 4/5 篇第一篇建立问题意识,第二篇给出三层分析框架,第三篇用框架做六大框架体检,本篇深入工程实现,第五篇讨论前端设计与 Automation Bias。另有番外篇讲微软 Agent 框架的分裂与统一。

——Agent 状态管理的深水区


上一篇我们用三层模型对六个框架做了体检,结论是:真正的分水岭不在 Layer 1 和 Layer 2,而在 Layer 3——Checkpoint 的有无决定了 HITL 是 demo 还是生产系统。

这篇展开讲为什么。

如果你写过一个带 HITL 的 Agent demo,你大概率经历过这样的时刻:在 Jupyter Notebook 里一切运行流畅——Agent 执行到审批节点暂停了,你在下一个 cell 里输入审批结果,Agent 继续执行,完美。然后你试着把它部署成一个 Web 服务。一切崩溃了。

这篇要讲的,就是这个崩溃背后的结构性矛盾,以及生产级 HITL 需要解决的几个核心技术问题。

核心矛盾:有状态的 Agent vs 无状态的 Web

Agent 的执行天然是有状态的。它维护着一条不断增长的执行轨迹——对话历史、中间推理结果、工具调用的返回值、当前执行到了哪一步。这些状态在步骤之间是有依赖的:第五步的决策依赖第三步的工具返回值,第八步的推理依赖第一步到第七步的全部上下文。

Web 服务天然是无状态的。一个 HTTP 请求进来,处理完,返回响应,连接关闭。服务器不记得上一个请求是什么。这是 Web 架构能水平扩展的基础——任何一台服务器都能处理任何一个请求,因为不需要"记住"任何东西。

当 Agent 需要 HITL 时,这两个世界发生了正面碰撞:

1
2
3
4
5
6
7
8
Agent 的期望:
  执行第1-4步 → 暂停 → 等人类审批(可能等几分钟到几天)→ 从第5步继续

Web 服务的现实:
  HTTP 请求 → 开始执行 → 需要暂停 → ???
  HTTP 连接超时(通常 30-60 秒)→ 连接断开
  Agent 线程被回收 → 内存中的执行状态消失
  人类审批回来了 → 找不到之前的执行上下文 → 失败

在 Notebook 里这个矛盾不存在,因为 Python 进程一直活着,变量一直在内存里。input() 阻塞等待,你输入完了,Agent 接着跑。

在 Web 服务里,你不能让一个 HTTP 请求阻塞等半小时。你也不能假设处理恢复请求的那台服务器和处理初始请求的是同一台——在 Kubernetes 等编排环境中,Pod 随时可能被重调度。

这就是 Checkpoint 要解决的根本问题:把"有状态的同步等待"转化为"无状态的异步等待"。

Checkpoint 到底要保存什么

说"保存 Agent 状态"听起来简单,做起来不简单。Agent 的"状态"比你想象的复杂得多。

第一层:对话历史

这是最直觉的部分——LLM 的 messages 列表。但即使是这个最基本的部分,也有几个要注意的点:

  • 消息可能包含大体积内容(图片、长文档的嵌入)
  • 工具调用的消息结构不是简单的 text(包含 function_name、arguments、result)
  • 系统消息(system prompt)是否要保存?如果系统消息可能在恢复后变化(比如你更新了 prompt),恢复时应该用哪个版本?

第二层:工具调用的中间状态

Agent 在第三步调用了一个 API 获取了数据,在第五步暂停了。恢复后,Agent 的推理需要用到第三步的数据。这个数据必须作为状态的一部分被保存。

更复杂的情况:第三步的 API 返回了一个临时令牌(token),这个令牌有效期是 10 分钟。人类在 30 分钟后才审批。恢复时令牌已经过期了。Agent 需要知道这一点,并重新获取令牌。这不是 Checkpoint 机制自身能解决的问题,但 Checkpoint 的设计需要为这类场景留出处理空间。

第三层:执行位置

Agent 在图的哪个节点暂停的?恢复后应该从哪里继续?这在 LangGraph 里是自动处理的——interrupt() 会记录暂停点,Command(resume=...) 会从那个点精确恢复。但在没有原生 Checkpoint 的框架中,你需要自己实现这个"书签"功能。

第四层:元数据

线程 ID、用户 ID、审批策略配置、当前的自主性级别……这些不是 Agent 的"业务状态",但是恢复执行时必需的上下文。

LangGraph 的做法值得参考:它把所有这些打包成一个 Checkpoint 对象,每一步执行后自动写入存储。恢复时,通过 thread_id 找到最新的 Checkpoint,反序列化后从暂停点继续。开发者不需要手动管理任何一层状态——这是"把持久化作为一等公民"的含义。

异步审批的工程实现

理解了 Checkpoint 之后,一个完整的异步审批流程长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1. 客户端发送请求:POST /agent/run {task: "帮我发一封邮件给团队"}
2. 服务端 Agent 开始执行
3. Agent 推理后决定调用 send_email
4. 框架检测到 send_email 是高风险工具,触发审批
5. 框架将完整状态写入持久化存储(Checkpoint
6. 释放 Agent 线程和计算资源
7. 返回响应:202 Accepted
   {
     thread_id: "abc-123",
     status: "awaiting_approval",
     pending_action: {
       tool: "send_email",
       params: {to: "[email protected]", subject: "会议纪要"}
     }
   }

--- 异步间隔:秒 /  /  /  ---

8. 客户端展示审批 UI,人类审查并批准(可能修改了收件人)
9. 客户端发送恢复请求:POST /agent/resume
   {
     thread_id: "abc-123",
     decision: "approve",
     modified_params: {to: "[email protected]"}
   }
10. 服务端从存储中恢复 Checkpoint
11. Agent 从暂停点继续,使用修改后的参数执行 send_email
12. 返回最终结果:200 OK {result: "邮件已发送"}

这个流程里有几个关键设计决策:

第一,步骤 7 返回 202 而不是 200。 HTTP 202 表示"请求已接受但处理尚未完成",语义上比 200 更准确。客户端收到 202 就知道需要进入"等待审批"的 UI 状态。

第二,步骤 6 释放线程。 这是核心——暂停期间不占用任何计算资源。Agent 的状态在数据库里,服务器可以处理其他请求。LangGraph 的博客原话:“interrupted threads don’t take up any resources (beyond storage space), and can be resumed many months later, on a different machine."——在不同机器上、几个月后恢复,这是生产级 Checkpoint 的标准。

第三,步骤 9 的恢复请求可以由任何服务器实例处理。 因为状态在外部存储中,不在任何特定服务器的内存里。这意味着你的 Agent 服务可以水平扩展——启动 10 个 Pod,任何一个都能处理恢复请求。

状态膨胀问题

Checkpoint 解决了"能暂停恢复"的问题,但引入了一个新问题:状态会膨胀

LangGraph 在每一步都写一个 Checkpoint。如果 Agent 执行了 20 步,就有 20 个 Checkpoint。每个 Checkpoint 包含完整的对话历史(随步骤增长)、工具调用结果、中间状态。对话历史本身可能包含长文档、图片描述、大量的工具返回数据。

粗略估算:一个中等复杂度的 Agent 任务,20 步执行,每步 Checkpoint 大小可能在 10KB-100KB,总计 200KB-2MB。这看起来不多。但如果你的服务每天处理 10000 个任务,每个月就是 600GB-60TB 的 Checkpoint 数据。

几个缓解策略:

策略一:只保留最新的 N 个 Checkpoint。 对于 HITL 来说,你只需要从最新的暂停点恢复,历史 Checkpoint 的价值递减。但注意"时间旅行”(回到之前的某个步骤重新执行)需要历史 Checkpoint,所以这是一个功能 vs 存储的权衡。

策略二:大对象外置。 不要把大文件、图片、长文档的内容直接放在 Agent 状态里。只存引用(URL 或文件 ID),需要时再去取。这是 LangGraph 社区推荐的做法。

策略三:Checkpoint 压缩和清理策略。 对已完成的线程,可以只保留最终状态,删除中间步骤。对长时间未活跃的线程(比如审批超过 30 天未处理),自动过期清理。

滑动自主性的工程实现

第二篇讲了滑动自主性的概念——用一个 L0-L4 的旋钮动态调节 Agent 的自主程度。这个概念落到工程实现上,本质是一个策略引擎

最简单的实现是一个配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
autonomy_policy:
  tools:
    search_web:
      level: L3_auto_approve
    read_file:
      level: L3_auto_approve
    write_file:
      level: L2_notify_after
    send_email:
      level: L1_require_approval
    execute_sql:
      level: L1_require_approval
      conditions:
        - if: "statement_type == 'SELECT'"
          then: L3_auto_approve
        - if: "estimated_rows_affected > 1000"
          then: L0_block
    delete_resource:
      level: L0_block
      exceptions:
        - if: "environment == 'staging'"
          then: L1_require_approval

进阶实现是动态策略——根据运行时信息调整自主性级别。比如:

  • Agent 连续 100 次 write_file 无误 → 自动从 L1 降到 L2
  • 检测到 Agent 的输出包含敏感信息模式 → 临时提升到 L1
  • 当前时间是凌晨 3 点(没有人能及时审批)→ 高风险操作排队到工作时间

目前没有任何一个框架内建了完整的滑动自主性引擎。最接近的是 Claude Code 的 allowlist 配置(静态策略)和 AWS 的 Agentic AI Security Scoping Matrix(概念框架,非具体实现)。这是一个开放的工程问题——也是一个有价值的开源项目方向。

一个经常被忽略的问题:审批 UI

到目前为止我们讨论的都是后端——框架怎么暂停、怎么存状态、怎么恢复。但 HITL 有一个前端问题经常被忽略:人类通过什么界面来审批?

大多数框架的 HITL demo 用的是 input() 在终端等待输入,或者在 Notebook 里写代码来提供审批结果。这在演示中没问题,但在生产环境中,你需要:

  • 一个审批队列 UI,显示所有等待审批的任务
  • 每个审批任务的详情页,展示 Agent 要做什么、为什么需要审批、参数是什么
  • 批准/拒绝/修改的交互控件
  • 审批超时的处理逻辑(如果没人处理,是自动拒绝还是升级到其他审批人?)
  • 移动端推送(高优先级审批需要及时处理)
  • 审批历史和审计日志

这些都不在任何 Agent 框架的范畴内。框架提供的是 API 层面的暂停/恢复能力,但从 API 到用户界面之间有一整层工作需要做。

LangGraph 的 LangGraph Studio 提供了一个开发者导向的审批界面(能看到图的执行状态、在暂停点手动操作),但它是开发调试工具,不是面向终端用户的审批 UI。

这意味着在你的 Agent 项目中,HITL 的"最后一公里"——审批 UI——几乎一定需要自建。 框架能帮你解决暂停和恢复,但"人类怎么看到审批请求、怎么做出决策"这个产品设计问题,是你的。

小结

从 demo 到生产,Agent HITL 需要跨越四个关卡:

  1. 有状态 vs 无状态的矛盾 → 用 Checkpoint 把同步等待转为异步等待
  2. 状态的完整性和复杂性 → 对话历史、工具中间结果、执行位置、元数据,缺一不可
  3. 状态膨胀 → 外置大对象、限制历史 Checkpoint 数量、自动清理策略
  4. 审批 UI 的最后一公里 → 框架管后端,产品管前端,中间的缝你得自己缝

这四个关卡中,第一个是最致命的——如果框架没有 Checkpoint,后面三个都无从谈起。这也是为什么在上一篇的选型建议中,我们把"框架怎么处理暂停和恢复"作为第一评估标准。


回顾整个系列:

  • **第一篇**确立了问题——Agent 能动手就需要控制权
  • **第二篇**给出了分析框架——三层模型把 HITL 拆解为可操作的维度
  • **第三篇**用框架做了验证——六个框架的横向对比暴露了 Layer 3 的关键性
  • 第四篇(本文)深入了工程细节——从 Checkpoint 到异步审批到状态管理

如果你从这个系列中只带走一个认知:HITL 不是在 Agent 里加一个 if need_approval: ask_user() 那么简单。它是一个跨越模型能力、框架机制、系统架构和产品设计的系统工程。 把它做对,你的 Agent 就获得了在真实世界中运行的资格。把它做错——或者不做——你的 Agent 终将在某个你无法预见的时刻,做出一件你无法撤回的事。


这是「AI Agent 的人类控制权」系列的第四篇。 完整系列: