AI API 调用延迟太高怎么办?实测 3 种方案把 P95 从 2s 压到 320ms

4 阅读1分钟

上周我们线上的 RAG 问答系统被用户投诉"回答太慢",我一查链路,好家伙,Claude Sonnet 4.6 的 API 调用 P95 延迟飙到了 2100ms,首 Token 等待时间经常超过 1.5s。用户那边体感就是点完按钮干等两三秒才开始出字,搁谁谁烦。

直接说结论:API 调用延迟高的核心原因是网络链路过长、没有就近、以及 SDK 默认配置不合理。解决方案有三种:优化 SDK 连接参数(省 200-400ms)、切换到有亚太的 API 网关(省 800-1200ms)、在应用层做 Streaming + 预连接(体感延迟再降 50%)。三种叠加用,我们最终把 P95 从 2100ms 压到了 320ms 左右。

为什么你的 API 调用这么慢

很多人以为延迟高是模型推理慢。不是,或者说不全是。

我用 curl -w 拆了一下耗时:

curl -w "DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nFirst Byte: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
 -X POST https://api.anthropic.com/v1/messages \
 -H "x-api-key: sk-xxx" \
 -H "content-type: application/json" \
 -d '{"model":"claude-sonnet-4-6","max_tokens":100,"messages":[{"role":"user","content":"hi"}]}'

结果让我有点意外:

  • DNS 解析:80ms
  • TCP 握手:320ms
  • TLS 协商:640ms(这里就吃掉了将近 1s)
  • 等待首字节:890ms
  • 总耗时:2040ms

TLS 握手 640ms 说明物理距离太远了,TCP 往返 RTT 大概 160ms 一跳,TLS 1.3 要 1-RTT 但加上 TCP 本身的握手就是 3 跳。如果你没有连接复用,每次请求都要重新走这一套。

sequenceDiagram
 participant App as 你的应用
 participant DNS as DNS 解析
 participant GW as API 网关(远端)
 participant Model as 模型推理

 App->>DNS: 域名解析 (~80ms)
 App->>GW: TCP 握手 (~320ms)
 App->>GW: TLS 协商 (~640ms)
 App->>GW: POST /v1/messages
 GW->>Model: 转发请求
 Model-->>GW: 首 Token (~890ms)
 GW-->>App: 响应流
 Note over App,Model: 总延迟 ≈ 2040ms

方案一:优化 SDK 连接参数

最简单的一步,不用换任何服务商,改几行代码就行。

Python 的 openaianthropic SDK 底层都用 httpx,默认不开连接池持久化。每次 client = OpenAI(...) 如果你是在函数里临时创建的,连接池根本没法复用。

# ❌ 错误写法:每次调用都新建 client
def ask(prompt):
 client = OpenAI(api_key="sk-xxx", base_url="https://api.anthropic.com/v1")
 return client.chat.completions.create(...)

# ✅ 正确写法:全局单例 + 连接池
from openai import OpenAI
import httpx

# 全局初始化一次
client = OpenAI(
 api_key="sk-xxx",
 base_url="https://api.anthropic.com/v1",
 http_client=httpx.Client(
 limits=httpx.Limits(
 max_connections=20,
 max_keepalive_connections=10,
 keepalive_expiry=30
 ),
 timeout=httpx.Timeout(60.0, connect=10.0)
 )
)

实测效果:连接复用后,第二次请求开始 TCP+TLS 那 960ms 直接省掉,P95 从 2100ms 降到 1100ms 左右。

但问题是——首次请求还是慢,keepalive 超时后又要重新握手。治标不治本。

方案二:切到有亚太的 API 网关

效果最明显的一步。

原理很简单:如果 API 网关在香港或东京有边缘,你的请求 RTT 从 160ms 降到 20-30ms,TLS 握手直接从 640ms 变成 80ms。

我测了几家聚合平台的延迟(4 月 22 号测的,华东阿里云 ECS 发起请求,模型统一用 Claude Sonnet 4.6,100 次取 P50/P95):

平台P50 首 TokenP95 首 Token备注
Anthropic 官方890ms1340ms美西
OpenRouter720ms1180ms有亚太 CDN 但加 5.5% 手续费
ofox.ai280ms420ms香港直连,0% 加价
AWS Bedrock 东京310ms480ms需要 AWS 账号+配置 IAM

