# 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 dataclass，47 字段，四层校验，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 > YAML（CLAUDE.md §4.5）。工程配置（API 密钥、LLM 超时等少变/敏感项）走 .env / pydantic-settings；科研实验配置（gate 阈值、batch 大小等会扫动的参数）走 YAML。RunConfig 统一归口。

与 TRM4 差异：`--resume`/`--fresh` 互斥校验移到 runner（workspace 初始化逻辑在 runner）。新增 .env 层合并（TRM4 仅 CLI > YAML）。

## 4 log.py

HarnessLog SQLite 薄包装 + RunLogImpl（RunLog 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 不重复追加 history，run 目录和 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→diagnosis（progressive 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 内 rollout（run_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" checkpoint，epoch 末落 "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 paths；correctness 二轨合并；清黑名单；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_ids；reverted 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` 直接导入 dataclass（GateParams, 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 Class（2273 行） | 违反 SRP，测试困难，阅读困难 |
| 分层编排（runner → slow_update.py 二级编排） | 慢更新与训练循环变更理由相同，拆分增加间接性但不解耦 |
| 纯函数替代 class Runner | 训练循环有状态（_TrainState 16 字段），参数爆炸比类更差 |
| workspace.py 单文件 | Store 与 Workspace 变更理由不同、稳定性不同（SRP + SDP） |
| 混合同步/异步（run_inference 内 asyncio.run） | 与外层 async runner 冲突 |
