AI 请求超时后,重试为何可能重复执行
超时仅表示某个环节停止等待,不能证明上游没有接收或完成请求。文章区分客户端超时、504 响应与连接池等超时类型,说明重试前应判断首次请求的状态及重复执行风险。
AI 深度解读
AI 请求超时后,再点一次「重试」,可能会让上游执行两次。原因并不神秘:超时只能说明某一层停止了等待,不能证明服务端没有收到请求,也不能证明生成已经取消。
原文讨论的重点,是如何判断一次请求能否安全重试,以及怎样管理等待时间、重复提交和中断状态。它没有提供真实供应商的实测数据,但梳理出了一套适合检查 AI 应用的工程思路:先确认上一轮发生了什么,再决定下一轮能不能发。
超时发生在哪里,决定你能推断什么
一次 AI 请求可能经过浏览器、应用服务、反向代理、网关和上游服务,每一层都可能设置超时。浏览器等待结束,和网关返回错误,表达的是不同事实。
例如,HTTP 客户端抛出读取超时异常时,可能根本没有收到 HTTP 状态码;收到 504 Gateway Timeout,则说明充当网关或代理的服务器没有及时得到上游响应。两种情况都不能证明上游没有执行。
原文用 HTTPX 的四类超时定位等待位置:
Pool:等待从客户端连接池取得连接。Connect:等待建立 socket 连接。Write:等待发送一块请求数据。Read:等待接收一块响应数据。
这些分类属于 HTTPX,不能直接套到所有 SDK。排查时要核对实际使用的库及其版本,原文没有指定版本号。
如果有可靠证据表明,请求因为连接池超时而尚未发出,那么这一次尝试没有触达上游。但请求体已经发送后的读取超时不同,服务端可能正在生成。还要检查 SDK 或其他层此前是否已经尝试过,不能只看当前报错。
另一个容易混淆的地方是:HTTPX 的 Read 超时限制的是等待一块数据的时间,不是整次回答必须完成的时间。流持续返回数据,就可能运行得更久;心跳也可能维持连接,却迟迟没有用户能看到的答案。
因此,连接和连接池等待、读写停滞、整个业务操作,需要分别设置限制。原文举例:一次操作总预算是 50 秒,首次尝试用了 30 秒,扣掉退避和清理后,重试剩下的时间不足 20 秒。重新启动一个 50 秒计时器,会悄悄拉长用户的等待。
进程内计算耗时应使用单调时钟。跨组件传递剩余预算,需要约定明确的协议,不能假设随便加一个 deadline 请求头,对方就会执行。只延长浏览器超时,也绕不过更短的代理限制。
同样的 POST 再发一次,为什么仍然可能重复执行
能不能重试,取决于第一次尝试的结果和端点契约,不能只看报错名称。
原文建议把结果分成五种应用状态:确认未发出、明确被拒绝、结果未知、收到部分输出、确认完成。它们不是新的 HTTP 状态码,而是业务侧对已知事实的记录。
其中最需要单独处理的是「结果未知」:请求可能已经被接受,但客户端没有拿到权威的最终结果。丢失响应与明确拒绝,恢复方式不该相同。
如果 API 提供有文档说明的任务状态或结果查询端点,应先核对已有操作,再考虑创建新操作。日志里的 request ID 可能只用于关联请求,不一定能当作可查询结果的 job ID。
许多 AI 生成端点使用 POST。重复发送相同 JSON,不会自动使它具备幂等性。幂等关注的是重复请求对服务端产生的预期效果,也不要求响应文字完全相同。看起来能让输出稳定的模型参数,不能替代请求去重协议。
原文引用 RFC 9110 第 9.2.2 节:客户端不应自动重试非幂等请求,除非已知其语义实际幂等,或者能够检测到原始请求从未生效。
即便 API 支持幂等键,也要确认具体支持哪个端点、账号或租户作用域、保留多久、并发同键请求如何处理,以及同键不同参数会发生什么。重试究竟返回原始结果、处理中状态还是冲突,同样需要文档说明。自行添加请求头,或看到网关转发了它,都不足以证明上游实现了去重。
应用内部也有自己的责任。首次发送前生成稳定的 operation ID,操作进行中阻止重复点击,并用持久化记录保存归属用户、参数指纹、尝试引用、状态和结果引用。领取操作必须是原子的,否则两个 worker 可能同时判断「还没有记录」,继而各自执行。
参数指纹用于发现同一个 operation ID 被错误地用于不同参数,不应因为提示词相同,就合并用户有意发起的两次生成。
这套机制仍有边界:worker 在上游接受请求后、保存本地结果前崩溃,本地状态依旧不确定。没有上游查询或去重契约,应用内部的记录无法保证跨服务只执行一次。
流式中断、用户取消和自动重试,需要分别处理
流式响应最容易制造「已经成功」的错觉。初始 HTTP 200 只代表响应开始,不能证明完整生成成功结束。
原文建议保留已经收到的内容,并标记「已中断」。用户再次生成时,应显示为新尝试,不能把新答案悄悄接到旧内容后面,伪装成原来的流恢复了。只有端点明确支持恢复,并定义游标或事件标识时,才有依据续传。
取消也类似。关闭标签页或中止本地 HTTP 调用,不能证明上游停止生成。取消需要穿过相关链路,上游也必须支持对应行为;是否继续计费,要另查 API 的使用和收费规则。用户取消后,应用应停止安排重试、释放本地资源,并区分「已请求取消」与「确认取消」。
对于会调用工具的 agent,还要在工具或业务动作边界处理重复副作用。模型请求去重,无法撤销已经执行的工具动作。
重试次数则要沿整条链路计算。原文的示例是:应用、SDK、网关各允许总共 3 次尝试,完全嵌套的失败路径最多可能触发 27 次上游尝试。这是计算示例,并非某家供应商的测试结果。
应指定一个重试预算的负责人,统一管理总尝试次数。只有端点语义允许重试时,才使用有上限、带随机抖动的退避,遵循文档中的服务端指引,并在再次发送前检查剩余时间。
退避能控制重试节奏,不能让结果未知的 POST 自动变得安全。
通过 YW Code API(ywcode)或其他 AI 网关集成时,也要同时检查客户端和转发路径的行为。一个逻辑操作与它产生的多次 HTTP 尝试,应在记录中分开。
我的判断
原文最有用的地方,是把「请求报错」和「业务没有执行」拆开。这能纠正一种常见实现习惯:捕获异常后直接循环重试,再把最终一次响应当作整个操作的结果。对流式生成和工具调用而言,这种写法会掩盖真实状态。
但它提供的是设计原则,没有给出供应商实测、性能对照或完整实现。文末明确披露文章由 AI agent 撰写,示例也不是线上测试结果。因此,不能据此认定某个 SDK 默认会重试几次,也不能认定某家网关支持幂等键或取消。
落地成本也需要取舍。仅禁用按钮,能处理当前页面的连续点击,却覆盖不了刷新、多端提交和 worker 崩溃;持久化操作记录能扩大保护范围,却仍然需要上游契约配合。异步任务适合有持久状态查询能力的 API,原文也没有证明所有生成端点都支持它。
我的取舍是:先把超时、部分输出、取消和未知结果表达准确,再补充去重与恢复能力。没有证据确认成功或失败时,让用户看见不确定状态,比用一次自动重试遮住它更可靠。
能拿来做什么
- 检查前端生成页面的结束状态,把「完成」「中断」「已请求取消」「结果未知」分开显示;保留部分输出,并把再次生成呈现为新尝试。
- 梳理 Node 服务到 SDK、网关、上游的完整调用链,记录各层超时和默认重试行为,指定一处统一管理总尝试次数与剩余时间。
- 对需要防重复提交的操作,在首次发送前创建稳定的 operation ID,通过原子操作和持久化记录管理状态;依赖上游幂等键前,逐项核对端点文档。
- 为排障记录 operation ID、尝试序号、超时类别、耗时、是否发出、是否收到内容及可安全记录的上游 request ID,避免把凭证和不必要的提示词写入日志。