上线前的大模型 API 测试要覆盖 8 个问题,其中 429 原因、成功率口径和重试幂等三处最容易出错,直接影响排障结果和账单。

重点摘要

  • 429 分两类:速率或并发超限可以退避重试,额度耗尽时重试无效。[1]
  • HTTP 200 不等于完整响应。示例数据中,HTTP 口径成功率为 95.0%,完整响应口径为 92.0%(算术示例,非实测)。
  • 平均延迟会掩盖慢请求,要同时看 P95 和首 token 时间。[3]
  • 99.5% 是服务目标,不是赔付承诺。按 30 天计算,允许约 216 分钟不可用(算术结果)。
  • 超时后直接重发,可能对应一次已经计费的请求。[2]

上线前要回答的 8 个问题是:模型版本是否一致、协议是否兼容、并发上限在哪、高峰期成功率多少、P95 延迟多少、故障时如何切换和重试、数据怎么留存、SLA 怎么算。下面按顺序展开,并给出可运行的测试脚本。

一、429 要先看返回体,再决定是否重试

同一个 429 可能来自速率限制、并发上限或额度耗尽,三者的处理方式不同。RFC 6585 将 429 定义为用户在一段时间内发送了过多请求,响应可以带 Retry-After 头,告诉客户端等待多久 [1],但它没有说明限制的来源。

情景常见返回特征处理方式
速率或并发超限错误消息提到 rate limit 或并发,常带 Retry-After指数退避加随机抖动,降低并发
额度耗尽错误码或消息提到 quota、余额不足重试无效,先调整额度或分组

错误码的字段名因服务商而异,以实际返回为准。

经网关转发时,429 可能来自上游账户、网关或用户自己的额度,三者的限制项不同。以灵眸AI(网关入口)这类网关为例,排查时要分别看返回体,不能只看状态码。

二、模型与协议:用同一组请求核对

模型名称一致,不能证明后端就是该版本。用同一组请求对比 usage 字段、停止原因、结构化输出和工具调用格式,结论比只看一次回答的文风可靠。

协议兼容要用实际 SDK 跑通以下四项,并记录 usage 字段与官方接口的差异:

  • 流式输出
  • Tool Calling 与结构化输出
  • 多模态输入
  • usage 字段

Base URL 配错会直接返回 401 或 404,所以先用一条最简请求确认端点和鉴权通过,再测其余功能。

三、阶梯并发找并发边界

RPM(每分钟请求数)、TPM(每分钟 token 数)和并发(同一时刻在途请求数)限制的是不同瓶颈,不能互相替代。把某个模型的 RPM 当作整个平台的并发能力,是常见误判。

阶梯并发的做法是:从低并发开始,每一级发出相同数量的请求,逐级升高,记录 429 比例和延迟。某一级的 429 超过设定比例就停止,停止前一级可近似视为该 Key 在该模型上的并发边界。脚本只依赖 Python 标准库,通过环境变量读取配置:

export LLM_BASE_URL=https://你的网关/v1
export LLM_API_KEY=sk-xxxx
export LLM_MODEL=你要测的模型名
python ladder.py
# ladder.py
import json
import os
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor

BASE_URL = os.environ["LLM_BASE_URL"].rstrip("/")  # 例如 https://你的网关/v1
API_KEY = os.environ["LLM_API_KEY"]
MODEL = os.environ.get("LLM_MODEL", "test-model")
LEVELS = [1, 2, 4, 8, 16]   # 阶梯并发:每一级保持相同请求数
PER_LEVEL = 20              # 每一级发出的请求数
MAX_429_RATIO = 0.05        # 某一级 429 超过 5% 即停止升档


def call_once():
    body = json.dumps({
        "model": MODEL,
        "max_tokens": 16,
        "messages": [{"role": "user", "content": "ping"}],
    }).encode()
    req = urllib.request.Request(
        BASE_URL + "/chat/completions",
        data=body,
        method="POST",
        headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
    )
    t0 = time.perf_counter()
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            resp.read()
            return resp.status, time.perf_counter() - t0, None, None
    except urllib.error.HTTPError as e:
        text = e.read().decode(errors="replace")
        return e.code, time.perf_counter() - t0, e.headers.get("Retry-After"), text[:300]
    except Exception as e:
        return None, time.perf_counter() - t0, None, repr(e)[:300]


def percentile(sorted_values, q):
    idx = min(len(sorted_values) - 1, round(q * (len(sorted_values) - 1)))
    return sorted_values[idx]


