# Video-Tree-TRM5 架构设计

**版本** v1 · **状态** 提案 · **读者** 工程团队

本文分两部分。**Part 1「架构」与具体技术栈无关**，只描述边界、依赖方向、接缝与核心算法——它是长期承诺，预期能跨越多次技术选型而不变。**Part 2「韧性与运维」描述 LLM 调用治理与可观测规范**——它随实现演进，但当前约束所有实现必须满足。

---

## §1 核心定位

**项目目标**：在层次化视频树上构建可自我进化的搜索 Agent，实现长视频理解。目标会议 EMNLP 2026。

**系统类比**——自进化循环对标 PyTorch 训练：

| PyTorch | 本项目 | 代码位置 |
|---------|--------|----------|
| DataLoader | 出题 question_gen | `app/question_gen/loader.py` |
| model.forward() | 推理 inference | `app/harness/inference.py` + `core/agent/loop.py` |
| loss.backward() | 诊断 diagnose | `core/evolution/diagnose.py` |
| optimizer.step() | 进化 evolve | `core/evolution/evolve.py` |
| nn.Parameter | Skills + Prompts（版本化） | `store/skills/`, `store/prompts/` |
| training loop | 外层循环 runner | `app/harness/runner.py` |
| checkpoint | Store 版本快照 | `app/harness/workspace.py` |

只有 inference 是 agent-controlled（LLM 自主调工具），其余三步是 code-controlled workflow。

**三大模块**：

| 模块 | 目录 | 一句话定义 |
|------|------|-----------|
| 建树 | `app/tree/` | 离线预处理——VLM 生成三层 TreeIndex（L1段落→L2片段→L3帧），支持字幕注入和后增强 |
| 训练 | `app/harness/` + `core/` | 自进化循环（推理→诊断→进化）+ CE-Gate 信息阶梯 |
| 新题构建 | `app/question_gen/` | 生成 Video-MME 风格训练题，原始 benchmark 作 held-out 泛化评测 |

---

## §2 分层架构（Clean Architecture）

### 2.1 四层依赖规则

```mermaid
flowchart TB
    subgraph 外部实现层
        AD_LLM["adapters/llm.py\nGovernedLLMClient"]
        AD_VLM["adapters/vlm.py"]
        AD_EMB["adapters/embedding.py"]
        AD_CACHE["adapters/redis_cache.py"]
        AD_TEL["adapters/telemetry.py"]
        AD_OCR["adapters/ocr.py"]
        AD_ASR["adapters/asr.py"]
    end

    subgraph 应用层
        TREE["app/tree/\n建树模块"]
        HARNESS["app/harness/\n训练循环"]
        QGEN["app/question_gen/\n新题构建"]
        SEARCH["app/search/\nAgent 装配"]
        PORTS["app/ports.py"]
    end

    subgraph 可提取内核
        AGENT["core/agent/\nAgentLoop 引擎"]
        EVO["core/evolution/\n诊断+进化引擎"]
        CPROTO["core/*/protocols.py"]
    end

    AD_LLM & AD_VLM & AD_EMB & AD_CACHE & AD_TEL -->|实现| CPROTO & PORTS
    HARNESS --> AGENT & EVO
    SEARCH --> AGENT
    TREE & QGEN --> PORTS
    AGENT & EVO -->|定义| CPROTO
```

依赖只能向内，源码 import 方向严格单向。内层不认识外层：`core/` 不知道自己被哪个 `app/` 模块组合，`app/` 不知道 Protocol 背后是哪个 adapter。判据：**任意一个 `core/` 模块，搬到没有 adapters 的环境里，用假实现替换 Protocol 即可原样运行**。

### 2.2 模块交互全景

```mermaid
flowchart TD
    CLI["main.py CLI"] --> RUNNER["app/harness/runner.py\n训练循环编排"]
    CLI --> BUILD["app/tree/video_builder.py\n建树"]
    CLI --> QGEN["app/question_gen/loader.py\n新题构建"]

    RUNNER --> INF["app/harness/inference.py\n推理 step"]
    RUNNER --> DIAG["core/evolution/diagnose.py\n诊断"]
    RUNNER --> EVOL["core/evolution/evolve.py\n进化"]
    RUNNER --> BATCH["app/harness/batching.py\nmini-batch"]
    RUNNER --> GATE["core/evolution/gate.py\nCE-Gate"]
    RUNNER --> WS["app/harness/workspace.py\nStore + Workspace"]

    INF --> LOOP["core/agent/loop.py\nAgentLoop"]
    LOOP --> SEARCH["app/search/\nprompt + skills"]
    LOOP --> LLM["adapters/llm.py\nGovernedLLMClient"]

    BUILD --> VLM["adapters/vlm.py"]
    BUILD --> EMB["adapters/embedding.py"]
```

### 2.3 目录结构

```text
project_root/
├── core/                    # 可提取内核（不依赖 app/、adapters/）
│   ├── protocols.py         # 共享端口：LLMProvider, VLMProvider, TelemetryRecorder
│   ├── agent/               # 【可提取包】AgentLoop 引擎
│   │   ├── loop.py          # Thinking+JSON 推理循环，pluggy hook
│   │   ├── types.py         # Step, LoopResult
│   │   └── protocols.py     # Agent 专属：ToolDispatcher, AgentLoopSpec
│   ├── evolution/           # 【可提取包】诊断+进化引擎
│   │   ├── diagnose.py      # 两阶段诊断管线
│   │   ├── evolve.py        # patch/rewrite 进化
│   │   ├── gate.py          # CE-Gate e-process
│   │   ├── validate.py      # 块顺序验证
│   │   ├── types.py         # DiagnosisResult, EvolutionRecord, ...
│   │   └── protocols.py     # Evolution 专属：SkillStore, PromptStore, RunLog
│   └── types.py             # 跨模块共享类型
│
├── app/                     # 应用层（组合 core + adapters，领域特化）
│   ├── tree/                # 模块1：建树
│   │   ├── index.py         # TreeIndex 数据结构（L1/L2/L3Node + Card）
│   │   ├── video_builder.py # VideoTreeBuilder（L2 轴心 + asyncio 并发）
│   │   ├── config.py        # TreeConfig 配置 dataclass
│   │   ├── subtitle.py      # SRT 解析 + Voronoi 字幕分配
│   │   ├── verify.py        # 交叉校验（entities/visible_text）
│   │   ├── environment.py   # 树环境语义搜索（分块 embedding + 祖先去重）
│   │   └── repair/          # 后修复管线
│   │       ├── detector.py  # 缺陷检测（空字段/缺帧/时间间隙）
│   │       ├── regenerator.py # VLM 重描述 + 自底向上级联修复
│   │       └── supplement.py  # Q&A 反向补全
│   ├── harness/             # 模块2：训练 harness
│   │   ├── runner.py        # 训练循环编排（对标 Trainer）
│   │   ├── inference.py     # 推理 step
│   │   ├── batching.py      # mini-batch 构建
│   │   ├── pools.py         # 三池切分（数据加载已移至 question_gen/loader.py）
│   │   ├── gate_ladder.py   # 信息阶梯
│   │   ├── momentum.py      # 慢速动量
│   │   ├── config.py        # RunConfig
│   │   ├── log.py           # HarnessLog (SQLite)
│   │   └── workspace.py     # Store + Workspace 版本管理
│   ├── question_gen/        # 模块3：出题（加载 + 采样 + 未来 LLM 生成）
│   │   └── loader.py        # benchmark 加载、分层采样
│   ├── search/              # 搜索 Agent 装配
│   │   ├── prompt.py        # PromptManager（prompt 加载与拼装）
│   │   ├── skills.py        # SkillRegistry + discover_skills
│   │   ├── summarizer.py    # 两轮 LLM 摘要（view_node / search_similar 用）
│   │   ├── vision.py        # observe_frame（VLM 两轮 + OCR 注入）
│   │   └── tools.py         # SearchToolDispatcher（实现 ToolDispatcher）
│   └── ports.py             # 应用层特有端口
│
├── adapters/                # 外部实现层
│   ├── llm.py               # GovernedLLMClient（四层治理栈）
│   ├── streaming.py          # 流式三层看门狗（TTFT/inter_token/total）
│   ├── breaker.py            # CircuitBreaker（内存级熔断器）
│   ├── vlm.py               # VLM 客户端
│   ├── embedding.py         # Embedding 服务实现
│   ├── redis_cache.py       # Redis 响应缓存
│   ├── ocr.py               # MonkeyOCR 客户端
│   ├── asr.py               # ASR (Whisper) 客户端
│   └── telemetry.py         # SQLite 遥测记录实现
│
├── config/                  # 声明性配置（YAML，禁止 .py）
├── store/                   # 版本化资源（skills/prompts/questions/videos）
├── workspaces/              # 实验工作区
├── prompts/                 # 诊断 prompt（不参与进化，是评估标尺）
├── tests/                   # 测试
├── data/                    # 数据（不提交 Git）
├── logs/                    # 日志（不提交 Git）
├── results/                 # 实验结果
├── main.py                  # CLI 入口
└── research-wiki/           # 单一事实源
```

### 2.4 依赖方向硬性规则

| 层 | 可依赖 | 禁止依赖 |
|---|--------|---------|
| `core/` | 标准库、typing、pluggy、json_repair | `app/`、`adapters/`、任何框架 |
| `app/` | `core/`、标准库 | `adapters/`（只通过 Protocol） |
| `adapters/` | `core/`、`app/ports.py`、第三方库 | `app/` 内部模块 |

### 2.5 可提取内核

`core/agent/` 和 `core/evolution/` 是两个独立可提取包，只依赖 Protocol 接口。将它们连同 `core/types.py` 复制到另一个项目，提供 Protocol 实现即可运行——这是验证依赖方向是否守住的判据。

---

## §3 接缝清单（Protocol 端口）

只在真正易变、需替换或需造测试替身处引入接口，其余写直白的领域函数。

### 3.1 核心端口（`core/` 内，可提取）

**共享端口（`core/protocols.py`，跨子包）：**

| Protocol | 关键方法 | 职责 |
|----------|---------|------|
| `LLMProvider` | `chat()` | LLM 文本调用（全异步） |
| `VLMProvider` | `chat_with_images()` | VLM 图文调用（全异步） |
| `TelemetryRecorder` | `record_llm_call()` | LLM 调用遥测 |

**Agent 专属端口（`core/agent/protocols.py`）：**

| Protocol | 关键方法 | 职责 |
|----------|---------|------|
| `ToolDispatcher` | `dispatch(tool_name, args, context)` | Agent 工具调度 |
| `AgentLoopSpec` | `before_step/after_tool/after_step/on_finish` | pluggy 生命周期 hook |

**Evolution 专属端口（`core/evolution/protocols.py`）：**

core/ 侧 Protocol 只读——core/ 返回结果 dataclass，写入由 app/harness/ 编排层执行。写方法保留在 app/ 侧的实现类中。

| Protocol | 关键方法 | 职责 |
|----------|---------|------|
| `SkillStore` | `read_skill()`, `list_skill_files()` | 版本化技能读取（只读） |
| `PromptStore` | `read_prompt()`, `list_prompt_files()` | 版本化提示词读取（只读） |
| `RunLog` | `get_predictions()`, `get_traces()` | 实验日志查询（只读） |

### 3.2 应用层端口（`app/ports.py`）

| Protocol | 关键方法 | 职责 | 当前实现 |
|----------|---------|------|---------|
| `EmbeddingProvider` | `embed(texts)` | 文本嵌入 | `adapters/embedding.py` |
| `TreeCache` | `get()`, `set()` | 树索引缓存 | `adapters/redis_cache.py` |
| `ASRProvider` | `transcribe(audio_path)` | 语音识别 | `adapters/asr.py` |
| `OCRProvider` | `transcribe_frames(frame_paths: list[Path]) -> str` | 帧文字转录 | `adapters/ocr.py` |

判据：这块代码会不会被换实现、或需要在测试里替换成假的？不会，就别抽象。

### 3.3 当前实现映射

| Protocol | adapter 实现 | 说明 |
|----------|-------------|------|
| `LLMProvider` | `adapters/llm.py` `GovernedLLMClient` | OpenAI 兼容 API，内置治理栈（§5） |
| `VLMProvider` | `adapters/vlm.py` | Qwen VL 等 OpenAI 兼容 VLM API |
| `ToolDispatcher` | `app/search/tools.py` `SearchToolDispatcher` | 搜索 Agent 工具分发（view_node / search_similar / observe_frame / submit_answer / read_skill） |
| `SkillStore` / `PromptStore` | `app/harness/workspace.py` | 文件系统版本化存储（`store/skills/v{N}/`） |
| `RunLog` | `app/harness/log.py` `HarnessLog` | SQLite 持久化 |
| `TelemetryRecorder` | `adapters/telemetry.py` | SQLite `telemetry.db` |
| `EmbeddingProvider` | `adapters/embedding.py` | local（sentence-transformers）/ remote 双后端 |
| `TreeCache` | `adapters/redis_cache.py` | Redis 键值缓存 |
| `ASRProvider` | `adapters/asr.py` | Groq Whisper API |
| `OCRProvider` | `adapters/ocr.py` | MonkeyOCR 自建端点 |

---

## §4 Agent 遥测规范

每次 LLM/VLM 调用必须经过 `TelemetryRecorder` 记录以下字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `call_id` | str | UUID，本次调用唯一标识 |
| `parent_call_id` | str? | 父调用（agent step → LLM call 链路） |
| `session_id` | str | epoch/step/question 关联 ID |
| `model_name` | str | 使用的模型名 |
| `provider` | str | API 端点标识 |
| `messages` | str (JSON) | 原始输入 |
| `response` | str | 原始输出 |
| `thinking` | str | thinking/reasoning 内容 |
| `prompt_tokens` | int | 输入 token 数 |
| `completion_tokens` | int | 输出 token 数 |
| `latency_ms` | int | 延迟毫秒 |
| `ttft_ms` | float? | 首 token 延迟（流式测量） |
| `max_inter_token_ms` | float? | 最大 token 间隔（流式测量） |
| `cache_hit` | bool | 是否命中 Redis 缓存 |
| `error` | str? | 异常信息（正常为 null） |

**存储**：SQLite `telemetry.db`，`llm_calls` 表。`adapters/telemetry.py` 实现 `TelemetryRecorder` Protocol，`adapters/llm.py` 的 `GovernedLLMClient` 在每次调用后自动写入。

---

## §5 LLM 调用韧性

所有 LLM/VLM 调用必须经过 `GovernedLLMClient`（`adapters/llm.py`）治理，禁止裸调 OpenAI SDK。治理栈包含四层：

| 层 | 机制 | 说明 |
|---|------|------|
| 1 | 熔断器 | 连续 N 失败 → 短路 M 秒 → 探针恢复（`adapters/breaker.py`） |
| 2 | Redis 响应缓存 | content-addressed：`hash(model + messages)` → response |
| 3 | 流式三层看门狗 | TTFT / inter_token / total 超时保护（`adapters/streaming.py`） |
| 4 | 指数退避重试 | `max_retries`、`base_delay`、`max_delay`（可配置） |

**流式三层看门狗**：GovernedLLMClient 内部始终使用 `stream=True` 消费 SSE，通过 `stream_with_liveness_timeouts()` 实现三层超时保护——TTFT（首 token，含 thinking token）、inter_token（token 间隔）、total（总时长）。thinking token 刷新看门狗但不计入 content，确保长时间思考不会被误判为连接挂死。

熔断参数（`LLM_CIRCUIT_BREAKER_THRESHOLD`、`LLM_CIRCUIT_BREAKER_COOLDOWN`）、超时（`LLM_TIMEOUT`、`LLM_TTFT_TIMEOUT`、`LLM_INTER_TOKEN_TIMEOUT`）通过 `.env` 工程配置管理。

**治理流程伪代码**：

```
chat(messages) →
  if breaker.is_open: raise CircuitOpen     # 层1: 熔断
  call_id = uuid4()
  if cache.get(model, messages):            # 层2: 缓存
    telemetry.record(..., cache_hit=True)
    return cached
  retry loop:                                # 层4: 重试
    stream = httpx.stream(POST, stream=True)
    guarded = stream_with_liveness_timeouts( # 层3: 三层看门狗
      sse_deltas, ttft, inter_token, total)
    content, thinking, ttft_ms = consume(guarded)
    breaker.record_success()
    cache.set(model, messages, response)
    telemetry.record(...)
    return response
  except transient → backoff + breaker.record_failure()
  except 401/403 → breaker.force_open(); raise
```

---

## §6 核心算法保真清单

