Claude Haiku 5.5 调用总是被限流 / 该先降并发还是先加重试?

文章导读
遇到 Claude Haiku 5.5 被限流,先别急着在代码里改某一个数字。批量调用场景下限流的直接原因,通常是短时间内的并发请求量超过了账号或接口当前允许的额度,而这个量往往被两件事共同放大:调度侧的并发数,以及失败后立刻重试带来的二次请求。判断顺序一般是先看日志做归因,再决定动作。如果重试既没有上限也没有退避,先压并发只是把问题往后推;反过来,如果失败集中在少数几分钟、其余时间很平稳,压并发
📋 目录
  1. Ⅰ 在日志里统计限流出现的时间分布
  2. Ⅱ 区分瞬时峰值与配额耗尽
  3. Ⅲ 给重试加退避、上限和抖动
  4. Ⅳ 压并发做对照测试
  5. Ⅴ 把稳定参数写回调度配置
A A

遇到 Claude Haiku 5.5 被限流,先别急着在代码里改某一个数字。批量调用场景下限流的直接原因,通常是短时间内的并发请求量超过了账号或接口当前允许的额度,而这个量往往被两件事共同放大:调度侧的并发数,以及失败后立刻重试带来的二次请求。判断顺序一般是先看日志做归因,再决定动作。如果重试既没有上限也没有退避,先压并发只是把问题往后推;反过来,如果失败集中在少数几分钟、其余时间很平稳,压并发会白白牺牲吞吐,改重试策略更对症。下面按“看分布、分类型、补重试骨架、压并发对照、固化配置”的顺序给可执行的做法。

限流先归因再动手:按分钟统计成功与失败次数,看失败是均匀铺开还是集中在少数几分钟。集中在少数时段、之后能自行恢复的,多为瞬时峰值,优先给重试加退避、上限和抖动;整个观察窗口内持续失败、长时间不恢复的,更像配额耗尽,优先把并发降到稳定区间。两种情况都建议一次只改一个变量,改完用同一套日志指标再对比,避免把重试造成的请求放大误当成解决方案。

在日志里统计限流出现的时间分布

这一步的目的不是算总量,而是看清限流的时间形状。前提是每次调用都在日志里落一行记录,至少包含时间戳、状态码或错误类型、本次耗时。如果现在的日志只有错误行、没有成功行,先补上成功行再统计,否则算不出失败比例。

下面这段脚本按分钟聚合成功与失败次数,用标准库即可,替换日志路径和字段格式就能跑:

import re, collections

# 假设每行形如: 2025-01-01T10:00:03Z status=429 cost=812ms
pat = re.compile(r"^(?P<ts>\S+).*status=(?P<code>\d+)")
buckets = collections.Counter()

for line in open("call.log", encoding="utf-8"):
    m = pat.search(line)
    if not m:
        continue
    minute = m.group("ts")[:16]          # 截到分钟
    code = m.group("code")
    if code == "429":
        buckets[(minute, "fail")] += 1
    else:
        buckets[(minute, "ok")] += 1

for (minute, kind), n in sorted(buckets.items()):
    print(minute, kind, n)

跑完看两件事:失败是几乎每个分钟都有、数量接近,还是只在某几分钟堆起来;以及失败分钟之间是否夹着大量成功分钟。如果日志量很大,也可以先用 grep -c 'status=429' call.log 确认总量,再决定要不要上聚合脚本。这一步只做观察,不要顺手改并发参数,否则后面就分不清是哪个动作起了作用。

Claude Haiku 5.5 调用总是被限流 / 该先降并发还是先加重试?

区分瞬时峰值与配额耗尽

同样是 429,峰值型和配额型的处理方向相反。峰值型说明额度在时间轴上有富余,只是被短时冲击打满,恢复后能继续跑;配额型说明在当前的观察窗口里,总量本身已经贴近上限,压住瞬时峰值也没用。判断依据是失败比例随时间的变化和恢复时间,可以按下面这张表对照:

