大模型 API 就像一座热门景区的水库:上游一条小溪以稳定速度注水,水库容量有限,下游农田和居民随时来取水。如果所有人同时涌来取水,水库瞬间见底,真正需要水的用户反而没水用。API 限流就是这个水库的管理员——平时任你取水,允许你偶尔多取一些,但长期来看,取水总量不能超过小溪的注水速度。令牌桶(Token Bucket)就是这套管理规则中最经典的一种实现。本文用“水库”这个比喻贯穿始终,带你从零看懂大模型 API 网关如何用令牌桶守住每一滴算力、每一分预算。
一句话:限流是 LLM API 的“水坝”,掌握令牌桶设计是 AI 运维与安全工程师保成本、保稳定的基本功。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
滥用风险与目标
水库管理员最怕的不是正常用水,而是有人开着水泵连夜抽水。对大模型 API 来说,恶意脚本、被攻破的账号、批量爬虫就是那台“水泵”——它们不关心下游还有没有水,只想趁管理员不注意把水库抽干。这就是接口被滥用的真实写照。
接口滥用的形态主要有三种:脚本高频轮询套取模型回复、多账号共享接口批量抓取数据、利用免费额度反复重试。传统 Web 服务被刷最多浪费一点带宽,但大模型 API 每次调用都在烧 GPU 和 token 费用,滥用直接转化为账单上的数字。
滥用的后果可以量化:单次请求平均消耗 token 成本乘以请求次数,就是一次攻击造成的经济损失。更糟的是,少数人的滥用会拖垮整座水库,让正常用户也拿不到水。因此限流的目标非常明确——保护后端资源、控制成本预算、保证租户间公平。
明白了“为什么要限流”,就像管理员终于看清了水泵的威胁。接下来要回答的问题是:用什么机制管住这些水泵?答案就是本文的主角——令牌桶。
---
- 请求次数单位时间内到达网关的调用总量,滥用者靠不断放大它来刷成本。
- 平均 token 成本每次请求平均消耗的 token 数换算成的费用,与模型规模和输入长度相关。
- 相乘得总成本两个因子相乘即滥用造成的费用损失,限流的目标就是把它们压回预算线以内。
令牌桶原理
假设水库管理员的规则很简单:上游小溪每秒注入 0.6 立方米水,水库最多存 10 立方米。任何人来取水,先看库里有没有水,有就取走 1 立方米,没有就请回。这就是令牌桶的全部直觉——令牌就是水,请求就是取水人。
把“水”翻译成工程技术语言,令牌桶是一个带容量的计数器:系统按固定速率 r 生成令牌放入桶中,桶满则新令牌丢弃;每个请求到来时必须消耗一个令牌才能通过,没有令牌就直接拒绝。桶的容量 C 决定了最多能“囤”多少令牌,也就是短期突发允许有多大。
这个机制的妙处在于“限均值、放突发”:从长期看,放行的平均速率约等于注入速率 r,水流不会超过小溪的补给能力;但从短期看,只要桶里有存货,一批请求可以同时冲进来,把积攒的令牌一口气花光。对 LLM API 来说,这正好匹配真实业务——平时请求平稳,促销、热点事件时允许短时并发峰值。
令牌桶用“水龙头 + 水桶”两个零件就实现了限均值、放突发,简单而优雅。但它是唯一的方案吗?当然不是,要理解它的优势,得先看看其他常见的水管工们是怎么干活的。
---
- 当前令牌数记录桶里此刻积攒的令牌数量,它是限流判断的“余额”。
- 注入增量在 Δt 时间内按速率 r 生成新令牌,等价于水龙头匀速注水。
- 取最小值封顶桶有物理上限 C,超过上限的令牌溢出丢弃,防止长期囤积造成巨大突发。
算法对比
水库管理员还有三种备选工具。第一种是“计数器法”:每分钟记一个数,超过阈值就拒绝,像在闸门上挂一块白板记人次;第二种是“滑动窗口”:把最近一分钟切成小格,滚动统计,比白板更平滑;第三种是“漏桶”:水进来先落进一个带小孔的漏斗,无论上游多猛,下游永远匀速滴水。
计数器法的缺点是临界突刺:假设阈值 60 次/分钟,攻击者可以在第 59 秒打 60 次、第 61 秒再打 60 次,两秒内打了 120 次,白板却只记了两次"60"。滑动窗口通过细分时间片解决这个漏洞,但实现更复杂。漏桶则走向另一个极端——它完全抹平突发,即使桶里积了水也必须是涓涓细流,对需要短时爆发的业务(比如批量推理任务)不够友好。
令牌桶和漏桶都是“桶”,但性格完全相反:漏桶是市政供水,恒压恒流;令牌桶是蓄水池,平时蓄水、关键时刻可以开闸放水。选型建议很简单——下游服务扛不住任何波动就选漏桶;业务有真实突发需求、又想约束长期均值,就选令牌桶;需要精细控制时间窗口边界的,可以在令牌桶之上叠加滑动窗口统计。
对比完四种算法,令牌桶在“简单、允许突发、均值可控”三个维度上取得了最佳平衡。但光会用还不够,参数怎么定才是真正的工程学问。
---
- 窗口区间在时间轴上取最近 ω 秒,这个区间随时间连续前移,不像固定分钟那样有硬边界。
- 请求计数统计该窗口内到达的请求总数,代表最近的真实压力。
- 平均速率请求总数除以窗口长度,得到平滑后的请求频率,用于评估是否逼近限流线。
参数设计
水库管理员要回答两个核心问题:水库修多大?小溪注入多快?修大了,短期能扛住洪峰,但长期注水不足时反而给人“有大量可用水”的错觉;注快了,下游又可能承受不住。令牌桶的参数设计,就是在这两个问题之间找平衡。
第一个参数是桶容量 C:它决定了一次性允许的突发上限。一个估算思路是,统计业务最坏情况下的峰值并发请求数,再乘上一个容忍系数。比如线上观测到瞬时峰值是 20 个并发,C 可以设在 20~30。第二个参数是补充速率 r:它决定了长期平均吞吐,通常按“预计正常流量 × 安全余量”来定,比如正常均值 50 请求/秒,r 可以设在 60~70,留出 20% 余量。
两个参数配合后,可以计算一个关键指标——突发窗口,即桶从满到空能撑多久。它告诉我们:如果攻击者以远高于平均速率的流量冲击,系统能硬扛多长时间才触发拒绝。这个数字对容量规划和告警阈值设计非常重要。
参数设计本质上是“钱怎么分”:容量是备用金,速率是月薪,备用金越厚越能扛短期冲击,但长期消费水平最终还是由月薪决定。设计好参数后,还有一个问题:是所有人都共用一座水库,还是每人分一座?这就进入多维度限流的讨论。
---
- 桶容量 C满载的令牌总量,是突发时可以透支的“储备水量”。
- 净消耗速率请求消耗速率减去令牌注入速率,代表每秒净花掉多少储备。
- 突发持续时长储备量除以净消耗速率,得到从满桶到耗尽的时间。
多维度限流
一座水库如果只在大坝上装一个总水表,管理员根本分不清是哪家农田在偷水。聪明的做法是给每户装分水表:按用户限流、按 IP 限流、按模型限流,层层设卡。这样任何一方想搞事,只会在自己那一层被拦住,影响不到别人。
按用户限流是最基础的维度,用 API Key 或账号作为分桶键,防止单用户脚本循环调用;按 IP 限流针对的是分布式爬虫——单个用户可能换号绕过,但出口 IP 资源有限;按模型限流则是给不同的模型(比如最贵的 GPT-4 级别模型)单独设配额,保护稀缺 GPU 资源。
组合策略的核心规则是“短板效应”:最终放行判断取各维度限流结果的最小值。任何一条线先到顶,就按最紧的那条来拦。比如用户维度允许 100 请求/分钟,但该用户的出口 IP 只允许 30,那就按 30 执行。这种设计让攻击者除非同时突破所有维度,否则总会撞上某一道墙。
多维度限流像小区的三道门禁:单元门、小区门、地下车库门,各管一段。规则定好了,接下来的问题是在哪里落地这些水表——这就是工程实现要解决的事。
---
- 用户维度阈值按调用方账号设定的配额,防止单人独占资源。
- IP 维度阈值按来源 IP 设定的配额,挡住分布式爬虫和代理池。
- 模型维度阈值按目标模型聚合的配额,保护高成本模型不被拖垮。
- 取最小值取三个维度的最小值,形成“一票否决”的短板效应。
工程实现
水库管理员的规则再合理,也得有人站在大坝上执行。在大模型服务架构中,这个“大坝”通常是 API 网关。网关是所有请求的必经之路,天然适合做限流前置校验——请求还没进入模型服务,先被网关拦住检查令牌。
单机部署用内存计数器就够了,但生产环境往往是多副本网关,每一份令牌桶必须共享状态,否则攻击者轮流打不同的网关副本就能绕过限制。这时 Redis 登场:桶的容量、当前令牌数、上次补充时间都存进 Redis。每次请求过来,网关执行一个 Lua 脚本完成“补充令牌 + 扣减令牌 + 判断是否放行”,三步一气呵成。
为什么一定要用 Lua 脚本?因为 Redis 是单线程执行脚本,整个检查过程原子完成,不会出现两个并发请求同时读到同一个令牌数、都认为自己拿到了最后一个令牌的竞态问题。这是分布式限流的灵魂。落地时还需要注意降级响应:当 Redis 故障或超时,网关不能无限等待,常见策略是“失败放行但记日志”或“本地降级为简易计数器”,并返回 429 时带上 Retry-After 头,引导客户端退避重试。
工程实现把令牌桶从一个数学模型变成了生产环境里真实运转的闸门。但上线只是开始,闸门的松紧还要根据水流动态调整——这就要靠最后一节:调优与监控。
---
- 存储值Redis 中上次请求更新后保存的令牌数,是桶的“账面余额”。
- 时间差当前时间与上次更新时间的差值,代表这段时间“漏补了多久”。
- 补齐并封顶按速率把欠下的令牌一次性补上,同时用容量 C 封顶。
调优与监控
水库管理员最关心的仪表盘上有三个数字:放水量、拒绝量、水库水位。对应到 API 限流,就是实际通过速率、429 拒绝率、令牌消耗曲线。没有这些指标,调参就像闭着眼拧水龙头,全凭感觉。
核心指标是限流利用率:实际请求速率占限流阈值的百分比。利用率长期低于 30%,说明限流阈值设得太宽,资源白白闲置;长期高于 90%,说明业务已经贴着天花板运行,需要扩容或调参。告警策略建议分级:利用率超过 70% 通知值班、超过 85% 触发预警、超过 95% 立即响应,避免告警疲劳。
动态调参要结合业务节奏:工作日白天调高速率,夜间和节假日调低;大促前临时增大桶容量,大促后恢复。容量规划则依据历史利用率数据推算——如果利用率月环比持续增长,预示着需要提升限流阈值、扩容后端模型服务,而不是一味地压紧闸门。
至此,一座完整的“限流水库”已经建成:目标清晰、原理扎实、算法选对、参数可算、维度齐全、落地可靠、监控闭环。下次有人问起“为什么大模型 API 要限流”,你可以用这座水库,把从风险到实现的整条链路讲得明明白白。
---
- 实际速率单位时间内真实放行的请求数,从网关日志聚合而来。
- 限流阈值令牌桶设定的额定速率上限。
- 利用率百分比两者相除并乘以 100%,得到资源压力指数。
AI 应用场景
场景一:LLM API 网关的共享限流
阶段:模型部署与运维 / 安全防护
解决的问题:传统无保护网关一旦暴露,恶意脚本或失误代码可能以每秒上百次的频率调用模型,导致 GPU 资源耗尽、账单飙升。AI 网关用令牌桶对每个 API Key 做分布式限流,既允许正常业务短时突发,又把长期均值锁死在预算线内。
具体案例:某 SaaS 公司提供 GPT 代理服务,为每个付费租户设置“60 请求/分钟、突发 5”的令牌桶配额,用 Redis + Lua 在网关层统一执行。某租户的自动化脚本因死循环疯狂调用,10 秒后即被 429 拦截,其他租户完全不受影响。
场景二:多租户成本配额管理
阶段:成本治理与资源规划
解决的问题:大模型按 token 计费,单纯限 QPS 无法防止“单次请求塞超长提示词”造成的成本爆炸。令牌桶可以按 token 消耗速率设计注入速率,把成本转化为“每秒烧多少钱”的物理量来控制。
具体案例:企业内部 Copilot 平台给每个部门分配“每小时 500 万 token”的配额,令牌桶按速率注入,桶满即停。市场部在做大批量文案生成时短时冲高,随后被平滑约束,财务月末对账时成本偏差小于 5%。
场景三:红蓝对抗中的限流绕过检测
阶段:安全测试与防御验证
解决的问题:蓝队(攻击模拟方)常用分布式 IP 池、多账号轮换绕过单维限流。红队(防御方)需要设计多维令牌桶策略,并通过对 429 分布、令牌消耗曲线的分析反推攻击规模。
具体案例:在安全演练中,蓝队用 200 个代理 IP 轮换调用 AI 接口,单 IP 维度完全无法识别。红队叠加“用户维度 + 模型维度”后,发现同一模型令牌消耗异常集中,定位到攻击团伙的共享账号池,成功阻断。
Python 演示
import threadingimport time class TokenBucket: """令牌桶:固定速率注入令牌,容量有限,允许短时突发。""" def __init__(self, capacity, refill_rate): self.capacity = capacity self.refill_rate = refill_rate self.tokens = capacity self.last_refill = time.monotonic() self.lock = threading.Lock() def _refill(self): now = time.monotonic() delta = now - self.last_refill self.tokens = min(self.capacity, self.tokens + delta * self.refill_rate) self.last_refill = now def try_acquire(self, need=1): with self.lock: self._refill() if self.tokens >= need: self.tokens -= need return True return False class LLMGateway: """模拟 LLM API 网关:每个 API Key 对应一个令牌桶。""" def __init__(self, rpm, burst): self.rate_limiters = {} self.rpm = rpm self.burst = burst def _bucket(self, api_key): if api_key not in self.rate_limiters: self.rate_limiters[api_key] = TokenBucket( capacity=self.burst, refill_rate=self.rpm / 60.0, ) return self.rate_limiters[api_key] def call_llm(self, api_key, prompt): bucket = self._bucket(api_key) if bucket.try_acquire(): return "ok: %s" % prompt[:12] raise RuntimeError("429 Too Many Requests") def stats(self, api_key): b = self.rate_limiters.get(api_key) return round(b.tokens, 2) if b else None if __name__ == "__main__": gw = LLMGateway(rpm=120, burst=10) for i in range(300): try: out = gw.call_llm("user-a", "prompt-%d" % i) except RuntimeError as exc: out = exc if i % 50 == 0: print(i, out, "剩余令牌:", gw.stats("user-a")) print("最终剩余令牌:", gw.stats("user-a"))
self.tokens = min(self.capacity, self.tokens + delta * self.refill_rate)惰性补充:只在请求到来时按时间差补齐令牌,省去后台定时任务。time.monotonic()用单调时钟而非time.time(),避免系统改时间导致限流失效。with self.lock:加锁保证多线程环境下扣减令牌的原子性,防止两个线程同时取走最后一个令牌。refill_rate=self.rpm / 60.0把“每分钟请求数”换算成“每秒令牌数”,注入速率与业务口径对齐。raise RuntimeError("429 Too Many Requests")模拟网关返回 429,调用方需据此做退避重试。if api_key not in self.rate_limiters:按 API Key 动态创建独立桶,实现多租户隔离。
常见误区
误区两个算法都带“桶”字,而且都是用来限流的,应该差不多吧?反正效果都是把请求挡住。
正解两者性格相反。漏桶像市政供水,无论上游来多少水,下游永远匀速滴答,完全抹平突发;令牌桶像蓄水池,平时囤水、关键时刻开闸放水。令牌桶允许短时突发,漏桶则强制匀速。选错算法,要么扛不住突发被骂“限得太死”,要么突发过大打垮下游。
为何常见:都带“桶”字且在限流场景出现,初学者容易把二者归为一类,忽略了“是否允许突发”这个本质差异。
误区把 C 设成很大,比如 10000,这样谁来了都能放行,用户体验最好,限流跟没限一样宽松。
正解桶容量只决定“短期能囤多少”,长期平均速率由注入速率 r 决定。容量设得过大,等于给了攻击者一个巨大的蓄水池,他可以在低峰期攒令牌、高峰期一次打光,造成远超预期的突发尖峰,直接打垮下游模型服务。容量应该按真实的业务峰值估算,而不是贪大。
为何常见:直觉上觉得“越大的桶越能装水”,忽略了突发本身也是一种风险,容量是把双刃剑。
误区限流不就是数每秒请求数吗?只要每秒放行数量不超过阈值,就安全了。
正解大模型 API 按 token 计费且成本随输入长度变化。攻击者可以把请求数压得很低,但每次塞进超长提示词,一样烧掉巨额费用。正确做法是把令牌桶的“令牌”抽象成 token 消耗量或成本金额,按“每秒烧多少预算”来限流,才能真正控制成本。
为何常见:传统 Web 限流习惯只关注 QPS,迁移到大模型场景时没有意识到计费单位从“次数”变成了"token"。
误区把每个 IP 的请求数限住,黑客换个 IP 不就抓瞎了吗?IP 维度足够防滥用。
正解攻击者可以用代理池、云函数、分布式僵尸网络轻松换 IP,单 IP 维度形同虚设。必须组合用户维度(API Key)、IP 维度、模型维度,取三者最小值为最终阈值。任何单一维度都能被绕过,多维组合才能形成纵深防御。
为何常见:IP 是最直观的标识,初学者容易高估它的可靠性,忽视攻击者拥有海量 IP 资源的事实。
误区令牌不够就返回 429 拒绝,请求丢了就丢了,客户端自然会重试。
正解直接丢弃会导致客户端雪崩式重试,反而加剧压力。成熟的限流降级要配合排队等待、返回缓存结果、熔断降级,并在 429 响应中带上 Retry-After 头,告诉客户端“多少秒后再来”。把“硬拒绝”变成“软引导”,系统才能优雅地度过峰值。
为何常见:网关默认行为就是拦截丢弃,开发者容易忽略限流之后“客户端会怎么反应”这个闭环。
面试题
Q1. 令牌桶算法的两个核心参数是什么?
- A.容量与补充速率← 正确答案
- B.阈值与窗口大小
- C.超时与重试次数
- D.并发与队列深度
答案:A。令牌桶的核心是桶容量 C(决定突发上限)和补充速率 r(决定长期均值),二者配合实现“限均值、放突发”。窗口大小是滑动窗口的概念,超时重试是客户端策略,并发队列是连接池概念,均不属于令牌桶的本体参数。
Q2. 与漏桶算法相比,令牌桶算法的关键优势是什么?
- A.完全杜绝突发流量
- B.允许短时突发请求← 正确答案
- C.不需要维护定时器
- D.实现代码更加简单
答案:B。漏桶强制匀速输出,彻底抹平突发;令牌桶则允许桶内积攒的令牌被一次性用完,支持短时突发。这正是大模型业务需要的特性。令牌桶同样需要时间机制来补充令牌,“不需要定时器”和“更简单”都不是它的本质优势。
Q3. 某桶容量 C=100、补充速率 r=50 请求/秒,桶当前为空,连续 3 秒每秒到达 80 个请求,这 3 秒内总共放行约多少请求?
- A.约 150 个← 正确答案
- B.约 240 个
- C.约 100 个
- D.约 300 个
答案:A。每秒注入 50 个令牌,需求 80 个,所以每秒放行 50 个、拒绝 30 个,3 秒共放行 150 个。240 是“80 乘以 3”的误算,100 是只算容量不算补充,300 是假设全部放行。令牌桶的长期放行量由补充速率决定而非需求速率。
Q4. 用 Redis 实现分布式令牌桶时,为什么不建议用“先 GET 再 SET”的方式扣减令牌?
- A.网络延迟过高
- B.存在竞态条件← 正确答案
- C.数据会莫名丢失
- D.无法设置过期时间
答案:B。两个并发请求可能同时 GET 到相同的令牌数,各自判断“令牌够”,随后都 SET 扣减后的值,导致超发。Redis 单线程执行 Lua 脚本时整个“补齐+扣减+判断”原子完成,杜绝竞态。延迟高可用管道缓解,数据丢失与过期问题都可以用持久化策略解决,均不是根因。
Q5. 令牌桶中令牌耗尽时,API 网关通常返回哪个 HTTP 状态码?
- A.200 正常结果
- B.302 临时跳转
- C.429 请求过多← 正确答案
- D.503 服务不可用
答案:C。429 Too Many Requests 专门用于表示“客户端在给定时间内发送了太多请求”,是限流的标准响应码,通常还会带上 Retry-After 头。200 表示成功,302 是重定向,503 表示服务端暂时不可用,语义都不匹配。
Q6. 某桶容量 C=10、补充速率 r=5 请求/秒,桶已满,突然以 20 请求/秒的速率持续请求,完全放行能持续多少秒?
- A.0.5 秒
- B.2 秒
- C.0.67 秒← 正确答案
- D.10 秒
答案:C。按突发窗口公式 T = C / (R_peak − R_avg) = 10 / (20 − 5) = 0.67 秒。0.5 是误把分母当成 20,2 是误把分母当成 5,10 是直接拿容量当时间。这个公式说明:突发能撑多久取决于储备量与净消耗速率的比值。
Q7. 多维度限流组合策略中,最终放行判断通常采用什么规则?
- A.取各维度最大值
- B.取各维度最小值← 正确答案
- C.所有维度求和
- D.随机选择一个维度
答案:B。多维度限流遵循“短板效应”,取用户、IP、模型等各维度阈值的最小值,任何一个维度超限就拒绝。取最大值等于最宽的那条线说了算,求和会额度膨胀,随机选择则完全失去确定性,三者都会破坏限流的一致性。
Q8. 下列哪些属于典型的大模型 API 滥用行为?
- A.高频轮询套取回复← 正确答案
- B.多账号共享接口← 正确答案
- C.批量抓取训练数据← 正确答案
- D.定时更新软件版本
答案:ABC。高频轮询、多账号共享、批量抓取数据都会造成资源消耗与数据泄露风险,是典型的滥用形态。定时更新软件版本是正常运维行为,不消耗模型资源,不属于 API 滥用。
Q9. 限流触发后,合理的降级响应策略包括哪些?
- A.排队等待处理← 正确答案
- B.返回缓存结果← 正确答案
- C.客户端退避重试← 正确答案
- D.无限扩大配额
答案:ABC。排队把瞬时压力摊平,缓存减轻后端负担,Retry-After 引导客户端退避,三者都是优雅降级。无限扩大配额等于取消限流,违背初衷,会加剧资源耗尽风险。好的限流不是硬丢请求,而是“削峰填谷、软性引导”。
Q10. 用 Redis 实现生产级令牌桶的常用手段有哪些?
- A.Lua 脚本保证原子性← 正确答案
- B.时间戳惰性补充令牌← 正确答案
- C.多节点分摊独立配额← 正确答案
- D.客户端本地随意修改变量
答案:ABC。Lua 脚本在 Redis 单线程模型下原子执行“补齐+扣减+判断”,惰性补充免去定时任务,多节点分摊配额实现水平扩展,三者都是标准做法。客户端本地修改变量会绕过服务端校验,根本不算分布式限流。