调用支付宝 AI 开放平台接口报错时,先别急着改重试次数。多数失败可以先用返回结构定位层次:鉴权失败、配额受限、权限不足这类判断通常发生在平台侧网关,业务代码能决定的是等多久、失败之后怎么办。建议的判断顺序是:先看返回类型,再看这次请求的耗时和已经重试过几次,最后才决定是去控制台调整凭证或配额,还是改自己代码里的重试与降级逻辑。
适用场景:业务系统调用支付宝 AI 开放平台接口,出现超时或错误返回。操作动作:鉴权、配额判断、错误结构交给平台侧控制台与网关,业务侧只维护超时阈值、重试次数和降级路径。验证方式:用日志里的返回类型与耗时判断失败发生在哪一层,重试后仍拿到同一类返回就应停止重试。风险边界:重试与降级是止血手段,不能替代配额申请和权限配置,超时阈值需要结合自身链路总耗时确认,不要照搬别人的数字。
列出平台侧已经兜住的部分
平台侧负责的内容通常集中在三件事上:鉴权校验、配额控制、统一错误结构。鉴权一般由网关校验应用凭证与令牌,失败时返回鉴权类错误码;配额按应用或接口维度统计调用量,达到上限时直接拒绝;错误则以统一的错误码、子码和信息返回。具体覆盖哪些能力,以控制台页面和接口文档的描述为准,文档里没写的部分不要自行假设。
| 事项 | 平台侧 | 业务侧 |
|---|---|---|
| 鉴权 | 校验凭证与令牌,返回鉴权类错误码 | 配置并保管凭证,按返回提示处理令牌失效,不自己判断签名对错 |
| 配额 | 统计调用量,超限时拒绝请求 | 不维护本地计数去猜配额,改走降级路径 |
| 错误结构 | 返回统一错误码、子码与信息 | 解析错误码做分类,决定重试还是降级 |
| 超时 | — | 设置连接超时、读取超时与总 deadline |
| 重试 | — | 控制最大次数、退避间隔与退出条件 |
业务代码里不要再写一遍「签名是否正确」「配额是否打满」的判断。本地计数和平台统计口径往往不一致,拿本地计数去拦截请求,容易出现平台还没限流、业务侧先把请求掐掉的情况。业务侧真正需要做的,只是把凭证配好、按返回提示处理令牌失效,以及处理错误分类。
在业务侧定义超时阈值与重试次数
超时阈值和重试次数的依据不是接口文档里的默认值,而是业务链路能接受的等待时间。可以先定一个总 deadline(例如前端入口的整体超时),再把它拆成「单次超时 × 尝试次数 + 退避等待」,保证最坏情况也在这个预算内结束,而不是无限等待。连接超时和读取超时建议分开设置,重试用带抖动的退避,退出条件写清楚:次数用尽,或者已经超过总 deadline。
MAX_RETRY = 2
BACKOFF = [0.3, 1.0]
CONNECT_TIMEOUT = 2.0
READ_TIMEOUT = 3.0
TOTAL_DEADLINE = 8.0
def call_ai(payload):
started = time.time()
for attempt in range(MAX_RETRY + 1):
left = TOTAL_DEADLINE - (time.time() - started)
if left <= 0:
logger.warning('ai_call type=deadline attempt=%d', attempt)
raise TimeoutError('total deadline exceeded')
try:
return client.invoke(
payload,
connect_timeout=CONNECT_TIMEOUT,
read_timeout=min(READ_TIMEOUT, left),
)
except (SocketTimeout, ConnectionResetError):
if attempt == MAX_RETRY:
logger.warning('ai_call type=network attempt=%d', attempt + 1)
raise
time.sleep(BACKOFF[attempt] + random.uniform(0, 0.2))
把 client 换成实际使用的 SDK 调用,把 BACKOFF 和 TOTAL_DEADLINE 换成按自己链路确认过的值。验证方式是:在测试环境制造一次慢响应,观察调用方是否在 deadline 内返回,而不是一直挂在那里等结果。
区分可重试与不可重试的返回
把鉴权失败、权限缺失、参数非法这类错误也重试一遍,只会拖长故障时间,还会让日志更难看清真正的问题。可以按返回类型做分支,错误码字符串按接口实际返回替换,下面只是结构示例。
| 返回类型 | 是否重试 | 处理动作 |
|---|---|---|
| 网络超时、连接重置 | 可重试 | 在次数和总预算内重试 |
| 系统繁忙、服务暂不可用类错误码 | 可重试 | 有次数上限地重试 |
| 签名无效、令牌失效、参数非法、权限不足 | 不可重试 | 直接失败并告警,人工确认配置 |
| 配额受限、限流 | 不重试或最多一次 | 转到降级路径 |
RETRYABLE = {'SystemBusy', 'ServiceUnavailable'}
AUTH_ERROR = {'InvalidSignature', 'TokenExpired'}
QUOTA = {'QuotaExceeded', 'RateLimited'}
def classify(resp):
code = resp.get('code', '')
if code in RETRYABLE:
return 'retryable'
if code in AUTH_ERROR:
return 'auth_error'
if code in QUOTA:
return 'quota_limited'
return 'fatal'
kind = classify(resp)
logger.info('ai_call type=%s code=%s retry=%d', kind, resp.get('code'), attempt)
if kind == 'retryable' and attempt < MAX_RETRY:
retry_later()
elif kind == 'quota_limited':
return fallback(payload)
分类结果要落进日志,而不是只用于控制流。这样事后翻日志时,能直接看出这是「重试两次仍失败」还是「一次鉴权失败就放弃」。
给配额受限留一条降级路径
配额打满时,业务侧如果没有备用路径,整个功能会跟着不可用。触发降级的判断条件可以写成三条:返回码明确指向限流或配额耗尽;同一个时间窗内连续多次拿到同类返回;重试之后结果没有变化。满足任意一条就应转降级,而不是继续加次数。
- 排队:把请求放入本地队列,按固定间隔再尝试,注意设置队列上限和丢弃策略,避免堆积占满内存。
- 返回兜底结果:使用上一次成功结果的缓存、模板化话术,或者明确提示稍后再试。
- 转人工或转异步:生成工单、走回调通知,把即时响应改成稍后处理。
if kind == 'quota_limited':
if cached_answer(payload):
return cached_answer(payload)
if queue.size < QUEUE_LIMIT:
queue.push(payload)
return FALLBACK_TEXT
return '当前请求较多,请稍后再试'
这些做法属于止血,不能当成提升配额的手段。配额本身的调整需要在控制台侧申请或变更,业务侧的降级只保证功能整体不被拖垮。
用日志串起一次完整失败的链路
要让事后能判断失败发生在平台侧还是业务侧,日志里至少保留这几个字段:请求时间、trace 或请求标识(平台返回就记平台的,没有就本地生成)、接口或模型名、本次耗时、返回类型、平台错误码、重试次数、最终结果(成功、降级还是失败)。
logger.info(
'ai_call ts=%s trace=%s api=%s cost_ms=%d type=%s code=%s retry=%d final=%s',
req_ts, trace_id, api_name, cost_ms, kind, code, attempt, final_state,
)
拿到一次失败反馈时,按 trace 找这几条日志:只有发起记录、没有返回、耗时接近读取超时,多半是网络或业务侧超时阈值偏紧;有平台错误码且 type 是 auth_error,先去核对凭证与令牌;final 是降级,说明配额或限流已经触发。先按这三个方向定位,再去决定改配置还是改代码,通常比直接加两次重试更有效。