案例以仓库依赖巡检为场景,用单个持久化状态文件实现游标恢复、每日摘要去重和标签理由审计。设计将模型分类包在确定性流程中,避免模型异常破坏任务状态。
一个夜间维护 worker 所需的东西很少:一个按计划启动的进程、一份持久化状态文件,以及一种低成本的分类方式。它真正需要做到的是,发生故障时依然平稳、可预测,让你即使一个月不管它,也能信任它的输出。
这篇案例研究会完整介绍一个小项目:一个扫描少量代码仓库、检查依赖版本偏移、为每项发现打标签并生成简短摘要的 worker。扫描本身很简单。难点在于如何在两次运行之间交接状态,让 worker 在你无权管理的主机上随时重启,也不会重复发布或丢失游标。
这周的热门内容里,既有各种 AI 功能巡礼,也有对 AI 疲劳的直白表达,这两种反应都不难理解。更实用的应对方式反而没那么起眼:用确定性的外壳包住概率性的部分,让模型偶尔失常也不会破坏你的数据。
目标可以用一句话说清楚:给定一组代码仓库和固定版本,每天生成一份摘要,最多发布一次,同时提供可恢复的游标,以及每个标签可供审计的判定依据。
明确不做什么同样重要,因为这些边界能让设计保持精简:
最后一点是整个设计的关键。分类质量可以波动,摘要本身必须保持可靠。
大多数团队都会跳过这张表,然后在事后后悔。请根据自己的约束条件,为可选方案打分。
如果任务涉及受监管数据,或者其他团队需要在固定时间使用它的输出,就直接排除免费套餐这一行。下文都假设你已经接受重启是常态,并且围绕这一点完成了设计。
把恢复运行所需的全部信息写进一个 JSON 文档,并将这个文档视为两次运行之间的接口。文件里没有的内容,一律在启动时重建。
{
"schema_version": 1,
"last_completed_run": "2026-10-15T02:10:04Z",
"cursor": {"repo": "acme/etl", "last_tag": "v3.14.0"},
"lease": {
"owner": "worker-7f3c",
"acquired_at": "2026-10-15T02:09:41Z",
"heartbeat_at": "2026-10-15T02:09:52Z"
},
"recent_labels": [{"repo": "acme/etl", "tag": "v3.14.0", "label": "routine"}]
}
两条规则可以保证这份文件可信。第一,每次结构发生变化,都要递增 schema_version;遇到未知版本时,拒绝运行。第二,提交文件时始终使用原子重命名,避免进程收到终止信号后,在磁盘上留下一份只写了一半的文档。
import json
import os
import tempfile
def save_state(path, state):
directory = os.path.dirname(os.path.abspath(path)) or "."
fd, tmp = tempfile.mkstemp(dir=directory, prefix=".state-", suffix=".tmp")
try:
with os.fdopen(fd, "w") as handle:
json.dump(state, handle, sort_keys=True)
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp, path) # commit point
except BaseException:
os.unlink(tmp)
raise
os.replace 调用就是提交点,在此之前的所有写入对读取方都不可见。如果进程在写入过程中退出,留下的只是一个临时文件,其他内容不受影响。如果进程在重命名之后退出,新状态就已经是当前状态。
两个 worker 争抢同一个游标,是夜间任务演变成夜间故障的经典原因。只要你能接受租约过期接管带来的有限风险,带心跳的租约就能在不引入协调服务的情况下解决这个问题。
from datetime import datetime, timedelta, timezone
LEASE_TTL = timedelta(minutes=15)
def parse_ts(value):
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def lease_blocks_run(state, now, owner):
lease = state.get("lease")
if not lease:
return False, "no lease recorded"
age = now - parse_ts(lease["heartbeat_at"])
if lease["owner"] == owner:
return False, "we already own the lease"
if age > LEASE_TTL:
return False, "stale lease from %s" % lease["owner"]
return True, "held by %s for %s" % (lease["owner"], age)
每隔几分钟更新一次心跳,始终使用当前 UTC 时间戳,并在每一条退出路径上记录原因字符串。当有人问某次运行为什么停止时,这一行日志就是完整的答案。
打标签这一步确实能发挥模型的作用,也确实是免费套餐容易出问题的地方。把模型调用封装在一个始终能返回结果的函数后面,并让基于规则的分类器负责默认路径。
ALLOWED = {"routine", "risky", "unknown"}
def classify(title, call_model, rules_fallback):
try:
label = call_model(title, timeout=20)
if label in ALLOWED:
return label, "model"
except Exception as exc:
print("model tier unavailable: %s" % exc)
return rules_fallback(title), "rules"
在每个标签旁边记录其来源:model 或 rules。只需要这一个字段,就能把模糊的质量抱怨转化为可查询的比例,也能让更换服务商的影响变得可衡量,而不只是凭感觉判断。
上面的代码是一个可以用桩函数 call_model 在本地运行的方案,并非已经发布的 benchmark。这个设计不依赖特定的 endpoint、模型名称或服务商——也正因为如此,即使免费套餐悄然发生变化,它依然能继续运行。
除了你的笔记本电脑,worker 还需要两样东西:运行进程的主机,以及执行标签分类的 endpoint。MonkeyCode 提供免费的模型访问和免费服务器选项,恰好符合这个设计原本就需要的两项能力。利益披露:本文是 MonkeyCode 产品推广活动的一部分。
应当把免费资源的可用性视为服务运营方提供、且随时可能变化的条件。免费套餐的 token 配额、速率限制、数据保留政策和实例存活时间都会变化。因此,在调度任何重要任务之前,请先阅读官网的现行条款,并保留基于规则的回退分类器。这样,政策变化带来的结果只是质量下降,而不是整个任务停摆。
这个结构的价值不在于速度,而在于它把故障行为收敛成了一份简短、可预测的清单。请使用下面的测试工具,亲自验证每一行。
留意这里没有出现什么:没有不完整的摘要,没有重复发布,也没有悄无声息的数据丢失。每一种故障模式,要么明确拒绝运行,要么给出质量有所下降、但已标明来源的结果。
编写一个 preflight 检查,并把它作为每次定时执行的第一步。它应当解析状态文件、比对 schema_version、验证租约,并对照已知时间源检查时钟偏差。
python -m worker.preflight --state .state/worker.json && \
python -m worker.run --once
接下来,直接测试幂等性,因为这才是生产环境中真正容易出问题的属性。对一组没有变化的输入连续运行两次 worker,并比较每次运行后的状态文件。
python -m worker.run --once && cp .state/worker.json /tmp/after-first.json
python -m worker.run --once && diff /tmp/after-first.json .state/worker.json
一次正常运行应该只更新 last_completed_run,其他内容都不变。如果第二次运行时游标发生移动,就说明你的幂等性假设有问题,再多日志也救不了你。
这个方案用吞吐量和保障程度换取简单性,而这种取舍并不总是合适。
如果你的任务属于上述任何一种情况,文章开头的表格会引导你选择其他方案,而这也是完全合理的结果。
下一轮迭代要把摘要本身也变成一份契约:固定键的顺序、标签词表和时间戳格式,然后像断言 schema_version 一样,在 preflight 中对它们进行断言。之后再增加一个集成测试:启动两个进程,让它们访问同一个状态目录,并断言其中恰好只有一个进程进入标签分类步骤。这两项检查能捕获你在第一年实际遇到的绝大多数故障。
如果你想在不自行配置基础设施的情况下试试这个循环,可以先在免费服务器实例上运行 preflight 检查,让它完整执行一轮,再添加定时调度。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。