观察项峰值型特征配额型特征先动的动作
失败在时间轴上的分布集中在少数几分钟,成簇出现各分钟都有,比例大致稳定峰值型改重试,配额型降并发
窗口内失败比例随并发变化降并发后明显回落降并发后改善有限用同一窗口做对照
恢复时间停一会儿再发,能自行恢复停一会儿仍持续失败持续失败时不要靠重试硬顶
重试效果退避后重试成功率较高退避后重试仍大量失败重试无效就回到降并发或排期

表里的“明显”“有限”需要结合自己环境的窗口长度和调用量来判断,建议固定一个观察窗口(例如最近若干分钟或最近一批任务),每次只改一个变量再重跑同一批数据。

给重试加退避、上限和抖动

重试的作用是吸收偶发失败,不是提升额度。如果失败后立刻重发,重试本身就会把请求量叠加到峰值上,限流只会更严重。通用骨架要满足三点:指数退避、最大次数上限、随机抖动,并且只对可重试的错误类型生效。

Claude Haiku 5.5 调用总是被限流 / 该先降并发还是先加重试?
import random, time

RETRYABLE = {429, 500, 502, 503, 504}

class ApiError(Exception):
    def __init__(self, status):
        self.status = status

def call_with_retry(fn, max_attempts=4, base=1.0, cap=15.0):
    for attempt in range(max_attempts):
        try:
            return fn()
        except ApiError as e:
            if e.status not in RETRYABLE or attempt == max_attempts - 1:
                raise
            delay = min(cap, base * (2 ** attempt))
            delay = delay * (0.5 + random.random())   # 抖动区间 0.5x ~ 1.5x
            time.sleep(delay)

几个容易踩的点:最大次数是上限而不是目标,不要为了“跑完”把它调得很高;抖动是让多个 worker 不要同一时刻一起重发,缺了抖动,退避在并发场景下效果会打折;重试的总耗时最好和单次调用的超时、整批任务的时间预算对齐,否则任务会在重试里悄悄拖长。不同平台对错误码的划分不同,哪些状态码算可重试,需要结合自己的接口返回确认后再填进集合。

压并发做对照测试

并发不要凭感觉调,做几组对照。选一组从小到大的取值,例如 1、2、4、8、16,每组跑固定请求数或固定时长,保持请求内容、超时和重试配置不变,只改并发这一个变量。记录方式建议用一张固定字段的表,跑完填进去:

Claude Haiku 5.5 调用总是被限流 / 该先降并发还是先加重试?
并发请求数成功数被限流数成功率记录到的恢复情况
1待填待填待填待填待填
2待填待填待填待填待填
4待填待填待填待填待填
8待填待填待填待填待填

每组要看的核心指标是成功率,其次是被限流次数在整批任务中的位置分布:是从头到尾均匀出现,还是越靠后越多。同时记录该组是否出现明显的恢复等待,这能帮你判断这一档并发是不是已经顶到上限。取“成功率能接受、再往上加就明显恶化”的那一档往下留一点余量,作为候选配置,不要贴着临界值用。

把稳定参数写回调度配置

对照测试得到的并发值、超时和重试参数,要落到调度配置文件里,而不是散在代码常量中。下面是一份通用示例,字段名按自己项目替换,重点是结构:并发、超时、重试、排队策略放在一起,改的时候能一眼看到全貌。

client:
  endpoint: "https://<your-endpoint>"
  model: "<model-id>"
  timeout_seconds: 30
  max_concurrency: 4          # 来自对照测试的稳定档位
  retry:
    max_attempts: 4
    base_delay_seconds: 1
    max_delay_seconds: 15
    jitter: true
    retry_on: [429, 500, 502, 503, 504]
  queue:
    mode: "bounded"
    max_pending: 200          # 队列满了先拒绝,不要无限堆积

上线之后看这几类指标:每分钟成功数与限流数、限流在批次中的位置、重试次数的分布(是否少数请求重试很多次)、重试后成功与最终失败的比例、单次调用耗时的高位值,以及队列是否有持续堆积。如果限流比例仍偏高,先把 max_concurrency 降一档再观察,不要同时改并发和重试次数,否则无法判断是哪一项起作用。队列设为有界并配合拒绝策略,比让它无限堆积更安全,能避免任务在内存里越积越多却始终打不出去。