做 AI 业务的团队几乎都遇到过这种情况:本地调 OpenAI / Claude API 好好的,部署到生产环境就开始大面积 timeout、connection reset,成功率忽高忽低,半夜跑批的日志惨不忍睹。
很多人第一反应是"网络的事,没办法"。但如果把整条链路拆开看,会发现问题是可以定位、可以分层解决的。这篇文章只讲技术原理:你的请求到底经历了什么,为什么会失败,以及哪些环节是代码能解决的、哪些不能。
一、先搞清楚:一次 API 调用到底走了多远
当你在离 AI 服务商机房较远的地方发起一次 API 调用,数据包大致经历这样一条路:
你的服务器
→ 本地运营商网络
→ 国际出口 / 跨境网关
→ 海底光缆 / 跨境骨干
→ 目标地区运营商
→ 云厂商网络(AWS / Azure / GCP)
→ 服务商负载均衡
→ 实际处理请求的 GPU 节点
每一跳都有两个独立的失败维度:延迟(这一跳花多久)和丢包(这一跳会不会丢)。一次请求要稳定返回,等于这条链上所有跳同时正常。链路越长、跳数越多,整体可靠性是各段相乘——这就是为什么"距离远"会以接近指数的方式放大不稳定性,而不是线性。
关键点:这条路径是由 BGP 路由决定的,而 BGP 的设计目标是"可达",不是"最快"或"最稳"。它会因为运营商之间的结算关系、策略配置而选择一条绕路的路径——你的包可能绕一大圈,只因为那条路对运营商成本更低。应用层对此没有控制权。
二、为什么 LLM API 比普通网页更容易崩
普通网页加载是"一次性"的:请求-响应,结束。慢一点就是多等几秒。但大模型 API 有两个特性,会把网络抖动放大成业务事故。
1. 长连接 + 流式输出(streaming)
大多数 AI 应用用的是流式返回——模型一边生成一边输出。这意味着一次请求会持续几十秒保持连接打开。
问题在于:连接持续的时间越长,期间遭遇一次网络抖动的概率就越高。 一次普通 HTTP 请求可能 200ms 就结束,躲过了抖动窗口;而一个 30 秒的流式连接,中间任何一次几百毫秒的链路波动,都可能触发 connection reset,表现出来就是"输出到一半卡死"。
更麻烦的是,流式中断往往无法简单重试——已经生成的内容丢了,重试要么从头再来(浪费 token),要么需要实现断点续传逻辑。
2. 延迟在多跳调用里会累加
现代 AI 应用很少只调一次 API。一个典型的 RAG 或 Agent 流程是:
向量检索 → embedding API → 拼 prompt → 调用 LLM → 解析 → 再调一次 LLM 修正
如果每次跨境调用的延迟是 800ms,四次串联就是 3.2 秒——还没算模型本身的推理时间。用户感知到的总延迟,是被链路最差的那一段拖垮的。你优化了代码、优化了 prompt,结果瓶颈在网络上,而这恰恰是最容易被忽视的一环。
三、为什么"重试"看似稳妥,实则是个坑
工程师遇到 API 失败,第一反应通常是加重试(retry)。这没错,但重试用错了会雪上加霜。
简单重试的三个问题
① 成本翻倍。 失败的请求很多时候是在服务端已经开始处理之后才断的——token 已经消耗了。你重试一次,就再付一次钱。高失败率下,账单可能悄悄翻 2-3 倍。
② 重试风暴(retry storm)。 当链路整体抖动导致大批请求同时失败,如果它们又同时重试,会在恢复的瞬间制造一个流量尖峰,把刚恢复的链路再次打垮,形成恶性循环。
③ 没有退避等于打自己。 不带指数退避(exponential backoff)和抖动(jitter)的重试,本质上是在攻击自己的上游。
正确的重试姿势
import time, random
def call_with_retry(fn, max_retries=4, base=0.5):
for attempt in range(max_retries):
try:
return fn()
except (TimeoutError, ConnectionError):
if attempt == max_retries - 1:
raise
# 指数退避 + 抖动,避免重试风暴
sleep = base * (2 ** attempt) + random.uniform(0, 0.3)
time.sleep(sleep)
但请注意:重试是在"症状"层面打补丁。 它能把成功率从 85% 救到 95%,代价是延迟和成本上升。它无法解决根因——根因在链路本身。
四、根因在哪:三层问题,对应三种解法
把上面的分析归纳一下,问题其实分三层,每层的解法完全不同:
| 层级 | 问题 | 谁能解决 | 代码层能做吗 |
|---|---|---|---|
| 应用层 | 重试逻辑、超时设置、并发控制 | 你自己的代码 | 能 |
| 链路层 | 绕路、丢包、抖动、BGP 选路差 | 网络层方案 | 不能 |
| 服务层 | 服务商自身限流、排队 | 多供应商容灾 | 部分 |
很多团队的误区是:用应用层的手段去硬扛链路层的问题。 拼命调超时、加重试、加并发限制,本质上是在一条烂路上反复试错。代码再优雅,也改变不了数据包要走的物理路径。
链路层的问题,只能用链路层的方法解决。这一层在工程上有几种成熟思路:
1. 多链路智能选路。 实时探测多条可用路径的质量(延迟、丢包、抖动),动态选当前最优,链路质量下降时自动切换。这是 SD-WAN 的核心思想,也是它和传统 VPN 的本质区别:VPN 通常只建一条隧道,而 SD-WAN 在多条链路上做实时调度。
2. 就近接入 + 优化骨干。 让请求先就近接入一个加速网络,再通过经过优化的骨干链路抵达目标,绕开拥塞的公共国际出口,把丢包和延迟从源头压下去。CDN、Anycast、专线都属于这个思路的不同实现。
3. 高可用与故障切换。 多节点冗余、单点故障自动切换,把"成功率看运气"变成"成功率可量化"。这是能对外承诺 SLA 的前提。
五、为什么 VPN 不适合扛业务
VPN 是最多人踩的坑,值得单独说。它的设计目标是"让一台设备安全地接入另一个网络"——解决的是可达性和安全性,不是性能和可用性。具体来说:
- 单隧道架构:所有流量挤一条加密隧道,并发一上来就成瓶颈;
- 不做选路:链路差也只能硬走,没有"换条路"的能力;
- 节点共享:多人共用出口,容易被风控限速;
- 无故障切换:节点挂了,业务直接断,没有自动恢复。
所以结论不是"VPN 不好",而是它本就是为"个人偶尔访问"设计的,拿它去扛一个需要高并发、低延迟、高可用的业务负载,是工具和场景不匹配。
六、一张表总结排查思路
下次再遇到 AI API 又慢又失败,按这个顺序排查:
- 先看应用层:超时设太短?没有退避重试?并发打满了自己的连接池?——先在代码里解决。
- 再看服务层:是不是触发了服务商限流(429)?要不要做多供应商容灾?——看错误码就能区分。
- 剩下基本都是链路层:错误是
timeout/connection reset、成功率随时段波动、本地正常而生产环境差——这类问题代码救不了,得从网络架构入手。
把问题归到正确的层级,是解决它的第一步。大量时间被浪费,就是因为用错了层级的工具。
本文只讨论技术原理,不涉及任何具体产品。如果对其中某一层的实现细节感兴趣,欢迎在评论区交流。