Taylent Labs
返回博客列表
LLM并发控制系统设计队列

LLM 应用的并发控制与队列设计:从令牌桶到优先级调度

深入探讨 LLM 应用中的并发控制策略,涵盖令牌桶、漏桶算法、优先级队列设计及实战中的权衡取舍

为什么 LLM 应用需要专门的并发控制

LLM API 的计费模型和响应特性与传统 HTTP 服务截然不同。上游厂商通常按 RPM(每分钟请求数)和 TPM(每分钟令牌数)双重限流,单次请求耗时从几百毫秒到几十秒不等,且流式响应让连接长时间占用。如果直接透传用户请求,很容易在高峰期触发 429 限流、让低优先级任务挤占关键业务、或因重试风暴耗尽配额。

一个设计良好的并发控制层应当实现三个目标:平滑地贴近但不超过上游限额、让高价值请求优先得到资源、在过载时优雅降级而非全盘崩溃。

令牌桶与漏桶:选哪个

令牌桶(Token Bucket)允许短时突发:桶内积累令牌,请求到达时消耗令牌,桶空则等待。适合处理用户交互场景——用户在短时间内连续提问时可以立即响应,而不是被强制匀速排队。

漏桶(Leaky Bucket)强制匀速输出:请求进入桶后以固定速率流出。适合后台批处理任务,能更精确地控制对上游的压力,避免因突发流量导致的限流。

实战中的选择取决于业务特征。假设你在构建一个客服助手,用户对话属于交互式场景,令牌桶能提供更好的体验;如果同时运行文档批量摘要任务,可以用漏桶单独管理这部分流量,防止它挤占对话资源。两者并非互斥,分场景组合使用是常见做法。

双维度限流的工程实现

上游厂商的 RPM 和 TPM 限制需要同时满足。一个常见错误是只控制请求数——假设限额是 500 RPM / 100K TPM,如果每个请求平均消耗 300 tokens,理论上 333 个请求就会触及 TPM 上限,但单纯的 RPM 控制会让你误以为还有 167 个请求的余量。

工程上可以这样处理:

class DualRateLimiter:
    def __init__(self, rpm_limit, tpm_limit):
        self.rpm_bucket = TokenBucket(rpm_limit, refill_rate=rpm_limit/60)
        self.tpm_bucket = TokenBucket(tpm_limit, refill_rate=tpm_limit/60)
    
    async def acquire(self, estimated_tokens):
        # 同时消耗 RPM 和 TPM 配额
        await self.rpm_bucket.consume(1)
        await self.tpm_bucket.consume(estimated_tokens)

关键在于 estimated_tokens 的预估。对于已知 prompt 的场景(如固定模板),可以预先计算;对于用户输入,可以用字符数除以 4 作为粗略估算(英文平均 1 token ≈ 4 字符),再加 20% 的安全边际。流式响应的输出 tokens 无法提前知道,需要在响应结束后异步补偿——如果实际消耗超出预估,从桶中追加扣除;如果低于预估,可以选择退还(但会增加复杂度,需权衡)。

优先级队列的设计权衡

当请求量超过限流器容量时,需要队列缓冲。简单的 FIFO 队列会让所有请求一视同仁,但实际业务中优先级差异明显:付费用户的对话、生产环境的 API 调用、内部监控任务的优先级显然不同。

优先级队列的核心是权重分配。可以用多级队列(如高/中/低三档)配合加权轮询:

async def schedule():
    while True:
        # 按 3:2:1 的比例从高/中/低优先级队列取任务
        for _ in range(3):
            if high_queue:
                yield await high_queue.get()
        for _ in range(2):
            if mid_queue:
                yield await mid_queue.get()
        if low_queue:
            yield await low_queue.get()

这种设计保证高优先级任务能更快得到处理,同时不会让低优先级任务完全饿死。权重比例需要根据实际业务调整——假设你的系统中 80% 是付费用户请求、15% 是免费用户、5% 是内部任务,可以设置为 8:1.5:0.5 的比例。

另一个实战问题是队列长度限制。无限队列会在过载时耗尽内存,有限队列则需要决定拒绝策略:是拒绝新请求(保护已排队任务),还是淘汰队尾(让新请求有机会)?前者适合保证 SLA,后者适合实时性要求高的场景。

过载保护与降级策略

即使有队列缓冲,极端情况下仍可能过载。此时需要主动降级:

  • 快速失败:检测到队列深度超过阈值(如等待时间 > 30 秒)时,直接返回 503 而非让请求无限等待
  • 模型降级:将部分请求路由到轻量档模型,牺牲输出质量换取吞吐量
  • 功能降级:关闭非核心功能(如对话历史摘要、多轮上下文),减少单次请求的 token 消耗

降级决策可以基于队列深度、限流器余量、上游错误率等多个指标。假设你的系统在队列深度 > 100 时触发一级降级(禁用历史摘要)、> 200 时触发二级降级(切换到轻量档模型)、> 500 时快速失败,这样的阶梯式策略能在保证核心可用性的前提下最大化吞吐。

监控与动态调整

静态配置的限流参数很难应对流量波动。建议采集以下指标:

  • 限流器的令牌消耗速率与剩余量
  • 队列深度与平均等待时间
  • 上游 API 的 429 错误率与响应延迟
  • 不同优先级任务的处理时长分布

当观察到 429 错误率持续 > 1% 时,说明限流参数设置过于激进,需要降低 10-20%;当令牌桶长期处于满载状态(利用率 < 60%)时,说明配置过于保守,可以适当提高限额以充分利用资源。


LLM 应用的并发控制本质上是在用户体验、成本、系统稳定性之间寻找平衡点。Taylent Labs 在为客户构建 AI 应用时,会根据具体业务场景设计限流与队列策略,并通过 Taylent AI 网关提供开箱即用的并发控制能力。如果你的团队正在面临类似挑战,欢迎联系我们探讨解决方案。