点击开始动手实验


Chat背景:为什么“Operation Timed Out”总在凌晨爆发

凌晨两点,监控群里突然告警:批量调用 ChatGPT 的链路超时率飙到 18 %。
日志里清一色 requests.exceptions.ReadTimeout502 Bad Gateway
根因往往逃不出下面三类:

  1. 网络抖动:跨境链路 RT 从 180 ms 涨到 1.2 s,TLS 握手阶段就把 5 s 超时吃光。

  2. 请求膨胀:为了“让模型一次答完”,开发者把 8 k token 的上下文全塞进去,结果首包时间(TTFB)线性增长,触发云端 idle timeout。

  3. 并发配额:组织级账号默认 3 k RPM / 350 k TPM,一旦流量突增,边缘节点直接丢包,客户端侧只能看到“timeout”,而实际收到的是 429 或 503,被网关吞掉响应体。

技术方案对比:短轮询、长轮询还是指数退避?

策略 适用场景 优点 缺点
短轮询(固定间隔重试) 低峰期、小并发 实现简单 易放大服务器压力,重试风暴
长轮询(阻塞到有响应) 需要实时结果、长连接 减少空转 客户端连接池易被挂起
指数退避 + 全抖动(Full Jitter) 高并发、跨地域 打散重试峰,对服务端友好 增加尾延迟

生产经验:

  • 对 ChatGPT 这种“全局限速”服务,优先选“指数退避 + 全抖动”,退避上限 64 s,重试 6 次即可覆盖 99.5 % 偶发抖动。
  • 若业务对尾延迟极度敏感(如客服坐席),可改用“断路器 + 长轮询”双模:正常走长连接,失败率超阈值 5 % 时自动降级到短轮询,30 s 后探测恢复。

核心实现:Python 重试装饰器(含 JWT 鉴权)

以下代码基于 tenacity==8.2.0,同时兼容 Azure OpenAI 的 JWT 换取,关键处中英双语注释。

import os, time, jwt, requests, logging
from datetime import datetime, timedelta
from tenacity import retry, stop_after_attempt, wait_exponential_jitter

