# CHSAnalyzer 迁移文档(迁移即验收) > **定位**: 本文是 `ARCHITECTURE.md §11.3` 的展开——库建成后如何合并进 CHSAnalyzer、替换哪些内部组件。与 ARCHITECTURE.md 冲突时**以 ARCHITECTURE.md 为准**。CHSAnalyzer 是三项目中迁移难度最高、能力对标要求最高的一个:库必须先达到其治理能力**逐项对等**(§1 对标清单),迁移才有动机。全部结论基于 2026-07-20 对 `reference/CHSAnalyzer/` 的代码实测(file:line 为证)。 ## 1. 迁移目标与验收定义 **验收公式**: 删除 §2 标注"删除"的文件 → 业务侧 import 换成 `polygateway` + shim → 原测试全绿(被删组件的自测如 `tests/unit/test_governance.py`、`tests/integration/test_redis_limiter.py` 随组件迁入库侧,不计入"原测试")。 | 阶段 | 可替换范围 | 前置里程碑 | |---|---|---| | M2 后 | VLM scope 治理核心(governance/limiter/scripts/provider_gate/selector/streaming + VLM invoker) | Redis 六道闸+契约测试、多源选源、跨进程熔断、背压 stall | | M3 后 | OCR scope(MonkeyOcrParseInvoker → `OcrLayoutPort`)、`core/eval/judge.py` 收编,**全量迁移** | OCR 端口族 + MonkeyOCR transport | **能力对标清单**(库必须逐项对等,任一缺失即库的边界缺口;✅ = M2 验收打钩 2026-07-21,证据为库测试): | # | 能力 | 项目侧证据 | M2 打钩 | |---|---|---|---| | 1 | 六道闸原子限流(全局/单源 × 并发/RPM/TPM),拒绝零副作用 | `app/coordination/scripts.py:6-34` | ✅ `backends/redis/limiter.py` Lua 逐字移植;契约 redis 参数全绿 | | 2 | TPM 预扣入场、settle 按实际 usage 落回 acquire 窗口多退少补 | `limiter.py:55-62`、`scripts.py:44-48` | ✅ 契约 `test_prededuct_and_settle_refund[redis]` | | 3 | 并发 lease 带 TTL(防进程死亡泄漏)+ release/settle 幂等 | `limiter.py:46-63`、`tests/contracts_limiter.py:66-73` | ✅ 真实等待变体 `test_variant_lease_expiry_reclaims_slot` + 幂等契约 | | 4 | 窗口 id 用 Redis 服务器时钟(多进程口径统一) | `limiter.py:95-98` | ✅ `test_rpm_window_rollover_resets_quota` + 跨连接 RPM 用例 | | 5 | 跨进程熔断:单探针半开、探针租约 TTL、epoch fencing、force_open | `scripts.py:82-225`、`provider_gate.py` | ✅ `backends/redis/breaker.py`;熔断契约 + 7 个真实等待变体 | | 6 | 背压 stall 双条件判卡死(本地等够 + 全局无进展)+ mark_progress 全局活性 | `governance.py:270-281`、`limiter.py:193-209` | ✅ `middleware/retry.py` + `test_backpressure` 四象限 + 跨连接进度可见 | | 7 | 换源重试:失败跨源累计、退避取 Retry-After 较大值、源冷却备忘 | `governance.py:105-118, 244-261` | ✅ M1 已交付(`test_retry`);M2 联合验证下重验 | | 8 | 错误分类:429 body 细分 insufficient_quota、工件级失败不熔断 | `invokers.py:144-166`、`governance.py:237-239` | ✅ M1 已交付(`test_openai_compat`/ResultInvalid 不熔断) | | 9 | scope 级不可用结构化错误(reason + retry_after_s),供 worker 延期重投 | `errors.py:140-180`、`workers/tracking.py:406-428` | ✅ M1 错误模型(5 值 reason 勘误后)+ M2 增 stalled 真实触发路径 | | 10 | 流式三层活性看门狗;thinking token 刷活性不计结果 | `streaming.py`、`invokers.py:55-79` | ✅ M1 已交付(`test_streaming`) | > 注: OCR invoker 能力(MonkeyOCR 两端点)不在本清单——归 M3。 ## 2. 现状盘点(行数 wc -l 实测) | 文件 | 行数 | 职责一句话 | 迁移后命运 | |---|---|---|---| | `app/providers/governance.py` | 350 | 多源治理编排(选源+限流+熔断+重试+背压) | **删除**(库 middleware 继任;Governed 外壳变 shim) | | `app/providers/invokers.py` | 552 | VLM SSE invoker + MonkeyOCR `/parse` invoker + 错误翻译 | **删除**(库 transport 继任) | | `app/providers/selector.py` | 42 | round_robin / least_inflight 选源 | **删除**(库 `SourceSelector` 同款) | | `app/providers/streaming.py` | 128 | 三层活性看门狗纯函数 | **删除**(库 `streaming.py` 同款移植) | | `app/coordination/limiter.py` | 209 | Redis 六道闸限流 + Permit | **删除**(库 `backends/redis`) | | `app/coordination/scripts.py` | 245 | 限流/熔断全部 Lua 脚本 | **删除**(随限流/熔断迁库) | | `app/coordination/provider_gate.py` | 179 | Redis 跨进程熔断门 | **删除**(库 `RedisBreakerState`) | | `tests/contracts_limiter.py` | 79 | 限流器契约(5 条,任一实现须过) | **迁入库**(M2 随实现交付) | | `core/eval/judge.py` | 201 | LLM 语义裁判,**同步裸 SDK 无治理**(反面教材) | **改写**(走库,见 §4) | | `app/domain/errors.py` | 226 | 领域异常;其中 82-180 行为 Provider 错误族 | **改写**(Provider 族删除换库 `errors.py`;业务异常保留) | | `app/config.py` | 574 | Settings + 多源/全局限额/retry/breaker/背压解析 | **改写**(治理配置解析约 43-410 行删除,由库 `from_env` 继任;Settings/S3/Zip/Stage 保留) | | `app/container.py` | 511 | 组装根;225-320 行手工装配治理栈 | **改写**(治理装配换库工厂,其余保留) | | `app/ports.py` | 704 | 全部端口;其中 396-529 行为限流/熔断端口 | **改写**(限流/熔断端口删除;`VlmProvider`/`TableLocator`/`ProviderOutcome`/`Usage` 保留为业务端口,由 shim 实现) | **明确留在业务侧(零改动或仅换上游类型)**: `position_scheduler.py`(347 行,按 Session 轮转的公平调度,ARCHITECTURE §2.3 明确不进库)、`table_locator.py`(111 行,裁剪/坐标映射/marker 推算等几何映射)、`marker_imaging.py`(96 行,拼图增强)、`preprocessing.py`(64 行)、`extractors.py`(108 行)/`classifiers.py`(47 行)/`vascular_positioner.py`(378 行)的业务解析与编排、`worker_liveness.py`(238 行,任务级活性,非调用治理)、`redis_pool.py`(22 行,arq/调度器仍需)。 ## 3. 组件替换映射表 | CHSAnalyzer | PolyGateway 对应物 | 签名兼容性(实测) | shim 形态 | |---|---|---|---| | `GovernanceCore.run(call)`(governance.py:200) | 中间件洋葱整体 | 泛型闭包 → `client.chat(request)`,范式不同 | 无需 shim,整体替换 | | `VlmProvider.complete(image, instruction)`(ports.py:552-555) | `client.chat(messages)` | **不兼容**:bytes+str vs messages 数组 | `PgwVlmProvider`:组 content 数组(text+image_url base64)→ chat → 映射回 `ProviderOutcome` | | `ProviderOutcome{text,source_name,model,usage,raw}`(ports.py:541-548) | `LLMResponse`(ARCH §5.1) | 字段可全映射:text←content、usage.total_tokens←prompt+completion、elapsed_s←latency_ms/1000 | shim 内一次转换 | | `Permit{release, settle(actual_tokens:int)}`(ports.py:495-504) | 库 `Permit.settle(actual_usage)`(ARCH §7.3) | 语义同;settle 参数类型 int vs usage 需 M2 定稿 | 库内部端口,业务不触及 | | `ConcurrencyLimiter`(ports.py:508-529,含 `mark_progress`/`progress_age_s`/`source_stats`) | 库限流端口(ARCH §7.3 同款契约) | 对等(§7.3 已列 mark_progress/progress_age) | 无 | | `ProviderGate` + `ProviderGateDecision`(epoch/is_probe/probe_owner)(ports.py:406-491) | 库熔断端口 + epoch fencing(ARCH §7.4) | 状态机对等;probe TTL 见 §9-G5 | 无 | | `TableLocator.locate(image)`(ports.py:577-580) | 业务端口保留,内部改调 `OcrLayoutPort.parse_layout` | 库返回 elements 全量(para_blocks 提取,与 tables 列表 bbox 逐一相等——M3 设计 §1.2 35 样本取证);项目 shim 约 5 行: 取首个 type=="table" 元素 + `int()` 四元组 | `table_locator.py` 改写取数逻辑 | | 错误三分类 + `OcrResultInvalidError` + `ExtractionParseError` | 库四分类(ARCH §6.1) | Transient/SourceDead/RequestRejected 一一对应;`OcrResultInvalidError` 与 `ExtractionParseError` 合并进 `ResultInvalidError` | worker `_TERMINAL`/重试分支改 except 库异常 | **ProviderUnavailableError 的专门论述**(errors.py:140-180): 这是项目独有的 **scope 级不可用**语义——不是"某次调用失败",而是"整个 VLM/OCR 作用域暂时无源可用,任务应延期且**不消耗业务失败预算**"。它携带 `scope`、7 种受控 `reason`(network_error/timeout/rate_limited/source_dead/circuit_open/retry_exhausted/stalled)、`retry_after_s`(读共享熔断门取全 scope 最早恢复时刻,governance.py:182-198)、`reasons`(per-source 失败原因字典)。消费点 `workers/tracking.py:406-428`:捕获后 `finish_deferred_attempt`(FAILED 但不计失败预算)并 `raise Retry(defer=retry_after_s 派生)`——**arq 级延期重投**。ARCHITECTURE §6.1 的承接:`CircuitOpenError`/`AllSourcesExhausted` 覆盖了"发生了什么",但**未定义结构化字段**(retry_after_s/reason/per-source reasons),而 tracking.py 的延期时长直接依赖 `retry_after_s`。承接方案:库两异常须携带 `retry_after_s`(熔断后端最早恢复时刻)与逐源原因;项目侧留 10 行翻译 shim 把库异常包成 `ProviderUnavailableError`(或 tracking.py 直接改 except 库异常)。字段缺失则该行为不可复现 → **⚠️ 架构缺口 G1**。 ## 4. 调用点清单(grep 实测) | 调用点 | 现状 | 迁移后形态 | |---|---|---| | `app/providers/extractors.py:101` | `await self._vlm.complete(cropped, instruction)` | 不变(经 `PgwVlmProvider` shim) | | `app/providers/classifiers.py:33` | 同上 | 不变(同上) | | `app/providers/vascular_positioner.py:329-330` | `scheduler.acquire(session_id)` 内 `vlm.complete` | 不变;公平调度留业务侧,包在库调用外 | | `app/pipeline/extract_table.py:141` | `await self._locator.locate(data)` | 不变(`table_locator` 内部改调 `OcrLayoutPort`) | | `app/workers/startup.py:123` | `container.build_extraction_pipeline(...)` | 装配体内部换库工厂(§5) | | `app/workers/startup.py:196` | `container.build_vlm_provider(environ)` | 同上 | | `app/workers/tracking.py:22,406-428` | `except ProviderUnavailableError` → 延期重投 | except 库异常(或经翻译 shim),**实质改动点之一** | | `core/eval/judge.py:34-52` | 同步裸调 anthropic/openai SDK,零治理;JSON 解析手写 `find('{')`+`rfind('}')`(:112-123) | `_call_llm` 改 `asyncio.run(client.chat(...))`(Judge 端口是同步的、runner 无事件循环,桥接安全);解析换库 `JsonRepairStrategy`,**实质改动点之二** | **"judge 是唯一需要业务代码实质改动的点?"——否。** 实质改动共两处:judge(新增治理能力,且默认 provider=anthropic,须经实验室 OpenAI 兼容中转网关接入,或等库增 Anthropic transport——D2 已预留端口但无里程碑)与 `workers/tracking.py` 的异常分支(§3 G1)。其余调用点全部躲在 `VlmProvider`/`TableLocator` 业务端口后,shim 保签名即零改动。**arq worker 与治理层的边界**清晰不变:worker(编排层,任务重试/死信/liveness)只透过这两个端口消费治理层,库恰好站在端口之下,边界无移动。 ## 5. 配置迁移 **多源键名**: 库的 `{SCOPE}__{PROVIDER}__{N}__{FIELD}` 约定即源自本项目(config.py:107-125),`VLM__QWEN__1__*`、`OCR__MONKEY__1__*` 及字段名(BASE_URL/API_KEY/MODEL/MAX_CONCURRENCY/RPM/TPM/EST_TOKENS/TIMEOUT_S/TTFT_TIMEOUT_S/INTER_TOKEN_TIMEOUT_S/ENABLE_THINKING)**原样继承,零改名**。 | 项目键(.env.example 实测) | 库对应 | 差异 | |---|---|---| | `{SCOPE}__{PROVIDER}__{N}__{FIELD}` | 同名继承 | 无;`EST_TOKENS` 见 ⚠️ G2 | | `{SCOPE}__GLOBAL__MAX_CONCURRENCY/RPM/TPM`(config.py:249-271) | 全局闸限额 | ARCH §9 未定义 GLOBAL 段命名,M2 设计须定(建议原样继承) | | `{SCOPE}__SELECTOR`(round_robin/least_inflight) | `SourceSelector` 策略选择 | 命名待 M1/M2 定稿,建议继承 | | `{SCOPE}__RETRY__MAX_ATTEMPTS/BACKOFF_BASE_S/BACKOFF_MAX_S` | RetryPolicy | ⚠️ G4:ARCH §9 只列平铺 `LLM_MAX_RETRIES` 等键,无 per-scope 形态 | | `{SCOPE}__BREAKER__FAIL_THRESHOLD/COOLDOWN_S` | BreakerConfig | 同 G4 | | `{SCOPE}__BACKPRESSURE__STALL_WINDOW_S/POLL_INTERVAL_S` | 配额满 wait+stall 判定参数 | 同 G4;项目还有废弃键守卫(config.py:374-378)可放弃 | | (代码常量)`_LEASE_TTL_MS=1_500_000`(container.py:99) | permit 租约 TTL | 项目是常量+装配守卫,库应配置化并保留守卫(G6) | | 新增 | `PGW_LIMITER_BACKEND=redis`、缓存 namespace/TTL、遥测后端等 | 净新增能力的配置 | **装配对比**: container.py:225-320 手工装配(约 96 行:load_sources → 守卫 → Redis 池 → limiter → selector → retry/backpressure/breaker → gate → 每源 httpx client + invoker → GovernanceCore,失败逐层清理)。迁移后: ```python # after(示意;实际 API 以 M1 设计文档为准) vlm_client = GatewayClient.from_env(role="VLM") # 限流/熔断/重试/缓存/遥测/资源生命周期全内置 ocr_client = GatewayClient.from_env(role="OCR") stack = ExtractionProviderStack( vlm=PgwVlmProvider(vlm_client), # shim: 实现 app.ports.VlmProvider table_locator=BusinessTableLocator(PgwOcrLayout(ocr_client)), # 几何映射留业务 ) ``` `load_single_vlm_capacity`(config.py:274-299,position 调度器容量与治理并发口径一致性校验)保留,改读库的 SourceConfig 聚合结果。 ## 6. 迁移步骤(生产服务,按 scope 灰度) 可灰度性依据:治理栈按 scope 独立装配(container.py:303-320),VLM 与 OCR 的 Redis key 前缀互不相交(`cclimit:{scope}:*`/`provider_gate:{scope}:*` vs 库前缀),且 worker 按角色分进程部署(startup.py:116/189)——**可以一个 scope 一个 scope 迁**。同一 scope 内禁止新旧栈并跑(限额会被双份计数),须整 scope 原子切换。 | 步骤 | 内容 | 验证 | 回滚点 | |---|---|---|---| | S0 | 基线:记录当前测试全绿证据;`chs` 环境安装 `polygateway[redis,...]`;git 提交回滚点 | `make test` 全绿 | git tag | | S1(M2) | 库侧先行:`contracts_limiter.py` 5 条契约对库 RedisLimiter 跑通(真实 Redis);项目零改动 | 契约测试全过 | 无风险 | | S2(M2) | 引入装配开关(env:`GOVERNANCE_IMPL=legacy\|pgw`,按 scope);实现 `PgwVlmProvider` shim + G1 异常翻译 | 单测 shim 映射;integration 双实现对拍 | 开关切回 legacy | | S3(M2) | **VLM scope 灰度**:先 position worker(消费面最小,仅 vascular_positioner)观察,再 pipeline worker(extract) | e2e 提取/定位测试;遥测比对错误率与延迟 | 开关 + 旧代码未删 | | S4(M3) | **OCR scope**:`table_locator.py` 改写消费 `OcrLayoutResult`,切 OCR scope | extract_table integration 全绿 | 同上 | | S5(M3) | judge 收编:`_call_llm` 走库(异步桥接),解析换 `JsonRepairStrategy` | `tests/unit/test_eval_judge.py` | judge 独立文件,单独回滚 | | S6 | 清场:执行 §2 删除清单、errors.py Provider 族删除、tracking.py 定稿、拆开关 | **原测试全绿(验收门)** + 独立 verifier | S5 前的 git 提交 | ## 7. 旧版行为审计(逐条:保留/替换/修复/有意放弃) | 现有行为(证据) | 语义 | 处置 | |---|---|---| | 六道闸全有或全无,拒绝零副作用(scripts.py:6-33) | 任一闸不过 return 0,不加计数 | **保留**(契约测试守护) | | token 预扣:成功按实际 settle、transient 按 est 保守结算、400/SourceDead 全额退款(governance.py:219, 252-254, 262-266) | 差异化结算防少计 | **保留** | | settle 落回 acquire 时刻窗口(limiter.py:55-62) | 跨窗口退款不污染新窗口 | **保留** | | Redis 服务器时钟生成窗口 id(limiter.py:95-98) | 多进程口径统一 | **保留**(ARCH §7.3 已承诺) | | lease TTL 1500s + `timeout_s ≤ TTL` 装配守卫(container.py:99-112) | 防慢请求并发槽被提前回收 | **保留**(库配置化,守卫进 from_env,G6) | | 单探针半开 + 探针租约 probe_ttl=max(timeout)+5s(scripts.py:96-105, container.py:274-276) | 防惊群 + 探针持有者死亡自动恢复 | **保留**(probe TTL 见 G5) | | epoch fencing:迟到结果 applied=False(scripts.py:110-143) | 防旧世代污染新状态 | **保留**(ARCH §7.4 已承诺) | | 取消/本地拒绝时 release_probe 不判健康(governance.py:230-243) | 探针让位但源保持 OPEN | **保留**(G5) | | mark_progress + 双条件 stall(本地等够 且 全局无进展,双时钟各自比较)(governance.py:270-281) | 防冷启动/多进程误判 | **保留**(ARCH §7.3 已承诺) | | 源冷却备忘:gate 拒绝后本地记冷却截止,期内不取 permit(governance.py:105-107, 129-131) | 治标:先 permit 后 gate 导致熔断期白烧 RPM | **修复**:库层序熔断在限流外(ARCH §4.3),开路源不再消耗限流;备忘作为选源优化保留(ARCH §7.4 已列) | | `OcrResultInvalid` 熔断记成功、不换源、消耗任务失败预算(governance.py:237-239) | 坏结果≠坏服务 | **保留**(库 `ResultInvalidError`,ARCH §6.3) | | RequestRejected 细分:provider 真实响应→gate 记成功;本地拒绝(如坏图格式)→仅释放探针(governance.py:228-236) | HTTP 响应证明服务活着 | **保留**(反向约束 M2) | | Retry-After 仅支持秒数形态(invokers.py:127-141) | HTTP-date 返回 None | **有意放弃** date 形态(ARCH §6.2 同款) | | 429 body 细分 insufficient_quota → SourceDead(invokers.py:144-166) | 欠费≠限流 | **保留**(ARCH §6.1 已承诺) | | 零 content 提前结束 → Transient "early_eof";有 content 缺 [DONE] → 打捞并埋点 "missing_done"(invokers.py:306-313) | 线路级异常定性(D2 的核心价值) | **保留** | | usage 缺失按 est_tokens 估算并标 `estimated`,不静默用 0(invokers.py:241-254) | 保守计量 | **保留**(`usage_source` 已进 ARCH §5.1;依赖 G2) | | reasoning_content 刷新活性但不计入结果;ttft=首个任意 token(invokers.py:55-79, 336-364) | 防 thinking 模型被看门狗误杀 | **替换+增强**:库把 thinking 收进 `LLMResponse.thinking`(不再丢弃);活性语义必须保留(反向约束 M1) | | enable_thinking=True 不注入参数、False 注入关闭参数(invokers.py:230-238) | 与 D11 注册表"声明注入方式"方向相反 | **替换**(provider 注册表须支持"注入关闭参数"形态) | | 图片 magic bytes 探测,非 PNG/JPEG 抛 RequestRejected(invokers.py:116-123) | 本地快速拒绝 | **保留**(移入库 transport) | | 换源重试失败跨源累计,`fails > max_attempts` → retry_exhausted(governance.py:255-260) | 重试预算是全局的不是 per-source | **保留**(M2 设计明确计数口径) | | 整圈 gate 全拒 → circuit_open 快失败;配额满则 poll+jitter 重探(governance.py:210-214, 283-285) | 区分"全熔断"与"配额满" | **保留**(映射 CircuitOpenError vs wait,G1) | | MonkeyOCR 两段协议(POST /parse → GET ZIP)+ bbox 有限性/顺序/退化校验(invokers.py:489-552, 427-479) | 数值防御 | **保留**(下沉库,ARCH §7.10 已承诺;首表选取留业务) | | `success!=true` 拒绝不带 status_code,gate 走"本地拒绝"分支不记成功(invokers.py:514-519 + governance.py:228-236) | 熔断口径 | **有意修复**(M3): 库附 status_code=200,按"响应即健康"记成功——200 响应确证服务活着 | | OCR 路径共用 `_translate_429`(insufficient_quota→SourceDead、Retry-After 解析) | 429 细分 | **有意放弃**(M3): MonkeyOCR 无鉴权无计费,429 语义不存在,防御性归 Transient | | 无响应缓存 | 相同图+指令重复付费 | **替换**:迁移后净增缓存(风险见 §8) | | judge 同步裸 SDK 零治理(judge.py:34-52) | 反面教材 | **修复**(§4) | ## 8. 行为差异与风险 | 差异/风险 | 论述与缓解 | |---|---| | **净增响应缓存**(最大行为变化) | 同图+同指令缓存命中对本项目语义:提取/分类是**确定性期望**任务(信度评估恰恰希望同输入同输出),命中可接受且省费;但科研上做"提取可靠性(信度)"实验时,重复采样**必须**绕过缓存——用 cache salt 按实验 epoch 强制重采样(ARCH §7.5)。医疗合规:缓存值含影像衍生文本,Redis 为项目私有实例,与现状(Redis 已存任务/调度数据)风险面一致。配置:`namespace="chsanalyzer:{scope}"`(单租户,无租户维度),TTL 必填;**灰度期建议 VLM scope 先关 CacheMW**,行为与旧版完全一致,稳定后再开 | | 熔断/限流检查顺序变化 | 旧:先 permit 后 gate(有 RPM 白烧缺陷);新:熔断在限流外。行为上开路期间 RPM 消耗下降,属改善;但整体准入顺序依赖 G3 澄清 | | thinking 内容保留 | 旧版丢弃 reasoning 文本,新版进 `LLMResponse.thinking` 与遥测——遥测库体积增大,属预期(遥测必录) | | Lua 契约漂移 | 库重写 Lua 时语义漂移风险 → `contracts_limiter.py` 5 条契约(并发占用型/RPM/TPM 预扣结算退款/release 幂等/progress 新鲜度)先于项目迁移在库侧对真实 Redis 跑通(S1),并随库永久交付 | | Redis key 迁移 | 新旧前缀不同,切换瞬间限流/熔断状态清零(短暂过放行)→ 选低峰切换;旧 key 有 TTL 自然过期,无需清理 | | 双栈并跑超限 | 同 scope 新旧并跑限额双计 → 整 scope 原子切换 + 部署脚本禁止混版 worker(§6) | ## 9. 对库的反向约束清单(⚠️ = 架构缺口) | # | 约束 | 里程碑 | 状态 | |---|---|---|---| | R1 | thinking token 刷新看门狗活性、ttft=首个任意 token(invokers.py:55-79) | M1 | ARCH §7.6 未明说,须写进 M1 设计 | | R2 | SSE 异常细分(malformed_json/early_eof/missing_done)分类+埋点;missing_done 打捞不失败 | M1 | ARCH §6.2 部分覆盖,细目须进 M1 设计 | | R3 | provider 注册表支持"注入关闭参数"形态(enable_thinking=False) | M1 | D11 方向相反,小修 | | R4 | 六道闸+契约 5 条、服务器时钟窗口、settle 落 acquire 窗口、transient 按 est 保守结算 | M2 | §7.3 大体覆盖 | | R5 | RequestRejected 二分(真实响应记成功/本地拒绝释放探针);换源重试跨源计数口径 | M2 | 须进 M2 设计 | | R6 | OCR ZIP 协议 + bbox 数值防御下沉;OCR Usage=0;glm 白名单预留 | M3 | §7.10 已覆盖 | | **G1** | ✅ 已闭(M3 核实): 库 `GatewayUnavailableError` 一族自 M1 起携 `scope/reason/retry_after_s/per_source_reasons`(errors.py:74-105),chat/embedding/OCR 三循环抛出点均已填充且有契约测试钉住;项目侧仅剩约 10 行翻译 shim(库异常 → ProviderUnavailableError)或 tracking.py 直接 except 库异常 | M2 | 已闭 | | **G2** | ⚠️ `est_tokens`(TPM 预扣常量 + usage 缺失兜底,config.py:55)不在 ARCH §7.7 SourceConfig 字段清单;§7.3 `try_acquire(source, est_tokens)` 的 est 来源未定义 | M2 | **架构缺口**,修订 §7.7 | | **G3** | ⚠️ §4.3 层序图文矛盾:图示 熔断→限流→重试(重试最内),但理由要求"每次重试重新过限流闸"且熔断/限流是 per-source 的、选源在重试循环内(governance.py:120-167 实践为每次尝试执行 选源→冷却备忘→permit→熔断门)。洋葱不澄清"逐次准入"机制则多源语义无法成立 | M2 | **架构缺口**,澄清 §4.3/§4.4 | | **G4** | ⚠️ per-scope 韧性配置命名(`{SCOPE}__RETRY__*`/`BREAKER__*`/`BACKPRESSURE__*`/`SELECTOR`/`GLOBAL__*`)未进 ARCH §9,现文只有平铺 `LLM_*` 键;CHSAnalyzer 的 VLM/OCR 两 scope 参数各异,平铺键无法表达 | M2 | **架构缺口**,修订 §9 | | **G5** | ⚠️ 半开探针租约 TTL(探针持有者死亡后 TTL 过期自动可再探,scripts.py:96-105)与 `release_probe` 操作未见于 ARCH §7.4(只写单探针/epoch fencing);缺失则探针死锁 | M2 | **架构缺口**,修订 §7.4 | | G6 | 装配期不变式守卫:`timeout_s*1000 ≤ lease TTL`(container.py:105-112)、`stall_window ≥ 最慢源 ttft/timeout`(container.py:115-124)须进库 from_env;契约中 settle/release **幂等性**须写进 §7.3 契约文字 | M2 | 缺口(轻),M2 设计补 | | G7 | judge 默认 provider=anthropic:走实验室 OpenAI 兼容中转即可接入,若须直连 Anthropic 官方 API 则需新 transport(D2 有端口无里程碑)——迁移前与人类确认网关路径 | M3 | 决策项,非缺口 | ## M2.5 追记(2026-07-21,治理韧性设计的行为偏离) | 偏离 | 对 CHS 迁移的影响 | |---|---| | 熔断增设失败率通道(窗口样本 ≥ min_calls 且失败率 ≥ fail_rate 即开路;429 不入两通道) | CHS 纯连续失败语义保留为通道 1;高失败率"半死源"将比 CHS 更早被隔离——P6 实证 CHS 语义对 10% 成功率源永不开路(病灶 1) | | 开路时长指数递增,`retry_after_s` 上限由 cooldown_s(60s)变为 max_cooldown_s(缺省 300s) | **G1 消费点注意**: tracking.py arq defer 延期时长最多放大 5 倍;属期望行为(反复坏的源就该等更久) | | 阈值自动抬升由全局并发改为源级并发 | CHS .env 注释约定的本意(防并发误熔)保留;全局并发抬升是 M2 错误移植,已废除 | | 缺省选源 round_robin → health_aware(EWMA×在途 P2C) | 显式配置 `LLM__SELECTOR=round_robin` 者行为不变;迁移时建议直接吃新缺省 | | 429 不消耗重试预算(pushback 语义),调用级时间兜底 = 双条件 stall(本地超窗且全局无进展) | 饱和期调用延迟大幅拉长(全局有进展时可超 stall_window);CHS 快失败偏好者可调小 `LLM__BACKPRESSURE__STALL_WINDOW_S` | | **AIMD 自适应并发**(库常量: 每源初始 8、429 削减 ×0.5、成功 +1/limit、下限 1、ceiling=max(64, 源级 max_concurrency)) | 冷启动每源并发从"无上限"变为 8 起步爬升——高吞吐下游首分钟吞吐低于旧行为;无 env 开关(韧性常量,同 jitter 系数先例),需要禁用者构造函数注入自定义 pacer | | 连败通道受健康证据抑制(窗口样本 ≥ min_calls 且失败率 < fail_rate 时 5 连败不开路) | CHS "连败必开"在高流量健康源上不再成立(防随机噪声误熔唯一好源);冷启动/低流量语义不变 | | 结构化重问缺省 1→2 | 解析失败时最多多一跳成本;`PGW_STRUCTURED_MAX_RETRIES=1` 可显式还原 |