只要 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 设计通常还会带一个 leaseEpoch 或 fencingToken。原因是:即便 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-30s | 5-10s |
| 多步工作流 | 30-60s | 10-20s |
| 长人工等待 | 不持续占 lease | 进入 waiting 状态 |
等待人工确认时,通常不应该继续占着 active lease。否则 worker 数量会被空耗掉。
如果流程里存在长等待节点,lease 状态最好也显式区分:
| 状态 | 含义 |
|---|---|
active | 当前 worker 正在持有执行权 |
paused_waiting_external | 等待人工或外部回调,不占执行 worker |
expired | 超时未续约 |
reclaimed | 已被系统收回并准备移交 |
revoked | 因冲突或手动操作被撤销 |
这样调度系统就不会把“正在干活”和“只是在等待”混成同一种占有状态。
四、任务回收不能直接重跑,要先做 lease reclaim guard
当 lease 过期后,系统不能立刻让另一个 worker 从头执行。更安全的流程是:
- 标记旧 lease 失效。
- 读取最近 checkpoint。
- 检查副作用是否已发生。
- 检查是否已有新的 worker 接手。
- 通过 guard 后再恢复。
如果跳过这些检查,就可能造成双执行:旧 worker 只是暂时网络抖动,但实际上还在跑;新 worker 又被系统放进来,两个实例同时执行同一任务。
回收动作最好也分两步:
suspect_dead: 先怀疑 lease 失效,但不立即移交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 管恢复。只有四者配合,长任务系统才不会在抖动时自己打自己。
延伸阅读:


