diff --git a/.env.example b/.env.example index ed3405c..f070589 100644 --- a/.env.example +++ b/.env.example @@ -91,3 +91,26 @@ PGW_TELEMETRY_BACKEND=none # sqlite | postgres | none(必填) # SOAK__MINIMAX__3__BASE_URL=http://10.255.255.1/v1 # 黑洞源: 防火墙 DROP → 连接超时 # SOAK__MINIMAX__4__RPM=5 # 紧闸源: 真实源 + rpm=5 / 并发 1 # SOAK__MINIMAX__4__MAX_CONCURRENCY=1 + +# ══ OCR scope(M3;MonkeyOCR 自建 LAN 服务,无鉴权故 API_KEY 填占位 "none")══ +# CHS 单源写法: +# OCR__MONKEY__1__BASE_URL=http://10.77.0.20:7866 +# OCR__MONKEY__1__API_KEY=none # 占位惯例: 服务无鉴权,SourceConfig 非空校验用 +# OCR__MONKEY__1__MODEL=monkey-ocr +# OCR__MONKEY__1__TIMEOUT_S=300 # /parse 两段协议较慢,给足 +# OCR__MONKEY__1__MAX_CONCURRENCY=4 +# OCR__MONKEY__1__RPM=120 +# VT 双实例写法(原 MONKEY_OCR_URLS 逗号列表拆多源;LAN 直连绕代理配 TRUST_ENV=false): +# OCR__MONKEY__2__BASE_URL=http://10.77.0.20:7867 +# OCR__MONKEY__2__API_KEY=none +# OCR__MONKEY__2__MODEL=monkey-ocr +# OCR__MONKEY__2__TIMEOUT_S=300 +# OCR__MONKEY__2__TRUST_ENV=false +# OCR__RETRY__MAX_ATTEMPTS=3 # per-scope 韧性键与 LLM scope 同一套 + +# ══ SOAK_OCR scope(P7 OCR 压测;tools/soak/run_soak.py --scenario P7 --scope SOAK_OCR)══ +# 双真实实例 + 黑洞(连接超时)+ 坏端口(连接拒绝)故障池; +# SOAK_OCR_FAULT_SOURCES 供记分板"坏源吸流占比"不变量归因 +# SOAK_OCR__GLOBAL__MAX_CONCURRENCY=16 +# SOAK_OCR__GLOBAL__RPM=300 +# SOAK_OCR_FAULT_SOURCES=monkey_3,monkey_4 diff --git a/research-wiki/ARCHITECTURE.md b/research-wiki/ARCHITECTURE.md index 48f0421..a85385f 100644 --- a/research-wiki/ARCHITECTURE.md +++ b/research-wiki/ARCHITECTURE.md @@ -205,7 +205,7 @@ HTTP API → arq 队列 → worker 协程 脚本 → asyncio.gather 协 ### D9 OCR 提前纳入;端口族而非单接口 -**决策**: OCR 优先级提前(高于音频)。因两个项目调用**同一套 MonkeyOCR 服务的两个不同端点、两种输出语义**,统一为单接口会强行合并不同类型的能力,故做成端口族:`OcrTextPort.recognize_text(bytes)`(对应 `/ocr/text`,纯文本转录)与 `OcrLayoutPort.parse_layout(bytes)`(对应 `/parse`,ZIP→结构化元素+bbox)。OCR 调用走与 LLM 同一套中间件治理栈(CHSAnalyzer 已有 VLM/OCR 共用泛型治理核心的先例)。 +**决策**: OCR 优先级提前(高于音频)。因两个项目调用**同一套 MonkeyOCR 服务的两个不同端点、两种输出语义**,统一为单接口会强行合并不同类型的能力,故做成端口族:`OcrTextPort.recognize_text(bytes)`(对应 `/ocr/text`,纯文本转录)与 `OcrLayoutPort.parse_layout(bytes)`(对应 `/parse`,ZIP→结构化元素+bbox)。OCR 调用复用与 LLM 同一套治理算法件与错误分类(限流/熔断门、退避公式、冷却备忘、遥测 helper、健康选源),循环形态取 EmbeddingClient 先例的独立精简循环(M2 §7.1 有限重复裁决;M3 设计 §2 方案 A——chat 循环已长出 AIMD/429 免预算/流式看门狗等 OCR 不适用机制,CHSAnalyzer 泛型核心的同构前提不再成立)。 **背景(调研结论)**: Video-Tree 用 `/ocr/text` 把帧文字作为"硬证据"并置进 VLM 提示词,**裸调**(无重试/限流/熔断,单帧失败跳过,手写双端点轮询)——迁移后免费升级为全治理。CHSAnalyzer 用 `/parse` 定位表格 bbox,已全治理。GovDoc 无 OCR 且是明确架构否决(旧系统被"OCR 挂死"拖垮,教训反哺 §6.4 取消语义)。多后端预留有直接证据:CHSAnalyzer provider 白名单已含 `glm`(延后),设计文档明确三种后端图片输入形态各异(VLM=base64、Monkey=multipart、GLM=URL)——因此端口收 `bytes`,编码差异封装在 invoker 内部。 @@ -334,7 +334,7 @@ flowchart TB ### 5.2 其他类型与 `chat()` 公共签名 -`ChatRequest`(model/messages/结构化输出参数/per-call 覆盖项)、`Usage`(tokens + elapsed,OCR 无计费填 0)、`OcrTextResult`(text + 溯源三件套 source_name/usage/raw)、`OcrLayoutResult`(elements 含 bbox/type + page_size + 溯源)。全部 frozen dataclass。空结果语义:合法"无内容"用空值/None 表达,调用失败必须走异常——二者严格区分。 +`ChatRequest`(model/messages/结构化输出参数/per-call 覆盖项)、`Usage`(tokens + elapsed,OCR 无计费填 0)、`OcrTextResult`(text + 溯源五件 source_name/usage/latency_ms/call_id/raw)、`OcrLayoutResult`(elements 含 bbox/type/page_index + page_sizes + 同套溯源五件;元素自 `_middle.json` para_blocks 全量提取,M3 设计 §1.2 取证)。全部 frozen dataclass。空结果语义:合法"无内容"用空值/None 表达,调用失败必须走异常——二者严格区分。 **`chat()` 公共签名定稿(2026-07-20,GovDoc 迁移缺口 G1/G2)**: `chat(messages, *, session_id=None, parent_call_id=None, cache_salt=None, cache_namespace=None, structured=None, stream=True)`。要点: ① `session_id`/`parent_call_id` 与三项目现有 `LLMProvider.chat` Protocol 逐字兼容——这是"调用点零改动"承诺的前提;② **per-call `cache_namespace`**: GovDoc 是单 client 服务多租户、tenant 每请求变化,装配级 namespace 只是默认值,per-call 传入时覆盖并进入缓存 key(§7.5);③ `cache_salt` per-call 可传(Video-Tree 跨 epoch 重采样);④ `structured` 三档语义(D14),类型定稿 `type[BaseModel] | Literal["json"] | None`(M1 设计): 不传 = 原始文本,`"json"` = 仅修复,pydantic 模型 = 完整阶梯(修复+形态校验+有界带反馈重问)。 @@ -456,7 +456,7 @@ flowchart TB | `OcrTextPort.recognize_text(image: bytes)` | `POST /ocr/text` | multipart 上传 → JSON `{content}` | `OcrTextResult`(多行纯文本) | | `OcrLayoutPort.parse_layout(image: bytes)` | `POST /parse` | multipart → JSON(download_url) → GET ZIP → 解包 `*_middle.json` | `OcrLayoutResult`(elements 含 bbox + page_size) | -设计要点:输入统一 `bytes`(路径读取/多帧批量拼接留业务侧);bbox 返回 OCR 原生页面坐标,几何映射(裁剪偏移/归一化/marker 推算)留业务侧;`None`/空表达"合法无内容",异常表达"调用失败";ZIP 内容不可解析抛 `ResultInvalidError`(坏图≠坏服务);OCR 走同一中间件栈(无 token 计费,Usage 填 0,elapsed 照记);多后端经 provider 注册表扩展(GLM 已在 CHSAnalyzer 白名单,输入形态为 URL,届时封装在其 invoker 内部,端口签名不变)。数值防御(bbox 有限性/顺序/退化校验)随协议解析下沉进库。OCR transport 支持 per-source `trust_env` 开关(LAN 直连绕过本地代理,Video-Tree `ocr.py:46` 教训;2026-07-20 缺口 R9);端口族含 `check_health() -> bool` 逐源健康预检(Video-Tree A/B 评测的启动门;缺口 R10)。 +设计要点:输入统一 `bytes`(路径读取/多帧批量拼接留业务侧);bbox 返回 OCR 原生页面坐标,几何映射(裁剪偏移/归一化/marker 推算)留业务侧;`None`/空表达"合法无内容",异常表达"调用失败";ZIP 内容不可解析抛 `ResultInvalidError`(坏图≠坏服务);OCR 复用同一套治理算法件、循环形态同 EmbeddingClient 先例(见 D9 修订;无 token 计费,Usage 填 0,latency_ms 照记);多后端经 provider 注册表扩展(GLM 已在 CHSAnalyzer 白名单,输入形态为 URL,届时封装在其 invoker 内部,端口签名不变)。数值防御(bbox 有限性/顺序/退化校验)随协议解析下沉进库。OCR transport 支持 per-source `trust_env` 开关(LAN 直连绕过本地代理,Video-Tree `ocr.py:46` 教训;2026-07-20 缺口 R9);客户端暴露 `check_health() -> dict[str, bool]` 逐源健康预检(M3 设计 §3.3 细化:bool 无法承载逐源结果,业务 `all()` 即得启动门布尔;Video-Tree A/B 评测消费;缺口 R10 已闭)。协议事实(2026-07-21 实测): `/parse` 的 download_url 为**相对路径**(`/static/*.zip`),transport 相对 base_url 解析并兼容绝对 URL。 ### 7.11 音频占位 diff --git a/research-wiki/ROADMAP.md b/research-wiki/ROADMAP.md index 15d3213..6b20852 100644 --- a/research-wiki/ROADMAP.md +++ b/research-wiki/ROADMAP.md @@ -54,7 +54,7 @@ ## 4. M3 OCR(目标: CHSAnalyzer 全量可迁、Video-Tree OCR 免费升级) -**内部顺序**: ① `OcrTextResult`/`OcrLayoutResult` 类型 + 两端口(设计文档人类门,因是公共 API)→ ② `transports/monkey_ocr.py`(两端点:multipart→JSON 与 multipart→ZIP→`_middle.json`,数值防御校验下沉)→ ③ 接入既有中间件栈(OCR 无 token 计费,Usage 置 0)→ ④ `ResultInvalidError` 语义联调(坏结果不熔断)。 +**内部顺序**: ① `OcrTextResult`/`OcrLayoutResult` 类型 + 两端口(设计文档人类门,因是公共 API)→ ② `transports/monkey_ocr.py`(两端点:multipart→JSON 与 multipart→ZIP→`_middle.json`,数值防御校验下沉)→ ③ 接入治理算法件(OcrClient 独立循环,EmbeddingClient 先例;OCR 无 token 计费,Usage 置 0)→ ④ `ResultInvalidError` 语义联调(坏结果不熔断)。 **验收出口**: 对真实 MonkeyOCR 服务(LAN)双端点集成测试通过;CHSAnalyzer 的 `MonkeyOcrParseInvoker` 路径可替换;Video-Tree 的裸调 OCR 换库后获得重试/熔断。 diff --git a/research-wiki/migrations/chsanalyzer.md b/research-wiki/migrations/chsanalyzer.md index e505f48..f1b274d 100644 --- a/research-wiki/migrations/chsanalyzer.md +++ b/research-wiki/migrations/chsanalyzer.md @@ -58,7 +58,7 @@ | `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 全量;项目只取首个 table bbox | `table_locator.py` 改写取数逻辑 | +| `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**。 @@ -147,6 +147,8 @@ stack = ExtractionProviderStack( | 换源重试失败跨源累计,`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) | @@ -171,7 +173,7 @@ stack = ExtractionProviderStack( | 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** | ⚠️ `CircuitOpenError`/`AllSourcesExhausted` 未定义结构化字段:须携 `retry_after_s`(全 scope 最早恢复时刻,读熔断后端)、reason(circuit_open/retry_exhausted/stalled)、per-source reasons——否则 tracking.py:406-428 的"延期重投不耗失败预算"不可复现(§3) | M2 | **架构缺口**,修订 §6.1 | +| **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 | diff --git a/research-wiki/migrations/video-tree-trm5.md b/research-wiki/migrations/video-tree-trm5.md index 0e539d1..cdd87b3 100644 --- a/research-wiki/migrations/video-tree-trm5.md +++ b/research-wiki/migrations/video-tree-trm5.md @@ -154,7 +154,7 @@ ocr = FrameOcrAdapter(OcrTextPort_from_env()) # 业务适配器包装库 OCR 端 | 10 | 装配时 Redis 不可用 → 降级无缓存(main.py:91-97);TTL≤0 拒绝启动(redis_cache.py:27-31) | **保留** | | 11 | 重试全部落在同一源退避等待(单源无换源) | **替换**:退避与换源结合(§4.4 步 3),单源配置下行为退化为等价 | | 12 | 遥测 messages 字段全量 JSON 落 SQLite——**VLM 调用的 base64 图片整段进 telemetry.db**(llm.py:330,391 `json.dumps(messages)`) | **修复建议**:库遥测对多模态 part 摘要(⚠️ §9-R12,ARCHITECTURE 未覆盖) | -| 13 | OCR:同步 requests + 线程局部 Session + `trust_env=False` 绕代理(ocr.py:41-47);双端点加锁轮询(ocr.py:108-109);单帧失败跳过返回空(ocr.py:117-119);行级过滤(len≤1)与帧内去重、`"帧N: "` 拼接(ocr.py:120-128);`check_health` 启动预检(ocr.py:64-70) | 传输/轮询/重试**替换**(库多源全治理);过滤/拼接/单帧跳过**保留业务侧**(§3 适配器);trust_env 与 health 见 ⚠️ §9-R9/R10 | +| 13 | OCR:同步 requests + 线程局部 Session + `trust_env=False` 绕代理(ocr.py:41-47);双端点加锁轮询(ocr.py:108-109);单帧失败跳过返回空(ocr.py:117-119);行级过滤(len≤1)与帧内去重、`"帧N: "` 拼接(ocr.py:120-128);`check_health` 启动预检(ocr.py:64-70) | 传输/轮询/重试**替换**(库多源全治理);过滤/拼接/单帧跳过**保留业务侧**(§3 适配器);trust_env(per-source 键)与 health(逐源 dict,`resp.ok` 收紧为 2xx)已按 M3 设计 §10.1 落地,R9/R10 已闭 | | 14 | `tools/build_trees.py` 实测 `cache=None`(build_trees.py:223,239)——建树不走缓存,断点续跑靠 `progress.json`+完整性双重跳过(build_trees.py:273-279) | 断点续跑纯业务**保留**;迁移后建树可统一开启缓存(行为增强,重跑段内调用免费) | | 15 | 三处写死 `stream=True`,短请求也走 SSE+看门狗(llm.py:508) | **替换可选**:库放开非流式快路径(§7.1),默认行为不变 | | 16 | `Runner` 注入 telemetry 从未使用(runner.py:738 死注入);EVOLVE_LLM_* 死配置(§4 点 1) | **修复**(顺带清理,属迁移必要改动非 gold-plating) | @@ -189,8 +189,8 @@ ocr = FrameOcrAdapter(OcrTextPort_from_env()) # 业务适配器包装库 OCR 端 | R6 | 非流式快路径、cache salt、`gather_bounded` | M1 | 已覆盖(§7.1/§7.5/D5) | | R7 | 库错误类型公共导出且可被业务 except(AgentLoop `retryable_exceptions` 注入 `TransientError`/`AllSourcesExhausted`) | M1 | 基本覆盖(§6),导出面需 M1 确认 | | R8 | 缓存 namespace 必填对单项目是新增负担 | M1 | 已覆盖(§7.5);建议 from_env 支持项目名默认 | -| R9 | ⚠️ **架构缺口**:MonkeyOCR transport 需 per-source `trust_env=False`(LAN 直连绕代理,ocr.py:46 实测),§7.10 未提代理/trust_env 配置 | M3 | **缺口** | -| R10 | ⚠️ **架构缺口**:OCR 端点健康预检(`check_health`,ocr.py:64-70,A/B 实验启动门消费)——库多源配置内聚后业务侧拿不到端点列表自检,库需暴露健康检查或等价能力 | M3 | **缺口** | +| R9 | ✅ 已闭(M3): `SourceConfig.trust_env` + `{SCOPE}__{PROVIDER}__{N}__TRUST_ENV` 键 M1 已落地,MonkeyOcrTransport 与 openai_compat 同款尊重;LAN 直连配 `OCR__MONKEY__N__TRUST_ENV=false` | M3 | 已闭 | +| R10 | ✅ 已闭(M3): `OcrClient.check_health() -> dict[str, bool]` 逐源并发预检(2xx=True,取消穿透);启动门语义 = `all(值)`,业务可逐源定位坏端点打日志 | M3 | 已闭 | | R11 | ⚠️ **架构缺口/勘误**:embedding 去向(Q3)——实测 `RemoteEmbeddingProvider` **无任何重试**(embedding.py:146-172,同步 OpenAI SDK 裸调),ARCHITECTURE §13-Q3"各有一套独立重试实现"对 Video-Tree 不成立(无治理反而更需要进库);且现端口为同步 `embed()`,库若 M2 纳入必为异步端口,业务调用点需适配 | M2 | **缺口**(Q3 决策输入) | | R12 | ⚠️ **架构缺口**:遥测 `messages` 字段对多模态 part 应摘要——现状 base64 整段进 SQLite(llm.py:330),§7.8 未规定,照搬会让库遥测继承 db 膨胀问题 | M1 | **缺口** |