AI agent Worker Lease 与心跳机制:防止重复执行、僵尸任务和失控重试

HTMLPAGE 团队
20 分钟阅读

AI agent 跑长任务时,worker 掉线、重复领取和僵尸任务很常见。本文讲清 lease、heartbeat、任务归还、超时回收和恢复策略。

#AI agent #Worker Lease #心跳机制 #工程实践

只要 AI agent 开始执行长任务、多步流程或队列任务,就迟早会遇到 worker 级问题:一个任务被两个 worker 同时执行、worker 已经挂了但系统以为它还活着、任务重试后和旧执行并发冲突、某个 Run 卡死几个小时没人接手。

这些问题通常不属于模型能力,而属于执行层调度。Lease 和 heartbeat 的存在,就是为了回答两个最基本的问题:

  • 这个任务现在归谁执行?
  • 如果执行者失联了,系统何时收回控制权?

建议先配合 AI agent 任务优先级队列AI agent Checkpoint 与断点恢复AI agent 幂等与去重实践AI Agent 并发与可靠性 一起看。

先给结论:Lease 解决归属,Heartbeat 解决存活

机制主要回答什么
Lease当前哪个 worker 有权继续执行
Heartbeat这个 worker 现在是否还活着
Timeout Reclaim多久后系统可以收回任务
Resume Guard收回后能否安全继续执行

少了其中任何一层,队列都可能出现“看起来没问题,但实际上已分叉”的状态。

一、为什么只靠队列 ack 不够

很多人第一反应是:消息队列不是已经有 ack 了吗?为什么还要 lease?

因为 agent 任务通常比一条消息复杂得多:

  • 一次任务会跨多个工具调用
  • 可能等待人工确认
  • 可能运行几分钟甚至更久
  • 可能在中途进入 checkpoint 和恢复

队列 ack 只适合说明“消息被消费了”,不适合表达“这整个长任务的控制权仍归某个 worker 所有”。

二、一个最小 Lease 结构

至少记录这些字段:

{
  "leaseId": "lease_123",
  "taskId": "run_456",
  "workerId": "worker_a",
  "acquiredAt": "2026-05-07T10:00:00Z",
  "expiresAt": "2026-05-07T10:00:30Z",
  "heartbeatAt": "2026-05-07T10:00:20Z",
  "status": "active"
}

expiresAt 不是可选项。没有过期时间,失联 worker 就会把任务永久占住。

更稳的 lease 设计通常还会带一个 leaseEpochfencingToken。原因是:即便 lease 已经过期,旧 worker 也可能因为网络抖动或线程阻塞,在稍后恢复后继续尝试写入。单靠 workerId 很难防住这种“过期持有者”继续动作。

例如:

{
  "leaseId": "lease_123",
  "taskId": "run_456",
  "workerId": "worker_a",
  "leaseEpoch": 17,
  "fencingToken": 17,
  "status": "active"
}

后续所有关键写入都带上 fencingToken,下游只接受最新 epoch,旧 worker 就算“活过来”也写不进去。

三、Heartbeat 不是越频繁越好

心跳太慢,回收不及时;心跳太频繁,系统会多出大量无意义写入。一个常见折中是:

任务类型lease 时长heartbeat 间隔
短任务15-30s5-10s
多步工作流30-60s10-20s
长人工等待不持续占 lease进入 waiting 状态

等待人工确认时,通常不应该继续占着 active lease。否则 worker 数量会被空耗掉。

如果流程里存在长等待节点,lease 状态最好也显式区分:

状态含义
active当前 worker 正在持有执行权
paused_waiting_external等待人工或外部回调,不占执行 worker
expired超时未续约
reclaimed已被系统收回并准备移交
revoked因冲突或手动操作被撤销

这样调度系统就不会把“正在干活”和“只是在等待”混成同一种占有状态。

四、任务回收不能直接重跑,要先做 lease reclaim guard

