fix(P1-D): 全局 LLM 并发限制 + 删除未接线的 provider 字段

- 删除 PredictRequest.provider(未接线,路由从未传入,符合禁令 #7)
- 新增 _GLOBAL_LLM_SEMAPHORE(默认 4) + _predict_with_concurrency:
  公开 /predict 所有模式汇总受全局并发限制,与 orchestrator 内 match 级
  Semaphore(8) 并存。

测试 test_p1_d_concurrency(4/4);全量 313 通过。
This commit is contained in:
shangfangjian
2026-09-22 03:44:09 +08:00
parent 1ddf697c97
commit b997c06ede
3 changed files with 91 additions and 8 deletions
+19 -7
View File
@@ -2,10 +2,12 @@
安全改进:
- 限流: 每分钟 10 次 / IP(内存实现)
- P1-D: 全局 LLM 并发限制(默认 4),防止过多并发 LLM 调用压垮服务
- DB 连接: 短 session 模式,LLM 调用期间不持有连接
"""
from __future__ import annotations
import asyncio
import logging
from fastapi import APIRouter, Depends, HTTPException, Query, Request
@@ -22,6 +24,21 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["predict"])
# P1-D: 全局 LLM 并发限制。与 orchestrator 内的 match 级 Semaphore(8) 并存,
# 此处在路由层限制单实例全 LLM 调用(所有模式汇总),默认 4。
_GLOBAL_LLM_SEMAPHORE = asyncio.Semaphore(4)
async def _predict_with_concurrency(req: PredictRequest) -> PredictResult:
"""P1-D: 在全局 LLM 并发限制下执行预测。"""
async with _GLOBAL_LLM_SEMAPHORE:
return await predict_match(
req.match_id,
model=req.model,
prompt_version=req.prompt_version,
mode=req.mode,
)
@router.post("/predict", response_model=PredictOut, dependencies=[Depends(rate_limit_predict)])
async def predict(req: PredictRequest, request: Request):
@@ -42,14 +59,9 @@ async def predict(req: PredictRequest, request: Request):
if m.match_status == "finished":
raise HTTPException(400, "该比赛已完赛,不再支持预测")
# 2. 预测调用(不持有任何 DB 连接)
# 2. 预测调用(不持有任何 DB 连接,受全局 LLM 并发限制)
try:
result = await predict_match(
req.match_id,
model=req.model,
prompt_version=req.prompt_version,
mode=req.mode,
)
result = await _predict_with_concurrency(req)
except ValueError as e:
msg = str(e)
if "已结算" in msg:
+1 -1
View File
@@ -36,7 +36,7 @@ class MatchListOut(BaseModel):
class PredictRequest(BaseModel):
match_id: int
provider: str | None = None
# P1-D: 删除未接线的 provider 字段(符合"名不副实则删除");provider 由服务端配置决定。
model: str | None = None
prompt_version: str | None = None
mode: str = Field(