跳转至

LLM 网关、Key Pool 与主动限流技术方案

  • 版本:v1.0
  • 日期:2026-05-19
  • 作者:Codex
  • 适用项目:Tender_Documents

1. 背景

当前系统有多个会调用 LLM 的入口:

  1. 正文生成:单章节生成、一键全文生成、章节重试。
  2. 标书解析:quick-parse、全文解析、基础信息/评分项提取。
  3. 辅助生成:目录 children、writing hint、outline skeleton。
  4. 产品库解析:产品文件解析、需求提取。
  5. 可能的对话能力:AI chat、图片理解等。

这些入口分布在 API 进程和多个 ARQ worker 中。现在后端主要使用同一个百炼 API Key;即使后续在百炼控制台创建多个 API Key,同一阿里云主账号下的多个 RAM 子账号、业务空间和 API Key 的模型调用仍按主账号维度汇总限流。

因此,同一主账号下“多 key”不能增加 TPM/RPM,也不能天然隔离免费用户、付费用户和后台任务。后端必须主动做统一调度和限流,把上游不可控的 429 转换为系统内可控排队。


2. 关键结论

  1. 百炼普通模型调用的限流按阿里云主账号维度统计,同一主账号内多 API Key 共享总 TPM/RPM。
  2. 后端仍然应该设计 key pool,但它的主要职责是凭证隔离、权限管理、归因审计、未来多供应商/BYOK 扩展,而不是在同一主账号下扩容 TPM。
  3. 真正保护上游限流的是统一 LLMGateway + Redis 全局 limiter。
  4. 用户购买的“正文生成字数”和平台 LLM TPM/RPM 是两套额度,必须分开建模。
  5. 正文生成才扣用户购买字数;解析、目录、writing hint 等不扣字数,但仍消耗平台 LLM 预算,必须进入平台限流和成本统计。

3. 目标与非目标

3.1 目标

  1. 所有 LLM 调用统一经过 LLMGateway
  2. 支持逻辑 key pool:免费池、付费池、后台池、交互池、企业 BYOK 池。
  3. 支持 Redis 全局限流:按主账号、pool、user、task_type、model 控制并发和 token 预算。
  4. 支持套餐权益:免费用户、付费用户、高级套餐、企业用户拥有不同优先级和额度。
  5. 支持正文生成字数扣减与 LLM 调用成本统计分离。
  6. 降低 429:请求先在后端排队,只有拿到额度后才调用百炼。
  7. 为未来接入多个阿里云主账号、其他模型供应商、企业 BYOK 预留结构。

3.2 非目标

  1. 不依赖同一百炼主账号下多 API Key 扩大 TPM。
  2. 不在第一期实现复杂优先队列调度器。
  3. 不强制第一期重写所有 LLM 调用点,可按风险逐步迁移。
  4. 不改变现有“正文生成按最终字数扣费”的业务规则。

4. 现有问题

4.1 分散调用

当前 LLM 调用分散在多个位置:

  1. app/core/llm_factory.py:正文写作、outline skeleton、图片理解等。
  2. app/services/streaming/suboutline_service.py:outline children。
  3. app/services/parse/parse_service.py:quick-parse。
  4. app/core/extraction/extractor.py:解析提取。

其中部分调用已有进程内 semaphore 和 429 backoff,但不能覆盖多个 API/worker 进程,也不能覆盖所有调用入口。

4.2 局部限流不足

现有配置能限制单个 worker 或单个进程:

  1. ARQ_GENERATION_MAX_JOBS
  2. ARQ_TENDER_PARSE_MAX_JOBS
  3. PARSER_EXTRACT_CONCURRENCY
  4. LLM_ACCOUNT_CHAT_CONCURRENCY

但这些不是主账号级全局限制。多个 worker、多个用户、多个任务类型叠加后,仍会同时打满百炼主账号 TPM。

4.3 业务额度与平台额度混在一起

正文生成会扣用户购买的字数;解析、目录、writing hint 不扣用户字数。但后者同样消耗 LLM token,会挤占正文生成的上游额度。

因此,不能只看用户是否有购买字数,还需要控制平台 LLM 预算。


5. 总体架构

