refactor: 自适应 RPM 从边界记忆改为多次观察确认 + 置信度衰减

将自适应 RPM 限制算法从"单次 429 即设限"重构为基于置信度的学习机制:

- 收到 429 时仅记录观察(本地 RPM + 上游 header 限制值),不立即设限
- 有 header 的观察需 2 次一致确认,无 header 需 3 次一致确认
- confidence 随时间自然衰减,确保限制永远不会固化
- confidence 低于阈值时停止本地 RPM 限制,429 直接透传客户端
- 统一 get_effective_limit 入口,消除多处重复的限制获取逻辑
- Admin API 增加置信度、观察数量等诊断字段
This commit is contained in:
fawney19
2026-02-10 14:58:15 +08:00
parent fc1ad44346
commit 4276e7835c
6 changed files with 388 additions and 282 deletions

View File

@@ -57,6 +57,12 @@ class AdaptiveStatsResponse(BaseModel):
last_429_type: str | None last_429_type: str | None
adjustment_count: int adjustment_count: int
recent_adjustments: list[dict] recent_adjustments: list[dict]
# 置信度相关
learning_confidence: float | None = Field(None, description="学习置信度 (0.0-1.0)")
enforcement_active: bool | None = Field(None, description="是否正在执行本地 RPM 限制")
observation_count: int = Field(0, description="429 观察记录数")
header_observation_count: int = Field(0, description="带 header 的观察记录数")
latest_upstream_limit: int | None = Field(None, description="最近一次上游 header 限制值")
class KeyListItem(BaseModel): class KeyListItem(BaseModel):
@@ -219,6 +225,7 @@ class ListAdaptiveKeysAdapter(AdminApiAdapter):
query = query.filter(ProviderAPIKey.provider_id == self.provider_id) query = query.filter(ProviderAPIKey.provider_id == self.provider_id)
keys = query.all() keys = query.all()
adaptive_manager = get_adaptive_rpm_manager()
return [ return [
KeyListItem( KeyListItem(
id=key.id, id=key.id,
@@ -227,11 +234,7 @@ class ListAdaptiveKeysAdapter(AdminApiAdapter):
api_formats=key.api_formats or [], api_formats=key.api_formats or [],
is_adaptive=key.rpm_limit is None, is_adaptive=key.rpm_limit is None,
rpm_limit=key.rpm_limit, rpm_limit=key.rpm_limit,
effective_limit=( effective_limit=adaptive_manager.get_effective_limit(key),
key.learned_rpm_limit # 自适应模式:使用学习值,未学习时为 None不限制
if key.rpm_limit is None
else key.rpm_limit
),
learned_rpm_limit=key.learned_rpm_limit, learned_rpm_limit=key.learned_rpm_limit,
concurrent_429_count=key.concurrent_429_count or 0, concurrent_429_count=key.concurrent_429_count or 0,
rpm_429_count=key.rpm_429_count or 0, rpm_429_count=key.rpm_429_count or 0,
@@ -275,16 +278,13 @@ class ToggleAdaptiveModeAdapter(AdminApiAdapter):
context.db.refresh(key) context.db.refresh(key)
is_adaptive = key.rpm_limit is None is_adaptive = key.rpm_limit is None
adaptive_manager = get_adaptive_rpm_manager()
return { return {
"message": message, "message": message,
"key_id": key.id, "key_id": key.id,
"is_adaptive": is_adaptive, "is_adaptive": is_adaptive,
"rpm_limit": key.rpm_limit, "rpm_limit": key.rpm_limit,
"effective_limit": ( "effective_limit": adaptive_manager.get_effective_limit(key),
key.learned_rpm_limit # 自适应模式:使用学习值,未学习时为 None不限制
if is_adaptive
else key.rpm_limit
),
} }
@@ -312,6 +312,11 @@ class GetAdaptiveStatsAdapter(AdminApiAdapter):
last_429_type=stats["last_429_type"], last_429_type=stats["last_429_type"],
adjustment_count=stats["adjustment_count"], adjustment_count=stats["adjustment_count"],
recent_adjustments=stats["recent_adjustments"], recent_adjustments=stats["recent_adjustments"],
learning_confidence=stats.get("learning_confidence"),
enforcement_active=stats.get("enforcement_active"),
observation_count=stats.get("observation_count", 0),
header_observation_count=stats.get("header_observation_count", 0),
latest_upstream_limit=stats.get("latest_upstream_limit"),
) )

View File

@@ -137,6 +137,22 @@ class RPMDefaults:
# 探测性扩容最小请求数 - 在探测间隔内至少需要这么多请求 # 探测性扩容最小请求数 - 在探测间隔内至少需要这么多请求
PROBE_INCREASE_MIN_REQUESTS = 10 PROBE_INCREASE_MIN_REQUESTS = 10
# === 置信度学习参数 ===
# 无 header 时,需要多少次一致的 429 观察才确认限制
MIN_CONSISTENT_OBSERVATIONS = 3
# 有 header 时,需要多少次一致的 header 观察才确认限制
MIN_HEADER_CONFIRMATIONS = 2
# 观察值之间的最大允许偏差比例30% 以内视为一致)
OBSERVATION_CONSISTENCY_THRESHOLD = 0.3
# header 声明限制的安全边际(使用 95%
HEADER_LIMIT_SAFETY_MARGIN = 0.95
# 纯观察限制的安全边际(使用 90%
OBSERVATION_LIMIT_SAFETY_MARGIN = 0.90
# confidence 低于此阈值时不执行本地 RPM 限制(透传上游 429
ENFORCEMENT_CONFIDENCE_THRESHOLD = 0.6
# confidence 自然衰减速率:每分钟衰减的比例
CONFIDENCE_DECAY_PER_MINUTE = 0.005 # 每分钟 -0.5%,约 200 分钟(~3.3h)从 1.0 衰减到 0
# 向后兼容别名 # 向后兼容别名
ConcurrencyDefaults = RPMDefaults ConcurrencyDefaults = RPMDefaults

View File

@@ -66,6 +66,7 @@ from src.services.rate_limit.adaptive_reservation import (
AdaptiveReservationManager, AdaptiveReservationManager,
get_adaptive_reservation_manager, get_adaptive_reservation_manager,
) )
from src.services.rate_limit.adaptive_rpm import get_adaptive_rpm_manager
from src.services.rate_limit.concurrency_manager import get_concurrency_manager from src.services.rate_limit.concurrency_manager import get_concurrency_manager
@@ -404,30 +405,8 @@ class CacheAwareScheduler:
raise ProviderNotAvailableException("服务暂时繁忙,请稍后重试") raise ProviderNotAvailableException("服务暂时繁忙,请稍后重试")
def _get_effective_rpm_limit(self, key: ProviderAPIKey) -> int | None: def _get_effective_rpm_limit(self, key: ProviderAPIKey) -> int | None:
""" """获取有效的 RPM 限制(委托给 AdaptiveRPMManager 统一逻辑)"""
获取有效的 RPM 限制 return get_adaptive_rpm_manager().get_effective_limit(key)
新逻辑:
- rpm_limit=NULL: 启用自适应,使用 learned_rpm_limit如无学习记录则不限制等待碰壁学习
- rpm_limit=数字: 固定限制,直接使用该值
Args:
key: API Key对象
Returns:
有效的 RPM 限制None 表示不限制)
"""
if key.rpm_limit is None:
# 自适应模式:使用学习到的值
learned = key.learned_rpm_limit
if learned is not None:
return int(learned)
# 未学习到值时,不限制,让其碰壁后再学习真实边界
return None
else:
# 固定限制模式
return int(key.rpm_limit)
async def _check_concurrent_available( async def _check_concurrent_available(
self, self,

View File

@@ -525,9 +525,14 @@ class ErrorClassifier:
logger.warning(f" [{request_id}] 并发限制触发不调整RPM") logger.warning(f" [{request_id}] 并发限制触发不调整RPM")
return "concurrent" return "concurrent"
elif rate_limit_info.limit_type == RateLimitType.RPM: elif rate_limit_info.limit_type == RateLimitType.RPM:
logger.warning( if new_limit is not None:
f" [{request_id}] 自适应调整: Key {key.id[:8]}... RPM限制 -> {new_limit}" logger.warning(
) f" [{request_id}] 自适应调整: Key {key.id[:8]}... RPM限制 -> {new_limit}"
)
else:
logger.info(
f" [{request_id}] 学习中: Key {key.id[:8]}... 观察已记录,暂不设限"
)
return "rpm" return "rpm"
else: else:
return "unknown" return "unknown"

View File

@@ -1,21 +1,23 @@
""" """
自适应 RPM 调整器 - 基于边界记忆的 RPM 限制调整 自适应 RPM 调整器 - 基于置信度衰减的 RPM 限制学习
核心算法:边界记忆 + 渐进探测 核心算法:多次观察确认 + 置信度衰减
- 触发 429 时记录边界last_rpm_peak这就是真实上限 - 收到 429 时记录观察(本地 RPM + 上游 header 限制值)
- 缩容策略:新限制 = 边界 - 步长,而非乘性减少 - 多次一致的观察才确认限制header 需 2 次,无 header 需 3 次)
- 扩容策略:不超过已知边界,除非是探测性扩容 - confidence 随时间自然衰减,限制永远不会固化
- 探测性扩容:长时间无 429 时尝试突破边界 - confidence 低于阈值时停止本地 RPM 限制执行,让上游 429 透传
设计原则: 设计原则:
1. 快速收敛:一次 429 就能找到接近真实的限制 1. 限制永远不固化 -- confidence 需要持续的 429 观察来维持
2. 避免过度保守:不会因为多次 429 而无限下降 2. 即使有上游 header 也要多次确认
3. 安全探测:允许在稳定后尝试更高 RPM 3. 学习期间 429 直接透传给客户端
4. 优先使用上游 header 声明的限制值,而非本地 RPM 计数
""" """
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone
from statistics import median
from typing import Any, cast from typing import Any, cast
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -39,24 +41,19 @@ class AdaptiveRPMManager:
""" """
自适应 RPM 管理器 自适应 RPM 管理器
核心算法:边界记忆 + 渐进探测 核心算法:多次观察确认 + 置信度衰减
- 触发 429 时记录边界last_rpm_peak = 触发时的 RPM - 收到 429 时记录观察(本地 RPM 计数 + 上游 header 限制值
- 缩容:新限制 = 边界 - 步长(快速收敛到真实限制附近) - 有 header 的观察需 MIN_HEADER_CONFIRMATIONS 次一致才确认
- 扩容:不超过边界(即 last_rpm_peak允许回到边界值尝试 - 无 header 的观察需 MIN_CONSISTENT_OBSERVATIONS 次一致才确认
- 探测性扩容长时间30分钟无 429 时,可以尝试 +1 突破边界 - confidence 随时间自然衰减CONFIDENCE_DECAY_PER_MINUTE
- confidence < ENFORCEMENT_CONFIDENCE_THRESHOLD 时停止本地限制执行
扩容条件(满足任一即可): 扩容条件(满足任一即可):
1. 利用率扩容:窗口内高利用率比例 >= 60%,且当前限制 < 边界 1. 利用率扩容:窗口内高利用率比例 >= 60%,且当前限制 < 边界
2. 探测性扩容:距上次 429 超过 30 分钟,可以尝试突破边界 2. 探测性扩容:距上次 429 超过 30 分钟,可以尝试突破边界
关键特性:
1. 快速收敛:一次 429 就能学到接近真实的限制值
2. 边界保护:普通扩容不会超过已知边界
3. 安全探测:长时间稳定后允许尝试更高 RPM
4. 区分并发限制和 RPM 限制
""" """
# 默认配置 - 使用统一常量 # 默认配置
DEFAULT_INITIAL_LIMIT = RPMDefaults.INITIAL_LIMIT DEFAULT_INITIAL_LIMIT = RPMDefaults.INITIAL_LIMIT
MIN_RPM_LIMIT = RPMDefaults.MIN_RPM_LIMIT MIN_RPM_LIMIT = RPMDefaults.MIN_RPM_LIMIT
MAX_RPM_LIMIT = RPMDefaults.MAX_RPM_LIMIT MAX_RPM_LIMIT = RPMDefaults.MAX_RPM_LIMIT
@@ -78,6 +75,15 @@ class AdaptiveRPMManager:
# 记录历史数量 # 记录历史数量
MAX_HISTORY_RECORDS = 20 MAX_HISTORY_RECORDS = 20
# 置信度学习参数
MIN_CONSISTENT_OBSERVATIONS = RPMDefaults.MIN_CONSISTENT_OBSERVATIONS
MIN_HEADER_CONFIRMATIONS = RPMDefaults.MIN_HEADER_CONFIRMATIONS
OBSERVATION_CONSISTENCY_THRESHOLD = RPMDefaults.OBSERVATION_CONSISTENCY_THRESHOLD
HEADER_LIMIT_SAFETY_MARGIN = RPMDefaults.HEADER_LIMIT_SAFETY_MARGIN
OBSERVATION_LIMIT_SAFETY_MARGIN = RPMDefaults.OBSERVATION_LIMIT_SAFETY_MARGIN
ENFORCEMENT_CONFIDENCE_THRESHOLD = RPMDefaults.ENFORCEMENT_CONFIDENCE_THRESHOLD
CONFIDENCE_DECAY_PER_MINUTE = RPMDefaults.CONFIDENCE_DECAY_PER_MINUTE
def __init__(self, strategy: str = AdaptiveStrategy.AIMD): def __init__(self, strategy: str = AdaptiveStrategy.AIMD):
""" """
初始化自适应 RPM 管理器 初始化自适应 RPM 管理器
@@ -87,106 +93,283 @@ class AdaptiveRPMManager:
""" """
self.strategy = strategy self.strategy = strategy
# ==================== 429 处理 ====================
def handle_429_error( def handle_429_error(
self, self,
db: Session, db: Session,
key: ProviderAPIKey, key: ProviderAPIKey,
rate_limit_info: RateLimitInfo, rate_limit_info: RateLimitInfo,
current_rpm: int | None = None, current_rpm: int | None = None,
) -> int: ) -> int | None:
""" """
处理429错误调整 RPM 限制 处理 429 错误,记录观察并基于一致性评估是否设置限制
Args: 不再单次 429 就设限,而是:
db: 数据库会话 1. 记录 429 观察(本地 RPM + 上游 header 限制值)
key: API Key对象 2. 评估历史观察的一致性
rate_limit_info: 速率限制信息 3. 一致性达标时设置 learned_rpm_limit 并赋予 confidence
current_rpm: 当前分钟内的请求数 4. 一致性不够时保持学习期429 透传给客户端)
Returns: Returns:
调整后的 RPM 限制 调整后的 RPM 限制,或 None学习期间
""" """
# rpm_limit=NULL 表示启用自适应rpm_limit=数字 表示固定限制
is_adaptive = key.rpm_limit is None is_adaptive = key.rpm_limit is None
if not is_adaptive: if not is_adaptive:
logger.debug(f"Key {key.id} 设置了固定 RPM 限制 ({key.rpm_limit}),跳过自适应调整") logger.debug(f"Key {key.id} 设置了固定 RPM 限制 ({key.rpm_limit}),跳过自适应调整")
return int(key.rpm_limit) # type: ignore[arg-type] return int(key.rpm_limit) # type: ignore[arg-type]
# 更新429统计 # 更新 429 统计
key.last_429_at = datetime.now(timezone.utc) # type: ignore[assignment] key.last_429_at = datetime.now(timezone.utc) # type: ignore[assignment]
key.last_429_type = rate_limit_info.limit_type # type: ignore[assignment] key.last_429_type = rate_limit_info.limit_type # type: ignore[assignment]
# 仅在 RPM 限制且拿到 RPM 数时记录边界
if (
rate_limit_info.limit_type == RateLimitType.RPM
and current_rpm is not None
and current_rpm > 0
):
key.last_rpm_peak = current_rpm # type: ignore[assignment]
# 遇到 429 错误,清空利用率采样窗口(重新开始收集) # 清空利用率采样窗口
key.utilization_samples = [] # type: ignore[assignment] key.utilization_samples = [] # type: ignore[assignment]
if rate_limit_info.limit_type == RateLimitType.RPM: if rate_limit_info.limit_type == RateLimitType.RPM:
# RPM 限制:减少 RPM 限制
key.rpm_429_count = int(key.rpm_429_count or 0) + 1 # type: ignore[assignment] key.rpm_429_count = int(key.rpm_429_count or 0) + 1 # type: ignore[assignment]
# 获取当前有效限制(自适应模式使用 learned_rpm_limit upstream_limit = rate_limit_info.limit_value
old_limit = int(key.learned_rpm_limit or self.DEFAULT_INITIAL_LIMIT)
new_limit = self._decrease_limit(old_limit, current_rpm)
logger.warning( # 记录 429 观察
f"[RPM] RPM 限制触发: Key {key.id[:8]}... | " self._record_429_observation(key, current_rpm, upstream_limit)
f"当前 RPM: {current_rpm} | "
f"调整: {old_limit} -> {new_limit}"
)
# 记录调整历史 # 评估观察一致性,决定是否设置/更新限制
self._record_adjustment( evaluated_limit, confidence = self._evaluate_observations(key)
key,
old_limit=old_limit,
new_limit=new_limit,
reason="rpm_429",
current_rpm=current_rpm,
)
# 更新学习到的 RPM 限制 old_limit = key.learned_rpm_limit
key.learned_rpm_limit = new_limit # type: ignore[assignment]
if evaluated_limit is not None and confidence >= self.ENFORCEMENT_CONFIDENCE_THRESHOLD:
# 一致性达标,设置限制
self._record_adjustment(
key,
old_limit=old_limit or 0,
new_limit=evaluated_limit,
reason="rpm_429",
current_rpm=current_rpm,
upstream_limit=upstream_limit,
confidence=round(confidence, 3),
learning_source="header" if upstream_limit else "observation",
)
key.learned_rpm_limit = evaluated_limit # type: ignore[assignment]
# 更新 last_rpm_peak优先使用 upstream header
if upstream_limit and upstream_limit > 0:
key.last_rpm_peak = upstream_limit # type: ignore[assignment]
elif current_rpm and current_rpm > 0:
key.last_rpm_peak = current_rpm # type: ignore[assignment]
logger.warning(
f"[RPM] 限制已确认: Key {key.id[:8]}... | "
f"当前 RPM: {current_rpm} | "
f"上游 header: {upstream_limit} | "
f"调整: {old_limit} -> {evaluated_limit} | "
f"confidence: {confidence:.2f}"
)
else:
# 一致性不够,保持学习期
logger.info(
f"[RPM] 学习中: Key {key.id[:8]}... | "
f"当前 RPM: {current_rpm} | "
f"上游 header: {upstream_limit} | "
f"观察已记录,暂不设限"
)
elif rate_limit_info.limit_type == RateLimitType.CONCURRENT: elif rate_limit_info.limit_type == RateLimitType.CONCURRENT:
# 并发限制:不调整 RPM只记录
key.concurrent_429_count = int(key.concurrent_429_count or 0) + 1 # type: ignore[assignment] key.concurrent_429_count = int(key.concurrent_429_count or 0) + 1 # type: ignore[assignment]
logger.info( logger.info(
f"[CONCURRENT] 并发限制触发: Key {key.id[:8]}... | " f"[CONCURRENT] 并发限制触发: Key {key.id[:8]}... | "
f"不调整 RPM 限制(这是并发问题,非 RPM 问题)" f"不调整 RPM 限制(这是并发问题,非 RPM 问题)"
) )
else: else:
# 未知类型:保守处理,轻微减少 # 未知类型:保守处理(仅在已有学习值时减少
logger.warning( old_limit = key.learned_rpm_limit
f"[UNKNOWN] 未知429类型: Key {key.id[:8]}... | " if old_limit is not None:
f"当前 RPM: {current_rpm} | " logger.warning(
f"保守减少 RPM" f"[UNKNOWN] 未知429类型: Key {key.id[:8]}... | "
) f"当前 RPM: {current_rpm} | "
f"保守减少 RPM: {old_limit} -> {max(int(old_limit * 0.95), self.MIN_RPM_LIMIT)}"
old_limit = int(key.learned_rpm_limit or self.DEFAULT_INITIAL_LIMIT) )
new_limit = max(int(old_limit * 0.9), self.MIN_RPM_LIMIT) # 减少10% else:
logger.info(
self._record_adjustment( f"[UNKNOWN] 未知429类型: Key {key.id[:8]}... | "
key, f"当前 RPM: {current_rpm} | "
old_limit=old_limit, f"无学习值,跳过调整"
new_limit=new_limit, )
reason="unknown_429", if old_limit is not None:
current_rpm=current_rpm, new_limit = max(int(old_limit * 0.95), self.MIN_RPM_LIMIT)
) self._record_adjustment(
key,
key.learned_rpm_limit = new_limit # type: ignore[assignment] old_limit=int(old_limit),
new_limit=new_limit,
reason="unknown_429",
current_rpm=current_rpm,
)
key.learned_rpm_limit = new_limit # type: ignore[assignment]
db.flush() db.flush()
get_batch_committer().mark_dirty(db) get_batch_committer().mark_dirty(db)
return int(key.learned_rpm_limit or self.DEFAULT_INITIAL_LIMIT) return key.learned_rpm_limit if key.learned_rpm_limit is not None else None
# ==================== 观察记录与评估 ====================
def _record_429_observation(
self,
key: ProviderAPIKey,
current_rpm: int | None,
upstream_limit: int | None,
) -> None:
"""在 adjustment_history 中记录一次 429 观察"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
observation: dict[str, Any] = {
"type": "429_observation",
"timestamp": datetime.now(timezone.utc).isoformat(),
"current_rpm": current_rpm,
"upstream_limit": upstream_limit,
}
history.append(observation)
key.adjustment_history = self._trim_history(history) # type: ignore[assignment]
def _evaluate_observations(self, key: ProviderAPIKey) -> tuple[int | None, float]:
"""
评估历史 429 观察的一致性,决定是否确认限制
优先使用有 header 的观察upstream_limit其次使用纯本地观察current_rpm
Returns:
(limit, confidence):
- limit: 新确认的限制值,或 None一致性不够不设/不更新限制)
- confidence: 置信度分数 0.0~1.0
"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
observations = [h for h in history if h.get("type") == "429_observation"]
if not observations:
return None, 0.0
# 优先评估有 header 的观察
header_obs = [
o
for o in observations
if o.get("upstream_limit") is not None and o["upstream_limit"] > 0
]
if len(header_obs) >= self.MIN_HEADER_CONFIRMATIONS:
recent = header_obs[-self.MIN_HEADER_CONFIRMATIONS * 2 :]
values = [o["upstream_limit"] for o in recent]
last_n = values[-self.MIN_HEADER_CONFIRMATIONS :]
if self._check_consistency(last_n):
limit_val = int(median(last_n) * self.HEADER_LIMIT_SAFETY_MARGIN)
limit_val = max(limit_val, self.MIN_RPM_LIMIT)
limit_val = min(limit_val, self.MAX_RPM_LIMIT)
return limit_val, 0.8
# 其次评估纯本地观察(无 header
local_obs = [
o for o in observations if o.get("current_rpm") is not None and o["current_rpm"] > 0
]
if len(local_obs) >= self.MIN_CONSISTENT_OBSERVATIONS:
recent = local_obs[-self.MIN_CONSISTENT_OBSERVATIONS * 2 :]
values = [o["current_rpm"] for o in recent]
last_n = values[-self.MIN_CONSISTENT_OBSERVATIONS :]
if self._check_consistency(last_n):
limit_val = int(median(last_n) * self.OBSERVATION_LIMIT_SAFETY_MARGIN)
limit_val = max(limit_val, self.MIN_RPM_LIMIT)
limit_val = min(limit_val, self.MAX_RPM_LIMIT)
return limit_val, 0.6
# 一致性不够,不设/不更新限制(已有的 learned_rpm_limit 不在此处处理)
return None, 0.0
def _check_consistency(self, values: list[int]) -> bool:
"""检查一组数值是否在 OBSERVATION_CONSISTENCY_THRESHOLD 偏差范围内"""
if not values:
return False
med = median(values)
if med <= 0:
return False
return all(abs(v - med) / med <= self.OBSERVATION_CONSISTENCY_THRESHOLD for v in values)
# ==================== 置信度计算 ====================
def get_confidence(self, key: ProviderAPIKey) -> float:
"""
计算当前 confidence 分数0.0~1.0),包含时间衰减
confidence 基于最后一次 429 评估的基础值,随时间自然衰减。
确保限制永远不会固化:长时间没有新 429 观察 → confidence 降至 0。
Returns:
当前 confidence0.0~1.0
"""
if key.learned_rpm_limit is None:
return 0.0
# 从历史中获取基础 confidence
base_confidence = self._get_base_confidence(key)
if base_confidence <= 0:
return 0.0
# 时间衰减
if key.last_429_at is not None:
last_429_at = cast(datetime, key.last_429_at)
minutes_since = max(
0.0, (datetime.now(timezone.utc) - last_429_at).total_seconds() / 60.0
)
time_decay = minutes_since * self.CONFIDENCE_DECAY_PER_MINUTE
else:
time_decay = 1.0 # 没有 429 记录,直接衰减到 0
final = max(0.0, base_confidence - time_decay)
return min(final, 1.0)
def is_enforcement_active(self, key: ProviderAPIKey) -> bool:
"""confidence 是否达到执行阈值,达标才执行本地 RPM 限制"""
return self.get_confidence(key) >= self.ENFORCEMENT_CONFIDENCE_THRESHOLD
def get_effective_limit(self, key: ProviderAPIKey) -> int | None:
"""
获取 key 当前有效的 RPM 限制(统一入口)
- rpm_limit=NULL自适应learned_rpm_limit + confidence 达标才返回
- rpm_limit=数字(固定):直接返回固定值
- 其余情况返回 None不限制
"""
if key.rpm_limit is not None:
return int(key.rpm_limit)
# 自适应模式
if key.learned_rpm_limit is not None and self.is_enforcement_active(key):
return int(key.learned_rpm_limit)
return None
def _get_base_confidence(self, key: ProviderAPIKey) -> float:
"""从最近的 adjustment 记录中获取基础 confidence"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
# 从最新的 adjustment 记录中查找 confidence
for record in reversed(history):
if record.get("type") != "429_observation" and "confidence" in record:
return float(record["confidence"])
# 没有 confidence 记录(旧数据迁移):尝试从观察中重新评估
evaluated_limit, confidence = self._evaluate_observations(key)
if confidence > 0:
return confidence
# 有 learned_rpm_limit 但无法从观察中确认(旧数据),给予低基线置信度
if key.learned_rpm_limit is not None:
return 0.3
return 0.0
# ==================== 成功处理 ====================
def handle_success( def handle_success(
self, self,
@@ -197,24 +380,23 @@ class AdaptiveRPMManager:
""" """
处理成功请求,基于滑动窗口利用率考虑增加 RPM 限制 处理成功请求,基于滑动窗口利用率考虑增加 RPM 限制
Args:
db: 数据库会话
key: API Key对象
current_rpm: 当前分钟内的请求数(必需,用于计算利用率)
Returns: Returns:
调整后的 RPM 限制(如果有调整),否则返回 None 调整后的 RPM 限制(如果有调整),否则返回 None
""" """
# rpm_limit=NULL 表示启用自适应
is_adaptive = key.rpm_limit is None is_adaptive = key.rpm_limit is None
if not is_adaptive: if not is_adaptive:
return None return None
# 未碰壁学习前,不主动设置限制,让系统自由运行直到遇到 429 # 未碰壁学习前,不主动设置限制
if key.learned_rpm_limit is None: if key.learned_rpm_limit is None:
return None return None
# confidence 太低时不做扩容逻辑(系统已在自由运行模式)
confidence = self.get_confidence(key)
if confidence < self.ENFORCEMENT_CONFIDENCE_THRESHOLD:
return None
current_limit = int(key.learned_rpm_limit) current_limit = int(key.learned_rpm_limit)
# 获取已知边界(上次触发 429 时的 RPM # 获取已知边界(上次触发 429 时的 RPM
@@ -267,6 +449,7 @@ class AdaptiveRPMManager:
sample_count=len(samples), sample_count=len(samples),
current_rpm=current_rpm, current_rpm=current_rpm,
known_boundary=known_boundary, known_boundary=known_boundary,
confidence=round(confidence, 3),
) )
# 更新限制 # 更新限制
@@ -291,38 +474,27 @@ class AdaptiveRPMManager:
return None return None
# ==================== 滑动窗口 ====================
def _update_utilization_window( def _update_utilization_window(
self, key: ProviderAPIKey, now_ts: float, utilization: float self, key: ProviderAPIKey, now_ts: float, utilization: float
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """更新利用率滑动窗口"""
更新利用率滑动窗口
Args:
key: API Key对象
now_ts: 当前时间戳
utilization: 当前利用率
Returns:
更新后的采样列表
"""
samples: list[dict[str, Any]] = list(key.utilization_samples or []) samples: list[dict[str, Any]] = list(key.utilization_samples or [])
# 添加新采样
samples.append({"ts": now_ts, "util": round(utilization, 3)}) samples.append({"ts": now_ts, "util": round(utilization, 3)})
# 移除过期采样(超过时间窗口)
cutoff_ts = now_ts - self.UTILIZATION_WINDOW_SECONDS cutoff_ts = now_ts - self.UTILIZATION_WINDOW_SECONDS
samples = [s for s in samples if s["ts"] > cutoff_ts] samples = [s for s in samples if s["ts"] > cutoff_ts]
# 限制采样数量
if len(samples) > self.UTILIZATION_WINDOW_SIZE: if len(samples) > self.UTILIZATION_WINDOW_SIZE:
samples = samples[-self.UTILIZATION_WINDOW_SIZE :] samples = samples[-self.UTILIZATION_WINDOW_SIZE :]
# 更新到 key 对象
key.utilization_samples = samples # type: ignore[assignment] key.utilization_samples = samples # type: ignore[assignment]
return samples return samples
# ==================== 扩容条件 ====================
def _check_increase_conditions( def _check_increase_conditions(
self, self,
key: ProviderAPIKey, key: ProviderAPIKey,
@@ -330,19 +502,7 @@ class AdaptiveRPMManager:
now: datetime, now: datetime,
known_boundary: int | None = None, known_boundary: int | None = None,
) -> str | None: ) -> str | None:
""" """检查是否满足扩容条件"""
检查是否满足扩容条件
Args:
key: API Key对象
samples: 利用率采样列表
now: 当前时间
known_boundary: 已知边界(触发 429 时的 RPM
Returns:
扩容原因(如果满足条件),否则返回 None
"""
# 检查是否在冷却期
if self._is_in_cooldown(key): if self._is_in_cooldown(key):
return None return None
@@ -354,17 +514,13 @@ class AdaptiveRPMManager:
high_util_ratio = high_util_count / len(samples) high_util_ratio = high_util_count / len(samples)
if high_util_ratio >= self.HIGH_UTILIZATION_RATIO: if high_util_ratio >= self.HIGH_UTILIZATION_RATIO:
# 检查是否还有扩容空间(边界保护)
if known_boundary: if known_boundary:
# 允许扩容到边界值(而非 boundary - 1因为缩容时已经 -步长 了
if current_limit < known_boundary: if current_limit < known_boundary:
return "high_utilization" return "high_utilization"
# 已达边界,不触发普通扩容
else: else:
# 无边界信息,允许扩容
return "high_utilization" return "high_utilization"
# 条件2探测性扩容(长时间无 429 且有流量,可以突破边界) # 条件2探测性扩容
if self._should_probe_increase(key, samples, now): if self._should_probe_increase(key, samples, now):
return "probe_increase" return "probe_increase"
@@ -373,60 +529,32 @@ class AdaptiveRPMManager:
def _should_probe_increase( def _should_probe_increase(
self, key: ProviderAPIKey, samples: list[dict[str, Any]], now: datetime self, key: ProviderAPIKey, samples: list[dict[str, Any]], now: datetime
) -> bool: ) -> bool:
""" """检查是否应该进行探测性扩容"""
检查是否应该进行探测性扩容
条件:
1. 距上次 429 超过 PROBE_INCREASE_INTERVAL_MINUTES 分钟
2. 距上次探测性扩容超过 PROBE_INCREASE_INTERVAL_MINUTES 分钟
3. 期间有足够的请求量(采样数 >= PROBE_INCREASE_MIN_REQUESTS
4. 平均利用率 > 30%(说明确实有使用需求)
Args:
key: API Key对象
samples: 利用率采样列表
now: 当前时间
Returns:
是否应该探测性扩容
"""
probe_interval_seconds = self.PROBE_INCREASE_INTERVAL_MINUTES * 60 probe_interval_seconds = self.PROBE_INCREASE_INTERVAL_MINUTES * 60
# 检查距上次 429 的时间
if key.last_429_at: if key.last_429_at:
last_429_at = cast(datetime, key.last_429_at) last_429_at = cast(datetime, key.last_429_at)
time_since_429 = (now - last_429_at).total_seconds() time_since_429 = (now - last_429_at).total_seconds()
if time_since_429 < probe_interval_seconds: if time_since_429 < probe_interval_seconds:
return False return False
# 检查距上次探测性扩容的时间
if key.last_probe_increase_at: if key.last_probe_increase_at:
last_probe = cast(datetime, key.last_probe_increase_at) last_probe = cast(datetime, key.last_probe_increase_at)
time_since_probe = (now - last_probe).total_seconds() time_since_probe = (now - last_probe).total_seconds()
if time_since_probe < probe_interval_seconds: if time_since_probe < probe_interval_seconds:
return False return False
# 检查请求量
if len(samples) < self.PROBE_INCREASE_MIN_REQUESTS: if len(samples) < self.PROBE_INCREASE_MIN_REQUESTS:
return False return False
# 检查平均利用率(确保确实有使用需求)
avg_util = sum(s["util"] for s in samples) / len(samples) avg_util = sum(s["util"] for s in samples) / len(samples)
if avg_util < 0.3: # 至少 30% 利用率 if avg_util < 0.3:
return False return False
return True return True
def _is_in_cooldown(self, key: ProviderAPIKey) -> bool: def _is_in_cooldown(self, key: ProviderAPIKey) -> bool:
""" """检查是否在 429 错误后的冷却期内"""
检查是否在 429 错误后的冷却期内
Args:
key: API Key对象
Returns:
True 如果在冷却期内,否则 False
"""
if key.last_429_at is None: if key.last_429_at is None:
return False return False
@@ -436,35 +564,7 @@ class AdaptiveRPMManager:
return bool(time_since_429 < cooldown_seconds) return bool(time_since_429 < cooldown_seconds)
def _decrease_limit( # ==================== 限制调整 ====================
self,
current_limit: int,
current_rpm: int | None = None,
) -> int:
"""
减少 RPM 限制(基于边界记忆策略)
策略:
- 如果知道触发 429 时的 RPM新限制 = RPM * 0.90(保留 10% 安全边际)
- 10% 的安全边际更保守,考虑到:
1. RPM 报告可能存在延迟,实际触发时的 RPM 可能略高于报告值
2. 上游 API 的限制可能有波动
3. 避免频繁在边界附近触发 429
- 相比固定步长,百分比方式更适应不同量级的限制值
"""
if current_rpm is not None and current_rpm > 0:
# 边界记忆策略:新限制 = 触发边界 * 0.9010% 安全边际)
candidate = int(current_rpm * 0.90)
else:
# 没有 RPM 信息时,减少 10%
candidate = int(current_limit * 0.9)
# 保证不会"缩容变扩容"
candidate = min(candidate, current_limit - 1)
new_limit = max(candidate, self.MIN_RPM_LIMIT)
return new_limit
def _increase_limit( def _increase_limit(
self, self,
@@ -472,39 +572,24 @@ class AdaptiveRPMManager:
known_boundary: int | None = None, known_boundary: int | None = None,
is_probe: bool = False, is_probe: bool = False,
) -> int: ) -> int:
""" """增加 RPM 限制(考虑边界保护)"""
增加 RPM 限制(考虑边界保护)
策略:
- 普通扩容:每次 +INCREASE_STEP但不超过 known_boundary
- 探测性扩容:每次只 +1可以突破边界但要谨慎
Args:
current_limit: 当前限制
known_boundary: 已知边界last_rpm_peak即触发 429 时的 RPM
is_probe: 是否是探测性扩容(可以突破边界)
"""
if is_probe: if is_probe:
# 探测模式:每次只 +1谨慎突破边界
new_limit = current_limit + 1 new_limit = current_limit + 1
else: else:
# 普通模式:每次 +INCREASE_STEP
new_limit = current_limit + self.INCREASE_STEP new_limit = current_limit + self.INCREASE_STEP
# 边界保护:普通扩容不超过 known_boundary
if known_boundary: if known_boundary:
if new_limit > known_boundary: if new_limit > known_boundary:
new_limit = known_boundary new_limit = known_boundary
# 全局上限保护
new_limit = min(new_limit, self.MAX_RPM_LIMIT) new_limit = min(new_limit, self.MAX_RPM_LIMIT)
# 确保有增长(否则返回原值表示不扩容)
if new_limit <= current_limit: if new_limit <= current_limit:
return current_limit return current_limit
return new_limit return new_limit
# ==================== 历史记录 ====================
def _record_adjustment( def _record_adjustment(
self, self,
key: ProviderAPIKey, key: ProviderAPIKey,
@@ -513,16 +598,7 @@ class AdaptiveRPMManager:
reason: str, reason: str,
**extra_data: Any, **extra_data: Any,
) -> None: ) -> None:
""" """记录 RPM 调整历史"""
记录 RPM 调整历史
Args:
key: API Key对象
old_limit: 原限制
new_limit: 新限制
reason: 调整原因
**extra_data: 额外数据
"""
history: list[dict[str, Any]] = list(key.adjustment_history or []) history: list[dict[str, Any]] = list(key.adjustment_history or [])
record = { record = {
@@ -534,31 +610,48 @@ class AdaptiveRPMManager:
} }
history.append(record) history.append(record)
# 保留最近N条记录 key.adjustment_history = self._trim_history(history) # type: ignore[assignment]
if len(history) > self.MAX_HISTORY_RECORDS:
history = history[-self.MAX_HISTORY_RECORDS :]
key.adjustment_history = history # type: ignore[assignment] def _trim_history(self, history: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
截断历史记录,优先保留 429_observation学习数据源
策略:超出 MAX_HISTORY_RECORDS 时,先淘汰最旧的非观察记录,
仍超限则淘汰最旧的观察记录。
"""
if len(history) <= self.MAX_HISTORY_RECORDS:
return history
observations = [h for h in history if h.get("type") == "429_observation"]
adjustments = [h for h in history if h.get("type") != "429_observation"]
# 按时间戳排序(最新在后)
observations.sort(key=lambda h: h.get("timestamp", ""))
adjustments.sort(key=lambda h: h.get("timestamp", ""))
# 保留尽可能多的观察记录:先缩减 adjustment再缩减 observation
overflow = len(history) - self.MAX_HISTORY_RECORDS
trim_adj = min(overflow, len(adjustments))
adjustments = adjustments[trim_adj:]
overflow -= trim_adj
if overflow > 0:
observations = observations[overflow:]
merged = observations + adjustments
merged.sort(key=lambda h: h.get("timestamp", ""))
return merged
# ==================== 统计与管理 ====================
def get_adjustment_stats(self, key: ProviderAPIKey) -> dict[str, Any]: def get_adjustment_stats(self, key: ProviderAPIKey) -> dict[str, Any]:
""" """获取调整统计信息"""
获取调整统计信息
Args:
key: API Key对象
Returns:
统计信息
"""
history: list[dict[str, Any]] = list(key.adjustment_history or []) history: list[dict[str, Any]] = list(key.adjustment_history or [])
samples: list[dict[str, Any]] = list(key.utilization_samples or []) samples: list[dict[str, Any]] = list(key.utilization_samples or [])
# rpm_limit=NULL 表示自适应,否则为固定限制
is_adaptive = key.rpm_limit is None is_adaptive = key.rpm_limit is None
current_limit = int(key.learned_rpm_limit or self.DEFAULT_INITIAL_LIMIT) effective_limit = self.get_effective_limit(key) if is_adaptive else int(key.rpm_limit) # type: ignore
effective_limit = current_limit if is_adaptive else int(key.rpm_limit) # type: ignore
# 计算窗口统计
avg_utilization: float | None = None avg_utilization: float | None = None
high_util_ratio: float | None = None high_util_ratio: float | None = None
if samples: if samples:
@@ -574,16 +667,29 @@ class AdaptiveRPMManager:
if key.last_probe_increase_at: if key.last_probe_increase_at:
last_probe_at_str = cast(datetime, key.last_probe_increase_at).isoformat() last_probe_at_str = cast(datetime, key.last_probe_increase_at).isoformat()
# 边界信息
known_boundary = key.last_rpm_peak known_boundary = key.last_rpm_peak
# 观察统计
observations = [h for h in history if h.get("type") == "429_observation"]
header_observations = [
o
for o in observations
if o.get("upstream_limit") is not None and o["upstream_limit"] > 0
]
latest_upstream = header_observations[-1]["upstream_limit"] if header_observations else None
confidence = self.get_confidence(key) if is_adaptive else None
enforcement_active = (
confidence >= self.ENFORCEMENT_CONFIDENCE_THRESHOLD if confidence is not None else None
)
return { return {
"adaptive_mode": is_adaptive, "adaptive_mode": is_adaptive,
"rpm_limit": key.rpm_limit, # NULL=自适应,数字=固定限制 "rpm_limit": key.rpm_limit,
"effective_limit": effective_limit, # 当前有效限制 "effective_limit": effective_limit,
"learned_limit": key.learned_rpm_limit, # 学习到的限制 "learned_limit": key.learned_rpm_limit,
# 边界记忆相关 # 边界记忆相关
"known_boundary": known_boundary, # 触发 429 时的 RPM已知上限 "known_boundary": known_boundary,
"concurrent_429_count": int(key.concurrent_429_count or 0), "concurrent_429_count": int(key.concurrent_429_count or 0),
"rpm_429_count": int(key.rpm_429_count or 0), "rpm_429_count": int(key.rpm_429_count or 0),
"last_429_at": last_429_at_str, "last_429_at": last_429_at_str,
@@ -600,16 +706,16 @@ class AdaptiveRPMManager:
# 探测性扩容相关 # 探测性扩容相关
"last_probe_increase_at": last_probe_at_str, "last_probe_increase_at": last_probe_at_str,
"probe_increase_interval_minutes": self.PROBE_INCREASE_INTERVAL_MINUTES, "probe_increase_interval_minutes": self.PROBE_INCREASE_INTERVAL_MINUTES,
# 置信度相关
"learning_confidence": round(confidence, 3) if confidence is not None else None,
"enforcement_active": enforcement_active,
"observation_count": len(observations),
"header_observation_count": len(header_observations),
"latest_upstream_limit": latest_upstream,
} }
def reset_learning(self, db: Session, key: ProviderAPIKey) -> None: def reset_learning(self, db: Session, key: ProviderAPIKey) -> None:
""" """重置学习状态(管理员功能)"""
重置学习状态(管理员功能)
Args:
db: 数据库会话
key: API Key对象
"""
logger.info(f"[RESET] 重置学习状态: Key {key.id[:8]}...") logger.info(f"[RESET] 重置学习状态: Key {key.id[:8]}...")
key.learned_rpm_limit = None # type: ignore[assignment] key.learned_rpm_limit = None # type: ignore[assignment]

View File

@@ -17,6 +17,7 @@ from src.core.logger import logger
from src.services.health.monitor import health_monitor from src.services.health.monitor import health_monitor
from src.services.provider.format import normalize_endpoint_signature from src.services.provider.format import normalize_endpoint_signature
from src.services.rate_limit.adaptive_reservation import get_adaptive_reservation_manager from src.services.rate_limit.adaptive_reservation import get_adaptive_reservation_manager
from src.services.rate_limit.adaptive_rpm import get_adaptive_rpm_manager
from src.services.request.candidate import RequestCandidateService from src.services.request.candidate import RequestCandidateService
@@ -103,13 +104,7 @@ class RequestExecutor:
current_key_rpm = 0 current_key_rpm = 0
# 获取有效的 RPM 限制(自适应或固定) # 获取有效的 RPM 限制(自适应或固定)
if key.rpm_limit is None: effective_key_limit = get_adaptive_rpm_manager().get_effective_limit(key)
# 自适应模式:使用学习值,未学习时为 None不限制等待碰壁学习
effective_key_limit = (
int(key.learned_rpm_limit) if key.learned_rpm_limit is not None else None
)
else:
effective_key_limit = int(key.rpm_limit)
reservation_result = reservation_manager.calculate_reservation( reservation_result = reservation_manager.calculate_reservation(
key=key, key=key,