迁移时逐一比对参考代码，不可简化。建树 4 项 + 训练 8 项 = 12 项：

| # | 算法 | 参考文件 | 核心逻辑 |
|---|------|---------|---------|
| 1 | L2 轴心建树策略 | `reference/video_tree_trm/video_tree_builder.py` | L2 先行→L3 向下→L1 向上，asyncio 链式并发 |
| 2 | VLM 批量帧描述 + JSON fallback | `reference/video_tree_trm/video_tree_builder.py` | `_L3_BATCH_SIZE=5` 批量调用，解析失败逐帧 fallback |
| 3 | 断点续跑机制 | `reference/video_tree_trm/video_tree_builder.py` | `progress.json` + L1 中间 JSON，按段恢复 |
| 4 | CE-Gate e-process | TRM4 `core/harness/eprocess.py` | 截断 Beta 混合、四出口门控 |
| 5 | 信息阶梯 | TRM4 `core/harness/gate_ladder.py` | 冷启动 2:1、gamma-EMA、反泄漏 |
| 6 | 块顺序验证 | TRM4 `core/harness/validate.py` | 基线缓存、INFRA 护栏、配对翻转 |
| 7 | 诊断瀑布 | TRM4 `core/harness/diagnose.py` | 错误归因级联、缺陷 vs 失误、D1-D5 |
| 8 | 进化 patch 引擎 | TRM4 `core/harness/evolve.py` + `patch.py` | 保护跨度、rank-and-clip、附录/动量 |
| 9 | Mini-batch 构建 | TRM4 `core/harness/batching.py` | FFD + round-robin + 正确率混合 |
| 10 | Agent Loop | TRM4 `core/loop.py` | Thinking+JSON、json_repair、pluggy hook |
| 11 | 树环境语义搜索 | TRM4 `core/tree/environment.py` | 分块 embedding、祖先去重、锚定验证 |
| 12 | 训练循环编排 | TRM4 `core/harness/runner.py` | 三级嵌套、慢更新10步、断点续训 |

> **TRM4** 指 `/home/iomgaa/Projects/Video-Tree-TRM4/`，**reference** 指 `/home/iomgaa/Projects/Video-Tree-TRM5/reference/`。

---

## §7 配置管理（双模式）

系统配置分两套，按用途隔离：

| 模式 | 载体 | 适用 |
|------|------|------|
| 工程配置 | `pydantic-settings` + `.env` | 系统运行所需、少变或敏感（API 密钥、Redis URL、LLM 超时等） |
| 科研实验配置 | per-experiment YAML + harness run 快照 | 会在实验中反复扫动/对比的参数（gate 阈值、batch 大小、评估口径等） |

**D7 归属判定规则（防串台，强制遵守）**：某参数是否会在科研实验中被反复扫动/对比？

- **是** → 科研配置（per-experiment YAML + harness run 快照，保证可复现）。
- **否**（系统运行所需、少变、或敏感）→ 工程配置（`pydantic-settings` + `.env`）。

CLI args 仅用于单次临时覆盖，不作为任何配置的常驻来源。两类载体之外，严禁在代码中散落硬编码默认值；缺失关键配置应直接报错而非兜底。

**参数归属示例**：

| 参数 | 归属 | 理由 |
|------|------|------|
| `SEARCH_LLM_API_KEY` | 工程 `.env` | 敏感，不变 |
| `LLM_TIMEOUT` | 工程 `.env` | 系统运行所需，少变 |
| `gate_e_confirm` | 科研 YAML | CE-Gate 阈值，实验中反复调优 |
| `batch_size` | 科研 YAML | mini-batch 大小，实验中反复扫动 |
| `embed.model_name` | 科研 YAML | 嵌入模型选型，实验中对比 |
| `REDIS_URL` | 工程 `.env` | 基础设施地址，少变 |

---

## 附：横切原则

- **溯源**：每条推理链（Agent 看了哪些节点、如何汇总出答案）都带来源。遥测（§4）记录全部 LLM 调用，Store 版本化记录 skill/prompt 变更历史。
- **可复现**：每次 harness run 冻结当前 `config/` YAML + Store 版本快照（`app/harness/workspace.py`），结果可重算。
- **安全/合规**：API 密钥走 `.env`，不提交 Git；Redis 缓存中不存敏感信息的明文。
