Agent 长任务租约与防重工程 2026
约 17 分钟5016 字1 次阅读

一、问题不是任务太长,而是所有权会过期
生产级 Agent 很少只执行一次模型调用。一次代码迁移、数据核对或跨系统工单,可能持续数分钟到数小时,并穿过模型推理、人工审批、外部工具与异步回调。进程重启、网络分区和队列重投递都会使同一任务被两个执行器同时看见。此时,单纯把状态写成“运行中”并不能证明谁仍有执行权;数据库里的旧记录、进程内锁以及消息的确认状态,都可能在故障后彼此矛盾。
工程目标因此不该是不可实现的“绝不重复”,而应是:允许消息至少投递一次,却把副作用收敛成业务上至多生效一次。可把一次长任务表示为六元组
其中 是任务标识, 是执行轮次, 是当前所有者, 是租约截止时间, 是单调递增的防栅栏令牌, 是状态。执行器只有同时满足“租约未过期”和“令牌仍为最新”时,才有资格提交外部副作用。租约解决谁暂时拥有任务,防栅栏令牌解决旧拥有者在暂停后复活的问题,幂等键解决重复请求已经到达下游的问题。三者不能互相替代。
二、失效模型:先承认没有可靠的执行时钟
长任务最危险的故障不是立即崩溃,而是停顿后继续。某个工作进程可能因垃圾回收、容器冻结、节点抢占或模型调用超时而沉默;协调器认为租约已失效,把任务交给新进程;旧进程随后恢复,并携带过期结果写回。如果只检查“我曾经拿到锁”,两个进程都可能成功。
设租约时长为 ,续租周期为 ,协调器允许的时钟与调度误差上界为 ,一次不可中断提交的最长时间为 。安全配置至少应满足
但这只是降低误判概率,并不能让时钟本身变成一致性协议。真正的提交门禁必须依赖存储端的原子比较:提交时验证任务的令牌 与当前记录一致,并在同一事务内推进状态。若执行器轮次为 ,可以把重复提交概率粗略分解为
这提示团队不要只调队列可见性超时。只要下游不校验幂等键,或存储端接受旧令牌,重复副作用仍会穿透。准确的故障率需要用本系统的暂停时长、重投递和下游响应分布测量;截至 2026 年 7 月,没有一个跨云、跨队列都成立的公开常数可以直接套用。
三、租约协议:领取、续租与放弃必须是条件写
领取任务应是一条带条件的原子更新,而不是先查询再修改。候选条件通常包括:任务处于待执行状态,或原租约已经过期;更新内容包括新所有者、新截止时间、递增后的防栅栏令牌以及执行轮次。数据库返回更新后的记录,执行器才算真正获得所有权。若更新影响行数为零,就必须退回队列,不能凭本地缓存继续。
续租同样需要比较所有者与令牌。一个实用策略是按剩余租约的三分之一到二分之一提前续租,并加入随机抖动,避免大批任务在同一秒争抢数据库。设单次续租失败概率为 ,允许连续失败 次才主动停工,则误停概率近似为 ;然而增加 也会扩大双执行窗口,所以它不是越大越好。应依据续租接口的尾延迟,而不是平均延迟设置阈值。
任务代码需要一个显式的失权信号。续租失败、令牌不匹配或协调器宣布取消后,执行器应停止启动新的工具调用,并让正在运行的不可取消调用进入隔离区。所谓隔离区,是指结果可以被记录为候选产物,却不能直接改变任务最终状态。这样,即使旧执行器稍后返回昂贵的模型答案,也只能进入审计日志,不能覆盖新轮次的有效结果。
四、防重协议:把幂等键下沉到副作用边界
仅在 Agent 编排层记录“这个工具调用做过了”并不够,因为编排器可能在写记录前崩溃。幂等键必须随请求一起抵达产生副作用的最末端。推荐键空间为
其中不要放随机重试编号,否则每次重试都会变成新请求。对于发邮件、创建工单、扣减额度等操作,下游应建立唯一约束,并缓存首次成功响应。重复请求命中同一个键时返回原结果,而不是再次执行。若第三方 API 不支持幂等键,可以在自有数据库建立“意图表”,通过唯一约束抢占执行权,再由单独的投递器调用第三方。
这里要区分内容去重与意图去重。两个参数相同的调用未必代表同一个业务意图,例如每月发送相同模板;而参数略有差异的重试,可能仍属于同一意图。正确的键来自工作流语义中的稳定步骤标识,不应对整个 JSON 盲目哈希。
提交结果时还要携带防栅栏令牌。下游若可改造,应记录“最后接受的令牌”,只接受 的写入;对要求严格顺序的资源,应采用 。租约判断依赖时间,令牌判断依赖单调序,因此后者能拒绝已经恢复的旧执行器。不能改造的第三方系统,则需要通过自有代理层串行化请求,把令牌检查放在代理层完成。
五、状态机与补偿:不要把回滚想成时间倒流
长任务应使用显式状态机,而不是散落的布尔字段。一个可操作的状态集合是:待领取、执行中、等待审批、补偿中、已完成、已失败、已取消。每次转换都记录事件编号、执行轮次、令牌、输入摘要和产物引用。大对象存对象存储,状态表只保存内容哈希与地址,避免续租事务被长文本拖慢。
图表加载中…
补偿不是数据库事务意义上的回滚。邮件发出后无法“撤销发送”,只能补发更正;工单创建后通常只能关闭;外部付款可能需要退款流程。因此每个副作用步骤都应声明三项属性:是否可重试、是否可补偿、补偿是否也幂等。设正向步骤集合为 ,可补偿子集为 ,则一个工作流的可逆覆盖率可定义为
权重 应按业务损失而非调用次数设置。覆盖率低并不必然禁止上线,但意味着必须加强人工门禁、额度限制和预演环境。
六、事件箱与恢复:把“完成计算”和“通知世界”拆开
最常见的双写漏洞是:状态数据库已提交完成,但向消息队列发布后续事件失败;或者消息已经发出,状态事务却回滚。事务型发件箱可以把任务状态更新和待发布事件写入同一数据库事务,再由独立中继器投递。消费者仍按至少一次语义处理,并用事件标识去重。这样无需假设数据库和消息队列共享分布式事务。
一个最小实现可遵循下面的伪代码。关键点是工具执行位于事务外,领取与提交位于短事务内,任何提交都再次比较令牌。
function run(job_id, worker_id):
claim = atomic_claim(job_id, worker_id)
if claim == NONE: return LOST
start_heartbeat(job_id, claim.fence)
for step in load_plan(job_id):
if lease_lost(): return QUARANTINED
key = stable_idempotency_key(job_id, step.id)
result = invoke_tool(step, key)
save_candidate(job_id, claim.epoch, step.id, hash(result))
transaction:
row = lock_job(job_id)
require row.owner == worker_id
require row.fence == claim.fence
mark_completed(row, candidate_manifest)
insert_outbox(event_id(job_id, claim.epoch), Completed)
return COMMITTED
恢复时只重放已持久化的步骤边界,不重放任意进程栈。检查点必须在进入人工审批前落盘,因为审批可能持续数小时;恢复后应生成新执行轮次和新令牌,旧轮次的回调只进入审计表。对于并行分支,可以给每个分支独立的步骤键,但最终汇合节点仍需一个全局比较屏障,防止部分旧分支混入新轮次。
七、生产参数与观测:从一次故障演练开始定标
第一,记录租约年龄而不只记录任务耗时。建议至少监控剩余租约分位数、续租失败率、失权后仍产生的工具结果数,以及同一任务并行活跃执行器数。后一个指标正常应接近一;若大于一,系统未必已经出错,但说明正在消耗双执行预算。
第二,建立重复副作用的闭环指标。可以定义每万次效果请求的去重命中数 、被防栅栏拒绝数 、人工确认的重复事故数 。理想情况下, 与 的上升代表保护层正在工作,而 应接近零。若只看最终事故,就会把被系统成功拦截的险情全部丢掉。
第三,把租约参数与真实尾延迟联动。租约时长可先按
估算,其中 是安全余量,再通过故障注入校正。不要用模型响应平均值替代进程暂停和数据库续租尾延迟。模型调用本身可以长于租约,只要心跳线程独立运行;若运行时无法保证独立心跳,就应把模型调用拆成可观察的远程作业,而不是盲目放大租约。
第四,为每类工具维护能力契约:幂等键支持、取消支持、超时上限、结果查询接口、补偿动作和数据驻留要求。上线门禁可以规定,高风险副作用若既无原生幂等又无代理层去重,就只能进入人工审批路径。该规则比“重试三次”更能降低真实损失。
第五,至少演练五种故障:领取后立即崩溃、工具成功后记录前崩溃、旧进程冻结后恢复、发件箱投递重复、人工审批回调迟到。每次演练都要验证最终状态、外部副作用次数、审计链和告警。未经过故障注入的租约系统,只能证明正常路径能跑,不能证明故障路径会收敛。
八、部署分层:协调平面与执行平面不要共命运
协调平面负责领取、租约、令牌、状态转换和发件箱;执行平面负责模型推理与工具调用。两者应使用不同的线程池与资源配额。否则,一批长推理把执行器线程耗尽时,续租也会饿死,系统会把健康任务误判为失权并制造重投递风暴。协调接口应短小、可降级,并优先使用数据库原子条件更新,而不是依赖单个常驻调度器的内存。
队列的可见性超时可以作为第一层租约,但不应成为唯一真相。不同队列对续期、最大投递次数和顺序性的保证不同;据各产品公开文档,具体边界还会随服务配置变化。工程上应把队列消息视为唤醒信号,把任务数据库视为所有权与状态的权威来源。这样即使消息重复、乱序或延迟,执行器也必须通过条件领取才能工作。
对于多租户 Agent,还要防止一个租户用大量将过期任务占满续租通道。可以按租户设置活跃租约配额,并让调度权重随失败债务下降。设租户 的基础权重为 ,近期失权重试数为 ,可用
做保守降权。这个公式只是可解释的启发式,并非公开验证的最优策略;实际参数要用生产回放确定。
九、落地清单与边界
落地可以分三周推进。第一周只做稳定任务标识、显式状态机、条件领取和审计事件,不改变现有工具调用。第二周接入租约续期、防栅栏令牌和失权隔离,并选择一个支持幂等键的低风险工具做端到端验证。第三周增加事务型发件箱、补偿契约、故障注入与指标面板。逐层上线有助于区分协议缺陷和业务适配缺陷。
本文方案也有边界。跨组织第三方接口若既不支持查询结果,也不支持幂等键,系统无法从网络超时中判断请求到底有没有成功,此时只能进入未知状态并人工核对,不能自动重试。长时间网络分区下,租约优先保证新所有者推进,而不是保证旧所有者继续;若业务更重视单执行而非可用性,需要更强的共识与隔离设施。至于用模型自动生成补偿动作,目前缺少足够公开生产数据,本文把它视为未公开验证的猜想,不建议直接用于资金、权限或对外通信场景。
真正成熟的长任务系统,不是从未重复执行,而是在重复、暂停、乱序和迟到都发生时,仍能用可审计的协议把业务结果收敛到唯一可接受状态。
参考文献
[1] Gray, C., & Cheriton, D. (1989). Leases: An efficient fault-tolerant mechanism for distributed file cache consistency. ACM SOSP.
[2] Kleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly Media.
[3] Helland, P. (2007). Life beyond distributed transactions: An apostate’s opinion. CIDR.
[4] Garcia-Molina, H., & Salem, K. (1987). Sagas. ACM SIGMOD.
[5] Bernstein, P. A., Hadzilacos, V., & Goodman, N. (1987). Concurrency Control and Recovery in Database Systems. Addison-Wesley.
[6] Richardson, C. (2018). Microservices Patterns. Manning.
[7] Amazon Web Services. (2026). Amazon SQS visibility timeout documentation. 访问日期:2026-07-14。
[8] Temporal Technologies. (2026). Temporal durable execution documentation. 访问日期:2026-07-14。
[9] Apache Kafka. (2026). Producer idempotence and transactions documentation. 访问日期:2026-07-14。
[10] PostgreSQL Global Development Group. (2026). Explicit locking and transaction isolation documentation. 访问日期:2026-07-14。
[11] Ongaro, D., & Ousterhout, J. (2014). In search of an understandable consensus algorithm. USENIX ATC.
[12] Bailis, P., Venkataraman, S., Franklin, M. J., Hellerstein, J. M., & Stoica, I. (2013). PBS at work: Advancing data management with consistency metrics. ACM SIGMOD.
一句话摘要:Agent 长任务的可靠性不靠禁止重试,而靠租约、防栅栏令牌、端到端幂等与事务型发件箱共同把重复执行收敛为唯一业务结果。