跳到主要内容

8 篇博文 含有标签「idempotency」

查看所有标签

流式中止后遗留的计费副作用

· 阅读需 12 分钟
Tian Pan
Software Engineer

用户正在查看你的智能体(Agent)流式传输回复。在 200 毫秒时,他们点击了停止。UI 清除了气泡,加载图标消失,产品的表现就像请求从未发生过一样。

但它确实发生了。Agent 已经调用了 send_invoice_email。供应商的邮件转发服务器返回了 250 OK。客户收到了一份用户从未批准的草案发票。你的计费系统向用户收取了中止前流式传输的 token 费用。但它无法撤回发送邮件产生的费用。

这是每个使用流式工具(streaming tool use)的团队至少都会遇到一次的失败模式,而且大多数团队甚至从未察觉。流层(stream layer)报告 cancelled。工具层(tool layer)报告 succeeded。你的面向客户的日志会根据最后刷新的子系统从中挑选一个,于是同一个请求的两个部分现在对于该请求是否发生产生了分歧。

你的智能体从未发送的幂等键

· 阅读需 12 分钟
Tian Pan
Software Engineer

一位客户曾因一次退货收到了三次退款。这不是因为模型幻觉出了某种政策,也不是因为人类填错了表单——而是因为退款工具超时了两次,智能体随之重试了两次,而每次重试都发起了一个全新的请求,导致支付处理器根本无法识别这其实是它处理过的工作。三个干净的 HTTP 200。三次真实的资金转移。智能体完全按照指令行事:当调用失败时,重试。

这个 Bug 不在模型中。Bug 出在一个从未发送的 Header(标头)中。

重试是智能体最自然不过的本能。工具调用返回错误,或者更糟,什么都不返回,而循环系统的直觉——无论是编码在框架、提示词还是模型自身的训练中——就是重新尝试该动作。这种直觉对于“读”操作是正确的,而对于“写”操作则是灾难性的。一个韧性十足的智能体与一个会向客户重复收费的智能体之间的区别,不在于智力高低,而在于每一次改变状态的工具调用是否携带了幂等键(Idempotency Key),以及另一端的系统是否真正履行了它。

原本运行良好的工具,直到两个智能体同时调用它

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个工具通过了测试。你从一个智能体(agent)调用它,看着它读取记录、转换、写回并返回一个清晰的结果。几周以来,它每次都表现完美。然后你将智能体集群从一个 worker 扩展到十二个,结果一个客户报告说他们的订阅在同一分钟内被升级了两次。工具没有变,只是调用它的并发量变了。

这是单智能体测试无法捕获的失败模式,因为单智能体测试永远不会产生触发该模式的条件。从结构上看,单个调用者是一个串行工作负载。你的工具默默依赖的所有并发假设——读取时没有其他人在写入、自增的计数器是属于它自己的、保存时正在编辑的草稿依然存在——在只有一个调用者时都是理所当然成立的。工具并非正确,只是未经过测试。这两者是不同的,而在第二个智能体出现之前,这种差异是不可见的。

改变答案的重试:针对非确定性 LLM 调用的幂等键

· 阅读需 10 分钟
Tian Pan
Software Engineer

你构建过的每个分布式系统都依赖于一个隐形的假设:超时后的重试是安全的。操作是幂等的,因此如果客户端放弃等待并重新发送,最坏的情况也只是重复工作,并最终收敛到相同的状态。两个 PUT 请求落地同一行。两个 DELETE 请求留下同样的空缺。重试只是伪装成第二次尝试的“无操作”(no-op)。

LLM 调用打破了这一假设,而且是悄无声息地打破。重试并不会重新获取相同的答案 —— 它会采样一个新的答案。当客户端因为响应在传输中丢失而在网络层超时,但提供商实际上已经完成了生成时,重试会产生第二个、不同的答案。现在,对于一个逻辑请求,存在两个不同的输出,而你的技术栈中没有任何部分知道哪一个是权威的。

这并非罕见的极端情况。在模型背后运行超时机制的从业者报告称,即使底层调用最终成功,仍有 5–10% 的请求会触发完整的超时加重试循环。其中的每一次重试都是一次抛硬币,而你的系统从未被设计成去裁定这种结果。

