支付宝 AI 开放平台的接口偶发超时,先别急着改业务代码,也不要直接加重试次数。按三条线索顺序排查:当前配额是否打满、请求体是否过大或带了冗余字段、单次调用的耗时到底落在哪一段。这三条分别对应平台侧的明确拒绝、本侧构造请求的开销、以及网络与对端处理时间,混在一起看就只能靠猜。
先用日志记下每次调用的开始时间、耗时、返回状态和错误类型,再对照控制台的配额用量页,能区分“配额受限”与“真实超时”,两者返回形态通常不同。请求体精简前后各跑一次,比较耗时变化,可判断是否本侧构造耗时偏高。用 curl 的 time_connect、time_starttransfer 分段,能看出慢在握手还是等对端首字节。超时阈值与重试上限要按业务可等待时间和日志里的实际耗时分位来定,只对幂等请求重试,超时仍失败时把请求 id、耗时、错误码一起留档。
记录每次调用的耗时与返回状态
没有耗时数据时,很难判断是“所有请求都慢”还是“个别请求偶发慢”。适用场景是所有调用 AI 接口的服务;操作动作是在统一封装层打点;验证方式是看日志里耗时分布是否集中、错误类型是否单一;风险边界是打点本身要轻,别把请求体全文写进日志。
import time, logging, requests
API_URL = '<接口地址>'
def call_ai(payload):
t0 = time.time()
status, biz_code, req_id = None, None, None
try:
resp = requests.post(API_URL, json=payload, timeout=(3, 30))
status = resp.status_code
data = resp.json()
biz_code = data.get('code') or data.get('errCode')
req_id = resp.headers.get('X-Request-Id') or data.get('request_id')
return resp
except requests.exceptions.ConnectTimeout:
status = 'connect_timeout'
except requests.exceptions.ReadTimeout:
status = 'read_timeout'
except requests.exceptions.ConnectionError:
status = 'connection_error'
finally:
elapsed_ms = int((time.time() - t0) * 1000)
logging.info(
'ts_start=%s elapsed_ms=%s status=%s biz_code=%s req_id=%s body_bytes=%s',
time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t0)),
elapsed_ms, status, biz_code, req_id,
len(str(payload).encode('utf-8')),
)
日志里至少要留下开始时间、耗时毫秒、返回类型(HTTP 状态、业务错误码)、平台返回的请求 id 和请求体字节数。区分 ConnectTimeout 和 ReadTimeout 很关键:前者是本侧还没连上,后者是连上了但等不到对端返回。平台返回 id 在提工单或对照配额页时能用上。
查看当前配额使用情况
配额打满时返回的往往是明确的错误码和提示信息,和连接超时是两回事,但如果不看配额页,容易把限流失败一律当成网络慢。进入开放平台的开发者控制台,找到对应应用或接口的调用量、配额、限流相关页面,查看当前时间窗内的已用次数与上限。不同接口的时间窗不一样,可能按日、按分钟或按秒计算,需要结合页面说明和自己的调用节奏确认。
配额受限时的返回信息形态以实际页面和实际返回为准:通常是带业务错误码和 message 的响应体,而不是连接建立失败。做法是把日志里出现错误的那个时间点,和配额页的用量曲线对齐,看是否正好打在限流窗口上。若确实是用量打满,优先做请求合并或错峰,而不是加大超时时间。
检查请求体是否过大或带有多余字段
请求体越大,序列化、上传和对端解析的时间都越长,这部分耗时算在本侧。可以用下面的清单逐项过一遍:
- 是否只传了必填字段,有没有把调试开关、测试标记一并带上;
- 是否整段塞入了历史对话、长文档或图片 base64,这些内容能否截断或改成分步提交;
- 是否传了大量空串、null 或默认值字段,有些实现对这类字段仍会做校验和序列化;
- 是否在循环里逐条调用,而不是把可合并的请求合成一次;
- body_bytes 是否明显超出该接口的常规量级。
验证方式是精简前后各跑一次同样的业务场景,用前面日志里的 body_bytes 和 elapsed_ms 做对照。如果精简后耗时明显下降,说明本侧构造和传输占了不小的比重;如果没有变化,把注意力转回网络段和对端处理段。风险边界是精简不能删除业务必需字段,改动前保留一份原始请求样例。
把超时拆成连接、传输、等待三段观察
同一个超时,原因可能完全不同。可以在本机用 curl 分段计时,观察不同阶段:
curl -o /dev/null -s \
-w 'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
-X POST '<接口地址>' \
-H 'Content-Type: application/json' \
-d @body.json
解读方向:dns 到 conn 是域名解析与 TCP 建连,conn 到 tls 是 HTTPS 握手,tls 到 ttfb 是发出请求后等待对端首字节,ttfb 到 total 是接收响应内容。如果 conn 或 tls 阶段就偏慢,更偏向本侧出口网络或链路问题,可以用 ping、mtr 对接口域名做链路观察,看是否丢包或某跳延迟异常;如果 ttfb 明显长于其它段,更偏向对端排队或处理耗时。用同一请求连续采样几次,稳定慢和偶发抖动的处理方向不同。
设定超时阈值与重试上限
阈值不要照搬示例值。连接超时通常设得短一些,读超时要看该接口在日志里的常规耗时分位,再留出业务可等待的余量。改动阈值后,用日志确认新阈值下报错数量与业务成功率的变化,别只看单次调用是否成功。
import time
def call_with_retry(payload, max_retry=2):
for attempt in range(max_retry + 1):
try:
return call_ai(payload)
except (requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout):
if attempt == max_retry:
raise
time.sleep(2 ** attempt) # 2s、4s,间隔按业务调整
重试只对幂等请求或带幂等键的写操作有意义;读超时并不代表对端没处理,盲目重试写请求可能造成重复。重试仍失败时,至少要记录:平台返回的请求 id、尝试次数、每次耗时、超时类型(连接还是读)、错误码、请求体字节数,以及错误发生时间点对应的配额用量。这些字段凑齐,才能判断下一步是调阈值、扩配额,还是排查链路。