diff --git a/research-wiki/designs/2026-07-07-app-harness-design.md b/research-wiki/designs/2026-07-07-app-harness-design.md new file mode 100644 index 0000000..6c7d9d9 --- /dev/null +++ b/research-wiki/designs/2026-07-07-app-harness-design.md @@ -0,0 +1,341 @@ +# 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//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 冲突 |