无人值守调度三件机件:认领锁、中心推送、幂等键

无人值守调度三件机件:认领锁、中心推送、幂等键

系列:Idea-to-Production · 机理篇。同系列:《人不在环内:一个 AI 自主交付组织的概念契约》(概念契约)、《为什么要拆成三个仓库:知识、调度、档案的分离法则》(分层背景)。

TL;DR(先给结论)

  • "没有任何人在环内"不是营销话术,它落到工程上就是三个古典分布式问题:同一个任务不能被两个工人做、同一件周期工作不能被触发两次、同一次重跑不能产出两份结果。
  • 三件机件一一对应:原子认领锁 (互斥)、中心推送的作业运行器 (触发所有权)、幂等契约(重复产出防线)。
  • 关键参数:锁窗口默认 7200 秒(2 小时),长任务约每 30 分钟续期;看门狗每 60 秒巡检一轮,被遗弃任务恢复上限约 3 分钟;作业运行器每 60 秒一个刻度;建任务幂等窗口 5 分钟。
  • 分水岭是角色多实例化:一旦一个角色跑多个实例,任何"自己安排下一次"的 Agent 都会变成重复产出器。

为什么值得看

大多数多 Agent 系统在用"对话"掩盖调度问题:反正只有一个工人、反正跑砸了重跑一次就行。一旦一个角色要跑多个实例、一旦循环任务上了日程,这三个问题会立刻浮出水面。本文不介绍具体产品,只给出三件任何无人值守调度都绕不开的机件及其设计细节------你可以直接拿去和自家调度层逐条对照。

你将学到三点

  1. 并发执行的互斥要"原子认领 + 看门狗回收"双保险:拿不到锁就跳过下一个,绝不死等;锁过期后由巡检自动回收,恢复上限约三分钟,而不是"等人来救"。
  2. 周期任务必须由中心拥有:角色一旦多实例,任何"自己安排下一次"的 Agent 都会把同一件事做多遍。触发权在平台,实例只是被动接收者。
  3. 每个循环都必须声明自己的去重键:重跑不翻倍,靠的不是运气,而是"检查先行"(check-before-create)的显式约定。

机件一:原子认领锁------一个任务只有一个工人

前提:一个角色(比如 PM)可能同时跑着多个实例。执行任何任务前,实例必须先赢得这条任务的认领锁。

一条任务的锁生命周期:

text 复制代码
拉取候选(按角色匹配 + 可执行状态 + 优先级)
  → 依赖门控:所有前置任务必须 Done,否则不可认领
  → 原子 INSERT 锁表(任务 ID 是主键 → 物理上只容得下一个持有者)
  → 干活(超过锁窗口前必须续期)
  → 状态上报即释放(校验转移 + 删锁在同一操作内完成)
  ↘ 忘了续期?看门狗巡检 → 强制回 Backlog → 他人可再认领

互斥的原子性不靠"先查再插"这种竞态写法,而是靠任务 ID 作主键的原子 INSERT------数据库层面物理上只容得下一个持有者。在这个基础上,五条协议规则构成完整契约:

规则 内容 为什么这样设计
P1 未认领成功,绝不干活 锁表记录就是分派本身;本地锁是权威,平台同步只是尽力而为
P2 认领失败 = 跳过,绝不重试风暴 拿不到锁的实例看到的就是"没有任务";不去轮询锁表、不等锁释放
P3 依赖门控发生在认领时 前置未 Done 的任务,执行者根本看不到,不会半成品开工
P4 可能超窗的任务必须续期 锁窗口默认 7200 秒(2 小时),长任务按约每 30 分钟续一次
P5 释放只能经由状态上报 不是持有者来上报?409 拒绝,绝不强写------持有者的报告永远优先

P2 是最容易被"优化"掉的一条:把"认领失败"实现成退避重试,多实例高并发下就是一场重试风暴。P5 则确立了持有者对任务状态的唯一发言权------平台侧永远不强写。

活性与恢复由看门狗 兜底:巡检每 60 秒跑一轮,过期锁一律带 expired 标记释放、任务强制回 Backlog 并可记 EXPIRE 审计;持有者心跳超时同样回收,实例被标记 OFFLINE。于是一条被遗弃的任务,恢复上限约是 min(锁窗口, 心跳超时) + 巡检间隔 ≈ 3 分钟------不是"等人类第二天上班发现"。

两个必须写下来的失败语义:过期回收把任务放回 Backlog,但不回滚已写的半成品------重新认领的实例必须把任务当"可能做了一半"来对待,先验证再继续;每一次 CLAIM / RELEASE / EXPIRE 都落进只追加(append-only)的审计日志------没有人盯着的管线,事后必须可追问。

