AI图鉴

AI图鉴首页 › AI 资讯 › 给大模型调用加一个熔断器:60 行代码,三个阈值

给大模型调用加一个熔断器:60 行代码,三个阈值

来源:掘金 · 发布时间:10-30 · 由 AI图鉴每小时自动聚合

去年冬天一个晚上,我们的写作助手挂了 62 秒。 不是崩了,是"一直转圈"。用户点了发送,屏幕上一个光标闪了一分钟,然后弹出一句"生成失败,请重试"。 那晚服务端 5xx 高峰持续了 11 分钟。我们

去年冬天一个晚上,我们的写作助手挂了 62 秒。

不是崩了,是"一直转圈"。用户点了发送,屏幕上一个光标闪了一分钟,然后弹出一句"生成失败,请重试"。

那晚服务端 5xx 高峰持续了 11 分钟。我们发了 4,200 次请求,其中 2,100 次以超时结束,平均每个用户等了 62 秒。事后看日志,前 300 次超时的时候我们就该停下来——但我们没有停,因为代码里写的是 max_retries=3,每一次都老老实实等满 60 秒超时。

重试本身不是问题,"在已知对方挂了的时候还重试"才是问题。

这篇文章讲我加的那个熔断器:三个阈值、60 行代码、上线半年拦住两次事故。

一、为什么大模型调用特别需要熔断

传统 HTTP 服务加熔断,通常是为了防雪崩。大模型调用有三个不一样的地方:

第一,单次请求特别慢。 首字延迟正常 800ms,但一旦服务端排队,能拖到 40 秒以上。超时设 60 秒是常规做法,这意味着一次失败要占用一个连接 60 秒。

第二,重试特别贵。 一次调用可能几千 token,重试三次就是三倍的钱。服务端挂的时候,你不但没拿到结果,还把钱花出去了。

第三,失败是成片出现的。 服务端抖动不是随机的,是一段一段的。你要是能识别出"这段是坏的",跳过这一段,收益极大。

那次事故的数字很直观:

多出来的 2,060 次,全是在服务端已经明确故障之后发出去的。

二、三个阈值,缺一不可

我的实现只有一个类,三个参数:

class Breaker:

def __init__(self, window=20, fail_ratio=0.5, cooldown=30.0):

self.window = window # 只看最近 20 次的结果

self.fail_ratio = fail_ratio # 失败率超过 50% 就打开

self.cooldown = cooldown # 打开后 30 秒进半开

阈值一:窗口 = 20 次。 不建议用"连续失败 N 次",因为大模型调用里混着用户取消、内容审核拦截这类非服务端失败,连续计数容易被个别请求带偏。用滑动窗口看比例,抗噪。

窗口太小(比如 5)会误判——一次批量任务是并发发的,5 个里有 3 个超时很正常;窗口太大(比如 200)反应太慢,200 次 × 60 秒的损失已经发生了。20 是我试出来的平衡点。

阈值二:失败率 = 50%。 这个不能设太高。日常我们的失败率在 0.3%–0.8%,50% 已经是明确异常。设 30% 会在早晚高峰误触发,设 80% 等于没设。

阈值三:冷却 = 30 秒。 打开后不再放行,30 秒后进"半开",放 3 个探测请求过去。3 个里 2 个成功就关闭,否则重新打开。30 秒的依据是:服务端抖动通常以分钟为单位的块状出现,探测太频繁只是给已经挂掉的服务端添乱。

三、状态机就三个状态

import time

from collections import deque

class Breaker:

def __init__(self, window=20, fail_ratio=0.5, cooldown=30.0, probes=3):

self.window, self.fail_ratio = window, fail_ratio

self.cooldown, self.probes = cooldown, probes

self.results = deque(maxlen=window)

self.state = "closed"

self.opened_at = 0.0

self.probe_ok = 0

self.probe_total = 0

def allow(self) -> bool:

if self.state == "closed":

return True

if self.state == "open":

if time.monotonic() - self.opened_at >= self.cooldown:

self.state, self.probe_ok, self.probe_total = "half", 0, 0

return True

return False

# half-open:只放行有限个探测

return self.probe_total < self.probes

def record(self, ok: bool):

