LLM 网关、Key Pool 与主动限流技术方案¶
- 版本:v1.0
- 日期:2026-05-19
- 作者:Codex
- 适用项目:
Tender_Documents
1. 背景¶
当前系统有多个会调用 LLM 的入口:
- 正文生成:单章节生成、一键全文生成、章节重试。
- 标书解析:quick-parse、全文解析、基础信息/评分项提取。
- 辅助生成:目录 children、writing hint、outline skeleton。
- 产品库解析:产品文件解析、需求提取。
- 可能的对话能力:AI chat、图片理解等。
这些入口分布在 API 进程和多个 ARQ worker 中。现在后端主要使用同一个百炼 API Key;即使后续在百炼控制台创建多个 API Key,同一阿里云主账号下的多个 RAM 子账号、业务空间和 API Key 的模型调用仍按主账号维度汇总限流。
因此,同一主账号下“多 key”不能增加 TPM/RPM,也不能天然隔离免费用户、付费用户和后台任务。后端必须主动做统一调度和限流,把上游不可控的 429 转换为系统内可控排队。
2. 关键结论¶
- 百炼普通模型调用的限流按阿里云主账号维度统计,同一主账号内多 API Key 共享总 TPM/RPM。
- 后端仍然应该设计 key pool,但它的主要职责是凭证隔离、权限管理、归因审计、未来多供应商/BYOK 扩展,而不是在同一主账号下扩容 TPM。
- 真正保护上游限流的是统一 LLMGateway + Redis 全局 limiter。
- 用户购买的“正文生成字数”和平台 LLM TPM/RPM 是两套额度,必须分开建模。
- 正文生成才扣用户购买字数;解析、目录、writing hint 等不扣字数,但仍消耗平台 LLM 预算,必须进入平台限流和成本统计。
3. 目标与非目标¶
3.1 目标¶
- 所有 LLM 调用统一经过
LLMGateway。 - 支持逻辑 key pool:免费池、付费池、后台池、交互池、企业 BYOK 池。
- 支持 Redis 全局限流:按主账号、pool、user、task_type、model 控制并发和 token 预算。
- 支持套餐权益:免费用户、付费用户、高级套餐、企业用户拥有不同优先级和额度。
- 支持正文生成字数扣减与 LLM 调用成本统计分离。
- 降低 429:请求先在后端排队,只有拿到额度后才调用百炼。
- 为未来接入多个阿里云主账号、其他模型供应商、企业 BYOK 预留结构。
3.2 非目标¶
- 不依赖同一百炼主账号下多 API Key 扩大 TPM。
- 不在第一期实现复杂优先队列调度器。
- 不强制第一期重写所有 LLM 调用点,可按风险逐步迁移。
- 不改变现有“正文生成按最终字数扣费”的业务规则。
4. 现有问题¶
4.1 分散调用¶
当前 LLM 调用分散在多个位置:
app/core/llm_factory.py:正文写作、outline skeleton、图片理解等。app/services/streaming/suboutline_service.py:outline children。app/services/parse/parse_service.py:quick-parse。app/core/extraction/extractor.py:解析提取。
其中部分调用已有进程内 semaphore 和 429 backoff,但不能覆盖多个 API/worker 进程,也不能覆盖所有调用入口。
4.2 局部限流不足¶
现有配置能限制单个 worker 或单个进程:
ARQ_GENERATION_MAX_JOBSARQ_TENDER_PARSE_MAX_JOBSPARSER_EXTRACT_CONCURRENCYLLM_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[正文成功后扣购买字数]
核心原则:
- 业务代码不直接创建
AsyncOpenAI。 - 业务代码声明
task_type、billing_mode、priority、model、max_tokens。 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 是否有价值¶
可以配置:
价值:
- 权限和 IP 白名单隔离。
- 泄露后影响面更小。
- 百炼控制台归因更清楚。
- 便于未来替换单个场景的供应商或企业 key。
限制:
- 同一阿里云主账号内仍共享总 TPM/RPM。
- 不能把
key_generation、key_parse理解成独立额度池。
7. 套餐与业务额度¶
系统需要区分两类额度:
7.1 正文生成字数¶
只对正文生成类任务扣减:
- 单章节生成。
- 一键全文生成中的章节正文。
- 失败章节重试成功后生成的新正文。
建议规则:
- 生成前检查用户是否有可用字数。
- 生成成功并保存正文后,按最终正文字符数扣减。
- 失败不扣。
- 取消后未保存不扣;已保存则按保存内容扣。
- 后续可增加预冻结:生成前冻结预估字数,成功后多退少补。
7.2 免费 LLM 调用¶
解析、目录、writing hint 不扣购买字数,但必须受平台额度控制:
- 每用户限制同时进行的解析任务数量。
- 免费用户限制每日解析次数或页数。
- 付费用户拥有更高优先级,但不意味着无限使用。
- 后台解析和产品库解析不能挤占正文生成槽位。
8. 主动限流设计¶
8.1 限流维度¶
Redis limiter 至少包含四层:
示例 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}
申请额度时:
- 如果当前窗口预计超限,等待到下个窗口或返回 defer。
- 如果请求已发出但百炼返回 429,设置 model/account cooldown。
- 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:
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¶
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 建议:
13. Worker 调整¶
13.1 一键全文生成¶
现有一键全文已经拆成 generation_runs 和 generation_section_jobs,并按批次投放。后续应调整为:
- job 可以正常进入 ARQ。
- job 执行到 LLM 调用前,必须经过
LLMGateway申请额度。 - 拿不到额度时,worker 等待或
Retry(defer=...)。 - 单个 run 不允许占满全部 global 槽位。
13.2 解析 worker¶
quick-parse、全文解析、产品库解析都不扣正文购买字数,但必须:
- 每用户同时最多 1 个重解析任务。
- 免费用户每日解析次数/页数可限制。
- 后台产品库解析优先级低于前台 quick-parse。
14. 429 处理¶
统一策略:
- 请求前先申请 limiter,避免无序冲击百炼。
- 百炼仍返回 429 时,识别错误文本:
TPM limit reachedRPM limit reachedRequests rate limit exceededAllocated quota exceededRequest rate increased too quickly- 尝试读取
retry-after。 - 设置 Redis cooldown:
- cooldown 期间同 account_group/model 的请求不打上游。
- 记录
llm_usage_events,便于后续调参。
15. 分阶段落地¶
阶段 1:最小可用¶
- 新增
LLMGateway。 - 新增 Redis 全局并发 limiter。
- 接入正文生成链路:
run_writing、run_writing_stream。 - 接入 quick-parse 的核心调用。
- 统一 429 cooldown。
- 记录基础 usage 日志。
阶段 2:覆盖主要入口¶
- 接入 outline children。
- 接入 writing hint。
- 接入 extractor。
- 接入产品库解析。
阶段 3:Quota Profile¶
- 增加免费/付费/高级/企业用户权益。
- 增加每日免费解析限制。
- 增加用户级 LLM 并发限制。
- 增加正文生成预冻结。
阶段 4:Token Bucket 与优先队列¶
- 增加 TPM/RPM 估算和分钟窗口。
- 根据 priority 做排队。
- 保留交互任务槽位。
- 后台任务在额度紧张时自动降速。
阶段 5:BYOK 与多供应商¶
- 企业用户可绑定自己的百炼 key。
- 支持多个阿里云主账号的独立 account_group。
- 支持 OpenAI、DeepSeek、SiliconFlow 等其他 provider。
- 按 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. 最终建议¶
- 需要设计 key pool,但不要依赖同一百炼主账号下多 key 扩容。
- key pool 主要解决凭证管理、权限隔离、归因审计和未来 BYOK。
- 主动限流必须做在后端,核心是 Redis 全局 limiter。
- 正文生成字数扣费和平台 LLM 预算要分离。
- 免费解析和辅助生成虽然不扣用户字数,但要严格纳入平台额度控制。
- 付费用户体验靠 priority、pool quota 和 user entitlement 保证。
- 真正物理隔离额度只能依赖企业 BYOK、多阿里云主账号或多供应商。