Files
Video-Tree-TRM5/research-wiki/designs/2026-07-07-app-harness-design.md
T

342 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design: app/harness/ 训练循环编排层
**日期** 2026-07-07 · **状态** 提案 · **范围** `app/harness/` 全部 14 个文件
## 1 定位
`app/harness/` 是自进化闭环的编排层,对标 PyTorch Trainer。它组合 `core/evolution/`(决策内核)+ `core/agent/`AgentLoop 推理引擎)+ `adapters/`LLM/VLM/telemetry),实现训练循环三级嵌套、块序贯验证、快慢双速进化、checkpoint/resume。
`core/evolution/` 的分工:core/ 做决策("候选好不好"),app/ 做编排("跑推理、写版本、管缓存、落观测")。
### 1.1 算法保真
| # | 算法 | 保真要求 |
|---|------|---------|
| 6 | 信息阶梯 | 冷启动 2:1 交错 + probe 探针、γ-EMA 更新、warm p̂(1-p̂) 排序、防泄露铁律 |
| 10 | mini-batch | FFD + round-robin + 正确率混合、确定性(seed) |
| 13 | 训练循环编排 | 三级嵌套 epoch→step→per-skill、快慢双速、checkpoint/resume、early stop、probation |
## 2 模块结构与依赖
```text
app/harness/
├── __init__.py # 公开 API
├── config.py # RunConfig frozen dataclass + 四层校验 + YAML 加载
├── log.py # HarnessLog SQLite 薄包装 + RunLog Protocol 实现
├── store.py # Store 版本操作 + Seed 管理(稳定基础设施)
├── workspace.py # Workspace 生命周期 + manifest + SkillStore/PromptStore 实现
├── inference.py # async run_inference(并发推理编排)
├── pools.py # 三池切分(test→validation→diagnosis
├── batching.py # FFD + round-robin mini-batch (#10)
├── gate_ladder.py # 信息阶梯 + BaselineCache (#6)
├── validate.py # 块序贯验证编排(唯一独立子编排器)
├── momentum.py # 慢更新动量生成
├── checkpoint.py # TrainState 序列化/反序列化 + 原子写
├── observation.py # 五张观测表 + step/epoch 报告(合并 metric_log + loop_report
└── runner.py # 瘦编排器:训练循环 + 慢更新十步序 (#13)
```
```mermaid
flowchart TD
runner["runner.py\n瘦编排器 #13"]
runner --> inference["inference.py\nasync 推理"]
runner --> validate["validate.py\n块序贯验证"]
runner --> momentum["momentum.py\n慢更新动量"]
runner --> batching["batching.py\nFFD mini-batch #10"]
runner --> pools["pools.py\n三池切分"]
runner --> gate_ladder["gate_ladder.py\n信息阶梯 #6"]
runner --> checkpoint["checkpoint.py\n状态序列化"]
runner --> observation["observation.py\n五表+报告"]
runner --> workspace["workspace.py\nmanifest+Protocol"]
runner --> config["config.py\nRunConfig"]
validate --> inference
validate --> gate_ladder
momentum --> inference
workspace --> store["store.py\n版本+Seed"]
inference --> log["log.py\nSQLite+RunLog"]
observation --> log
runner -.->|LLMProvider| core_p["core/protocols.py"]
runner -.->|evolve/diagnose| core_e["core/evolution/"]
runner -.->|AgentLoop| core_a["core/agent/"]
```
依赖规则:`app/harness/``core/evolution/` + `core/agent/` + `core/protocols`。模块间扁平无环。
## 3 config.py
RunConfig frozen dataclass47 字段,四层校验,YAML + CLI 加载。
- `frozen=True` 保证运行中不可变
- 四层校验:`_validate``_validate_edit_budget` + `_validate_minibatch` + `_validate_gate`
- `val_size >= eval_min_per_class × _VIDEO_MME_TASK_TYPE_COUNT(11)`
- `min_class_per_batch < batch_size`(严格小于)
- `gate_lambda_dir < 0`(方向拒绝阈值语义)
- `load_config(yaml_path, cli_overrides)`:合并优先级 CLI args > .env > YAMLCLAUDE.md §4.5)。工程配置(API 密钥、LLM 超时等少变/敏感项)走 .env / pydantic-settings;科研实验配置(gate 阈值、batch 大小等会扫动的参数)走 YAML。RunConfig 统一归口。
与 TRM4 差异:`--resume`/`--fresh` 互斥校验移到 runnerworkspace 初始化逻辑在 runner)。新增 .env 层合并(TRM4 仅 CLI > YAML)。
## 4 log.py
HarnessLog SQLite 薄包装 + RunLogImplRunLog Protocol 实现)。
**HarnessLog** 与 TRM4 一致:WAL 模式 + threading.Lock 线程安全;`INSERT OR IGNORE` 幂等;query 也持锁(共享连接下并发 SELECT + INSERT 会损坏游标状态)。
**RunLogImpl** 新增,实现 `core/evolution/protocols.py::RunLog`
- 只读端口(core/ Protocol 定义为只读)
- 用独立 sqlite3.connect 做 SELECT,不经 HarnessLog 生命周期(不触发 _runs INSERT
- `asyncio.to_thread` 包装同步 SQL(避免 aiosqlite 新依赖)
## 5 store.py
Store 版本操作(`advance_version``next_version``list_versions``_write_meta``_parse_version`+ Seed 管理(`init_seed``read_seed``promote_to_seed``extract_run_db`)。
- `list_versions` 只接受 `v\d+` 格式并按**数字值**排序(非字典序),保证 v10 排在 v2 后面。`next_version` 依赖此排序取 latest+1。
- `extract_run_db` 用原始 CREATE 语句重建表(保留 PRIMARY KEY 约束)
- `promote_to_seed` 强校验 eval run 版本与 --version 一致且非 NULL
- 临时 db 用 finally 清理
与 workspace.py 分离理由:Store 是跨实验共享的稳定基础设施(变更理由不同、稳定性更高),依赖方向单向(workspace → store)。
## 6 workspace.py
Workspace 生命周期 + manifest 读写 + SkillStore/PromptStore Protocol 实现。
- `ResolvedPaths(frozen=True)`skills_dir/prompts_dir 解析到 workspace(非 store
- `init_workspace_from_seed` 创建前校验 questions ref 存在(fail-fast:归档旧 ws 后才发现缺失则旧 ws 已毁)
- `record_run` 幂等(同 run_id 不重复追加 historyrun 目录和 per-video wiki 目录用 `exist_ok=True` 创建)
- `update_best`/`read_best`best 指针独立于 current(可指向已存储但非 current 的版本)
**Protocol 实现**
| 类 | Protocol | 实现 |
|---|---------|------|
| `VersionedSkillStore` | `core/evolution/protocols::SkillStore` | `Path.read_text` + `Path.glob("*.md")` |
| `VersionedPromptStore` | `core/evolution/protocols::PromptStore` | 同构 |
各 ~15 行。构造参数为 Path(由 resolve_paths 提供)。accept 推进版本后 runner 重建实例。
## 7 inference.py
async run_inference:全异步 + 依赖注入。
### 关键签名
```
async def run_inference(
questions, *, llm, tool_dispatch_fn, log, run_id, concurrency, max_steps, skill_mode, plugins
) -> InferenceResult
```
- `asyncio.Semaphore(concurrency)` + `asyncio.gather` 控制并发(替代 ThreadPoolExecutor
- LLM/VLM/OCR/Embedding 均由调用方构造注入(inference 不感知具体实现)
- `run_id: str` 必传(不再可选),空串 → ValueError
- `_aggregate_results` 从内存聚合(不再 SELECT predictions 表)
### 保留的防御性设计
- 悲观默认值:单题 record 初始 `stop_reason="error"`,成功后覆盖
- prediction 必落库:`log.insert` 在 try/except 之后(无论成败)
- `_to_text_field`:非 str 的 evidence/reasoning JSON 序列化入库
- 5 张表 schema 保留(predictions/traces/validation_flags/anchor_check/observe_frame_health
## 8 pools.py
三池切分,与 TRM4 1:1 迁移。
- 切分顺序 test→validation→diagnosisprogressive exclusion
- test 池自然分布(correct_ratio=None
- `build_or_load_pools`pools.json 存在即冻结复用
- 旧格式拒绝(无 test 键 → ValueError
## 9 batching.py — 算法保真 #10
FFD + round-robin + 正确率混合,与 TRM4 1:1 迁移。
- 先小类后大类装箱(小类 first-fit-decreasing 整组不拆、大类全局指针 round-robin 散布)
- `_validate_params` 防御性自校验(min_class_per_batch < batch_size
- `correctness.get(qid) is False` 精确匹配(排除 None/未知题)
- 只有有错题的题型参与混合
- 全部确定性(seed 控制 shuffle、题型按名称排序)
## 10 gate_ladder.py — 算法保真 #6
信息阶梯 + BaselineCache,与 TRM4 1:1 迁移。
- 冷启动 p̂ = Beta(1,1) 平滑(错=1/3, 对=2/3),2:1 交错 + probe 探针
- warm 排序 p̂(1-p̂) 信息量降序,剔除 [p_low, p_high] 外零信息题
- entries 保持存储序(warm 排序在 ladder_for 取用时做)
- GatePools 原子写(.tmp + os.replace
- 指纹不一致 → RuntimeError(不静默重建)
- BaselineCache 四维内容寻址(task_type, skill_hash, prompts_version, qid
- BaselineCache "先盘后存"(磁盘成功后才更新内存,无分裂窗口)
- 防泄露铁律:gate 内 rolloutrun_id 含 `_gate_`)永不回流 p̂
## 11 validate.py
块序贯验证编排(唯一独立子编排器),async 化。
### 关键类型
| 类型 | 说明 |
|------|------|
| `InferenceRunConfig` | 推理配置三元组(concurrency, max_steps, skill_mode |
| `ValidationOutcome` | 三态动作 + e-process 证据 + 已观测题逐题对错(candidate_correctness 只含已观测题) |
| `Probation` | 在途试用账本(anchor_skills_version + correctness_snapshot + pending_edits |
### 核心函数
`async validate_skill_local(...)` → ValidationOutcome:接收 LLMProvider + ToolDispatchFn + HarnessLog 注入。
内部流程:
1. `materialize_candidate_skill`workspace `.cand_tmp/`,唯一命名,finally 清理)
2. 按 gate_block 切块
3. 每块:`_resolve_baseline_block`(缓存优先,miss 才 await run_inference)→ `_run_candidate_block`(全块 await)→ INFRA 护栏(跨块累计,分母≥10)→ `pair_block`core/evolution)→ `gate_decision`core/evolution
4. 最后一块判定即终态(无循环外补判)
- `gate_run_prefix` 必须含 `_gate_`(防泄露过滤依赖此标记,入口校验)
- 只有终态题的证据行才携带 stop_reason
- **块级屏障**:每块必须严格顺序执行(基线补齐 → 候选跑完 → INFRA 累计 → pair → gate_decision),不得跨块流式判定或部分观测提前进入 gate。async 化不改变此顺序约束。
## 12 momentum.py
慢更新动量生成,async 化。
- 四类常量单一真源(IMPROVED/REGRESSED/PERSISTENT_FAIL/STABLE_SUCCESS
- 展示顺序 REGRESSED 优先(伤害信号最高)
- `_format_comparison_pairs` 在 try 外(KeyError 不被 ValueError 吞)
- 解析失败保留 prev_guidance(保守回退);基础设施异常不捕
导出:`run_slow_momentum`async)、四类常量、`_format_comparison_pairs``_categorize_pair`。编排逻辑(采样、两版 rollout、版本推进)在 runner。
## 13 checkpoint.py
_TrainState 序列化/反序列化 + 原子写 + 配置指纹。
**不持久化**gate_pools/baseline_cache(各自文件自持久化,resume 按指纹重载)、best_*(从 manifest best 指针读)、global_step(存 progress 块,由 train 单独赋值)。
**完整持久化字段集**serialize_state 输出,缺一不可):correctness, eval_prev_acc, eval_prev_run_id, baseline_skills_version, baseline_prompts_version, steps_since_best_improved, epoch_start_skills, changed_task_types_this_epoch (set→sorted list), rejected_buffer, system_packs, tool_packs, probations, gate_cooldown, gate_epoch_observed。
**嵌套 dataclass 复活规则**SystemCasePack 含 CaseSample 列表(需 `CaseSample(**d)` 逐个重建);Probation 含 RejectedEdit 列表 pending_edits(先重建 RejectedEdit 再构造 Probation);ToolCasePack 字段均为标量/dict`Cls(**d)` 直接构造。
- 反序列化 `d[...]` 不用 `.get` 兜底(缺键 = checkpoint 损坏 → 硬失败)
- 结构性键变化拒绝 resume,决策性键仅告警
- 原子写(.tmp + os.replace
## 14 observation.py
五张观测表 + step/epoch 报告(合并 metric_log + loop_report)。
### 五表
| 表 | 用途 |
|---|------|
| dual_metric_eval | epoch 末 hard+soft+mixed 双轨度量 |
| shadow_gate | mixed 影子 best 候选 |
| holdout_eval | 四向 held-out 在 test 池的度量 |
| quadrant_pair | fast gate 后逐题四象限 |
| gate_evidence | CE-Gate 逐题可回放审计 |
- 所有 write 函数幂等建表
- 所有 read 函数用独立只读连接(不经 HarnessLog 生命周期)
- soft/mixed 为 None → 存 NULL(绝不存 0
### 报告
- `write_step_report`per (epoch, step, task_type)skipped/cooldown 路径 gate 字段传 None
- `write_epoch_report`system_tool_action + momentum_updated_task_types + best_val_acc
## 15 runner.py — 算法保真 #13
瘦编排器(~400 行),class Runner 作为 DI 容器 + 顶层控制流。
### Runner 类
```python
class Runner:
def __init__(self, config, *, llm, evolve_llm, vlm, telemetry): ...
async def train(self, pools) -> None: ...
async def infer(...) -> InferenceResult: ...
async def eval(version) -> InferenceResult: ...
async def diagnose(run_id) -> DiagnosisResult: ...
def promote(version, eval_run_id, name) -> None: ...
```
`self` 只持注入依赖 + _paths。_TrainState 是 train() 内局部变量,显式传参。
### Runner 编排职责补充
以下职责由 Runner 承担(不在 inference/validate 等子模块内):
- **record_run**:每次调用 run_inference 前,Runner 调 `workspace.record_run(workspace_dir, run_id)` 负责 manifest history 追加和 `runs/<run_id>/wiki` 目录创建。inference.py 不感知 workspace。
- **eval 版本回填**`eval()` 跑完后显式 `UPDATE _runs SET skills_version=?, prompts_version=?, questions_ref=?`(promote 依赖此行读版本对建种子)。
- **run_diagnosis 参数组装**Runner 负责加载 tree_data(从 TreeEnvironment)、DiagnosePrompts bundle(从根 prompts/ 目录读取诊断 prompt 文件)、questions 列表,组装后传给 `core.evolution.run_diagnosis`。diagnose 模式的完整参数契约:`run_id, questions, tree_data, llm, run_log, skill_store, prompts: DiagnosePrompts, concurrency`
### _TrainState
19 个可变字段(TRM4 为 20 个,移除 evolve_client——由 Runner.self._evolve_llm 持有)。
关键设计:correctness 增量更新;epoch_start_skills 按文件名索引;gate_epoch_observed 必须持久化并 resume 恢复(阶梯排序开关,丢失会回退冷启动序);probations 每题型至多一个。
### train() 三级嵌套
epoch → step → per-skill。resume 用 saved_batches 恢复 batch 划分。新 epoch 清空累加器。每 step 后落 "in_epoch" checkpointepoch 末落 "epoch_done"。early stop 在慢更新后判。训练收尾 deliver_best + final_test_eval。
### _run_step
rollout → correctness 增量 → diagnose → 累加 slow packs → gate_batch_skills → 冷却递减。
### _gate_batch_skills
按 task_type 排序处理:cooldown 跳过 → evolve → 无改动跳过 → 排除案例包题构造 ladder → validate_skill_local → accept/reject/probation。
accept 关键语义:开账快照在合并前拍取;promote → update_manifest → refresh pathscorrectness 二轨合并;清黑名单;probation 分岔(default-strategy.md 不开账)。
reject 黑名单防污染:`_rejected_summary` 只记录 apply_report 中 status 为 applied 的 edit(与 edits 同位对齐筛选),未 applied 的 edit 从未写进候选正文、从未被 gate 验证过,进黑名单会污染"已验证无效"语义。0 applied 时生成防御性文案。
rollback:文件级 revert(读锚版本内容 → promote 新版本),不整体回退 manifest。
### _slow_update_cycle 十步序
```
1. 捕获版本快照 → 全 val 重跑 R
2. soft score + dual_metric 落库
3. R 逐题对错无条件回写
4. probation 结算(回滚者覆盖 step 3
5. best argmax(严格大于)
6. momentum(不可变新版本,按 skill 文件分组)
7. system/tool 慢更新(edit_budget_end
8. R2 闭环(R2 发生在 momentum 推进 skills 后,其版本对为 (r2_skills_version, new_prompts_version),绝不沿用 R 的 eval_skills_version。退步 revert prompts;保留则回写 R2 + best argmax 绑定 R2 版本对 + eval_prev ← R2
9. 三态标签 + epoch_report + 四向 held-out
10. gate 阶梯刷新(精确三源:step rollout GLOB `{base}_e{epoch}_s*` 排除 `*_gate_*` + slow R 精确 run_id + kept R2 的 extra_run_idsreverted R2/shadow/held-out 观测绝不吸收——防泄露同族原则)
```
### 辅助函数
`resume_plan``_guard_infra_failures``_apply_batch_correctness``_should_early_stop` 等提取为模块级函数(不依赖 self,显式参数,独立可测)。
## 16 与 core/evolution/ 的接缝
| app/harness/ 消费 | core/evolution/ 提供 | 交互模式 |
|-------------------|---------------------|---------|
| validate.py | gate_decision, pair_block, classify_quadrants | 纯函数,返回 GateVerdict/PairResult/QuadrantClassification |
| runner._gate_batch_skills | evolve_single_skill, edit_budget_at, resolve_skill_file | 纯函数/异步,返回 EvolutionRecord |
| runner._slow_update_cycle | evolve_system_prompt, evolve_single_tool, run_diagnosis | 异步,需 LLMProvider + RunLog + SkillStore |
| runner._settle_probations | probation_verdict | 纯函数 |
| momentum | replace_momentum, momentum_inner | 纯函数,文本操作 |
| checkpoint | types.* (dataclass 序列化) | dataclasses.asdict / Cls(**d) |
**类型导入约定**app/harness/ 从 `core.evolution.types` 直接导入 dataclassGateParams, GateVerdict, PairResult, QuadrantClassification, DiagnosisResult, EvolutionRecord, RejectedEdit, SkillCasePack, SystemCasePack, ToolCasePack, EvolvePrompts, DiagnosePrompts)。`core/evolution/__init__.py` 当前只导出函数;types 作为子模块公开 API 的一部分,直接 import 合法(不违反依赖方向)。实现阶段需在 `__init__.py` 补导出这些类型。
## 17 rejected approaches
| 方案 | 拒绝理由 |
|------|---------|
| 保持 Runner 单体 God Class2273 行) | 违反 SRP,测试困难,阅读困难 |
| 分层编排(runner → slow_update.py 二级编排) | 慢更新与训练循环变更理由相同,拆分增加间接性但不解耦 |
| 纯函数替代 class Runner | 训练循环有状态(_TrainState 16 字段),参数爆炸比类更差 |
| workspace.py 单文件 | Store 与 Workspace 变更理由不同、稳定性不同(SRP + SDP) |
| 混合同步/异步(run_inference 内 asyncio.run | 与外层 async runner 冲突 |