def run_level(concurrency):
    with ThreadPoolExecutor(max_workers=concurrency) as pool:
        results = list(pool.map(lambda _: call_once(), range(PER_LEVEL)))
    codes = [r[0] for r in results]
    lat = sorted(r[1] for r in results)
    retry_after = sorted({r[2] for r in results if r[0] == 429 and r[2]})
    n429 = codes.count(429)
    n5xx = sum(1 for c in codes if c is not None and c >= 500)
    n_other = len(codes) - codes.count(200) - n429 - n5xx
    print(
        f"并发={concurrency:>2}  200={codes.count(200):>2}  429={n429:>2}  "
        f"5xx={n5xx}  其他={n_other}  P50={percentile(lat, 0.5):.2f}s  "
        f"P95={percentile(lat, 0.95):.2f}s  Retry-After={retry_after or '-'}"
    )
    return results


def classify_429(body):
    # 原因以返回体里的错误码为准。示例只区分两类。
    if body and "insufficient_quota" in body:
        return "额度耗尽,重试无效,需要调整额度"
    return "速率或并发超限,可指数退避并降低并发"


if __name__ == "__main__":
    last_ok = None
    for n in LEVELS:
        results = run_level(n)
        bad = [r for r in results if r[0] == 429]
        if bad:
            print(f"  429 原因:{classify_429(bad[0][3])}")
        if len(bad) / len(results) > MAX_429_RATIO:
            break
        last_ok = n
    print(f"该 Key 在该模型上的并发边界(近似):{last_ok}")

用本地模拟服务(最多同时处理 3 个请求,超出返回 429)验证脚本,输出如下。这只说明脚本逻辑正确,不代表任何服务商的真实限额。

并发= 1  200=20  429= 0  5xx=0  其他=0  P50=0.21s  P95=0.23s  Retry-After=-
并发= 2  200=20  429= 0  5xx=0  其他=0  P50=0.21s  P95=0.23s  Retry-After=-
并发= 4  200= 5  429=15  5xx=0  其他=0  P50=0.02s  P95=0.21s  Retry-After=['1']
  429 原因:速率或并发超限,可指数退避并降低并发
该 Key 在该模型上的并发边界(近似):2

并发 4 那一级的 P50 降到 0.02 秒,是因为多数请求被 429 快速拒绝。只看 P50 会误以为服务变快了,所以验收要同时看 P95。

四、成功率按完整响应计算

成功率不能只看 HTTP 状态码。完整获得预期响应的请求才算成功:

成功率 = 完整获得预期响应的请求数 ÷ 有效请求总数

算术示例(非实测):1000 次请求中 950 次返回 HTTP 200,其中 30 次在流式输出中途断开。HTTP 层计算为 95.0%,完整响应计算为 (950 − 30) ÷ 1000 = 92.0%。两种口径相差 3 个百分点,统计时要写明用的是哪一种。

测试要覆盖低并发、正常负载、高并发和突发流量,分别统计 429、500、502、503、504。RFC 9110 中,503 表示服务暂时过载或维护,504 表示网关未在时限内收到上游响应,两者的处理方式不同,不能合并为“5xx”[2]。

五、延迟看 P95,流式请求另记首 token 时间

平均延迟会掩盖慢请求。Google 的 SRE 手册举例:平均延迟 100 毫秒、每秒 1000 个请求的服务,仍可能有 1% 的请求耗时 5 秒 [3]。所以验收要看 P50、P95 和 P99。

流式请求还要单独统计首 token 时间(TTFT)和完整耗时。本文的 ladder.py 只统计完整耗时,没有统计 TTFT,需要另写带 stream=true 的测试。

示例数据(非实测):某次 1000 次流式调用得到 P50 1.8 秒、P95 6.2 秒、P99 12.5 秒。P50 很好看,但约每 20 个请求中仍有 1 个超过 6.2 秒。

六、重试前确认幂等,否则可能重复计费

重试前先确认请求是否幂等,非幂等的 POST 请求不能直接重发。RFC 9110 规定,客户端不应自动重试非幂等的请求,除非能确认请求语义可以安全重放,或能确认原请求没有被执行 [2]。Chat 接口一般是 POST,超时后直接重发,可能对应一次已经生成并计费的请求。