# 生成 AAD JWT,用于 Azure OpenAI
def _get_aad_token(audience: str) -> str:
    # 使用托管身份或 Service Principal 换取 token
    resp = requests.post(
        f"https://login.microsoftonline.com/{os.getenv('TENANT_ID')}/oauth2/v2.0/token",
        data={
            "grant_type": "client_credentials",
            "client_id": os.getenv("CLIENT_ID"),
            "client_secret": os.getenv("CLIENT_SECRET"),
            "scope": f"{audience}/.default"
        },
        timeout=5
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

# 统一超时参数,方便压测时调节
DEFAULT_TIMEOUT = (3.5, 10)  # (connect, read)

@retry(
    reraise=True,
    stop=stop_after_attempt(6),
    wait=wait_exponential_jitter(initial=1, max=64, jitter=True)
)
def chat_completion(payload: dict) -> dict:
    """
    调用 OpenAI / ChatGPT completions 接口
    支持 OpenAI 官方与 Azure 两种 endpoint
    """
    is_azure = bool(os.getenv("AZURE_OPENAI_ENDPOINT"))
    if is_azure:
        token = _get_aad_token("https://cognitiveservices.azure.com")
        url = f"{os.getenv('AZURE_OPENAI_ENDPOINT')}/openai/deployments/{os.getenv('DEPLOYMENT')}/chat/completions?api-version=2023-05-15"
        headers = {"Authorization": f"Bearer {token}"}
    else:
        url = "https://api.openai.com/v1/chat/completions"
        headers = {"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"}

    # 记录首包时间,方便排查慢查询
    start = time.perf_counter()
    try:
        resp = requests.post(url, json=payload, headers=headers, timeout=DEFAULT_TIMEOUT)
        # 429/503 也抛异常,交给重试器处理
        if resp.status_code in {429, 503, 502}:
            logging.warning("Hit rate limit or gateway error, will retry")
            resp.raise_for_status()
        resp.raise_for_status()
        return resp.json()
    finally:
        logging.info(f"TTFB={time.perf_counter()-start:.3f}s status={resp.status_code}")

请求分块(Chunking)与负载测试伪代码

当输入 token 超过 4 k 时,即使模型支持 8 k+,也建议按“段落”切分,再并发拼接,降低单请求 hang 死概率。

def chunk_text(text: str, max_tokens: int = 1500) -> list[str]:
    """简易按双换行分段,可换成 tiktoken 精确计算"""
    paragraphs = text.split("\n\n")
    buf, chunks = [], []
    for p in paragraphs:
        buf.append(p)
        if len(" ".join(buf)) > max_tokens:
            chunks.append(" ".join(buf[:-1]))
            buf = [p]
    if buf:
        chunks.append(" ".join(buf))
    return chunks

# 并发调用示例(伪代码)
async def async_map_chat(chunks):
    tasks = [asyncio.create_task(chat_completion(chunk)) for chunk in chunks]
    return await asyncio.gather(*tasks, return_exceptions=True)

负载测试:
使用 locust -f locustfile.py --u 100 -r 10 -t 5m 观察 P99 延迟,若 > 8 s 占比 > 2 %,则调低并发或继续细化 chunk。

性能考量:QPS、冷启动与连接池

  1. QPS 与 TPM 双层限速
    官方返回的 x-ratelimit-limit-requestsx-ratelimit-limit-tokens 需缓存到本地内存,令牌桶算法按 100 ms 粒度填充,否则极易“突刺”后超时。

  2. 冷启动延迟
    当部署在 Azure 且选择“按量付费”时,若 5 min 无调用,实例会被回收,首请求 RT 可能陡增 4–7 s。解法:

    • 使用“预置吞吐量”(PTU) 保底;
    • 在连接池里加 30 s 一次的空转探活(keep-alive),携带 max_tokens=1 的 dummy 请求。
  3. 连接池优化
    requests 默认池大小 10,高并发下立即耗尽。
    推荐 requests.adapters.HTTPAdapter(pool_maxsize=100, pool_connections=20),并打开 HTTP/2 (hyper) 减少 TLS 重复握手。

避坑指南:生产环境三大血泪教训

  1. 忽略 429 状态码
    很多 SDK 只把 429 当“稍后再试”,却没回读 Retry-After 头,导致退避失效。务必在重试器里解析该字段并动态设置 wait=retry_after

  2. 日志缺失 request_id
    OpenAI 返回的 x-request-id 是官方排障唯一凭证。未落盘导致后续工单无法定位,被退回“请复现”。

  3. 未配置连接读超时差异
    timeout=30 一把梭,结果内网代理 5 s 就返回 504,客户端空等到 30 s 才抛异常,线程池被占满。正确姿势:connect / read 分离,connect 3.5 s,read 10 s,既给网络抖动留余地,也避免挂死。

互动思考

  1. 在分布式微服务架构中,如何设计一套基于 Redis+Lua 的限流方案,既支持滑动窗口又避免单点热点 key?
  2. 当指数退避遇上消息类长连接(WebSocket),如何权衡“重试尾延迟”与“消息顺序”冲突,保证业务幂等?

动手拓展:把“稳定调用”升级为“实时对话”

当你已经能把超时率压到 < 0.3 %,不妨再往前一步:让模型“开口说话”。
从0打造个人豆包实时通话AI 这个动手实验,用火山引擎豆包·语音系列模型,把 ASR→LLM→TTS 整条链路串成低延迟 Web 通话。
我本地跑通只花了 45 min,官方模板已帮你搞定回声消除、流式语音合成等脏活,小白也能顺利体验。
把上面沉淀的“超时治理”套进去,就能得到一个既稳又能“聊”的 AI 伙伴,或许下一个深夜告警的就是“用户聊得太嗨,RPM 又打满了”。

点击开始动手实验


Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