flowchart TD
  A[API / Worker 调用 LLM] --> B[LLMGateway]
  B --> C[CredentialResolver]
  C --> D[Credential Pool]
  B --> E[QuotaProfileResolver]
  E --> F[用户套餐 / 权益]
  B --> G[Redis LLM Limiter]
  G --> H{额度是否可用}
  H -- 否 --> I[等待 / defer / cooldown]
  H -- 是 --> J[百炼 / 其他 Provider]
  J --> K[LLMUsageEvent]
  K --> L[正文成功后扣购买字数]

核心原则:

  1. 业务代码不直接创建 AsyncOpenAI
  2. 业务代码声明 task_typebilling_modeprioritymodelmax_tokens
  3. LLMGateway 负责选 key、限流、重试、cooldown、日志。

6. Key Pool 设计

6.1 Pool 不是单纯 API Key 列表

在百炼同一主账号内,多 key 不增加总 TPM。pool 的作用是逻辑资源分组和后续扩展。

建议定义以下 pool:

Pool 用途 是否扣购买字数 默认优先级
platform_paid_pool 正文生成、付费用户关键前台能力 正文生成扣
platform_free_pool 免费解析、未付费辅助能力 不扣 中低
platform_assist_pool outline children、writing hint、outline skeleton 不扣
platform_background_pool 全文后续章节、产品库解析、补偿重试 视任务而定
platform_interactive_pool AI chat、用户正在等待的轻交互 视业务而定
enterprise_byok_pool 企业用户自带 key 由企业 key 承担

第一期只有一个百炼 key 时,多个 pool 可以指向同一个 key,但 limiter 策略不同。

platform_paid_pool       -> DASHSCOPE_API_KEY
platform_free_pool       -> DASHSCOPE_API_KEY
platform_assist_pool     -> DASHSCOPE_API_KEY
platform_background_pool -> DASHSCOPE_API_KEY

6.2 每类 LLM 一个 key 是否有价值

可以配置:

key_generation
key_parse
key_assist
key_chat

价值:

  1. 权限和 IP 白名单隔离。
  2. 泄露后影响面更小。
  3. 百炼控制台归因更清楚。
  4. 便于未来替换单个场景的供应商或企业 key。

限制:

  1. 同一阿里云主账号内仍共享总 TPM/RPM。
  2. 不能把 key_generationkey_parse 理解成独立额度池。

7. 套餐与业务额度

系统需要区分两类额度:

业务额度:用户购买的正文生成字数。
平台额度:平台百炼主账号的 LLM TPM/RPM。

7.1 正文生成字数

只对正文生成类任务扣减:

  1. 单章节生成。
  2. 一键全文生成中的章节正文。
  3. 失败章节重试成功后生成的新正文。

建议规则:

  1. 生成前检查用户是否有可用字数。
  2. 生成成功并保存正文后,按最终正文字符数扣减。
  3. 失败不扣。
  4. 取消后未保存不扣;已保存则按保存内容扣。
  5. 后续可增加预冻结:生成前冻结预估字数,成功后多退少补。

7.2 免费 LLM 调用

解析、目录、writing hint 不扣购买字数,但必须受平台额度控制:

  1. 每用户限制同时进行的解析任务数量。
  2. 免费用户限制每日解析次数或页数。
  3. 付费用户拥有更高优先级,但不意味着无限使用。
  4. 后台解析和产品库解析不能挤占正文生成槽位。

8. 主动限流设计

8.1 限流维度

Redis limiter 至少包含四层:

global:provider_account:model
pool:pool_code:model
user:user_id:model
task:task_type:model

示例 key:

llm:global:aliyun_bailian_main:qwen-plus
llm:pool:platform_paid_pool:qwen-plus
llm:user:{user_id}:qwen-plus
llm:task:section_generation:qwen-plus
llm:cooldown:aliyun_bailian_main:qwen-plus

8.2 第一期限流策略

第一期优先做全局并发 + cooldown,先不做精确 TPM。

推荐配置:

LLM_GLOBAL_CONCURRENCY=2
LLM_PAID_POOL_CONCURRENCY=1
LLM_FREE_POOL_CONCURRENCY=1
LLM_ASSIST_POOL_CONCURRENCY=1
LLM_BACKGROUND_POOL_CONCURRENCY=1
LLM_USER_CONCURRENCY=1
LLM_429_COOLDOWN_BASE_SEC=20
LLM_429_COOLDOWN_MAX_SEC=180

注意:pool 并发之和可以大于 global 并发,但实际请求必须同时拿到 global 槽位和 pool 槽位。

8.3 第二期 Token Bucket

第二期增加估算 token:

estimated_prompt_tokens = ceil(len(prompt_text) / 2)
estimated_total_tokens = estimated_prompt_tokens + max_tokens

Redis 按分钟窗口记录:

llm:tpm:global:{account_group}:{model}:{yyyyMMddHHmm}
llm:tpm:pool:{pool_code}:{model}:{yyyyMMddHHmm}
llm:tpm:user:{user_id}:{model}:{yyyyMMddHHmm}

申请额度时:

  1. 如果当前窗口预计超限,等待到下个窗口或返回 defer。
  2. 如果请求已发出但百炼返回 429,设置 model/account cooldown。
  3. cooldown 期间同模型请求不再打上游。

9. 优先级设计

优先级由用户权益、任务类型和是否后台任务共同决定。

任务 免费用户 付费用户 说明
单章正文生成 75 90 正文成功后扣购买字数
一键全文当前章节 60 80 批量任务中正在执行的章节
quick-parse 45 60 不扣字数,但影响上传体验
outline children 35 50 辅助生成
writing hint 30 45 辅助生成
产品库解析 20 35 后台/准后台任务
一键全文后续章节 20 30 防止单个 run 占满资源
429 后重试 10 10 遵守 cooldown

第一期可以不实现严格优先队列,但每个请求都应带 priority,供后续调度使用。


10. 请求决策流程

1. API 或 Worker 发起 LLMRequest。
2. 根据 user_id 查询用户套餐和权益。
3. 根据 task_type 判断 billing_mode:
   - section_generation -> paid_words
   - quick_parse -> platform_free
   - outline_children -> platform_assist
   - product_parse -> platform_background
4. 选择 credential_pool。
5. 计算 priority。
6. 估算 tokens。
7. Redis limiter 申请额度:
   - global
   - pool
   - user
   - task
8. 拿到额度后调用百炼。
9. 记录 LLM usage event。
10. 如果是正文生成且成功保存正文,扣购买字数。

11. 建议数据结构

11.1 llm_credential_pools

id
code
provider
account_group
priority_base
max_concurrency
tpm_limit
rpm_limit
enabled
created_at
updated_at

account_group 用于表示同一个上游限流池。当前百炼主账号可统一设为 aliyun_bailian_main

11.2 llm_credentials

id
pool_id
provider
base_url
api_key_encrypted
key_hash
owner_type
owner_user_id
enabled
weight
created_at
updated_at

owner_type

platform_shared
platform_dedicated
enterprise_dedicated
byok

11.3 llm_quota_profiles

id
code
priority_bonus
user_tpm_limit
user_rpm_limit
max_active_generation_runs
max_active_parse_jobs
daily_parse_limit
daily_assist_limit
enabled
created_at
updated_at

11.4 user_llm_entitlements

user_id
quota_profile_id
pool_override_id
byok_credential_id
valid_until
created_at
updated_at

11.5 llm_usage_events

id
user_id
pool_code
provider
account_group
task_type
billing_mode
model
prompt_tokens_est
completion_tokens
total_tokens
status
error_code
latency_ms
created_at

第一期可以先不建所有表,先用配置文件和日志落地。


12. LLMGateway 接口草案

@dataclass
class LLMRequest:
    user_id: UUID | None
    task_type: str
    billing_mode: str
    model: str
    messages: list[dict]
    max_tokens: int | None = None
    temperature: float | None = None
    priority: int = 50
    stream: bool = False
    trace_label: str | None = None


class LLMGateway:
    async def chat(self, request: LLMRequest) -> str:
        ...

    async def stream_chat(self, request: LLMRequest) -> AsyncIterator[str]:
        ...

billing_mode 建议:

paid_words
platform_free
platform_assist
platform_background
byok