请求情况能否自动重试说明
429,错误码是速率或并发超限可以,退避后重试这类请求通常未被执行,仍以服务商文档为准
429,额度耗尽不可以重试仍会失败
超时,未收到任何响应谨慎原请求可能已执行,先查用量明细
流式输出中途断开谨慎已生成的 token 可能已计费,续传是否支持以服务商说明为准

退避时,等待时间优先采用 Retry-After;没有该头时用指数退避,每次加倍并加入随机抖动,并设置最大重试次数。这是常见做法,不是某个服务商的承诺。

七、数据留存、SLA 与合同

数据留存和 SLA 口径都要写进书面合同,口头说明不能作为验收依据。数据留存要确认请求内容是否落盘、日志留存多久、是否用于训练、删除机制和跨境路径。《生成式人工智能服务管理暂行办法》第十一条规定,服务提供者不得非法留存能够识别使用者身份的输入信息和使用记录,该办法自 2023 年 8 月 15 日起施行 [4]。发布前请查证该办法是否有更新版本。按数据类别分别规定可以发送的范围,并用数据处理协议(DPA)约定保留期限、转包的上游厂商和删除时限。

SLA 的数字要按公式核算。99.5% 是服务目标,不是赔付承诺。按 30 天计算,它允许约 216 分钟(3.6 小时)不可用;99.9% 允许约 43.2 分钟。以上是算术结果。

核对 SLA 口径时,要确认三件事:

  • 哪些原因不计入(通常包括用户网络、参数错误和上游的内容拒绝)
  • 单次故障多长才计入
  • 月报按模型还是按整体统计

灵眸AI 的官方文档写明,企业档可用性目标为 99.5%,单次故障少于 60 秒不计入,月度报告含分模型、分时段的成功率与 P95 延迟。可以逐条对照本节列出的三个口径问题核对,结论以文档和合同为准。

八、方案边界

自动重试和多上游都有适用范围,超出范围的问题要靠额度、服务商或架构解决。

  • 自动重试解决不了额度耗尽,也解决不了模型本身的故障。
  • 多上游能降低单一通道的影响,但流式输出中断后不一定能无感恢复。
  • 测试结果只对测试时的 Key、模型、时段和网络有效,换时段要重测。
  • 本文脚本只统计 HTTP 结果和整体延迟,不替代压测平台,也不计算真实账单。

上线前检查清单

  • 用同一组请求核对模型版本和 usage 字段
  • 用 SDK 跑通流式、工具调用、结构化输出
  • 阶梯并发测出每个 Key、每个模型的并发边界,并分开记录 429 原因
  • 成功率按完整响应计算,分别记录 429、5xx 和超时的数量
  • 延迟同时记录 P50、P95;流式请求另记首 token 时间
  • 重试前确认请求幂等,设置退避间隔和最大重试次数
  • 书面确认数据留存、训练用途、跨境路径和 DPA 条款
  • 书面确认 SLA 的不计入原因、故障门槛和月报口径

常见问题

429 出现后应该立刻重试吗?

先看返回体。速率或并发超限时,退避后可以重试;额度耗尽时重试无效,需要先调整额度。

成功率应该怎么算?

完整获得预期响应的请求数,除以有效请求总数。HTTP 200 但流式中途断开的请求不计为成功。

为什么不用平均延迟判断服务是否稳定?

平均值会被大量快速返回的错误拉低。示例中并发 4 那一级的 P50 降到 0.02 秒,是因为多数请求被 429 快速拒绝。验收时应同时看 P50、P95 和超时数量。

99.5% 的 SLA 是什么意思?

它是服务目标,不是赔付承诺。按 30 天计算,允许约 216 分钟不可用;是否赔付取决于合同,核对时要看不计入的原因和统计口径。

【建议补上 EEAT|可信度:标示看得见的更新日期;写明本文不适用的场景,例如没有并发和 SLA 需求的个人小项目】

参考资料

[1] RFC 6585,Additional HTTP Status Codes,IETF,2012 年 4 月,https://www.rfc-editor.org/rfc/rfc6585

[2] RFC 9110,HTTP Semantics,IETF,2022 年 6 月,https://www.rfc-editor.org/rfc/rfc9110

[3] Site Reliability Engineering,第 6 章 Monitoring Distributed Systems,Google,2016 年,https://sre.google/sre-book/monitoring-distributed-systems/

[4] 生成式人工智能服务管理暂行办法,国家互联网信息办公室等部门,2023 年 7 月发布,2023 年 8 月 15 日起施行,https://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm

Logo

更多推荐