当 lease 过期后,系统不能立刻让另一个 worker 从头执行。更安全的流程是:

  1. 标记旧 lease 失效。
  2. 读取最近 checkpoint。
  3. 检查副作用是否已发生。
  4. 检查是否已有新的 worker 接手。
  5. 通过 guard 后再恢复。

如果跳过这些检查,就可能造成双执行:旧 worker 只是暂时网络抖动,但实际上还在跑;新 worker 又被系统放进来,两个实例同时执行同一任务。

回收动作最好也分两步:

  1. suspect_dead: 先怀疑 lease 失效,但不立即移交
  2. reclaimed: 通过 guard 后,才允许新 worker 领取

这个小小的中间态非常有用,因为它能让系统把“心跳迟到”与“执行者真的失联”分开处理。

五、关键写入前要做 fencing 校验,不只是本地比对 lease

很多团队做到这里会停在“写入前比对当前 lease.owner 是否还是自己”。这还不够,因为真正的冲突通常发生在跨服务调用或数据库提交阶段。

更稳的做法是让下游系统也理解 fencing token:

update task_state
where run_id = ?
  and lease_epoch = 17

只有当提交时携带的 epoch 仍然是最新值,写入才被接受。这样防线就不只停留在本地内存判断。

六、Lease 和幂等必须一起设计

Lease 能减少重复执行,但不能单独保证安全。原因很简单:

  • worker 可能在 lease 过期前已经发出请求
  • 外部系统不一定知道 lease 概念
  • 网络分区时旧 worker 可能误以为自己仍有控制权

所以真正稳的设计是:

  • lease 控制“谁有资格继续跑”
  • 幂等控制“即使重复跑也不重复产生副作用”

这两者不是替代关系,而是前后两道闸。

把它说得更具体一点:

  • lease/fencing 解决“谁现在还能继续写”
  • 幂等解决“就算重复提交,副作用也只落一次”

在真实生产系统里,这两层通常都要有,缺一层都容易在边界抖动时出事。

七、上线后要看调度稳定性指标

Lease 机制上线后,建议持续看这些指标:

指标用途
lease 过期回收次数判断心跳与时长是否合理
reclaim 后恢复成功率判断 checkpoint 与 guard 是否有效
fencing 拒绝写入次数判断是否存在旧 worker 复活写入
等待态占用时长判断是否有 worker 被无效占用
双执行告警数判断 lease 机制是否真的挡住冲突

如果 lease 很复杂,但这些指标没有明显改善,说明机制可能只是增加了复杂度,没有真正收敛并发问题。

八、失败案例:旧 worker 恢复后继续写,和新 worker 冲突

一个审批 Agent 的 worker 因网络问题短暂失联,lease 过期后系统把任务交给了新 worker。新 worker 根据 checkpoint 正常继续;几秒后旧 worker 网络恢复,又继续执行后续写入,结果同一任务出现两次状态变更。

根因不是 lease 机制没有,而是旧 worker 在执行前没有再次验证 lease 是否仍然属于自己。

修复后,每个关键写入前都加了一层 lease validation:

if currentLease.workerId !== self.workerId -> stop immediately

同时所有写入仍然走幂等 actionId,避免最后一道防线失守。

九、Worker Lease Checklist

  • 每个长任务是否有明确 lease owner
  • lease 是否有 expiresAt
  • 是否有 leaseEpoch 或 fencingToken 防止旧持有者写入
  • heartbeat 间隔是否按任务类型设计
  • 长等待节点是否释放 active lease 并进入显式等待态
  • 等待人工时是否释放 active lease
  • lease 回收前是否执行恢复 guard
  • 回收是否区分 suspect_dead 和 reclaimed 阶段
  • 每个关键写入前是否重新校验 lease
  • 下游写入是否也校验 fencing token
  • lease 与幂等是否配合使用

结语

AI agent 的 worker 机制,本质上是在解决“谁现在有资格推动任务继续前进”。Lease 管归属,heartbeat 管存活,幂等管副作用,checkpoint 管恢复。只有四者配合,长任务系统才不会在抖动时自己打自己。

延伸阅读: