# M2.5 治理韧性实现计划 - **目标**: 按已批准设计 `designs/2026-07-21-m25-resilience-design.md`(方案 B 三件套)实现半死源隔离与健康感知调度,P6 原场景成功率 ≥98%。 - **方案概述**: 熔断加失败率通道(双 30s 桶,429 不入)+ 开路指数退避;新增 HealthAwareSelector(EWMA×在途,0.05 地板探索)设为缺省;RetryMW 调用内失败降权。全部进程本地或并入既有 Redis gate HASH,零新依赖。**端口面变更两处(如实声明)**: ports.py 新增 OutcomeAwareSelector;`ProviderGate.record_success` 增关键字参数 `count_attempt: bool = True`——波及点全清单: ports.py:155、backends/memory/breaker.py:134、backends/redis/breaker.py:280、middleware/breaker.py:28(BreakerGate 包装)、middleware/retry.py 三处(:244 真实成功 True、:255 ResultInvalid False、:281 健康拒绝 False)、**embedding.py 两处**(:272 真实成功 True、:306 `_gate_on_terminal` ResultInvalid/健康拒绝 False)。`record_failure` 现签名已含 reason 透传(middleware/breaker.py:36),**无需改动**。 - **分支**: `feature/m25-resilience`(已建,设计已提交)。执行者须先通读设计文档全文——本计划不重复设计论证,只给任务分解;**冲突时以设计文档为准**。 - **保真校验**: 本计划触及 M2 移植的熔断 Lua(蓝本 CHS scripts.py)。铁则: §T2 只允许**新增**域与判据分支,五操作的既有语义(epoch 仅开断+1、探针租约、release_probe 置 open_until=now、fencing 判据)逐字不动;每个 Lua 改动完成后逐段比对 M2 版本,确认既有行为无漂移(契约旧用例全绿即证据)。 ## 文件结构(改动全景) | 文件 | 动作 | |---|---| | `src/polygateway/types.py` | BreakerConfig 增 4 字段(frozen 只增): `min_calls:int=10` `fail_rate:float=0.6` `window_s:float=60.0` `max_cooldown_s:float` | | `src/polygateway/ports.py` | 新增 `@runtime_checkable OutcomeAwareSelector` Protocol(仅 `record_outcome(source_name:str, ok:bool)->None`) | | `src/polygateway/config.py` | `_load_breaker`: 全局并发抬升改源级;读 4 个新可选键 `{SCOPE}__BREAKER__{MIN_CALLS,FAIL_RATE,WINDOW_S,MAX_COOLDOWN_S}`(缺省 10/0.6/60/max(300,cooldown));`_SELECTORS` 增 `health_aware` 且缺省值改之 | | `src/polygateway/sources.py` | 新增 `HealthAwareSelector`(EWMA α=0.2 初始 1.0、score=max(ewma,0.05)/(1+inflight)、P2C 头名、rng 注入) | | `src/polygateway/backends/memory/breaker.py` | 双桶窗口 + 率通道 + reopen_streak/closed_since + 429 旁路 | | `src/polygateway/backends/redis/breaker.py` | 同上,Lua 内实现(gate HASH 固定域 win_id/a0/f0/a1/f1) | | `src/polygateway/middleware/breaker.py` | BreakerGate.record_failure 透传 reason(现签名若已含 reason 则不动,核对后定) | | `src/polygateway/middleware/retry.py` | attempt_fails 局部变量传参、降权重排、record_outcome 喂数(isinstance 缓存) | | `src/polygateway/client.py` | 装配 health_aware 缺省 + selector 注入 rng | | `.env.example` | 阈值抬升注释改源级;新增 4 键注释与 SELECTOR=health_aware | | `research-wiki/migrations/chsanalyzer.md` | 标注: retry_after 上限 300s(arq defer 放大)、双通道偏离、缺省选源变更 | | `src/polygateway/embedding.py` | record_success 两调用点补 count_attempt(:272 True、:306 False);embedding 独立选源循环**不装配 health_aware、不喂 record_outcome**(其源池独立且流量小,沿用既有选源;设计范围仅 chat 主循环——口径写死于此) | | 测试 | 契约测试 = `tests/contracts/test_breaker_contract.py`(conftest `backend` fixture 参数化 memory+redis,redis 时间用例 SkipClock 跳过、由 `tests/integration/test_redis_governance_time.py` 真实等待变体补齐——新时间用例照此双轨) | ## 任务清单(每任务 = 一次提交,先红后绿) ### T1 类型与配置层 - [ ] `types.py` BreakerConfig 增 4 字段;`config.py` `_load_breaker` 改源级抬升 + 4 新键(缺省注释写明依据"韧性参数缺省先例");`_SELECTORS` 增 health_aware 并设缺省。 - 测试(先红): `tests/unit/test_config.py`(实际文件名以 grep 为准)——① 无新键时缺省 10/0.6/60/max(300,cooldown);② 源级抬升: 源 max_concurrency=8 → 阈值 max(配置,16),**无源级并发时不抬升**(全局并发 100 不再影响);③ SELECTOR 缺省 health_aware、显式 round_robin 仍可选。 - 验证: `conda run -n PolyGateway pytest tests/unit/test_config*.py -q` PASS。 ### T2 熔断双后端(核心,契约驱动) - [ ] 先在 `tests/contracts/test_breaker_contract.py` 加设计 §5 七组用例(①高失败率序列 min_calls 处开路 ②样本 None: ... # ProviderGate.record_success 签名演进(唯一端口触碰,关键字参数带默认值) async def record_success(self, entry: GateDecision, *, count_attempt: bool = True) -> GateUpdate: ... # HealthAwareSelector 构造 def __init__(self, *, rng: Callable[[], float] = random.random) -> None: ... ``` BreakerConfig 新字段名: `min_calls` `fail_rate` `window_s` `max_cooldown_s`(env 键 `{SCOPE}__BREAKER__MIN_CALLS` 等四个)。gate HASH 新域名: `win_id a0 f0 a1 f1 reopen_streak closed_since`。 ## 风险与回滚 - 每任务独立提交,T2 是最大风险面(Lua)——契约旧用例全绿是保真门,任何旧用例破坏立即停下按 systematic-debugging 根因。 - P6 重跑约 1-2 小时、成本约 25 元/轮(已有价格表),预算帽/守卫机制原样生效。