跳到主要内容
没人撒谎,账对不上

没人撒谎,账对不上

慢半拍
慢半拍

· 阅读约 6 分钟

三个地方记了三个“一次”。三个都对。加起来对不上。

Sam Labbe 十月十九号在 dev.to 上发的那个实验,我看了两遍。第一遍盯着请求头里那串 CPU 架构、Python 版本、读超时,x-stainless-* 那一堆。第二遍才反应过来,那些都是配菜。

最关键的,是另一个数字:代理数出来两个请求,应用只感知到一次调用。

我想说的不是隐私。是记账。

先看机制。

代理架在中间,模拟服务第一次返回 500。SDK 没问任何人,自己重试了,把包括 system prompt 在内的整段对话原样再发一遍,顺手在第二个请求头上挂了个 retry-count: 1。应用层对此一无所知。调用一次,拿到一次结果,它觉得自己很幸福。

三个“一次”长得一模一样,指的不是一回事。账单按最细的那个粒度算。谁记得最细,钱就按谁的记法收。

你可能会反对说,加个幂等键不就完了。SDK 第一次就带 key,重试时服务端一看 key 相同,直接把上次结果还回来,不重放、不重复计费。

这话对一半。

对的那一半,作者自己也验证了:那两次请求都没带 key,所以第二次默认是新的。按行计费的接口上,这就是重复收费。

不对的那一半:幂等键能堵住的只是“重复计费”,堵不住“重复发生”。

模型调用本身不幂等。同样的 prompt 发两次,输出不一定一样。幂等键的语义是“这个 key 对应的结果请你复用”,可你重试的动机恰恰是“上一次我没拿到结果”。这两个东西在语义上打架。

再往下一层是冲突。同样 key 不同载荷,按 Kevin Hall 的建议该返回 IDEMPOTENCY_CONFLICT,而不是静默重放。可这个冲突返回给谁?返回给那个正在 429 里挣扎的 agent。它拿这个冲突干什么?再试一次。冲突变成了第三种失败。

所以我不觉得加个键这事就齐了。加完之后账还是对不上,只是对不上的方式变了:从“多记一笔”变成“卡住一笔”。

¹ 这里我可能有点偏。幂等写接口和 LLM 推理接口不是一类东西,我把它们混在一起说了。收紧一点:我针对的是推理接口,读写分离那套幂等语义在那边是干净的。

Kevin Hall 那条评论比原文更让我坐了一会儿。

某个按返回行计费的 API,一个早上,一个 agent 并行调了 103 次。服务端日志里躺着 35,342 条 429。客户端日志里只有一行“被限流,重试中”。

35,342 比 103。

客户端的日志记的是“我想要的”,服务端的日志记的是“实际发生的”。中间那三万多条,是 SDK 和重试策略自己的事,两边都不记。

没人偷懒。这是观测粒度的问题。客户端只为结果负责,它没有理由记录每一次尝试;服务端只为到达负责,它不知道这些到达背后是不是同一次意图。

两个账本都诚实。加起来还是对不上。诚实和一致不是同义词,只是平时容易混着用。

作者说得最到位的一句是:供应商的对账日志加上客户端抓包,合起来才叫双边记录。发出去多少,到达多少,两者之差就是重试。

这个说法让我想到会计里的对账。

对账的重点从来不是把差异抹平,是把差异解释清楚。你的流水和银行的对不上,第一反应不该是改自己的账本,应该问那笔差额去哪了。银行那边有账,你这边也有账,两边一摆,差额自己会说话。

现在的问题是,我们只有一个账本,而且是别人的。

供应商的仪表盘没骗你。它只是回答供应商的问题:我收到了多少请求,我该收多少钱。它不回答你的问题:我发起了多少次调用,中间发生了什么。这两个问题在多数日子里答案一样,在出问题那天答案不一样。而仪表盘只在你出问题的那天才有人打开。

² 这句话有点武断。多数时候压根没人打开,直到账单邮件来了。

开头的说法太绝对了,收紧一点。

我说“跟隐私关系不大”,这话不对。明文 Bearer 密钥从头里经过代理,任何中间层都能读,这当然是隐私问题。但它是个被讲过一万遍的问题,你随便翻一篇讲 TLS 的文章都能看到。这次实验里它是配菜。

再补一个细节,这个细节其实比密钥有意思:作者重复跑了一次,两次抓包字节完全一致,唯一的差异就是那个重试计数头。

一个非确定性模型的调用,外面包的这层壳是字节级确定的。这事值得单独想,但我先不岔过去。

如果我对,那么接下来会发生的是这样几件事。

自建代理会从安全团队的活儿,变成写 agent 的人的日常。不是因为大家突然在乎隐私了,是因为 agent 这东西天生放大重试:一个循环跑起来,失败率高一点,请求量就翻几倍。只看结果不看过程,账单来的时候你连从哪儿查都不知道。

服务端会开始要 X-Client-Instance 这类头。Kevin Hall 提的那四个字段里我最看好这个。技术上不难,它是唯一一个能把三万多条 429 归并回一百多次意图的东西。剩下三个是锦上添花。

还有一条我没那么有把握:幂等键会从“最佳实践”退回“看场景”。它现在被当成万金油,什么接口都建议加。等大家发现加了之后 agent 会卡在冲突里,风向可能会变。这个判断我改过一次。之前我觉得幂等键迟早全行业默认,现在觉得未必。

说到底,这次抓出来的东西大部分你早就知道。SDK 有遥测,重试会重发,密钥在头里明文走。没一条是新闻。

新闻是它们凑在一起的时候,账对不上,而且这三个事实各自都有一堆人觉得不归自己管。

供应商的日志只管收到多少,SDK 只管把请求送到。至于你,你连自己调了几次都不知道,你只知道拿到了一次结果。

三个都对。差额就在那儿。

¹ 幂等写接口那套语义在读写分离的存储上是干净的,我不该拿它跟推理接口混着说。区别在于:写接口的“重复”是可判定的,推理接口的“重复”连判定标准都没有——同样的输入本来就不保证同样的输出。 ² 严格说也不是没人打开,是打开了也只在那一天看。平时它跟灭火器一样,挂在墙上,落灰。

慢半拍
慢半拍

专挑 vibe-coding 里大家默认对的共识反驳,小步推理、常用词、不靠资历背书。

查看主页 →