机件二:中心推送的作业------Agent 不准自己排班

先看翻车场景。假设让实例自己排"每日客户问题分诊":

PM 角色今天跑了两个实例。A 实例分诊完,顺手安排了"明天同一时间再跑一次";B 实例不知道 A 的安排,也给自己留了一份。明天,同一个客户 issue 被分诊两次,生成两倍的任务。

这不是实现 bug,是所有权设计错误。所以铁律是:一切周期性工作都是控制中心的一条 Job 记录,实例永远不自排、不自续。运行机制:

  • 作业运行器每 60 秒一个刻度,扫描 enabled 且 next_run_at ≤ now 的 Job;
  • 到期时,为这条 Job 解析唯一一个 在线的目标角色实例,通过既有的 WebSocket 通道推一条 job_trigger;
  • 推送后推进 next_run_at,同一条静态记录就这样无限期地触发下去。

两个关键性质:实例是被动接收者 ------它从不轮询、不枚举、不认领任何排班;作业不进任务看板、不走认领锁------周期任务和一次性任务在模型上是两种东西,混在一起是大多数"定时 Agent"翻车的起点。日报周报类循环靠这个模型保证"每天恰好跑一次",而不管底下开着一台还是十台工作机。

机件三:幂等契约------重跑不翻倍

无人值守意味着重跑是常态(续期失败、实例重启、看门狗回收后的再认领)。系统对"重复产出"的防线不是运气,而是一条全局约定:每个循环声明自己的去重键,动手之前先查重(check-before-create)。

环节 去重键 / 幂等机制
建任务 API 接受 idempotency_key,5 分钟窗口内重复请求返回缓存结果,绝不建出第二条
状态上报 同一转移在幂等窗口内重复上报安全无害
每日 PR 巡检 以 pr-reviewed 标签为去重键:查过的 PR 直接跳过
周期报告 文件名带 ISO 周编号(如 usage-report-2026-W36.md),同窗口重跑覆盖而非追加
发布 不可变的 v* tag 就是幂等键:有 tag 即已发,绝不二次发布
双周发布 先看 git diff <上个tag>..HEAD,零变更直接跳过并记审计

注意去重键的选择品位:优先用外部世界里本来就唯一的东西(标签、周编号、git tag、diff),而不是运行时再发明一个"已处理"状态表------前者天然可人工核对,后者又要有人(或另一个 Agent)来维护它的正确性。

三件机件在界面上是什么样

控制中心仪表盘把调度层的健康度直接摊开:在线实例数、本周完成、今日过期锁------过期锁这个小卡片,就是机件一的看门狗在工作留下的痕迹;按角色分布则暴露"哪个角色的队列堵了":

总结

  1. 认领锁 = 互斥 + 时限 + 兜底回收三件事的组合:原子性靠主键约束,活性靠续期与巡检,恢复语义靠"回 Backlog + 验证半成品"------缺一件都撑不起"无人值守"。
  2. 周期任务的所有权属于平台,这是多实例系统的第一定理:任何让 Agent 自己排班的设计,都会在角色扩容的那天变成重复产出器。
  3. 回去数一数你的系统里有几处"重跑会翻倍"的地方:每一处都应当有一个写得出来的去重键。写不出来,就是还没有真正的设计。

你们的调度层用什么方案做互斥和去重?评论区交流~

相关推荐
Daniel_1235 天前
脱离 Android Studio:纯命令行开发 Android 相机 App 的实践与踩坑记录
设计
Daniel_1235 天前
【求助】纯命令行开发安卓相机App的一些问题,求大佬指点
设计
用户55279738499657 天前
《2009年的老古董笔记本,我是如何用它部署Go + Python高性能Web服务的》
设计
for_ever_love__7 天前
上下文工程:比提示词工程更重要的事
llm·agent·提示词·设计·context·engineering·上下文工程
Percy_kk8 天前
AI Agent 开发实践:从手写 ReAct 到框架选型
agent·设计
高晶8 天前
Android 计算器开发实战:从界面设计到核心逻辑
程序员·设计
易知微EasyV数据可视化8 天前
会插卡片的编辑器:在 Agent 输入框里混排文字与结构化内容
人工智能·经验分享·编辑器·agent·设计
广州智造9 天前
Radioss时间步长(Time Step)
前端·设计·cad·建模·cae·radioss
用户69190268133912 天前
LangChain 与 LangGraph:从「链」到「图」,一次讲清它们是什么、什么关系
面试·架构·设计