feat: default two structured re-asks and sharper AIMD cut
Round 5 left three residual failure classes; ladder exhaustion (3.2% of structured calls with a single re-ask) and 429 leakage (5.8%, AIMD oscillating above the sustainable point) are addressable: re-ask default goes 1 to 2 (conservative vs instructor's 3) and the AIMD cut factor drops to 0.5.
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ PGW_TELEMETRY_BACKEND=none # sqlite | postgres | none(必填)
|
|||||||
# PGW_PRICING_PATH=config/prices.json # 可选: {"<model>": {"input_per_1m": x, "output_per_1m": y}};缺省 cost 恒 None
|
# PGW_PRICING_PATH=config/prices.json # 可选: {"<model>": {"input_per_1m": x, "output_per_1m": y}};缺省 cost 恒 None
|
||||||
# PGW_CACHE_NAMESPACE=<项目名或租户前缀> # 缓存启用时必填(防跨项目毒化)
|
# PGW_CACHE_NAMESPACE=<项目名或租户前缀> # 缓存启用时必填(防跨项目毒化)
|
||||||
# PGW_CACHE_TTL_S=604800 # 缓存启用时必填,须 > 0
|
# PGW_CACHE_TTL_S=604800 # 缓存启用时必填,须 > 0
|
||||||
# PGW_STRUCTURED_MAX_RETRIES=1 # 0 = 解析失败不重问(CHS 策略)
|
# PGW_STRUCTURED_MAX_RETRIES=2 # 缺省 2(M2.5);0 = 解析失败不重问(CHS 策略)
|
||||||
# PGW_LEASE_TTL_S=1500 # permit 租约;须 ≥ 最大源 timeout
|
# PGW_LEASE_TTL_S=1500 # permit 租约;须 ≥ 最大源 timeout
|
||||||
|
|
||||||
# ══ Redis(缓存 + 分布式限流/熔断)══
|
# ══ Redis(缓存 + 分布式限流/熔断)══
|
||||||
|
|||||||
@@ -100,6 +100,11 @@
|
|||||||
|
|
||||||
**修正**: `OutcomeAwareSelector` 协议增 `health(source_name) -> float`(EWMA 裸值);RetryMW 降权仅在存在**可信替代**(某未失败候选 health ≥ 0.5 × 失败源 health)时生效,否则原地第三试。无健康视图的选源器(round_robin 等)保持无条件降权(冷启动保护原语义)。
|
**修正**: `OutcomeAwareSelector` 协议增 `health(source_name) -> float`(EWMA 裸值);RetryMW 降权仅在存在**可信替代**(某未失败候选 health ≥ 0.5 × 失败源 health)时生效,否则原地第三试。无健康视图的选源器(round_robin 等)保持无条件降权(冷启动保护原语义)。
|
||||||
|
|
||||||
|
### 3.37 迭代 4 补遗: 结构化重问缺省 2 + AIMD 削减 0.5(2026-07-21,第五轮数据驱动)
|
||||||
|
|
||||||
|
**动因**: 第五轮(正午高峰,外部条件比首跑更严苛)剩余失败三分: 阶梯耗尽 ~1.1%(结构化档死亡率 3.2%,重问仅 1 次)、retry_exhausted ~0.9%(源1 尝试失败率 20.5% = 空补全 14.4% + **429 残漏 5.8%**,后者说明 AIMD 0.7 削减在临界点上方震荡)、400 拒绝 ~0.5%(语料固有毒负载,环境常量)。
|
||||||
|
**修正**: ① `PGW_STRUCTURED_MAX_RETRIES` 库缺省 1→2(instructor 库缺省 3 的保守版;成本只在解析失败时新增一跳);② AIMD `_CUT` 0.7→0.5(更快收敛到网关水位之下,Netflix 建议区间 0.5-0.9 内取激进端)。预期: 阶梯死亡 ~3.2%→~0.6%、429 出链后 retry_exhausted ~0.9%→~0.3%,合计残余 ≈ 1.0-1.4%(含 0.5% 环境常量)。
|
||||||
|
|
||||||
### 3.4 不做与预留(方案 C 组件的接入点)
|
### 3.4 不做与预留(方案 C 组件的接入点)
|
||||||
|
|
||||||
- 账号级 429 共享退避: 不做;预留 = 冷却备忘 key 从 source_name 换 account_key 即可接入(LiteLLM 先例,治理粒度=配额粒度原则记入 ARCHITECTURE)。
|
- 账号级 429 共享退避: 不做;预留 = 冷却备忘 key 从 source_name 换 account_key 即可接入(LiteLLM 先例,治理粒度=配额粒度原则记入 ARCHITECTURE)。
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ def _load_cache_keys(
|
|||||||
|
|
||||||
def _load_structured_retries(env: Mapping[str, str]) -> int:
|
def _load_structured_retries(env: Mapping[str, str]) -> int:
|
||||||
found = _first(env, "PGW_STRUCTURED_MAX_RETRIES")
|
found = _first(env, "PGW_STRUCTURED_MAX_RETRIES")
|
||||||
value = int(_cast(found[1], "int", found[0])) if found else 1
|
value = int(_cast(found[1], "int", found[0])) if found else 2 # M2.5 迭代4: 1→2
|
||||||
if value < 0:
|
if value < 0:
|
||||||
raise ValueError("PGW_STRUCTURED_MAX_RETRIES 不能为负")
|
raise ValueError("PGW_STRUCTURED_MAX_RETRIES 不能为负")
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class AdaptivePacer:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_INITIAL = 8.0
|
_INITIAL = 8.0
|
||||||
_CUT = 0.7
|
_CUT = 0.5 # M2.5 迭代4: 0.7→0.5,残漏 5.8% 的 429 证明在临界点上方震荡
|
||||||
_FLOOR = 1.0
|
_FLOOR = 1.0
|
||||||
|
|
||||||
def __init__(self, *, ceiling: float) -> None:
|
def __init__(self, *, ceiling: float) -> None:
|
||||||
|
|||||||
@@ -148,6 +148,13 @@ class TestResilienceKeys:
|
|||||||
|
|
||||||
|
|
||||||
class TestAssemblyGuards:
|
class TestAssemblyGuards:
|
||||||
|
def test_structured_retries_default_two(self):
|
||||||
|
# M2.5 迭代 4: 缺省重问 1→2(instructor 缺省 3 的保守版;P6 阶梯死亡 3.2% 实证)
|
||||||
|
s = GatewaySettings.from_env("LLM", env=_env())
|
||||||
|
assert s.structured_max_retries == 2
|
||||||
|
s2 = GatewaySettings.from_env("LLM", env=_env(PGW_STRUCTURED_MAX_RETRIES="0"))
|
||||||
|
assert s2.structured_max_retries == 0
|
||||||
|
|
||||||
def test_cache_requires_namespace_and_ttl(self):
|
def test_cache_requires_namespace_and_ttl(self):
|
||||||
env = _env(PGW_CACHE_BACKEND="memory")
|
env = _env(PGW_CACHE_BACKEND="memory")
|
||||||
with pytest.raises(ValueError, match="NAMESPACE"):
|
with pytest.raises(ValueError, match="NAMESPACE"):
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class TestAdaptivePacer:
|
|||||||
|
|
||||||
pacer = AdaptivePacer(ceiling=32.0)
|
pacer = AdaptivePacer(ceiling=32.0)
|
||||||
pacer.on_backpressure("s1")
|
pacer.on_backpressure("s1")
|
||||||
assert pacer.limit("s1") == pytest.approx(8.0 * 0.7)
|
assert pacer.limit("s1") == pytest.approx(8.0 * 0.5)
|
||||||
before = pacer.limit("s1")
|
before = pacer.limit("s1")
|
||||||
pacer.on_success("s1")
|
pacer.on_success("s1")
|
||||||
assert pacer.limit("s1") == pytest.approx(before + 1.0 / before)
|
assert pacer.limit("s1") == pytest.approx(before + 1.0 / before)
|
||||||
|
|||||||
@@ -482,8 +482,8 @@ class TestAdaptivePacing:
|
|||||||
pacer=pacer,
|
pacer=pacer,
|
||||||
)
|
)
|
||||||
await mw(_REQ)
|
await mw(_REQ)
|
||||||
# 429 削减一次(8→5.6),随后成功加性增长(5.6 + 1/5.6)
|
# 429 削减一次(8→4),随后成功加性增长(4 + 1/4)
|
||||||
assert pacer.limit("a") == pytest.approx(8.0 * 0.7 + 1.0 / (8.0 * 0.7))
|
assert pacer.limit("a") == pytest.approx(8.0 * 0.5 + 1.0 / (8.0 * 0.5))
|
||||||
|
|
||||||
async def test_inflight_returns_to_zero_after_call(self):
|
async def test_inflight_returns_to_zero_after_call(self):
|
||||||
from polygateway.sources import AdaptivePacer
|
from polygateway.sources import AdaptivePacer
|
||||||
|
|||||||
Reference in New Issue
Block a user