说实话测完数据我人傻了,差距这么大。Anthropic 官方的在美西,光物理距离就摆在那里,没法优化。

切换方式很简单,改个 base_url:

from openai import OpenAI

client = OpenAI(
 api_key="your-ofox-key",
 base_url="https://api.ofox.ai/v1"
)

response = client.chat.completions.create(
 model="claude-sonnet-4-6",
 messages=[{"role": "user", "content": "解释一下 TCP 三次握手"}],
 stream=True
)

for chunk in response:
 if chunk.choices[0].delta.content:
 print(chunk.choices[0].delta.content, end="", flush=True)

ofox.ai 是大模型云厂商官方授权的服务商,Claude 走的是 Anthropic 和 AWS Bedrock 的官方通道,不是那种个人搞的中转站。OpenRouter 也能用但有 5.5% 手续费,长期跑下来一个月能差出好几十刀。

方案三:应用层 Streaming + 预连接

前两步解决了网络层的问题,这一步解决体感问题。

即使首 Token 要 300ms,如果你用 Streaming 模式,用户 300ms 后就能看到第一个字开始蹦出来,心理感受完全不一样。但很多人虽然开了 stream=True,前端却是攒够一整段再渲染——白开了。

另一个技巧是预连接。在用户还在打字的时候,就把 TCP+TLS 握手先做了:

import httpx
import asyncio

class APIPool:
 def __init__(self):
 self._client = httpx.AsyncClient(
 base_url="https://api.ofox.ai/v1",
 limits=httpx.Limits(max_keepalive_connections=5),
 timeout=httpx.Timeout(60.0, connect=5.0)
 )
 self._warmed = False

 async def warmup(self):
 """应用启动时调用,预热连接池"""
 if not self._warmed:
 # 发一个轻量请求把连接建立起来
 try:
 await self._client.get("/models", headers={"Authorization": "Bearer your-key"})
 except Exception:
 pass # 404 也行,重点是连接建立了
 self._warmed = True

 async def chat(self, messages):
 resp = await self._client.post(
 "/chat/completions",
 json={"model": "claude-sonnet-4-6", "messages": messages, "stream": True},
 headers={"Authorization": "Bearer your-key"}
 )
 async for line in resp.aiter_lines():
 if line.startswith("data: "):
 yield line[6:]

预热之后,实际请求时 TCP+TLS 的开销就是 0 了。

踩坑记录

折腾过程中踩了几个坑,记一下:

坑 1:httpx 的 keepalive 默认 5 秒

我一开始设了连接池但效果不明显,后来发现 httpx 默认 keepalive_expiry=5,也就是 5 秒没请求连接就断了。对于用户交互场景,两次提问间隔经常超过 5 秒。改成 30 秒好很多。

坑 2:Streaming 模式下 timeout 要设大

httpx.ReadTimeout: timed out

这个报错坑了我半天。stream 模式下模型可能要生成很长的回复,默认 timeout 5 秒根本不够。要把 read timeout 设到 60s 以上,但 connect timeout 保持 5-10s 就行。

坑 3:429 和延迟飙高分不清

有段时间延迟突然从 300ms 跳到 3s,我以为是网络问题,结果是触发了速率限制,API 返回的不是 429 而是排队等待——Anthropic 的 API 在高峰期会排队而不是直接拒绝。日志里看着像是正常响应只是很慢,实际上 header 里有 retry-after 字段。

最终效果

三个方案叠加之后的数据(4 月 23 号跑了一整天的监控):

  • P50 首 Token:180ms
  • P95 首 Token:320ms
  • P99 首 Token:580ms

相比优化前 P95 的 2100ms,降了 85%。用户那边反馈"快了很多,跟本地跑似的"。

P99 的 580ms 我也不确定还能不能再压,可能跟模型端的排队有关系,这个我控制不了。但 P95 在 320ms 这个水平,对于在线问答场景已经够用了。

反正结论就是:别光盯着模型推理速度,网络链路才是延迟大头。连接复用 + 就近 + Streaming 预热,三板斧下去基本能解决 80% 的延迟问题。剩下的 20% 是模型本身的推理时间,那就只能换更快的模型(比如 Claude Haiku 4.5 或者 Gemini 3.1 Flash)了。