改变答案的重试:针对非确定性 LLM 调用的幂等键
你构建过的每个分布式系统都依赖于一个隐形的假设:超时后的重试是安全的。操作是幂等的,因此如果客户端放弃等待并重新发送,最坏的情况也只是重复工作,并最终收敛到相同的状态。两个 PUT 请求落地同一行。两个 DELETE 请求留下同样的空缺。重试只是伪装成第二次尝试的“无操作”(no-op)。
LLM 调用打破了这一假设,而且是悄无声息地打破。重试并不会重新获取相同的答案 —— 它会采样一个新的答案。当客户端因为响应在传输中丢失而在网络层超时,但提供商实际上已经完成了生成时,重试会产生第二个、不同的答案。现在,对于一个逻辑请求,存在两个不同的输出,而你的技术栈中没有任何部分知道哪一个是权威的。
这并非罕见的极端情况。在模型背后运行超时机制的从业者报告称,即使底层调用最终成功,仍有 5–10% 的请求会触发完整的超时加重试循环。其中的每一次重试都是一次抛硬币,而你的系统从未被设计成去裁定这种结果。
为什么“超时重试”不再安全
重试原语源自一个确定性的世界。无论你调用一次还是五次,GET /users/42 都会返回同一个用户。带有完整正文的 PUT 请求在构建上就是幂等的。整个可靠性堆栈 —— 负载均衡器、重试中间件、断路器 —— 都建立在这样一个前提下:重新发起请求要么不产生新操作,要么会收敛。
采样生成则不具备这种结构。温度(Temperature)高于零意味着输出是从分布中抽取的,而不是查表得出的。即使温度为零,你也不一定能得到保证:跨 GPU 批次的浮点非结合性、取决于批次中其他请求的混合专家(MoE)路由,以及提供商端无声的模型更新,都意味着“相同输入”并不意味着“相同输出”。请求不再是读取。它更像是一次同样需要耗钱的掷骰子。
因此,超时造成了客户端无法从其端解决的三种歧义状态:
- 请求从未到达模型。重试是安全且必要的。
- 请求已到达模型,生成仍在运行。重试会开启第二次生成。
- 请求已完成,响应在返回途中丢失。重试会为一个已经回答过的问题产生一个不同的答案。
单纯的“超时重试”对这三种情况一视同仁。在确定性的世界里,这没问题,因为第二和第三种情况是无害的。但在非确定性的世界里,第二和第三种情况正是资金流失和 Bug 滋生的地方。
