docs: address M3 plan review findings
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
- **方案概述**: 方案 A——仿 `EmbeddingClient`(`src/polygateway/embedding.py`)的独立精简治理循环,复用 QuotaGate/BreakerGate/backoff_delay/_failure_reason/SourceCooldownMemo/TelemetryEmitter/选源器;新增 `OcrTransport` 端口与 `transports/monkey_ocr.py`。不触碰 `middleware/retry.py`。
|
- **方案概述**: 方案 A——仿 `EmbeddingClient`(`src/polygateway/embedding.py`)的独立精简治理循环,复用 QuotaGate/BreakerGate/backoff_delay/_failure_reason/SourceCooldownMemo/TelemetryEmitter/选源器;新增 `OcrTransport` 端口与 `transports/monkey_ocr.py`。不触碰 `middleware/retry.py`。
|
||||||
- **涉及技术**: Python 3.11 + httpx + zipfile/BytesIO(标准库,无新依赖);真实 MonkeyOCR 服务 10.77.0.20:7866/7867;分支 `feature/m3-ocr`;所有命令 `conda run -n PolyGateway <cmd>`。
|
- **涉及技术**: Python 3.11 + httpx + zipfile/BytesIO(标准库,无新依赖);真实 MonkeyOCR 服务 10.77.0.20:7866/7867;分支 `feature/m3-ocr`;所有命令 `conda run -n PolyGateway <cmd>`。
|
||||||
- **执行方式**: 中等规模、任务间强顺序依赖 → 直接按计划实现,不用 subagent-driven-development。
|
- **执行方式**: 中等规模、任务间强顺序依赖 → 直接按计划实现,不用 subagent-driven-development。
|
||||||
|
- **structured-logging 已完成**(SOP Phase1-2,先于本计划): 无新表,复用 `llm_calls`;metric `ocr-call-success` 已注册(`research-wiki/metrics/ocr-call-success.md`,含口径/埋点位置/基线,commit e51f077),边 metric→schema、schema→design 已连。埋点清单落 T5.4。
|
||||||
|
- **测试结果门(全任务统一)**: 每个行为任务的新测试用例必须**先于实现运行一次并出示失败证据**(ImportError/AttributeError/断言失败),实现后转 PASS——T1-T8 的验证行均按此执行,不再逐条重复。
|
||||||
|
|
||||||
## 文件结构(锁定分解)
|
## 文件结构(锁定分解)
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ class OcrTransport(Protocol):
|
|||||||
- `SourceDeadError`/`TransientError`: `record_failure(entry, reason, dead)` + `selector.record_outcome(name, False)` + reasons[name]=reason;fails+=1,`fails >= retry.max_attempts` → `AllSourcesExhausted(retry_exhausted, retry_after_s=backoff_base_s, per_source_reasons=reasons)`;否则非 dead 退避 `backoff_delay(...)` 后换源。
|
- `SourceDeadError`/`TransientError`: `record_failure(entry, reason, dead)` + `selector.record_outcome(name, False)` + reasons[name]=reason;fails+=1,`fails >= retry.max_attempts` → `AllSourcesExhausted(retry_exhausted, retry_after_s=backoff_base_s, per_source_reasons=reasons)`;否则非 dead 退避 `backoff_delay(...)` 后换源。
|
||||||
- permit 在 finally `settle(0)` + `release()`(恒 0,OCR 无 token)。
|
- permit 在 finally `settle(0)` + `release()`(恒 0,OCR 无 token)。
|
||||||
- 无可运行源: 全 gate 拒 → `CircuitOpenError(retry_after_s=await breaker.retry_after_s(names))`;fail_fast → `AllSourcesExhausted(quota_exhausted)`;双条件 stall(本地超 stall_window_s AND `progress_age_s()` 超窗)→ `AllSourcesExhausted(stalled)`;否则 poll_interval 抖动等待。
|
- 无可运行源: 全 gate 拒 → `CircuitOpenError(retry_after_s=await breaker.retry_after_s(names))`;fail_fast → `AllSourcesExhausted(quota_exhausted)`;双条件 stall(本地超 stall_window_s AND `progress_age_s()` 超窗)→ `AllSourcesExhausted(stalled)`;否则 poll_interval 抖动等待。
|
||||||
4. 遥测(设计 §7): 每次尝试(成败/取消)经 `TelemetryEmitter.emit_attempt` 单 helper;request 占位 `[{"role":"user","content":f"<ocr:{kind} image_bytes={len(image)}>"}]`;成功 response 用 LLMResponse 包装: content = text 截断 200 字 / `f"<elements n={len(elements)} pages={len(page_sizes)}>"`,tokens=0、ttft/inter=None、cache_hit=False;写失败降级不冒泡(Emitter 内建)。
|
4. 遥测(设计 §7): 每次尝试(成败/取消)经 `TelemetryEmitter.emit_attempt` 单 helper;request 占位 messages **包成 `ChatRequest`**(embedding.py `_emit`@345 先例)——`[{"role":"user","content":f"<ocr:{kind} image_bytes={len(image)}>"}]`;成功 response 用 LLMResponse 包装: content = text 截断 200 字 / `f"<elements n={len(elements)} pages={len(page_sizes)}>"`,tokens=0、ttft/inter=None、cache_hit=False;写失败降级不冒泡(Emitter 内建)。
|
||||||
5. `check_health() -> dict[str, bool]`: `asyncio.gather` 并发逐源 `transport.check_health(source=s)`,返回 `{name: bool}`;不吞 CancelledError(gather 天然穿透);不健康源 logger.warning。
|
5. `check_health() -> dict[str, bool]`: `asyncio.gather` 并发逐源 `transport.check_health(source=s)`,返回 `{name: bool}`;不吞 CancelledError(gather 天然穿透);不健康源 logger.warning。
|
||||||
6. 工厂: `from_settings(OcrSettings, *, limiter=None, breaker=None, telemetry=None)` 复用 client.py 的 `_build_limiter/_build_breaker/_build_selector/_build_telemetry`;**装配防御**: 任一 source.provider != "monkey" → `ValueError`(D9 GLM 预留档,严禁静默用 MonkeyOcrTransport 打别家端点);`from_env(scope="OCR")` 委托。`aclose()`/async context manager 与 EmbeddingClient 对称。
|
6. 工厂: `from_settings(OcrSettings, *, limiter=None, breaker=None, telemetry=None)` 复用 client.py 的 `_build_limiter/_build_breaker/_build_selector/_build_telemetry`;**装配防御**: 任一 source.provider != "monkey" → `ValueError`(D9 GLM 预留档,严禁静默用 MonkeyOcrTransport 打别家端点);`from_env(scope="OCR")` 委托。`aclose()`/async context manager 与 EmbeddingClient 对称。
|
||||||
7. G1 钉住: 上述三处 scope 级异常全部携带非空 `per_source_reasons` 与 `retry_after_s`(circuit_open/stalled 读熔断后端)。
|
7. G1 钉住: 上述三处 scope 级异常全部携带非空 `per_source_reasons` 与 `retry_after_s`(circuit_open/stalled 读熔断后端)。
|
||||||
@@ -123,7 +125,7 @@ class OcrTransport(Protocol):
|
|||||||
- SourceDead: 立即换源(immediate,不退避)、force_open;
|
- SourceDead: 立即换源(immediate,不退避)、force_open;
|
||||||
- ResultInvalid: 直接上抛、gate 记成功且 count_attempt=False(RecordingGate 桩断言)、不喂 selector;
|
- ResultInvalid: 直接上抛、gate 记成功且 count_attempt=False(RecordingGate 桩断言)、不喂 selector;
|
||||||
- RequestRejected 带 status_code: 同上直抛记成功;不带 status_code: 仅探针归还;
|
- RequestRejected 带 status_code: 同上直抛记成功;不带 status_code: 仅探针归还;
|
||||||
- retry_exhausted: 抛 AllSourcesExhausted,`per_source_reasons` 非空、`retry_after_s > 0`(G1 契约);circuit_open 与 stalled 同断言;
|
- retry_exhausted: 抛 AllSourcesExhausted,`per_source_reasons` 非空、`retry_after_s > 0`(**此三条断言即设计 §6 ③ 的 G1 契约钉,落在本单测文件,不另进 tests/contracts/**——contracts 层是限流/熔断后端双实现契约,G1 是客户端循环行为);circuit_open 与 stalled 同断言;
|
||||||
- stall 双条件: 本地超窗但 progress 新鲜 → 继续等(不抛);双超 → stalled;
|
- stall 双条件: 本地超窗但 progress 新鲜 → 继续等(不抛);双超 → stalled;
|
||||||
- 取消穿透: sleep 中取消、transport 调用中取消(探针归还断言)、permit finally 释放;
|
- 取消穿透: sleep 中取消、transport 调用中取消(探针归还断言)、permit finally 释放;
|
||||||
- fail_fast: quota 满即抛 quota_exhausted;
|
- fail_fast: quota 满即抛 quota_exhausted;
|
||||||
@@ -155,9 +157,9 @@ class OcrTransport(Protocol):
|
|||||||
## T8 P7 OCR soak 场景
|
## T8 P7 OCR soak 场景
|
||||||
|
|
||||||
1. `.env` 增 OCR soak 池(**先读现有 .env 再追加,严禁编造/覆盖**;凭据只在 python 内经 dotenv 读): `SOAK_OCR__MONKEY__{1..4}__*`——源1=`http://10.77.0.20:7866`(真)、源2=`http://10.77.0.20:7867`(真)、源3=`http://10.255.255.1:7866`(黑洞)、源4=`http://10.77.0.20:7899`(坏端口,连接拒绝);api_key 全 "none";源级 RPM/并发参照 CHS 惯例(MAX_CONCURRENCY=4/RPM=120)+ TIMEOUT_S=120;scope 键 `SOAK_OCR__RETRY__MAX_ATTEMPTS=3`。
|
1. `.env` 增 OCR soak 池(**先读现有 .env 再追加,严禁编造/覆盖**;凭据只在 python 内经 dotenv 读): `SOAK_OCR__MONKEY__{1..4}__*`——源1=`http://10.77.0.20:7866`(真)、源2=`http://10.77.0.20:7867`(真)、源3=`http://10.255.255.1:7866`(黑洞)、源4=`http://10.77.0.20:7899`(坏端口,连接拒绝);api_key 全 "none";源级 RPM/并发参照 CHS 惯例(MAX_CONCURRENCY=4/RPM=120)+ TIMEOUT_S=120;scope 键 `SOAK_OCR__RETRY__MAX_ATTEMPTS=3`。
|
||||||
2. `tools/soak/scenarios.py`: 新增 P7 生成器——语料 `data/soak/chs_images/` 全量循环,kind 新增 `"ocr_text"`/`"ocr_layout"`(权重 0.8/0.2),kwargs=`{"image": bytes}`;总量 1500 调用。
|
2. `tools/soak/scenarios.py`: 新增 P7 生成器——语料 `data/soak/chs_images/` 全量循环(实测 469 张,设计 §8 "约 300" 以实际为准),kind 新增 `"ocr_text"`/`"ocr_layout"`(权重 0.8/0.2),kwargs=`{"image": bytes}`;总量 1500 调用。
|
||||||
3. `tools/soak/run_soak.py`: 装配 `OcrClient`(P7 时替代 GatewayClient;复用 `_paced_dispatch` 有界分发);按 kind 分派两方法;失败打印含 `per_source_reasons` 链(既有格式)。
|
3. `tools/soak/run_soak.py`: 装配 `OcrClient`(P7 时替代 GatewayClient;复用 `_paced_dispatch` 有界分发);按 kind 分派两方法;失败打印含 `per_source_reasons` 链(既有格式)。
|
||||||
4. `tools/soak/scoreboard.py`: P7 口径裁剪——无 429/缓存/token 列;不变量: ① 成功率 ≥98%(双真源在池);② 坏源(源3/4)尝试占比 ≤15%(健康选源压制);③ 熔断对坏源开路且真源零误熔(gate 事件核对);④ RPM 逐源不超限(准入时刻+服务器钟偏移聚桶,复用既有函数);⑤ 全程 RSS 有界(< 500MB,ps 当前值);⑥ tables/para_blocks 一致性抽查(layout 成功调用抽 20 条);⑦ 失败链全部可解释(reason ∈ 已知集合);⑧ 零 CancelledError 泄漏/零未分类异常。
|
4. `tools/soak/scoreboard.py`: P7 口径裁剪——无 429/缓存/token 列;不变量: ① 成功率 ≥98%(双真源在池);② 坏源(源3/4)尝试占比 ≤15%(健康选源压制);③ 熔断对坏源开路且真源零误熔(gate 事件核对);④ RPM 逐源不超限(准入时刻+服务器钟偏移聚桶,复用既有函数);⑤ 全程 RSS 有界(< 500MB 绝对值,ps 当前值——**新增检查函数**,不改既有 `inv_rss_stable` 增长口径);⑥ tables/para_blocks 一致性抽查(layout 成功调用抽 20 条);⑦ 失败链全部可解释(reason ∈ 已知集合);⑧ 零 CancelledError 泄漏/零未分类异常(**有意裁决**: 设计 §8 的"取消穿透抽查"由 T5 单测主动注入覆盖,soak 只做被动泄漏核查——harness 无取消注入能力,不为此扩建)。
|
||||||
5. 跑 P7(tmux + 禁日志缓存),结果落 `research-wiki/findings/2026-07-21-p7-ocr-soak.md`(阈值不达标 → systematic-debugging 迭代,测试环境不放水)。
|
5. 跑 P7(tmux + 禁日志缓存),结果落 `research-wiki/findings/2026-07-21-p7-ocr-soak.md`(阈值不达标 → systematic-debugging 迭代,测试环境不放水)。
|
||||||
- 验证: 记分板 8 不变量全 PASS。
|
- 验证: 记分板 8 不变量全 PASS。
|
||||||
- [ ] 提交 `feat: add P7 OCR soak scenario and scoreboard`(代码)+ `docs: record P7 OCR soak acceptance`(findings)
|
- [ ] 提交 `feat: add P7 OCR soak scenario and scoreboard`(代码)+ `docs: record P7 OCR soak acceptance`(findings)
|
||||||
|
|||||||
Reference in New Issue
Block a user