# Video-Tree-TRM5 迁移文档（迁移即验收）

> [!IMPORTANT]
> **本迁移已放弃(2026-07-22 用户拍板: VT 项目本体已放弃,不再迁移)。** 全文保留作历史记录与能力倒推依据——VT 倒推的库能力(R1-R12,含多逻辑角色/cache salt/多模态摘要/OcrTextPort/trust_env/check_health)均已在 M1-M3 交付,不因放弃而回收。v1.0 验收标准改为 GovDoc + CHS 两项目(见 ROADMAP §5)。

> **定位**: 本文是 `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) 嵌入 | **remote 路径删除,换 `polygateway.EmbeddingClient`**(Q3 已拍板纳入 M2,2026-07-20;库已交付);local(sentence-transformers)保留。迁移要点: 库为 async `embed(list[str]) -> EmbeddingResponse(list[list[float]])`——VT 同步调用点需自包同步壳(`asyncio.run` 或事件循环内 await),ndarray/tensor 转换自带 adapter(`np.asarray(resp.vectors, dtype=np.float32)`);装配传 `EMBED__NORMALIZE=true` 保 L2 归一化语义 |
| `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`) | `polygateway.EmbeddingClient.embed`(async,M2 已交付) | 同步→异步适配 + list[list[float]]→ndarray 转换留业务侧 adapter;normalize 走库开关 | M4 执行 |

---

## 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(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） |
| 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 | ✅ 已闭(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 | **缺口** |

---

*不确定项声明*: 库侧 `from_env(role=...)`、OCR 装配工厂的具体 API 名称以 M1/M3 设计文档定稿为准，本文 §5.2/§6 中相应伪代码届时同步修订；`telemetry.db` 是否存在项目外的离线分析脚本消费方未能实测（项目内 grep 无读取方）。