当工具撒谎时:智能体默认信任的“伪成功”失败模式

· 阅读需 12 分钟
Tian Pan
Software Engineer

智能体自信地告诉用户:“我已经发送了确认邮件,并将退款退回到你的账户。”追踪记录很干净:两次工具调用,均返回 {"success": true},模型生成了精练的摘要,对话在 3.2 秒内结束。一周后,客户发起投诉,因为邮件从未送达,退款也从未入账。审计日志中全是绿色的勾选标记。没有任何环节失败——除了实际的工作本身。

这就是在大多数智能体技术栈中尚未被命名的故障模式:撒谎的工具。这里的“撒谎”并非恶意——它们返回了其契约规定的响应。这种谎言是结构性的。HTTP 层返回 “200 OK” 是因为请求被接受了,而不是因为操作完成了。邮件服务商返回 success: true 是因为消息进入了发送队列,而不是因为它已经发出了。数据库写入返回且无报错,是因为它写入了一个从未同步的副本。被训练成“乐于助人”且在“绿色代表完成”的示例上训练过的模型,将这些信号编织成一份自信的摘要,然后继续下一步。

隐藏的 SDK 重试机制:为什么你付了两倍的钱却浑然不知

· 阅读需 12 分钟
Tian Pan
Software Engineer

打开 OpenAI Python SDK 的源代码,你会发现一行安静的代码:DEFAULT_MAX_RETRIES = 2。Anthropic SDK 也采用了同样的默认设置。大多数 TypeScript SDK 也是如此。两次重试,指数级退避(exponential backoff),在连接错误、408、409、429 以及任何 5xx 错误时自动触发——这些都在你的代码看到失败之前就执行了。你没有配置它。你没有选择加入。你通常甚至不知道它的发生,因为你的应用记录的指标是 request_count(请求数),而不是 attempt_count(尝试数),并且你的追踪器(tracer)唯一能看到的 span 是 SDK 在最后一次尝试后关闭的最外层 span。

这在大多数情况下都没问题,直到出问题为止。如果在该 SDK 调用之上再添加一个应用级的重试装饰器——那种每个团队在遇到第一个 429 错误后都会写的代码——你就构建了一个 3x3 的风暴:SDK 尝试三次,你的包装层围绕 SDK 又尝试三次,在服务商降级期间,一个单一的用户请求会扇出为九次推理调用。服务商的账单会计算每一次尝试。而你的仪表盘只记录了一次。当最终有人进行账实对账时,那将是一场谁都不会喜欢的季度末谈话。

智能体幂等性是一项编排契约,而非工具属性

· 阅读需 12 分钟
Tian Pan
Software Engineer

客服工单在上午 9:41 送达:“我被扣了三次费。”链路追踪看起来无异常。一条用户消息,一次规划器轮转,三次对 charge_card 的调用 —— 每次都有唯一的工具调用 ID,每次都返回 200 OK,每次都写入了不同的 Stripe 扣款。工具本身有幂等键,后端有去重表,支付处理器也遵循 Idempotency-Key。每一层都是幂等的,但客户依然支付了三次。

如果你构建 Agent 的时间足够长,这类 Bug 迟早会出现在你的桌上。它不是任何工具的 Bug,而是 Agent 循环与工具之间契约的 Bug,而这种契约几乎总是只存在于资深工程师的脑海中。

智能体幂等性:为什么你的 AI Agent 会发送两次邮件

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的 Agent 处理了一笔退款,但响应超时了。框架进行了重试。结果客户收到了两次退款。你的 Agent 发送了一封跟进邮件,触碰了速率限制,在退避(backoff)后重试,结果客户收到了两条完全相同的消息。这些并非假设的场景——它们是 Agent 系统中最常见的生产故障类型,而且几乎每个 Agent 框架自带的重试逻辑都让这些问题变得不可避免。

根本原因看似简单:Agent 框架对所有工具调用的处理方式都一样,无论它是读取数据还是改变现实世界。get_user_profile() 调用重试一百次也是安全的。但 send_payment() 调用则不然。然而,大多数框架都将两者封装在相同的指数退避重试逻辑中,并美其名曰“可靠性”。