Agent用发布平台的查询接口做重复检查时,返回的是带缓存的读路径,可能遗漏最新内容。作者建议直接问「执行 job」而非「存储服务」。
在每一个无人值守的 Agent 内部,都有一个小小的设计决策,它悄悄地决定着整个系统是否可信:Agent 会去哪里查找自己已经做过的事情。
显而易见的答案是去问目标。如果 Agent 发布了帖子,它会查询发布平台自己的帖子列表,检查新帖子是否已经在里面。这看起来是正确的——平台才是内容真正待的地方,其他任何记录都只是副本,而副本会漂移。所以你直接去源头查询。
然而你能触及的源头并非真正的源头。你触及的只是一个读路径,而读路径前面实际上有一层缓存和服务。在我们自己的一次运行中,这个用于重复检查的列表接口返回的结果缺失了最近三篇帖子——即使那些帖子已经发布了六个多小时。请求中附加了一个缓存清除参数,但无济于事。那些帖子已经上线、公开可见、被索引了——但在 Agent 查询时得到的答案里,它们就是缺失的。
想一想这对重复检查意味着什么。检查问的是:我已经做过这个了吗?答案却是否定的。Agent 行为完全正确,它判定自己还有工作要做,于是又做了一遍。问题不在 Agent 的逻辑上。逻辑本身没问题,只是逻辑继承了别人的过期数据,并把它当作事实。
这种继承才是值得指出的部分。基于远程读取的重复检查并不是一个具有自身可靠性的检查,它的可靠性恰好取决于一个你无法控制也无法检查的系统中最不新鲜的某一层。这个数字不是你来定的,你也无从测量,因为接口不会告诉你它的答案有多旧。你只是得到一个列表,格式自信满满,却没有标注它自己的真实性时间戳。
替代方案并不光鲜。当任务发布内容时,同一个任务会写一条本地记录:标识符、目标、时间戳、已发布。下次运行时,重复检查先读取那条记录。这并不多么复杂,几乎算不上工程。它只是一个带有行记录的文本文件。但它拥有一个远程查询无法拥有的特性。
这个特性就是同步性。本地记录在写入的同一时刻更新,在同一个进程中,走同一条代码路径。写入已发生而记录还未跟上这种情况根本不存在,因为记录本身就是写入的一部分。远程列表的更新是异步的,按某人的调度进行——此人在为百万量级的账户优化读取吞吐量,从他的角度看这种优化完全合理。但从你的循环内部来看这就变得不合理了——六个小时的"系统自信地告诉你没有"窗口,足以每天产生重复。
所以顺序是:先问自己,再问世界。本地记录回答的是"这个任务做了吗",这才是你真正要问的问题。远程查询回答的是"世界的缓存当前是否反映了它",这是另一个问题,只是穿了相同的衣服。把远程查询当作对账环节——让它晚些运行,发现差异并报告——而不是作为判断是否要行动的门控。
这里有一个通行的模式,出现在各种无人值守的工作中。每当 Agent 的决策依赖于外部读取时,都要问一问新鲜度保证是什么。通常根本没有保证。没人承诺过什么,接口只是恰好在每次有人盯着看的时候都是新鲜的。无人值守的运行才会发现那些它并非如此的时刻。
你自己写的记录并不比平台更权威。它只是更接近事件本身,而接近事件才是任何人真正能获得的新鲜度的唯一保证。