docs: add per-project migration documents as design constraints
Add research-wiki/migrations/ (govdoc-saas, video-tree-trm5, chsanalyzer): deletion lists, component mappings, call-site inventories, config migration, stepwise rollback plans, legacy behavior audits, and reverse constraints on the library design including flagged architecture gaps.
This commit is contained in:
@@ -109,6 +109,7 @@ project_root/
|
||||
|---|---|
|
||||
| 架构全貌: 决策 D1-D13 及讨论过程、端口清单、错误分类、子系统设计、迁移验收 | `research-wiki/ARCHITECTURE.md`(单一事实源) |
|
||||
| 开发顺序与里程碑状态 | `research-wiki/ROADMAP.md`(活文档,随进度更新) |
|
||||
| 三项目迁移文档(ARCHITECTURE §11 的展开,库设计的常驻约束) | `research-wiki/migrations/`(govdoc-saas / video-tree-trm5 / chsanalyzer) |
|
||||
| 功能设计文档(每次实现新功能时新增) | `research-wiki/designs/` |
|
||||
| 实现计划 | `research-wiki/plans/` |
|
||||
| 治理网关参考实现 | `reference/Video-Tree-TRM5/adapters/`(llm/breaker/streaming/redis_cache/telemetry) |
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
## 5. M4 迁移验证(目标: 三项目验收,发 v1.0)
|
||||
|
||||
**顺序**(按难度递增,每个项目的缺口回补后再迁下一个): ① GovDoc-SaaS → ② Video-Tree-TRM5 → ③ CHSAnalyzer。每项目按 ARCHITECTURE §11 的删除清单+处置表执行,原测试全绿为过关;发现的边界缺口回补进库(可能触发小版本迭代)后重验。全部通过后打 `v1.0.0`,分发方式按 Q1 拍板结果执行。
|
||||
**顺序**(按难度递增,每个项目的缺口回补后再迁下一个): ① GovDoc-SaaS → ② Video-Tree-TRM5 → ③ CHSAnalyzer。每项目按其迁移文档(`research-wiki/migrations/<project>.md`,含删除清单、调用点映射、配置迁移、分步回滚点、旧版行为审计)执行,原测试全绿为过关;发现的边界缺口回补进库(可能触发小版本迭代)后重验。全部通过后打 `v1.0.0`,分发方式按 Q1 拍板结果执行。
|
||||
|
||||
**主要风险**: 迁移中发现隐性行为依赖(参考 Video-Tree CLAUDE.md 的"前序版本对照"教训)→ 每项目迁移前先做旧版行为审计(brainstorming skill 已内置该环节)。
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# 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 |
|
||||
|
||||
**能力对标清单**(库必须逐项对等,任一缺失即库的边界缺口):
|
||||
|
||||
| # | 能力 | 项目侧证据 |
|
||||
|---|---|---|
|
||||
| 1 | 六道闸原子限流(全局/单源 × 并发/RPM/TPM),拒绝零副作用 | `app/coordination/scripts.py:6-34` |
|
||||
| 2 | TPM 预扣入场、settle 按实际 usage 落回 acquire 窗口多退少补 | `limiter.py:55-62`、`scripts.py:44-48` |
|
||||
| 3 | 并发 lease 带 TTL(防进程死亡泄漏)+ release/settle 幂等 | `limiter.py:46-63`、`tests/contracts_limiter.py:66-73` |
|
||||
| 4 | 窗口 id 用 Redis 服务器时钟(多进程口径统一) | `limiter.py:95-98` |
|
||||
| 5 | 跨进程熔断:单探针半开、探针租约 TTL、epoch fencing、force_open | `scripts.py:82-225`、`provider_gate.py` |
|
||||
| 6 | 背压 stall 双条件判卡死(本地等够 + 全局无进展)+ mark_progress 全局活性 | `governance.py:270-281`、`limiter.py:193-209` |
|
||||
| 7 | 换源重试:失败跨源累计、退避取 Retry-After 较大值、源冷却备忘 | `governance.py:105-118, 244-261` |
|
||||
| 8 | 错误分类:429 body 细分 insufficient_quota、工件级失败不熔断 | `invokers.py:144-166`、`governance.py:237-239` |
|
||||
| 9 | scope 级不可用结构化错误(reason 7 种 + retry_after_s),供 worker 延期重投 | `errors.py:140-180`、`workers/tracking.py:406-428` |
|
||||
| 10 | 流式三层活性看门狗;thinking token 刷活性不计结果 | `streaming.py`、`invokers.py:55-79` |
|
||||
|
||||
## 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 全量;项目只取首个 table bbox | `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 已承诺;首表选取留业务) |
|
||||
| 无响应缓存 | 相同图+指令重复付费 | **替换**:迁移后净增缓存(风险见 §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** | ⚠️ `CircuitOpenError`/`AllSourcesExhausted` 未定义结构化字段:须携 `retry_after_s`(全 scope 最早恢复时刻,读熔断后端)、reason(circuit_open/retry_exhausted/stalled)、per-source reasons——否则 tracking.py:406-428 的"延期重投不耗失败预算"不可复现(§3) | M2 | **架构缺口**,修订 §6.1 |
|
||||
| **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 | 决策项,非缺口 |
|
||||
@@ -0,0 +1,169 @@
|
||||
# GovDoc-SaaS 迁移文档(迁移即验收)
|
||||
|
||||
> **定位**: 本文是 `ARCHITECTURE.md` §11.1 的展开——库建成后如何合并进 GovDoc-SaaS、替换其哪些内部组件。它既是 M4 迁移的操作指南,也是 M1-M3 设计的反向约束(库公共 API 必须让本文描述的迁移成立)。**与 ARCHITECTURE.md 冲突时以 ARCHITECTURE.md 为准。**
|
||||
> 证据基线: 2026-07-20 对 `reference/GovDoc-SaaS/` 的代码实测(Read/Grep/wc),所有 file:line 相对该仓库根。
|
||||
|
||||
---
|
||||
|
||||
## 1. 迁移目标与验收定义
|
||||
|
||||
**验收公式**: 删除清单(§2)全部删除 → import 替换(§3/§4)完成 → GovDoc 原测试全绿(`make ci`,含 docagent-core 余量测试 + govdoc 业务测试 + import-linter 契约)。凡替换不掉的能力即库的边界缺口,回补后重验。
|
||||
|
||||
两个阶段:
|
||||
|
||||
| 阶段 | 时机 | 范围 | 不做什么 |
|
||||
|---|---|---|---|
|
||||
| 最小接入冒烟 | M1 后 | 不删任何 GovDoc 文件;新增一个装配 spike(`GatewayClient.from_env()` 注入 `AgentLoop`),真实打通一次 chat 调用,验证 `LLMProvider` 结构兼容与 `LLMResponse` 字段消费 | 不动 llm/ 子包、不改 protocols/types、不迁 .env |
|
||||
| 全量迁移 | M4 | §2 删除清单 + §3 替换映射 + §5 配置迁移 + §6 步骤全量执行,验收公式达成 | 不迁 embedding(待 Q3,见 §2)、不做任务外重构 |
|
||||
|
||||
特别说明: GovDoc 的**装配层(配置→client)从未存在**——`.env` 定义了全部 LLM 参数但零 Python 消费者(grep `LLM_MODEL|LLM_BASE_URL|LLM_API_KEY|LLM_TIMEOUT` 于 `src/` 与 `packages/` 无命中),`src/govdoc/` 现仅 api 骨架(鉴权桩 `api/deps.py` 16 行,任意 token 归 `t_mock` 租户)且零处 import `docagent_core`。因此"最小接入"对 GovDoc 而言不是替换,而是**库补上从未写完的那一段**;`GovernedLLMClient` 迄今没有任何生产调用方,迁移风险天然低。
|
||||
|
||||
## 2. 现状盘点
|
||||
|
||||
治理相关文件全量清单(行数 `wc -l` 实测):
|
||||
|
||||
| 文件(相对 GovDoc 根) | 行数 | 职责一句话 | 迁移后命运 |
|
||||
|---|---|---|---|
|
||||
| `packages/docagent-core/src/docagent_core/llm/client.py` | 582 | GovernedLLMClient: 熔断→缓存→重试+SSE 流式→遥测四层治理 + provider 字符串猜测 | **删除**(库 client+middleware+transport 继任) |
|
||||
| `.../llm/breaker.py` | 70 | 进程内熔断器(now 注入、force_open) | **删除**(库 `backends/memory` 继任) |
|
||||
| `.../llm/streaming.py` | 131 | 三层活性看门狗纯函数 | **删除**(库 `streaming.py` 近原样继任) |
|
||||
| `.../llm/redis_cache.py` | 94 | sha256 内容寻址响应缓存,静默降级 | **删除**(库 `backends/redis` 缓存继任) |
|
||||
| `.../llm/telemetry_sqlite.py` | 204 | SQLite 遥测(WAL/幂等/to_thread) | **删除**(库 `telemetry/sqlite.py` 继任) |
|
||||
| `.../llm/__init__.py` | 0 | 空 | **删除**(随子包) |
|
||||
| `.../docagent_core/protocols.py` | 151 | 10 个共享 Protocol,其中 LLM 相关 2 个 | **改写**: 删 `TelemetryRecorder`(唯一消费者是被删的 client.py/telemetry_sqlite.py);`LLMProvider` 保留(agent/workflow 的消费契约,GatewayClient 结构化满足);其余 8 个非 LLM 端口不动 |
|
||||
| `.../docagent_core/types.py` | 25 | `LLMResponse` frozen dataclass(11 字段) | **改写**: 改为 re-export 库的 `LLMResponse`(一行 shim,超集兼容),下游 import 路径零改动 |
|
||||
| `.../retrieval/embedding.py` | 165 | Embedding 客户端,含独立手写重试(embedding.py:107-131,第三处重复) | **保留(暂)**: 待 ARCHITECTURE Q3 决策(建议 M2 纳库),本次迁移不动 |
|
||||
| `packages/docagent-core/tests/test_breaker.py` | 32 | 熔断状态机单测 | **删除**(等价测试随库实现交付) |
|
||||
| `packages/docagent-core/tests/test_imports.py` | 10 | 公共 API import 冒烟(第 5-6 行 import 被删模块) | **改写** import 目标 |
|
||||
| `packages/docagent-core/tests/conftest.py` | — | `ScriptedLLM` fake(构造 `LLMResponse`) | **保留**(经 types.py re-export 零改动) |
|
||||
| `.env.example` | 51 | LLM/Redis/韧性参数定义(现无人读) | **改写**(键名映射见 §5) |
|
||||
| `packages/docagent-core/pyproject.toml` | 23 | 核心依赖 httpx/loguru/pluggy/json-repair/pydantic;extras: redis/postgres | **改写**: 移除 `redis` extra(grep 实证 `redis` 在 docagent_core 内唯一消费者是被删的 redis_cache.py);httpx/json-repair 保留(embedding.py 与 agent/loop.py 仍用) |
|
||||
| 根 `pyproject.toml` | 85 | govdoc 业务包依赖与 import-linter 契约 | **改写**: dependencies 增 `polygateway[redis,telemetry-sqlite]` |
|
||||
|
||||
`agent/`、`workflow/`、`taskrun/`、`retrieval/`(除 embedding 的重试外)与 `src/govdoc/` 全部保留——它们是编排层/业务层,不在库边界内(ARCHITECTURE §2.1)。
|
||||
|
||||
## 3. 组件替换映射表
|
||||
|
||||
| GovDoc 组件 | PolyGateway 对应物 | 签名兼容性(实测对比) | shim |
|
||||
|---|---|---|---|
|
||||
| `GovernedLLMClient`(client.py:206) | `GatewayClient` | GovDoc 端口 `LLMProvider.chat(messages, *, session_id, parent_call_id) -> LLMResponse`(protocols.py:19-25);ARCHITECTURE §5.2 只定义 `ChatRequest` 未明列 chat() 签名,§11.1 承诺兼容 → **见 §9-G2,M1 设计必须定稿此签名** | 若库签名定稿一致: 无 shim,GatewayClient 结构化满足 `LLMProvider`(runtime_checkable);否则业务侧 5 行包装类 |
|
||||
| `LLMResponse`(types.py:9-25,11 字段) | 库 `LLMResponse`(§5.1 超集: 同名 11 字段 + source_name/cost/usage_source) | 字段名逐一比对一致(content/thinking/model/provider/prompt_tokens/completion_tokens/latency_ms/ttft_ms/max_inter_token_ms/cache_hit/call_id);§5.1"只增不删不改名"硬约束覆盖 | types.py 改一行 re-export |
|
||||
| `CircuitBreaker`(breaker.py:10) | BreakerMW + `InMemoryBreakerState` | 内部组件,无外部签名消费(grep 证实 `CircuitOpenError` 与 `StreamLivenessTimeout` 在 llm/ 之外零捕获点) | 无 |
|
||||
| `RedisResponseCache`(redis_cache.py:15) | CacheMW + Redis 缓存后端 | 同上,内部组件;key 公式变化见 §7-B1 | 无 |
|
||||
| `SQLiteTelemetryRecorder`(telemetry_sqlite.py:20) | `telemetry/sqlite.py` | GovDoc 遥测端口参数名 `model_name`(protocols.py:38)vs 库 §7.8 `model`;因端口连同实现一起删除、业务侧零直接调用,不构成兼容问题 | 无 |
|
||||
| `stream_with_liveness_timeouts`(streaming.py:72) | 库 `streaming.py` | 同源纯函数(注释自证"参考 CHSAnalyzer2"),库方向近原样移植 | 无 |
|
||||
| `CircuitOpenError`(client.py:36) | 库 `CircuitOpenError` | 异常类型全变(docagent_core.llm.client.* → polygateway.errors.*);业务侧无捕获点,风险为零 | 无 |
|
||||
| 装配层 | `GatewayClient.from_env()` | **净新增**——GovDoc 没有可对比的旧物 | 无 |
|
||||
|
||||
## 4. 调用点清单
|
||||
|
||||
grep 实测:全仓 `.py` 中 `.chat(` 仅 1 处(测试 fake 除外);`GovernedLLMClient` 无任何构造调用点。
|
||||
|
||||
| 调用点(file:line) | 现状 | 迁移后形态 |
|
||||
|---|---|---|
|
||||
| `packages/docagent-core/src/docagent_core/agent/loop.py:377` | `await self._llm.chat(messages, session_id=..., parent_call_id=...)`——唯一直接调用点 | **零改动**(前提: §9-G2 签名定稿) |
|
||||
| `.../workflow/executor.py:117` | `AgentLoop(self._llm, max_steps=...)` 透传 `LLMProvider` | **零改动** |
|
||||
| `.../agent/loop.py:98` | AgentLoop 步级重试默认 `retryable_exceptions=(TimeoutError, OSError)`,注释明言用于兜底穿透治理层的瞬时异常 | **非零改动**: 库异常(TransientError/AllSourcesExhausted/CircuitOpenError)非 OSError 子类,不改则步级重试**静默失效**。修法: 装配处构造 `AgentLoop(..., retryable_exceptions=(TransientError, AllSourcesExhausted))`——改在装配调用点,loop.py 本体不动 |
|
||||
| `packages/docagent-core/tests/test_imports.py:5-6 | import 被删的 `CircuitBreaker`/`GovernedLLMClient` | **改写**为 import polygateway 对应物 |
|
||||
| `tests/conftest.py:39` 与 `test_agent_loop.py` 各构造点 | fake 构造 `docagent_core.types.LLMResponse` | **零改动**(types.py re-export;库新增字段须有默认值,见 §9-R3) |
|
||||
| 装配调用点 | **不存在**(见 §1) | 净新增: govdoc api lifespan / arq worker 入口各一处 `from_env()` |
|
||||
|
||||
## 5. 配置迁移
|
||||
|
||||
`.env` 键名映射(旧键实测自 `.env.example:24-41`;库键按 ARCHITECTURE §9,`PGW_*` 命名待 M1 设计定稿):
|
||||
|
||||
| 旧键 | 库键 | 语义差异 |
|
||||
|---|---|---|
|
||||
| `LLM_MODEL` / `LLM_BASE_URL` / `LLM_API_KEY` | `LLM__{PROVIDER}__1__MODEL` / `__BASE_URL` / `__API_KEY` | 平铺单源 → 多源命名(单源=长度 1 特例);**改名成本为零**,因旧键无任何代码消费 |
|
||||
| (无) | `LLM__{PROVIDER}__1__ENABLE_THINKING` | 旧实现 thinking 是构造参数(client.py:235)但 .env 从未定义——净新增键 |
|
||||
| `LLM_TIMEOUT` / `LLM_MAX_RETRIES` / `LLM_RETRY_BASE_DELAY` / `LLM_RETRY_MAX_DELAY` / `LLM_TTFT_TIMEOUT` / `LLM_INTER_TOKEN_TIMEOUT` | 同名沿用(§9 承诺) | 无 |
|
||||
| `LLM_CIRCUIT_BREAKER_THRESHOLD` / `_COOLDOWN` | 同名沿用 | 旧 .env 注释"实际阈值 = max(此值, concurrency*2)"是手动约定(.env.example:38),库自动计算(§7.4) |
|
||||
| `REDIS_URL` | 库缓存后端连接配置 | 旧值同时服务 arq/SSE(.env.example:13 注释),迁移后业务自用部分保留原键 |
|
||||
| `REDIS_CACHE_TTL` | 库缓存 TTL 键 | 旧实现允许 `None`=永不过期(redis_cache.py:89-92);库强制 TTL>0(§7.5) |
|
||||
| (无) | namespace(租户)、`PGW_LIMITER_BACKEND`、`PGW_TELEMETRY_BACKEND`、`PGW_QUOTA_FULL` | 净新增: 缓存命名空间**必填**;限流从"完全没有"变为存在(§7-B7) |
|
||||
|
||||
装配对比——before 是"整段缺失":
|
||||
|
||||
```python
|
||||
# BEFORE(现状): 不存在。.env 定义了 LLM_* 但无人读;
|
||||
# GovernedLLMClient(15 个构造参数)从未被任何入口构造。
|
||||
|
||||
# AFTER(govdoc api lifespan / arq worker 入口,净新增约 5 行):
|
||||
from polygateway import GatewayClient, TransientError, AllSourcesExhausted
|
||||
|
||||
client = GatewayClient.from_env() # 读 .env 装配全治理栈
|
||||
loop = AgentLoop(client, max_steps=...,
|
||||
retryable_exceptions=(TransientError, AllSourcesExhausted))
|
||||
# 关闭: lifespan 退出时 await client.aclose()(见 §9-R5)
|
||||
```
|
||||
|
||||
## 6. 迁移步骤
|
||||
|
||||
| # | 步骤 | 验证方式 | 回滚点 |
|
||||
|---|---|---|---|
|
||||
| 1 | feature 分支;记录基线 `make ci` 全绿输出 | ci 日志留存 | 分支起点 commit |
|
||||
| 2 | 根 pyproject 加 `polygateway[redis,telemetry-sqlite]`,`make install` | `python -c "import polygateway"` | commit |
|
||||
| 3 | **冒烟阶段**(M1 后即可做): 新增装配 spike,`from_env()` + AgentLoop 真实调用一次,校验 `isinstance(client, LLMProvider)` 与响应字段 | spike 输出 + 遥测库有记录 | commit(spike 可独立丢弃) |
|
||||
| 4 | `.env`/`.env.example` 按 §5 映射改键、补 namespace 等新键 | `from_env()` 启动无缺配置报错 | commit |
|
||||
| 5 | 装配进 api lifespan 与 arq worker 入口(含 retryable_exceptions 显式传参与 aclose) | govdoc 业务测试绿 | commit |
|
||||
| 6 | 删除 `llm/` 六文件 + `test_breaker.py`;改写 `types.py`(re-export)、`protocols.py`(删 TelemetryRecorder)、`test_imports.py`;docagent-core pyproject 移除 redis extra | `make ci` 全绿 + import-linter 通过(含"docagent-core 子包互不依赖"契约,根 pyproject:71-88) | commit |
|
||||
| 7 | 验收: 验收公式逐项核对 + 全新上下文 verifier 独立验证 | ci 输出 + verifier 报告 | — |
|
||||
|
||||
任一步失败即回退上一 commit;第 6 步前的所有步骤不删旧代码,新旧可共存。
|
||||
|
||||
## 7. 旧版行为审计
|
||||
|
||||
| # | 现有行为(file:line) | 处置 |
|
||||
|---|---|---|
|
||||
| B1 | 缓存 key = `llm_cache:` + sha256({model, messages}),**无租户/命名空间/salt**(redis_cache.py:42-48)——违反 GovDoc 自身"缓存 key 含租户维度"铁律(其 CLAUDE.md §4.2) | **修复**: 库 key 必含 namespace(§7.5);须支持 per-call 租户,见 §9-G1 |
|
||||
| B2 | 缓存 TTL 可为 None=永不过期(redis_cache.py:89-92) | **修复**: 库强制 TTL>0 |
|
||||
| B3 | 缓存读取时 `json.loads(raw)` 与 `LLMResponse(**data)` 在 try 块**之外**(redis_cache.py:70-71)——损坏的缓存条目会抛异常击穿整次调用,违背"静默降级"自述 | **修复**: 库反序列化失败按 miss 处理 |
|
||||
| B4 | 熔断半开无单探针互斥: 冷却到期 `is_open` 对**所有**并发调用返回 False(breaker.py:36-37),惊群探测 | **修复**: 库半开只放一个探针(§7.4) |
|
||||
| B5 | 错误二分类: transient={429,500,502,503,504,ConnectError,ReadTimeout,WriteTimeout,SSE 异常,看门狗}(client.py:169-186);fatal={401,403} force_open(client.py:170,401);其余(含 400)记遥测后直接抛(client.py:463-481) | **替换**: 四分类。429+insufficient_quota→SourceDead、400→RequestRejected 显式化是升级 |
|
||||
| B6 | `httpx.ConnectTimeout`/`PoolTimeout` 不在瞬时判定内(client.py:182 仅列 ConnectError/ReadTimeout/WriteTimeout;按 httpx 异常层级 ConnectTimeout 继承 TimeoutException 而非 ConnectError)——**连接超时不重试**的缺陷 | **修复**: 库将全部 httpx Transport 错误归 Transient(§6.2) |
|
||||
| B7 | 限流完全没有(连 semaphore 都无;全仓无并发闸) | **修复/净新增**: M1 内存限流。注意 arq 多 worker 下内存限流按进程各自计数,全局限额需 M2 Redis 后端——迁移文档明示此过渡期语义 |
|
||||
| B8 | 单源、无换源、无 Retry-After 解析(重试仅指数退避+jitter,client.py:451-460) | **替换**: 多源+换源+Retry-After 取大者;GovDoc 配单源即长度 1 特例 |
|
||||
| B9 | 遥测调用 5 处逐字复制(client.py:315缓存命中/376成功/403致命/425瞬时/464非重试) | **替换**: 库单一 helper 铁律(§7.8) |
|
||||
| B10 | 缓存命中也记遥测(cache_hit=True, latency_ms=0)(client.py:309-331);每次 attempt 独立 call_id(client.py:337);thinking 帧 content 优先于 reasoning_content(client.py:79-91) | **保留**(库同款语义) |
|
||||
| B11 | SSE 流提前断开且未见 `[DONE]` 时正常返回:`usage_sink["done"]` 写入后无人检查(client.py:115-117),截断响应被当成功**并写入缓存** | **修复**: 库把"断流无 [DONE]"定性 TransientError(§6.1),且坏结果不进缓存 |
|
||||
| B12 | provider 差异靠字符串猜: `"deepseek" in provider`/`"qwen" in provider` 注入 thinking 参数(client.py:139-144)、`<think>` 剥离(client.py:348) | **替换**: provider 注册表(D11) |
|
||||
| B13 | usage 帧缺失时 prompt/completion_tokens 落 0(client.py:354-355),无标注 | **升级**: 库 `usage_source=measured/estimated` |
|
||||
| B14 | 遥测 schema 无 source_name/cost/usage_source(telemetry_sqlite.py:29-48) | **升级**: 库超集 schema;GovDoc 骨架期无生产遥测数据,直接换新库文件,不做数据迁移 |
|
||||
| B15 | 双层重试: 治理层 max_retries + AgentLoop 步级 step_retries(loop.py:308-356,默认延迟 (20,40)s) | **有意保留**(业务侧任务级重试,ARCHITECTURE §7.2 允许留在库外),但须按 §4 改 retryable_exceptions,否则静默失效 |
|
||||
| B16 | `CancelledError` 穿透重试循环(client.py:396 `except Exception` 天然放行),取消的调用**不记遥测** | **保留**穿透;取消是否记遥测库未定义,见 §9-R6 |
|
||||
| B17 | 熔断按 provider 字符串 key(client.py:295)而非源 | **替换**: 库按 source_name 分别计数 |
|
||||
|
||||
## 8. 行为差异与风险
|
||||
|
||||
| 差异/风险 | 影响 | 缓解 |
|
||||
|---|---|---|
|
||||
| 缓存 key 公式全变(B1/B2) | 迁移后既有缓存全 miss,一次性成本 | GovDoc 未上生产,成本≈0;无需灰度 |
|
||||
| 库新增限流(B7) | 此前无限流的调用可能开始排队/fail-fast | 初期配宽限额;`PGW_QUOTA_FULL=wait`;M2 前明知内存限流是每进程口径 |
|
||||
| 步级重试静默失效风险(B15) | 忘改 retryable_exceptions 则韧性兜底悄然消失 | 迁移步骤 5 显式传参;冒烟阶段注入 TransientError 验证兜底触发 |
|
||||
| 异常类型全换(§3) | 未来业务代码若按旧类型捕获会漏 | 现零捕获点(grep 实证);删除旧类即编译期暴露 |
|
||||
| `LLMResponse` 新增字段 | `dataclasses.asdict` 序列化面变宽(审计/遥测消费) | §5.1 只增不删;conftest 构造点依赖新字段有默认值(§9-R3) |
|
||||
| 截断流从"假成功"变为重试(B11) | 行为更正确但延迟分布变化(多一轮重试) | 属预期升级,遥测可观测 |
|
||||
| 遥测表结构变化(B14) | 旧 `llm_calls` 表不兼容 | 新库文件起步;生产 Postgres 遥测属 M2(§9-R4) |
|
||||
|
||||
## 9. 对库的反向约束清单
|
||||
|
||||
**⚠️ 架构缺口**(ARCHITECTURE.md 现设计与项目实际的不匹配,M1 设计文档必须解决):
|
||||
|
||||
| # | 缺口 | 证据 | 里程碑 |
|
||||
|---|---|---|---|
|
||||
| G1 | **缓存租户维度缺 per-call 通道**: §7.5 将 namespace 描述为装配级("项目名/租户 id"),但 GovDoc 是**单 GatewayClient 服务多租户**,tenant 每请求变化;§5.2 ChatRequest 的"per-call 覆盖项"未明确含缓存 namespace/tenant。若只有装配级 namespace,GovDoc 的多租户缓存隔离铁律无法满足,B1 修复不成立 | GovDoc CLAUDE.md §4.2 多租户铁律;api/deps.py 每请求解析租户 | M1 |
|
||||
| G2 | **`GatewayClient.chat()` 公共签名未定稿**: §11.1 承诺兼容 `chat(messages, *, session_id, parent_call_id)` "或一行 shim",但 §5 未把 session_id/parent_call_id(及 G1 的 per-call 租户)列入 chat 签名/ChatRequest 字段。不定稿则 §4 的"loop.py:377 零改动"无法承诺 | protocols.py:19-25;loop.py:377 | M1 |
|
||||
|
||||
**其余反向约束**(现设计已覆盖或属细化,逐条对应里程碑):
|
||||
|
||||
| # | 约束 | 里程碑 |
|
||||
|---|---|---|
|
||||
| R1 | `from_env()` 工厂必须存在且覆盖单源装配(GovDoc 装配层缺失,库是唯一装配来源);缺关键配置报错而非默认值兜底 | M1 |
|
||||
| R2 | 错误类型(TransientError/AllSourcesExhausted/CircuitOpenError)必须在库顶层公开导出,供业务侧 AgentLoop 步级重试引用(§4 非零改动点的前提) | M1 |
|
||||
| R3 | `LLMResponse` 新增字段(source_name/cost/usage_source)须带默认值,保证 GovDoc 测试 fake 的旧 11 字段构造(conftest.py:39)零改动 | M1 |
|
||||
| R4 | Postgres 遥测后端(GovDoc 生产要求,telemetry_sqlite.py:1 docstring 自证"生产环境必须由业务侧注入 Postgres 实现") | M2 |
|
||||
| R5 | `GatewayClient` 须提供显式关闭 API(如 `aclose()`)供 FastAPI lifespan/arq shutdown 释放 httpx 连接——§7.1 每源一个 AsyncClient 但未定义客户端生命周期公共 API(旧实现有 `close()`,client.py:580) | M1 |
|
||||
| R6 | 定稿"被取消的调用是否记遥测"(旧行为不记,B16;库"遥测必录"铁律未涵盖取消路径) | M1 |
|
||||
| R7 | 内存限流在多进程(arq)下的口径须在文档/配置中显式警示(B7 过渡期语义),避免误配全局限额 | M1 |
|
||||
|
||||
以上 G1/G2 若按本文方向解决,GovDoc 全量迁移中业务代码(agent/workflow/govdoc api)对治理层的消费**零改动**,改动收敛于: 装配净新增、types.py/protocols.py/test_imports.py 三处改写、`.env` 改键。
|
||||
@@ -0,0 +1,199 @@
|
||||
# Video-Tree-TRM5 迁移文档(迁移即验收)
|
||||
|
||||
> **定位**: 本文是 `ARCHITECTURE.md §11.2` 的展开——库建成后如何合并进 Video-Tree-TRM5、替换其哪些内部组件。它既是 M4 迁移的操作指南,也是 M1-M3 设计的反向约束(库公共 API 必须让本文描述的迁移成立)。**与 ARCHITECTURE.md 冲突时以 ARCHITECTURE.md 为准。** 全部结论基于 2026-07-20 对 `reference/Video-Tree-TRM5/` 的代码实测(file:line 均为实测证据)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 迁移目标与验收定义
|
||||
|
||||
**验收公式**: 删除清单文件全删 → import 替换为 `from polygateway import ...` → 项目原业务测试全绿(治理层自身单测随文件一并删除,由库的测试继任)。凡替换不掉的能力 = 库的边界缺口,回补后重验。
|
||||
|
||||
| 阶段 | 范围 | 验收 |
|
||||
|---|---|---|
|
||||
| **最小接入冒烟**(M1 后) | 独立分支上,用 `GatewayClient.from_env()` 装配 SEARCH 角色单 client,替换 `tools/build_trees.py` 的 `_build_clients()` 跑通一个小批量建树(LLM+VLM 路径、缓存、遥测落库) | 冒烟脚本成功 + telemetry 落库可查 + 缓存命中率非零(重跑同视频) |
|
||||
| **全量迁移**(M4,需 M3 完成 OCR) | 删除清单全删、8 处装配点统一 `from_env`、OCR 换 `OcrTextPort`、AgentLoop 异常集合改造 | `make test` 业务测试全绿 + 一次完整 train run 对照旧遥测指标无回归 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状盘点
|
||||
|
||||
行数为 `wc -l` 实测。治理相关约 2555 行,其中**可删 1296 行**。
|
||||
|
||||
| 文件 | 行数 | 职责一句话 | 迁移后命运 |
|
||||
|---|---|---|---|
|
||||
| `adapters/llm.py` | 595 | `GovernedLLMClient`:熔断→缓存→重试+SSE 流式→写缓存→遥测五层内联于一个 chat 方法 | **删除**(库 client+middleware 继任) |
|
||||
| `adapters/breaker.py` | 85 | 进程内熔断器(时钟注入、半开单探针、force_open) | **删除**(库 `backends/memory`,本身即移植蓝本) |
|
||||
| `adapters/streaming.py` | 131 | 三层活性看门狗纯函数 | **删除**(库 `streaming.py`,近原样移植) |
|
||||
| `adapters/redis_cache.py` | 128 | sha256 内容寻址响应缓存,TTL 校验、静默降级 | **删除**(库 `backends/redis` 缓存继任) |
|
||||
| `adapters/telemetry.py` | 229 | SQLite 遥测(单连接+锁+WAL+幂等+to_thread+降级) | **删除**(库 `telemetry/sqlite.py` 继任) |
|
||||
| `adapters/ocr.py` | 128 | MonkeyOCR `/ocr/text` **裸调**:同步 requests、线程轮询双端点、单帧失败跳过 | **删除**(换库 `OcrTextPort`,升级为全治理;行过滤/拼接逻辑上移业务侧,见 §4) |
|
||||
| `adapters/vlm.py` | 131 | base64 编码图片并注入最后一条 user message,委托 LLM client | **保留改写**(业务侧封装:`_inject_images` 保留,委托对象换成库 client) |
|
||||
| `adapters/embedding.py` | 184 | local(sentence-transformers)/remote(OpenAI SDK) 嵌入 | **保留**(Q3 未决;remote 实测**无任何重试**且为同步调用,见 §9-R11) |
|
||||
| `adapters/baseline_diagnosis_store.py` | 183 | 基线诊断结果 SQLite 存储(业务数据) | **保留**(业务侧) |
|
||||
| `core/protocols.py` | 69 | `LLMProvider`/`VLMProvider`/`TelemetryRecorder` 三 Protocol | **改写**(LLMProvider 由库满足;VLMProvider 指向改写后的 vlm.py;TelemetryRecorder 删除,见 §3) |
|
||||
| `core/types.py` | 182 | `LLMResponse`(11 字段) + 业务类型 | **改写**(LLMResponse 改为 re-export 库类型;其余保留) |
|
||||
| `main.py` `_build_adapters()` | 337(全文件) | Composition Root:手写装配 breaker/cache/telemetry/llm×2/vlm/embed/ocr | **改写**(按角色 `from_env`) |
|
||||
| `app/ports.py` | 173 | 应用层端口,含 `OCRProvider.transcribe_frames` | **改写**(OCRProvider 保留但实现改为包装库 `OcrTextPort` 的业务适配器) |
|
||||
| `tools/{build_trees,repair_trees,generate_questions}.py`、`app/harness/video_split_cli.py` | 368/492/1462/824 | 各自复制一份手写装配(共 6 处 `GovernedLLMClient(...)` 构造) | **改写**(装配段换 `from_env`,业务编排保留) |
|
||||
| 治理层单测 `tests/unit/test_{governed_llm,streaming,vlm_adapter,breaker,ocr_adapter,redis_cache,telemetry,infra_settings}.py` 等 | — | 治理栈自身测试 | **删除**(库测试继任);业务测试(假 LLMProvider 注入)保留为验收基准 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 组件替换映射表(签名实测对比)
|
||||
|
||||
| 项目组件 | 库对应物 | 签名兼容性(实测) | shim 形态 |
|
||||
|---|---|---|---|
|
||||
| `LLMProvider.chat(messages, *, session_id=None, parent_call_id=None, cache_salt=None)` (`core/protocols.py:22-29`) | `GatewayClient.chat()` | ARCHITECTURE §5.2/§7.8/§7.5 含这三个语义(链路 id 调用方传入、salt 进 key),但 **§5.1 未固化 `chat()` 便捷方法的 kwargs 形态** | 库若原生支持同名 kwargs 则零 shim(首选,见 §9-R1);否则 3 行包装函数 |
|
||||
| `VLMProvider.chat_with_images(messages, images: list[str\|Path], ...)` (`core/protocols.py:36-44`) | **无库端口**(VLM 封装属业务侧,ARCHITECTURE §11.2) | 库 `chat()` 须原生接受多模态 content parts(`image_url` data URL 数组,`adapters/vlm.py:112-130` 组装格式) | `vlm.py` 改写为包装库 client:保留 `_encode_image`/`_inject_images`,`self._llm.chat(...)` 换成库调用,对 33 处业务调用点零改动 |
|
||||
| `TelemetryRecorder.record_llm_call(15 个关键字参数)` (`core/protocols.py:51-69`) | 库内部遥测(单一 helper 铁律) | 业务侧**无人调用** `record_llm_call`(grep 实测仅 Protocol 定义与 adapter 实现);`Runner` 注入 telemetry 后从未使用(`runner.py:738` 赋值即终点,死注入) | 无需 shim:删 Protocol、删 Runner 参数 |
|
||||
| `LLMResponse`(11 字段) (`core/types.py:19-29`) | 库 `LLMResponse`(§5.1 超集) | 字段逐一比对**完全一致**(content/thinking/model/provider/prompt_tokens/completion_tokens/latency_ms/ttft_ms/max_inter_token_ms/cache_hit/call_id);库新增 source_name/cost/usage_source 只增不删 | `core/types.py` 改 re-export:`from polygateway import LLMResponse` |
|
||||
| `CircuitOpenError` (`adapters/llm.py:36`) | 库 `CircuitOpenError`(§6.1) | 业务侧无 except 该异常(grep 实测),仅治理层内部 | 无需 shim |
|
||||
| `MonkeyOCRClient.transcribe_frames(frame_paths) -> str` (`adapters/ocr.py:88`) | `OcrTextPort.recognize_text(bytes)`(§7.10) | **不兼容**:项目端口收路径列表、返回拼接文本、单帧失败跳过;库端口收单帧 bytes、失败抛异常 | 业务适配器(约 30 行):读文件→逐帧 `recognize_text`→行过滤去重→`"帧N: ..."` 拼接→单帧异常捕获跳过;实现 `app/ports.py:OCRProvider` 不变,`vision.py:105` 调用点零改动 |
|
||||
| `EmbeddingProvider`(同步 `embed()`) (`app/ports.py:17-36`) | 暂无(Q3 开放) | 若 M2 纳入:库必为异步端口,同步调用点需适配 | 本次迁移不动 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 调用点清单(grep 实测,33 处 await)
|
||||
|
||||
业务调用点全部经 Protocol,**迁移后形态不变**(前提是 §3 的 shim 策略成立);需要改动的只有装配点与两个特殊点。
|
||||
|
||||
| 分组 | 调用点 (file:line) | 迁移后形态 |
|
||||
|---|---|---|
|
||||
| `llm.chat` (16 处) | `core/agent/loop.py:336`、`core/evolution/evolve.py:566,881,1074,1162`、`core/evolution/diagnose.py:389,982,1917`、`app/tree/video_builder.py:1117`、`app/search/summarizer.py:236`、`app/tree/repair/{regenerator.py:436,480, supplement.py:460}`、`app/harness/momentum.py:163`、`app/question_gen/{gates.py:354,389,423, pipeline_v2.py:753}` | 不变(库 client 直接满足签名) |
|
||||
| `vlm.chat_with_images` (14 处) | `app/tree/video_builder.py:940,1047,1084`、`app/search/vision.py:126,148`、`app/tree/repair/regenerator.py:390`、`app/question_gen/{adversarial_filter.py:393, distractor_selector.py:150,185, gates.py:314,322, synthesizer.py:720, generator_v2.py:424}` | 不变(改写后的 vlm.py 继续满足 VLMProvider) |
|
||||
| `ocr.transcribe_frames` (1 处) | `app/search/vision.py:105`(外层已有降级边界 105-107:失败 warning 不注入) | 不变(业务适配器满足 OCRProvider);底层升级为多源+重试+熔断 |
|
||||
| 装配点 (8 处构造) | `main.py:105`(经 `_make_llm` 实例化于 122,130)、`app/harness/video_split_cli.py:328`、`tools/build_trees.py:216,232`、`tools/repair_trees.py:154,172`、`tools/generate_questions.py:265,988,1007` | 全部换 `from_env` 按角色装配(§5) |
|
||||
|
||||
**三个必须专门论述的点**:
|
||||
|
||||
1. **四角色装配与 `evolve_llm = llm` 别名**(`main.py:128`):`.env` 定义了 SEARCH/JUDGE/VL/EVOLVE 四组配置(`.env.example:6-23`),但实测 `InfraSettings` 只读 search/vl/evolve 三组(`main.py:24-34`)且 **EVOLVE_LLM_\* 读入后从未使用**——`_build_adapters` 直接 `evolve_llm = llm` 静默共享(配置面看似独立实则死配置);JUDGE 组不在 InfraSettings 里,仅 `tools/generate_questions.py:1008-1010` 直接 `os.environ` 裸读。迁移后按 §7.7 逻辑角色装配,**共享必须显式**:EVOLVE 要么显式声明共享 SEARCH client,要么启用独立配置——需人类拍板(行为变化见 §8)。
|
||||
2. **`tools/build_trees.py` 跨视频共享 semaphore**:`api_sem = asyncio.Semaphore(api_concurrency)`(build_trees.py:294,`TREE_BUILD_API_CONCURRENCY=16`)注入 `VideoTreeBuilder(api_semaphore=)`,包裹 LLM 与 VLM **两个 client 的每次调用**(video_builder.py:365-366)。迁移后由库限流承接而非 `gather_bounded`——`gather_bounded` 限的是任务列表并发,这里限的是"跨两个 client 的全局在途调用数",语义对应库的**全局并发闸**。前提:SEARCH 与 VL 两个 client 共享同一 limiter 后端实例(⚠️ 见 §9-R5);配额满行为配 `wait` 才与 semaphore 等价。
|
||||
3. **AgentLoop 双层重试**(`core/agent/loop.py:84-101,295-315`):步级重试 `step_retries=2, delays=(20,40)`,捕获 `(TimeoutError, OSError)`——即"穿透治理层的瞬时异常"。库单层重试原则(§7.2)下,transport 会把这些异常翻译为领域错误,**`TimeoutError/OSError` 将不再穿出,原配置下步级重试静默失效**(TransientError 不是 OSError 子类)。去留论述:这层的语义实为"任务步兜底"(治理层重试预算耗尽后再给整步一次机会),属业务侧任务级重试,库允许在库外包。**建议保留但必须改造**:利用 AgentLoop 已参数化的 `retryable_exceptions` 注入库的 `(TransientError, AllSourcesExhausted)`;若判定治理层预算已足够则删除该层——二选一,禁止保留原异常集合(静默失效 = 隐性行为删除)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置迁移
|
||||
|
||||
### 5.1 .env 键名映射
|
||||
|
||||
| 旧键 (`.env.example` 实测) | 库键(形态待 M1 定稿,按 §9 约定) | 语义差异 |
|
||||
|---|---|---|
|
||||
| `SEARCH_LLM_{MODEL,BASE_URL,API_KEY}` | `SEARCH__{PROVIDER}__1__{MODEL,BASE_URL,API_KEY}` | 单源→源列表长度 1 的特例;provider 由键名显式给出,消灭 `model.split("-")[0]` 猜测(main.py:109) |
|
||||
| `JUDGE_LLM_*` | `JUDGE__...` | 现状仅被 generate_questions.py 裸读 os.environ;迁移后归入统一装配 |
|
||||
| `VL_LLM_*` | `VL__...` | 同 SEARCH |
|
||||
| `EVOLVE_LLM_*` | `EVOLVE__...` **或删除** | 现状为死配置(§4 点 1);显式共享则删键,独立则启用——人类拍板 |
|
||||
| `LLM_TIMEOUT` / `LLM_MAX_RETRIES` / `LLM_RETRY_BASE_DELAY` / `LLM_RETRY_MAX_DELAY` / `LLM_CIRCUIT_BREAKER_{THRESHOLD,COOLDOWN}` / `LLM_TTFT_TIMEOUT` / `LLM_INTER_TOKEN_TIMEOUT` | **同名沿用**(§9 承诺) | 熔断阈值 `max(cfg, concurrency*2)` 从 .env 注释手动约定(.env.example:44)变为库内自动计算(§7.4) |
|
||||
| `REDIS_URL` / `REDIS_CACHE_TTL` | 库缓存后端配置(TTL>0 校验继承,redis_cache.py:27-31) | 语义同 |
|
||||
| (无) | **缓存 namespace(新增必填)** | §7.5 key 公式新增字段,建议 `video-tree-trm5` |
|
||||
| `MONKEY_OCR_URLS`(逗号列表) | `OCR__MONKEY__{1,2}__BASE_URL` | 逗号列表→多源配置;轮询升级为选源策略 |
|
||||
| `TREE_BUILD_API_CONCURRENCY` | 库全局并发限额键 | 语义:跨 SEARCH+VL 两 client 的全局在途上限(⚠️ §9-R5) |
|
||||
| `ASR_*`(Groq whisper) | **删除** | 死配置零实现(D10 已确认不作需求证据) |
|
||||
| `EMBED_API_KEY/URL` | 保留业务侧 | Q3 未决 |
|
||||
| `no_proxy`/`NO_PROXY` | 保留环境级;OCR 的 `trust_env=False` 需库支持 | ⚠️ §9-R9 |
|
||||
|
||||
### 5.2 装配代码 before/after
|
||||
|
||||
Before(`main.py:_build_adapters`,手写约 110 行,节选骨架):
|
||||
|
||||
```python
|
||||
breaker = CircuitBreaker(fail_threshold=..., cooldown_s=...)
|
||||
cache = RedisResponseCache(redis=aioredis.from_url(...), ttl_s=ttl) if settings.redis_url else None
|
||||
telemetry = SQLiteTelemetryRecorder(Path("logs/telemetry.db"))
|
||||
def _make_llm(model, base_url, api_key, *, thinking):
|
||||
return GovernedLLMClient(model=..., provider=model.split("-")[0], breaker=breaker,
|
||||
cache=cache, telemetry=telemetry, ...) # 15 个参数
|
||||
llm = _make_llm(settings.search_llm_model, ..., thinking=True)
|
||||
evolve_llm = llm # 静默别名,EVOLVE_LLM_* 配置被忽略
|
||||
vlm = GovernedVLMClient(_make_llm(settings.vl_llm_model, ..., thinking=False))
|
||||
ocr = MonkeyOCRClient(urls=settings.monkey_ocr_urls.split(","))
|
||||
```
|
||||
|
||||
After(伪代码,具体 API 以 M1 设计定稿为准):
|
||||
|
||||
```python
|
||||
from polygateway import GatewayClient
|
||||
|
||||
llm = GatewayClient.from_env(role="SEARCH")
|
||||
evolve_llm = llm # 共享必须显式:或 from_env(role="EVOLVE")
|
||||
vlm = GovernedVLMClient(GatewayClient.from_env(role="VL")) # 业务侧 base64 封装保留
|
||||
ocr = FrameOcrAdapter(OcrTextPort_from_env()) # 业务适配器包装库 OCR 端口
|
||||
# breaker/cache/telemetry/limiter 全部由 from_env 按配置内建,共享后端见 §9-R5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 迁移步骤(每步一个 commit 作回滚点)
|
||||
|
||||
| # | 步骤 | 验证方式 | 回滚 |
|
||||
|---|---|---|---|
|
||||
| 0 | M1 后最小接入冒烟(§1,独立分支) | 冒烟脚本 + 遥测/缓存实测 | 丢弃分支 |
|
||||
| 1 | feature 分支;`.env` 增库键(旧键暂双存) | `from_env` 装配冒烟 | git revert |
|
||||
| 2 | `core/types.py` LLMResponse 改 re-export;`core/protocols.py` 删 TelemetryRecorder、LLMProvider 指库 | `make test`(类型层面无行为变化) | 单 commit revert |
|
||||
| 3 | `adapters/vlm.py` 改写为包装库 client;`main.py:_build_adapters` 换 from_env(EVOLVE 决策落地);Runner 删 telemetry 死注入参数 | `make test` + `--mode infer` 小样本跑通 | 单 commit revert |
|
||||
| 4 | AgentLoop `retryable_exceptions` 注入库异常(或删除步级重试,按 §4 点 3 决策) | 断网/假 transport 注错的集成测试证明步级重试可触发 | 单 commit revert |
|
||||
| 5 | 删除 `adapters/{llm,breaker,streaming,redis_cache,telemetry}.py` 及其单测 | `make test` 全绿;grep 确认无残留 import | 单 commit revert |
|
||||
| 6 | 四个工具/CLI 装配点换 from_env(build_trees 删 semaphore 改库全局并发闸;generate_questions 删 JUDGE 裸读) | 每个工具 `--limit 1` 级小跑 | 逐工具 commit |
|
||||
| 7 | (M3 后)`adapters/ocr.py` 删除,业务适配器接 `OcrTextPort` | vision.py 路径集成测试 + OCR 注入率遥测对照 | 单 commit revert |
|
||||
| 8 | 一次完整 train run 对照旧遥测(准确率/成本/缓存命中率);删旧 .env 键 | 指标无回归 | 保留分支不合并 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 旧版行为审计(逐条:保留/替换/修复/有意放弃)
|
||||
|
||||
| # | 现有行为(含非功能行为,file:line) | 迁移后 |
|
||||
|---|---|---|
|
||||
| 1 | VLM 响应**也走缓存**,且 base64 data URL 整段进 sha256 key(vlm→llm 委托 + redis_cache.py:69-73 全量 messages 进 hash) | **替换**:多模态 part 先摘要再 hash(§7.5)——key 全变,见 §8 风险 1 |
|
||||
| 2 | `cache_salt` 仅非 None 才进 key payload(保证旧键不失效,redis_cache.py:63-71) | **有意放弃**:库 key 公式重构,无旧键兼容义务 |
|
||||
| 3 | SSE 流耗尽未收 `[DONE]` → `_SseAnomaly("truncated_no_done")` 判瞬时、不写缓存(llm.py:584-586) | **保留**(归 TransientError,D2 明确此信号是选手写 transport 的理由) |
|
||||
| 4 | qwen `<think>` 剥离 + deepseek `reasoning_content`;thinking 注入靠 `"qwen" in provider` 字符串猜(llm.py:130-164,357) | **替换**:D11 provider 注册表,行为等价、机制显式 |
|
||||
| 5 | 熔断按 provider 字符串计数;main.py 中 SEARCH 与 VL **共享同一 breaker 实例**(main.py:80,111),provider 不同故计数分开 | **替换**:按 source_name 计数(§7.4),语义更细 |
|
||||
| 6 | 半开只放一个探针防惊群(breaker.py:44-48);401/403 `force_open` 一击即熔(llm.py:410-411) | **保留**(库蓝本即此实现) |
|
||||
| 7 | 429 一律判瞬时重试,**不细分 insufficient_quota**;无 Retry-After 解析(llm.py:169,185-189) | **修复**:429+insufficient_quota → SourceDeadError;Retry-After 取大者(§6.2) |
|
||||
| 8 | 缓存命中:独立 call_id、latency_ms=0、遥测必录 cache_hit=True(llm.py:309-341) | **保留**(§4.4 同款) |
|
||||
| 9 | 遥测降级不冒泡(初始化失败 conn=None、写失败 warning 丢弃,telemetry.py:77-98,126-153);`INSERT OR IGNORE` 幂等 | **保留**(降级方向铁律同向) |
|
||||
| 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 |
|
||||
| 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) |
|
||||
| 17 | AgentLoop 步级重试捕 `(TimeoutError, OSError)`(loop.py:92) | **改造或删除**(§4 点 3;保留原样 = 静默失效,禁止) |
|
||||
| 18 | `CancelledError` 全栈穿透(loop.py:280-282 明确不捕获;治理层无 `except BaseException`) | **保留**(库铁律同向) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 行为差异与风险
|
||||
|
||||
| # | 差异/风险 | 影响与缓解 |
|
||||
|---|---|---|
|
||||
| 1 | **缓存 key 全量变化**(key 前缀、namespace、多模态摘要、salt 进 payload 方式均变)→ 迁移瞬间全量缓存失效 | 一次性重付费;TTL 本为 7 天(`REDIS_CACHE_TTL=604800`)自然滚动。缓解:迁移窗口选在训练间隙,首个 run 按无缓存成本预算 |
|
||||
| 2 | OCR 从裸调升级全治理:失败行为从"静默跳过"变"重试→换源→熔断→最终抛异常" | 延迟分布变化(重试引入等待);`vision.py:105-107` 降级边界保留,最终失败仍不中断推理。收益:双端点一台挂掉不再损失 50% 帧证据 |
|
||||
| 3 | 429 语义变化:insufficient_quota 从"白烧重试预算"变"立即熔断换源" | 行为更优;单源配置下表现为快速失败——排查配额问题更快但对"等配额恢复"场景需配 wait |
|
||||
| 4 | semaphore→限流闸:等待语义需显式配 `wait`,且依赖跨 client 共享后端(§9-R5 未闭合前 build_trees 迁移被阻塞) | M1 设计必须先解决 R5,否则该工具只能临时保留 semaphore |
|
||||
| 5 | 遥测 schema 变化:表结构/字段名(model_name→model)与新增字段 | grep 实测项目内无 telemetry.db 读取方(仅写入),风险低;旧 db 留档即可 |
|
||||
| 6 | 熔断阈值改库内自动 `max(cfg, concurrency*2)` | 与 .env 注释的手动约定等价,风险低 |
|
||||
| 7 | EVOLVE 角色决策(共享或独立)可能改变进化步的模型/配额行为 | 迁移前人类拍板并写进配置注释 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 对库的反向约束清单(本文档最重要产出)
|
||||
|
||||
| # | 约束 | 里程碑 | 状态 |
|
||||
|---|---|---|---|
|
||||
| R1 | `chat()` 便捷方法必须接受 `session_id` / `parent_call_id` / `cache_salt` 关键字参数(33 处调用点零改动的前提) | M1 | 语义已覆盖(§7.5/§7.8),**签名形态需 M1 设计固化** |
|
||||
| R2 | `chat()` 原生接受多模态 content parts 数组(`image_url` data URL),VLM 封装才能留业务侧 | M1 | 已覆盖(§2.3/§11.2) |
|
||||
| R3 | `LLMResponse` 11 字段只增不删不改名 | M1 | 已覆盖(§5.1,实测一致) |
|
||||
| R4 | 逻辑角色 `from_env(role=...)` 须支持 SEARCH/JUDGE/VL/EVOLVE 四角色 + 显式共享声明 | M1 | 已覆盖(§7.7) |
|
||||
| R5 | ⚠️ **架构缺口**:多角色 client 须能**共享同一限流/熔断状态后端**(`TREE_BUILD_API_CONCURRENCY` 的全局并发闸横跨 SEARCH+VL 两 client;main.py 现状 breaker 也跨 client 共享)。`from_env` 按角色装配多个 client 时的后端共享语义 ARCHITECTURE 未定义 | M1 | **缺口** |
|
||||
| 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 | **缺口** |
|
||||
| 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 | **缺口** |
|
||||
|
||||
---
|
||||
|
||||
*不确定项声明*: 库侧 `from_env(role=...)`、OCR 装配工厂的具体 API 名称以 M1/M3 设计文档定稿为准,本文 §5.2/§6 中相应伪代码届时同步修订;`telemetry.db` 是否存在项目外的离线分析脚本消费方未能实测(项目内 grep 无读取方)。
|
||||
Reference in New Issue
Block a user