晚高峰排查线上告警,十有八九是这两种情况:上游偶发 429 限流,或者一个请求挂在那里六十秒不返回。GPT、Claude 这类接口链路长、上游负载不受你控制,裸调 SDK 在生产环境迟早翻车。这篇教程从零写一个调用层,把超时、重试、退避、故障转移逐步做进去。
很多人只设一个总超时,结果要么误杀长输出,要么连接挂死等满全程。正确做法是连接、读取、写入分开:
import os, httpx
from openai import OpenAI
client = OpenAI(
base_url=os.environ["LLM_BASE_URL"], # 兼容 OpenAI 规范的入口
api_key=os.environ["LLM_API_KEY"],
timeout=httpx.Timeout(connect=5.0, read=60.0,
write=10.0, pool=5.0),
)连接 5 秒连不上就是链路问题,没必要等;读取超时留给生成过程,按你业务的最长输出评估。流式请求体感不同,可适当放宽读取超时,由"首 token 是否按时到达"来兜底。
429 还有一个细节:响应头里如果带了 Retry-After,按它给的秒数等,不要按自己的退避节奏硬撞;上游已经明确告诉你什么时候恢复了,撞回去只会被更严格地限流。
import random, time
from openai import APIStatusError, APITimeoutError, APIConnectionError
RETRYABLE = {429, 500, 502, 503, 504}
def call_with_retry(fn, max_retries=4, base=1.0, cap=30.0):
for i in range(max_retries + 1):
try:
return fn()
except APIStatusError as e:
if e.status_code not in RETRYABLE or i == max_retries:
raise
except (APITimeoutError, APIConnectionError):
if i == max_retries:
raise
time.sleep(min(cap, base * 2 ** i) * random.uniform(0.5, 1.5))抖动那个随机系数不是装饰:没有它,一次上游故障恢复后,所有客户端会在同一时刻齐刷刷重试,等于亲手制造第二轮拥塞。
单个模型重试用尽仍失败时,按预设顺序转移到备选模型:
CHAIN = ["gpt-4o", "claude-sonnet-4-6", "deepseek-chat"]
def chat_failover(client, messages):
last = None
for model in CHAIN:
try:
return call_with_retry(lambda: client.chat.completions.create(
model=model, messages=messages))
except Exception as e:
last = e
raise last两个提醒:备选模型的提示词效果要提前验证过,不是随手换一个就行;每次转移都要打日志,事后才能统计各模型的真实可用率。
转移链的顺序也有讲究:把效果最好的放第一位没有疑问,第二位建议选"和第一位不同厂商"的模型——同一家上游出故障时往往全系列一起抖,备选选同门兄弟等于没备。另外给整条链设一个总耗时上限,比如 90 秒,超过就直接向用户返回降级结果,不要让重试加转移把单个请求拖到无限长。
更进一步,可以在转移之上再加一层简单熔断:某个模型连续失败超过阈值(比如 1 分钟内 10 次),直接把它标记为不可用、跳过重试,5 分钟后再放少量探测请求进去试探恢复。熔断能避免明知上游已挂还反复撞墙,把延迟留给还活着的模型。
这套封装只假设你有一个兼容 OpenAI 规范的入口。它可以是官方接口、自建网关,也可以是第三方统一接入服务(如 OneAPI、kkaiapi 等,需自行实测),封装逻辑完全一样,差别只是 base_url 指向哪里。
做完这四步,上游抖动就从"事故"降级成了"日志里的一行记录"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。