Files
Video-Tree-TRM5/app/harness/config.py
T
iomgaa b929a5db6c fix(harness): Codex functional review 修复 — 命名/集成测试/delta_min/promote 消息
- _apply_env_overrides → _apply_env_var_overrides,docstring 明确从 os.environ 读取
- 新增 TestLoadConfigRealYaml:用真实 config/default.yaml 验证嵌套 harness 解析
- 新增 test_delta_min_negative_rejected:覆盖 gate_delta_min >= 0 校验
- 恢复 promote 模式独立错误消息(从合并分支分离回 TRM4 原始提示)
- 77 个单元测试全部通过,radon 全部 Grade B 或更好

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-07 12:23:27 -04:00

428 lines
16 KiB
Python
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.
"""运行配置:RunConfig frozen dataclass 与 YAML + CLI + .env 三层加载。
三层合并优先级:CLI > .env > YAML(遵循 CLAUDE.md §4.5 配置管理规范)。
- YAML:科研实验配置(会在实验中反复扫动的参数),存放于 config/ 下。
- .env:工程配置(少变路径如 workspace_dir、store_dir),通过环境变量注入。
- CLI:单次临时覆盖。
"""
from __future__ import annotations
import dataclasses
import os
from dataclasses import dataclass
from pathlib import Path
import yaml
_VALID_MODES = {"infer", "train", "diagnose", "evolve", "eval", "promote"}
_VALID_SKILL_MODES = {"auto", "manual", "none"}
_VALID_SKILL_UPDATE_MODES = {"patch", "rewrite"}
_PATH_FIELDS = {"workspace_dir", "store_dir"}
# Video-MME 的任务类型数量:验证池每类至少保底 eval_min_per_class 题,共 11 类。
_VIDEO_MME_TASK_TYPE_COUNT = 11
# .env 工程配置字段映射(环境变量名 → RunConfig 字段名)。
# 仅路径类工程配置走 .env,科研实验参数走 YAML。
_ENV_FIELD_MAP: dict[str, str] = {
"HARNESS_WORKSPACE_DIR": "workspace_dir",
"HARNESS_STORE_DIR": "store_dir",
}
@dataclass(frozen=True)
class RunConfig:
"""实验运行配置,所有参数的唯一归口。
frozen=True 确保配置在创建后不可变,防止运行中被意外修改。
三层合并优先级:CLI > .env > YAML。
字段:
workspace_dir: Workspace 根目录。
store_dir: Store 根目录。
mode: 运行模式,"infer" / "train" / "diagnose" / "evolve" / "eval" / "promote"。
concurrency: 并行 worker 数。
max_steps: AgentLoop 单题最大步数。
skill_mode: Skill 加载模式,"auto" / "manual" / "none"。
n_samples: 题目截取数,0 表示全量。
questions: 题目在 questions/ 下的相对路径。
skills_version: Skills 版本号。
prompts_version: Prompts 版本号。
epochs: 训练轮数。
diag_size: 诊断池题目数。
diag_correct_ratio: 诊断池中正确题目占比。
val_size: 验证池题目数。
val_correct_ratio: 验证池中正确题目占比。
edit_budget_start: 编辑预算前期上限。
edit_budget_end: 编辑预算后期下限。
batch_size: mini-batch 单批题目数。
min_class_per_batch: 单批中每个任务类型至少保留的题目数(< batch_size)。
eval_min_per_class: 验证池中每个任务类型至少保底的题目数。
early_stop_patience: 全局 best 连续未提升的容忍轮数,达到即早停。
test_size: held-out 测试池题目数。
use_slow_momentum: 是否启用快慢双速进化中的慢速 momentum 更新。
gate_e_confirm: CE-Gate CONFIRMED 接受的 e 值门槛(1/alphaVille 界假阳率 alpha)。
gate_e_provisional: 题尽暂定接受门 + futility 提前止损的代数界。
gate_w_net_min: 题尽暂定接受要求的最小净胜数(win - loss)。
gate_delta_min: 最小点估计效应量下限(承接旧 margin 语义)。
gate_lambda_dir: Wald 方向拒绝的对数似然比阈值(必须为负)。
gate_e_rollback: 试用期对称回滚门(回滚 e 值门槛)。
gate_block: 块序贯验证的块大小(=推理并发度,块内跑满)。
gate_n_max: 单次 gate 消耗的题数上限。
gate_p_low: 信息量阶梯 p-hat 保留区间下界(剔除必错零信息题)。
gate_p_high: 信息量阶梯 p-hat 保留区间上界(剔除必对零信息题)。
gate_probe_quota: 冷启动探针集比例(全错题中插尾的比例)。
gate_gamma_decay: 逐题正确率估计 p-hat 的 EMA 衰减系数。
gate_cooldown_steps: 回滚后该题型跳过进化的冷却 step 数。
gate_guard_err: gate 内跨块累计 INFRA 错误率护栏。
skill_update_mode: skill 进化模式,"patch"(局部 edit/ "rewrite"(整篇重写)。
appendix_consolidate_threshold: appendix note 条数达此值触发 LLM consolidation。
run_id: diagnose/evolve 模式要分析的运行 ID,默认空字符串。
batch_correct_ratio: 单批中正确题目占比,范围 [0, 1)。
momentum_samples: 慢速 momentum 更新时从诊断池采样的题目数,必须 >= 1。
seed: fresh 训练的种子名(对应 seed.json),默认 "initial"。
version: eval/promote 模式指定的 store 版本号(如 "v3")。
resume: train 模式是否从已有 checkpoint 续训。
fresh: train 模式是否从种子全新开始。
"""
# ── 必填字段(无默认值,来自 YAML 或 CLI) ──
workspace_dir: Path
store_dir: Path
mode: str
concurrency: int
max_steps: int
skill_mode: str
n_samples: int
questions: str
skills_version: str
prompts_version: str
epochs: int
diag_size: int
diag_correct_ratio: float
val_size: int
val_correct_ratio: float
edit_budget_start: int
edit_budget_end: int
batch_size: int
min_class_per_batch: int
eval_min_per_class: int
early_stop_patience: int
test_size: int
use_slow_momentum: bool
gate_e_confirm: float
gate_e_provisional: float
gate_w_net_min: int
gate_delta_min: float
gate_lambda_dir: float
gate_e_rollback: float
gate_block: int
gate_n_max: int
gate_p_low: float
gate_p_high: float
gate_probe_quota: float
gate_gamma_decay: float
gate_cooldown_steps: int
gate_guard_err: float
skill_update_mode: str
appendix_consolidate_threshold: int
# ── 有默认值的字段(通常由 CLI 传入或可选) ──
run_id: str = ""
batch_correct_ratio: float = 0.5
momentum_samples: int = 20
seed: str = "initial"
version: str = ""
resume: bool = False
fresh: bool = False
def _validate(config: RunConfig) -> None:
"""校验 RunConfig 全部字段约束。
六层校验链:mode → 基础标量 → 编辑预算 → mini-batch → gate 阈值 → gate 阶梯。
参数:
config: 待校验的配置实例。
异常:
ValueError: 任一字段值不合法。
"""
_validate_mode(config)
_validate_mode_deps(config)
_validate_basic(config)
_validate_edit_budget(config)
_validate_minibatch(config)
_validate_gate(config)
def _validate_mode(config: RunConfig) -> None:
"""校验运行模式枚举合法性。
参数:
config: 待校验的配置实例。
异常:
ValueError: mode 值不在合法集合中。
"""
if config.mode not in _VALID_MODES:
raise ValueError(f"mode 必须为 {_VALID_MODES} 之一,实际: {config.mode!r}")
def _validate_mode_deps(config: RunConfig) -> None:
"""校验各运行模式的依赖字段(run_id、version)。
参数:
config: 待校验的配置实例。
异常:
ValueError: 模式依赖字段缺失。
"""
if config.mode in ("diagnose", "evolve") and not config.run_id:
raise ValueError(f"mode 为 {config.mode!r} 时必须提供 run_id。")
if config.mode in ("eval", "promote") and not config.version:
raise ValueError(f"mode 为 {config.mode!r} 时必须提供 --version。")
if config.mode == "promote" and not config.run_id:
raise ValueError("promote 必须提供 --run-id(指定 canonical eval run)。")
_validate_train_run_id(config)
def _validate_train_run_id(config: RunConfig) -> None:
"""校验 train 模式非 resume/fresh 时必须提供 run_id。
参数:
config: 待校验的配置实例。
异常:
ValueError: train 模式既非 resume 也非 fresh 且缺少 run_id。
"""
if config.mode != "train":
return
if config.resume or config.fresh:
return
if not config.run_id:
raise ValueError("train 非 resume/fresh 时必须提供 run_id(旧式基线 run)。")
def _validate_basic(config: RunConfig) -> None:
"""校验基础标量字段:枚举合法性与正整数约束。
参数:
config: 待校验的配置实例。
异常:
ValueError: 任一基础字段值不合法。
"""
if config.skill_mode not in _VALID_SKILL_MODES:
raise ValueError(
f"skill_mode 必须为 {_VALID_SKILL_MODES} 之一,实际: {config.skill_mode!r}"
)
if config.concurrency <= 0:
raise ValueError(f"concurrency 必须 > 0,实际: {config.concurrency}")
if config.max_steps <= 0:
raise ValueError(f"max_steps 必须 > 0,实际: {config.max_steps}")
if config.n_samples < 0:
raise ValueError(f"n_samples 必须 >= 0,实际: {config.n_samples}")
if config.epochs <= 0:
raise ValueError(f"epochs 必须 > 0,实际: {config.epochs}")
if config.skill_update_mode not in _VALID_SKILL_UPDATE_MODES:
raise ValueError(
f"skill_update_mode 必须为 {_VALID_SKILL_UPDATE_MODES} 之一,"
f"实际: {config.skill_update_mode!r}"
)
if config.appendix_consolidate_threshold < 1:
raise ValueError(
f"appendix_consolidate_threshold 必须 >= 1"
f"实际: {config.appendix_consolidate_threshold}"
)
def _validate_edit_budget(config: RunConfig) -> None:
"""校验编辑预算退火的前期/后期上限约束。
参数:
config: 待校验的配置实例。
异常:
ValueError: edit_budget_start < edit_budget_end,或 end <= 0。
"""
if config.edit_budget_start < config.edit_budget_end:
raise ValueError(
f"edit_budget_start({config.edit_budget_start}) 必须 >= "
f"edit_budget_end({config.edit_budget_end})"
)
if config.edit_budget_end <= 0:
raise ValueError(f"edit_budget_end 必须 > 0,实际: {config.edit_budget_end}")
def _validate_minibatch(config: RunConfig) -> None:
"""校验 mini-batch 自进化闭环参数约束。
参数:
config: 待校验的 RunConfig 配置对象。
异常:
ValueError: 任一约束被违反。
关键实现细节:
val_size 必须 >= eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT,保证验证池
能为 Video-MME 的全部 11 个任务类型各保底 eval_min_per_class 题。
"""
if config.batch_size <= 0:
raise ValueError(f"batch_size 必须 > 0,实际: {config.batch_size}")
if not (1 <= config.min_class_per_batch < config.batch_size):
raise ValueError(
f"min_class_per_batch 必须满足 1 <= 值 < batch_size"
f"({config.batch_size}),实际: {config.min_class_per_batch}"
)
if config.eval_min_per_class < 1:
raise ValueError(f"eval_min_per_class 必须 >= 1,实际: {config.eval_min_per_class}")
floor = config.eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT
if config.val_size < floor:
raise ValueError(
f"val_size 必须 >= eval_min_per_class * {_VIDEO_MME_TASK_TYPE_COUNT}"
f"(={floor})Video-MME 共 {_VIDEO_MME_TASK_TYPE_COUNT} 个任务类型,"
f"每类需 eval_min_per_class 题保底,故验证池下限为 {floor}"
f"实际: {config.val_size}"
)
if config.early_stop_patience <= 0:
raise ValueError(f"early_stop_patience 必须 > 0,实际: {config.early_stop_patience}")
if config.test_size <= 0:
raise ValueError(f"test_size 必须 > 0,实际: {config.test_size}")
if not (0 <= config.batch_correct_ratio < 1):
raise ValueError(
f"batch_correct_ratio 必须满足 0 <= 值 < 1,实际: {config.batch_correct_ratio}"
)
if config.momentum_samples < 1:
raise ValueError(f"momentum_samples 必须 >= 1,实际: {config.momentum_samples}")
def _validate_gate(config: RunConfig) -> None:
"""校验 CE-Gate 全部参数:判据阈值 + 信息量阶梯。
参数:
config: 待校验的配置实例。
异常:
ValueError: 任一 gate 参数不合法。
"""
_validate_gate_thresholds(config)
_validate_gate_ladder(config)
def _validate_gate_thresholds(config: RunConfig) -> None:
"""校验 CE-Gate 判据阈值参数(e 值、净胜数、效应量、方向拒绝)。
参数:
config: 待校验的配置实例。
异常:
ValueError: 任一阈值参数不合法。
"""
if config.gate_e_confirm <= 1:
raise ValueError(f"gate_e_confirm 必须 > 1,实际: {config.gate_e_confirm}")
if not (1 < config.gate_e_provisional <= config.gate_e_confirm):
raise ValueError(
f"gate_e_provisional 必须在 (1, gate_e_confirm] 内,实际: {config.gate_e_provisional}"
)
if config.gate_e_rollback <= 1:
raise ValueError(f"gate_e_rollback 必须 > 1,实际: {config.gate_e_rollback}")
if config.gate_w_net_min < 1:
raise ValueError(f"gate_w_net_min 必须 >= 1,实际: {config.gate_w_net_min}")
if config.gate_delta_min < 0:
raise ValueError(f"gate_delta_min 必须 >= 0,实际: {config.gate_delta_min}")
if config.gate_lambda_dir >= 0:
raise ValueError(f"gate_lambda_dir 必须 < 0,实际: {config.gate_lambda_dir}")
def _validate_gate_ladder(config: RunConfig) -> None:
"""校验 CE-Gate 信息量阶梯与块序贯参数。
参数:
config: 待校验的配置实例。
异常:
ValueError: 任一阶梯参数不合法。
"""
if config.gate_block <= 0 or config.gate_n_max < config.gate_block:
raise ValueError(
f"需 0 < gate_block <= gate_n_max"
f"实际: block={config.gate_block}, n_max={config.gate_n_max}"
)
if not (0 <= config.gate_p_low < config.gate_p_high <= 1):
raise ValueError(
f"需 0 <= gate_p_low < gate_p_high <= 1"
f"实际: [{config.gate_p_low}, {config.gate_p_high}]"
)
if not (0 <= config.gate_probe_quota <= 1):
raise ValueError(f"gate_probe_quota 须在 [0,1],实际: {config.gate_probe_quota}")
if not (0 < config.gate_gamma_decay < 1):
raise ValueError(f"gate_gamma_decay 须在 (0,1),实际: {config.gate_gamma_decay}")
if config.gate_cooldown_steps < 1:
raise ValueError(f"gate_cooldown_steps 必须 >= 1,实际: {config.gate_cooldown_steps}")
if not (0 < config.gate_guard_err < 1):
raise ValueError(f"gate_guard_err 须在 (0,1),实际: {config.gate_guard_err}")
def _apply_env_var_overrides(data: dict) -> None:
"""从环境变量覆盖路径字段(原地修改)。
.env 文件由入口脚本 load_dotenv 加载到环境变量,本函数仅从 os.environ 读取。
仅覆盖 _ENV_FIELD_MAP 中声明的工程配置字段(workspace_dir、store_dir)。
参数:
data: 待覆盖的配置字典。
"""
for env_key, field_name in _ENV_FIELD_MAP.items():
env_val = os.environ.get(env_key)
if env_val is not None:
data[field_name] = env_val
def load_config(
yaml_path: Path,
cli_overrides: dict[str, object] | None = None,
) -> RunConfig:
"""从 YAML 加载配置,叠加 .env 和 CLI 覆盖层后构造 RunConfig。
三层合并优先级:CLI > .env > YAML。
参数:
yaml_path: YAML 配置文件路径,需包含 ``harness`` 段。
cli_overrides: CLI 参数字典,值为 None 表示未传入(不覆盖)。
返回:
构造并校验后的 RunConfig 实例。
异常:
FileNotFoundError: YAML 文件不存在。
ValueError: 校验失败。
"""
# Phase 1: 加载 YAML 基础层
with open(yaml_path, encoding="utf-8") as f:
raw: dict = yaml.safe_load(f)
# 支持嵌套 harness 段和扁平 YAML 两种格式
yaml_data: dict = raw.get("harness", raw)
# Phase 2: .env 覆盖层(仅工程配置字段)
_apply_env_var_overrides(yaml_data)
# Phase 3: CLI 覆盖层(最高优先级)
valid_fields = {f.name for f in dataclasses.fields(RunConfig)}
if cli_overrides:
for key, value in cli_overrides.items():
if value is not None and key in valid_fields:
yaml_data[key] = value
# Phase 4: 类型转换 — 路径字段转 Path
for field_name in _PATH_FIELDS:
if field_name in yaml_data:
yaml_data[field_name] = Path(yaml_data[field_name])
# Phase 5: 构造并校验
config = RunConfig(**{k: v for k, v in yaml_data.items() if k in valid_fields})
_validate(config)
return config