AI Agent常见的「空队列=正常」陷阱:producer因错误yield nothing也会产生绿色运行记录,与真正无事可做无法区分,提供了识别和修复思路。
我们的无人值守 Agent 普遍呈现出一种两阶段形态,许多自动化最终都会收敛到这一形态。第一步判断是否存在需要完成的工作,后续步骤执行工作。两者之间有一个队列,第二步实际只查看队列内容。
正常情况下,第二步打开队列、发现任务并处理它们。空闲时,它打开队列、什么也没找到、写下一行"没有需要处理的任务"、然后成功退出。这行日志就是问题所在。它不是一次运行情况的报告,而是关于某个容器状态的报告。两种完全不同的运行历史会产生完全相同的容器状态。
历史一:Producer 执行了,检查了所有应该检查的内容,正确判断没有待处理的工作,于是什么都没写入。行为正确。确实没有工作。
历史二:Producer 执行了,遇到了某种异常但没有报告,退出时什么都没写入。同样产生空队列,同样得到一次绿色运行,因为 Consumer 完美地处理了收到的零个任务。
空队列无法告诉你究竟发生了什么。成功标志也不行。Consumer 是诚实的,但它在这里毫无用处,因为它从未接触过真正的事实。它看到的只是残渣。
我们做的改动很小,但效果很好:Producer 不再被允许通过"absence"来传递信息。它必须为自己的输出签名,包括当输出为"nothing"时。不是写入队列——而是一条独立的声明,写在自己的记录里,说明它检查了多少个候选对象、拒绝了多少个、以及拒绝的理由。从四十个检查对象中产出零个是一句话。从零个检查对象中产出零个是另一句话。没有任何声明的零产出不是一句话,而是一个缺口,现在这个缺口被视为一次事故,而不是平静的一天。
Consumer 的规则也相应改变了。它不再将空队列视为关于世界的既定事实,而是将其视为一个疑问,然后去查找本轮中 Producer 的签名。如果签名存在且表明因可理解的原因检查了零个项目,则正常退出。如果签名缺失、过时或来自昨天,则这次运行不算成功——而是无法确定是否有工作的运行。这是一个真实的状态,它值得拥有自己的名称,区别于 pass 和 fail。
在没有人盯着的情况下,这个问题变得更加重要,因为一旦房间里没有人,沉默就变得很廉价。人工操作员手动运行同一条管道时,会注意到工作的缺失,因为缺失是无聊的,而无聊本身就是一种信号。他们在第二个安静的早晨开始感到疑惑。无人值守的 Agent 永远不会感到疑惑。它读取队列、发现队列为空、然后对空队列执行正确的操作、并真诚地报告成功。只要你允许它这样做,它就会一直这样做,而且所有这些运行在摘要中看起来都完全像那些一切正常工作的日子。
所以我们不断重新学到的 Instrumentation 教训是关于方向的。我们一直试图在下游检测失败——在效果显现的地方——而下游恰恰是信息已经被销毁的地方。队列就是销毁点。队列上游的一切都知道它为什么是空的。队列下游的一切只能猜测,而默认乐观的猜测不是猜测,它是一种策略。
缺失必须归因于某个原因。要么某个步骤声明并解释它,要么没有任何东西声明它,而你就有一个本应有决策的空白记录在日志中。我们以前把那个空白当作一个正常的早晨来记录。