13. Worker 调整

13.1 一键全文生成

现有一键全文已经拆成 generation_runsgeneration_section_jobs,并按批次投放。后续应调整为:

  1. job 可以正常进入 ARQ。
  2. job 执行到 LLM 调用前,必须经过 LLMGateway 申请额度。
  3. 拿不到额度时,worker 等待或 Retry(defer=...)
  4. 单个 run 不允许占满全部 global 槽位。

13.2 解析 worker

quick-parse、全文解析、产品库解析都不扣正文购买字数,但必须:

  1. 每用户同时最多 1 个重解析任务。
  2. 免费用户每日解析次数/页数可限制。
  3. 后台产品库解析优先级低于前台 quick-parse。

14. 429 处理

统一策略:

  1. 请求前先申请 limiter,避免无序冲击百炼。
  2. 百炼仍返回 429 时,识别错误文本:
  3. TPM limit reached
  4. RPM limit reached
  5. Requests rate limit exceeded
  6. Allocated quota exceeded
  7. Request rate increased too quickly
  8. 尝试读取 retry-after
  9. 设置 Redis cooldown:
    llm:cooldown:{account_group}:{model}
    
  10. cooldown 期间同 account_group/model 的请求不打上游。
  11. 记录 llm_usage_events,便于后续调参。

15. 分阶段落地

阶段 1:最小可用

  1. 新增 LLMGateway
  2. 新增 Redis 全局并发 limiter。
  3. 接入正文生成链路:run_writingrun_writing_stream
  4. 接入 quick-parse 的核心调用。
  5. 统一 429 cooldown。
  6. 记录基础 usage 日志。

阶段 2:覆盖主要入口

  1. 接入 outline children。
  2. 接入 writing hint。
  3. 接入 extractor。
  4. 接入产品库解析。

阶段 3:Quota Profile

  1. 增加免费/付费/高级/企业用户权益。
  2. 增加每日免费解析限制。
  3. 增加用户级 LLM 并发限制。
  4. 增加正文生成预冻结。

阶段 4:Token Bucket 与优先队列

  1. 增加 TPM/RPM 估算和分钟窗口。
  2. 根据 priority 做排队。
  3. 保留交互任务槽位。
  4. 后台任务在额度紧张时自动降速。

阶段 5:BYOK 与多供应商

  1. 企业用户可绑定自己的百炼 key。
  2. 支持多个阿里云主账号的独立 account_group。
  3. 支持 OpenAI、DeepSeek、SiliconFlow 等其他 provider。
  4. 按 provider/model 维护不同 limiter。

16. 第一版推荐配置

ARQ_GENERATION_MAX_JOBS=1
ARQ_TENDER_PARSE_MAX_JOBS=1
ARQ_PRODUCT_PARSE_MAX_JOBS=1

PARSER_EXTRACT_CONCURRENCY=1
PARSER_CONCURRENCY_MAX=1

LLM_GLOBAL_CONCURRENCY=2
LLM_PAID_POOL_CONCURRENCY=1
LLM_FREE_POOL_CONCURRENCY=1
LLM_ASSIST_POOL_CONCURRENCY=1
LLM_BACKGROUND_POOL_CONCURRENCY=1
LLM_USER_CONCURRENCY=1

LLM_429_COOLDOWN_BASE_SEC=20
LLM_429_COOLDOWN_MAX_SEC=180

第一版目标是稳定,不是压满吞吐。等 usage 日志能看清楚真实 QPS、TPM、耗时和 429 后,再逐步上调。


17. 最终建议

  1. 需要设计 key pool,但不要依赖同一百炼主账号下多 key 扩容。
  2. key pool 主要解决凭证管理、权限隔离、归因审计和未来 BYOK。
  3. 主动限流必须做在后端,核心是 Redis 全局 limiter。
  4. 正文生成字数扣费和平台 LLM 预算要分离。
  5. 免费解析和辅助生成虽然不扣用户字数,但要严格纳入平台额度控制。
  6. 付费用户体验靠 priority、pool quota 和 user entitlement 保证。
  7. 真正物理隔离额度只能依赖企业 BYOK、多阿里云主账号或多供应商。