if self.state == "half":

self.probe_total += 1

self.probe_ok += int(ok)

if self.probe_total >= self.probes:

if self.probe_ok >= 2:

self.state, self.results = "closed", deque(maxlen=self.window)

else:

self.state, self.opened_at = "open", time.monotonic()

return

self.results.append(ok)

if len(self.results) == self.window:

ratio = 1 - sum(self.results) / len(self.results)

if ratio >= self.fail_ratio:

self.state, self.opened_at = "open", time.monotonic()

调用处的改动很小:

from openai import OpenAI

import os

client = OpenAI(

api_key=os.environ["API_KEY"],

base_url="https://api.tokenfreeai.com/v1", # OpenAI 兼容

timeout=60, max_retries=0, # 重试交给熔断器判断

)

breaker = Breaker()

def ask(prompt: str) -> str | None:

if not breaker.allow():

return None # 快速失败,别让用户干等

try:

r = client.chat.completions.create(

model="glm-5",

messages=[{"role": "user", "content": prompt}],

)

except Exception:

breaker.record(False)

raise

breaker.record(True)

return r.choices[0].message.content

注意两件事:max_retries=0 必须写,否则 SDK 自带的退避重试会绕过熔断器;返回 None 的时候上层要能接住,这一步比熔断逻辑本身更容易漏。

四、返回 None 之后干什么

这是很多人漏掉的一半。熔断的价值不在"拦",在"拦了之后给什么"。

我们的三级降级:

- 换模型。 主力模型熔断后,切到同厂的轻量版,输出质量掉一档但能用。实测切换后用户投诉量没有明显上升,因为大部分请求是润色、起标题这类任务。

- 排队。 非实时任务(夜间批处理、报告生成)进延迟队列,5 分钟后重试,用户端显示"处理中,完成后通知"。

- 明说。 实时对话类如果两样都不行,直接告诉用户"当前服务繁忙",而不是让他等一分钟再看失败。

第三条最难做,但最重要。 那个 62 秒的晚上,用户真正愤怒的不是失败,是"你不知道它会不会成功"。

五、上线半年的两次拦截

第一次是四月份,服务端区域性故障,熔断在 38 秒后打开,拦掉了 1,800 多次请求。用户平均等待从预估的 50 秒降到 1.1 秒。

第二次更隐蔽:我们自己的一个脚本死循环在重试,一小时打了 9,000 次调用。熔断器本身没拦(因为服务端是好的),但同一套状态机加了个"每小时调用量超阈值就打开"的旁路,把它挡住了——那次省了大约 380 元。

六、一个坑:别把熔断器做成分区无关的

我们刚开始只有一个全局熔断器。后来发现一个问题:主力模型熔断后,所有请求都被挡在外面,包括本来可以走备用模型的那部分。

后来改成按"模型 + 区域"分桶。每个桶一个熔断器,桶之间互不影响。这个改动只多了三行(用 dict 键存实例),但可用性提升比熔断器本身还大。

如果你的调用是走 OpenAI 兼容协议的,这件事做起来很省——同一份客户端代码,换个 model 字符串就是一个新桶,不用为每个厂商写一套适配。我们这边用的是词元云,一个密钥管着几个模型,横评和切桶都在同一个客户端里;不过你直接用各家的官方接口也完全能跑,熔断逻辑是一样的。

七、要不要上现成的

有现成的库,pybreaker、tenacity 都行。我最后自己写,原因是三个阈值的语义要按大模型调用的特点调(窗口用次数不用时间、失败判据要能排除内容审核拦截),现成库把这层封得比较死。

如果你只是想要个能跑的,用现成的完全不丢人。这 60 行真正值钱的不是代码,是那三个数字背后的判断。

本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。

相关 AI 工具

相关免费羊毛

同源资讯

阅读原文(juejin.cn)

标题、摘要与正文均来自该信源的公开页面,版权归原作者所有;本站自动聚合与翻译,仅作出处跳转。

← 返回 AI 资讯列表AI工具图鉴首页

返回 AI图鉴首页 · 提交工具 · 商务合作 · 关于本站 · 600 款 AI 工具分类导航 · 每日 AI 热门项目与动态