""" 配置管理模块 ============ 定义所有超参数的 dataclass 类型(无默认值)+ 多源加载。 三层优先级: CLI args > .env > YAML,统一归口到 Config dataclass。 使用方式:: from video_tree_trm.config import Config cfg = Config.load("config/default.yaml") cfg = Config.load("config/default.yaml", cli_args={"retriever.num_heads": "8"}) """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any, Optional import yaml from dotenv import dotenv_values from utils.logger_system import ensure, log_msg # --------------------------------------------------------------------------- # 子配置 Dataclass(全部无默认值,YAML 必须写全) # --------------------------------------------------------------------------- @dataclass class TreeConfig: """树索引构建参数。 属性: max_paragraphs_per_l2: 每个 L2 节点包含的最大段落数(文本模式)。 l1_segment_duration: L1 段时长,秒(视频模式)。 l2_clip_duration: L2 clip 时长,秒(视频模式)。 l3_fps: L3 帧提取频率(视频模式)。 l2_representative_frames: L2 VLM 描述用的代表帧数。 cache_dir: TreeIndex 缓存目录。 """ max_paragraphs_per_l2: int l1_segment_duration: float l2_clip_duration: float l3_fps: float l2_representative_frames: int cache_dir: str concurrency: int @dataclass class EmbedConfig: """嵌入模型参数。 属性: backend: 嵌入后端类型,"local"(sentence-transformers)或 "remote"(OpenAI 兼容 API)。 model_name: 本地模式为 HuggingFace 模型名,远程模式为 API 模型名。 embed_dim: 嵌入维度 D。 device: 推理设备,"cuda" 或 "cpu"(仅本地模式使用)。 api_key: 远程模式 API 密钥,从 .env 加载。本地模式为空串。 api_url: 远程模式 API 端点。本地模式为空串。 """ backend: str model_name: str embed_dim: int device: str api_key: str api_url: str @dataclass class LLMConfig: """大语言模型参数。 属性: backend: 后端类型,"qwen" | "openai" | "ollama"。 api_key: API 密钥,从 .env 加载。 model: 模型名称。 api_url: API 端点 URL。 max_tokens: 最大生成 token 数。 temperature: 采样温度。 """ backend: str api_key: str model: str api_url: str max_tokens: int temperature: float @dataclass class VLMConfig: """视觉语言模型参数。 属性: backend: 后端类型,"qwen" | "openai" | "ollama"。 api_key: API 密钥,从 .env 加载。 model: 模型名称。 api_url: API 端点 URL。 max_tokens: 最大生成 token 数。 temperature: 采样温度。 """ backend: str api_key: str model: str api_url: str max_tokens: int temperature: float @dataclass class RetrieverConfig: """TRM 检索器参数。 属性: embed_dim: 嵌入维度,须与 EmbedConfig.embed_dim 一致。 num_heads: Cross-Attention 头数。 L_layers: ReasoningModule 层数。 L_cycles: 每级推理迭代次数。 max_rounds: ACT 最大遍历轮次。 ffn_expansion: SwiGLU 扩展比。 checkpoint: 训练好的模型权重路径,推理时必填。 """ embed_dim: int num_heads: int L_layers: int L_cycles: int max_rounds: int ffn_expansion: float checkpoint: Optional[str] # 多路径检索配置 (Top-k) k_l1: int = 1 k_l2: int = 1 k_l3: int = 1 max_paths: int = 5 @dataclass class TrainConfig: """训练参数。 属性: lr: 学习率。 weight_decay: 权重衰减。 batch_size: 批大小。 max_epochs_phase1: Phase 1 导航训练轮数。 max_epochs_phase2: Phase 2 ACT 训练轮数。 nav_loss_weight: 导航损失权重。 act_loss_weight: ACT 损失权重。 act_lambda_step: ACT 步数惩罚系数。 act_gamma: ACT 折扣因子。 eval_interval: 每 N epoch 评估一次。 save_dir: 模型权重保存目录。 dataset: 数据集名称,"longbench" | "narrativeqa" | "videomme"。 dataset_path: 数据集路径。 """ lr: float weight_decay: float batch_size: int max_epochs_phase1: int max_epochs_phase2: int nav_loss_weight: float act_loss_weight: float margin_loss_weight: float act_lambda_step: float act_gamma: float eval_interval: int save_dir: str dataset: str dataset_path: str @dataclass class HierRetrieverConfig: """Hierarchical Cross-Encoder 检索器参数。 属性: backbone_model: 预训练语言模型名称。 num_heads: Cross-Attention 头数。 hidden_dim: 隐藏层维度。 dropout: Dropout 概率。 use_query_dep_weights: 是否使用 Query-dependent 层级权重。 level_weight_type: 层级权重类型,"fixed" | "query_dependent" | "hybrid"。 # Stage 1 参数 stage1_epochs: Stage 1 训练轮数。 stage1_lr: Stage 1 学习率。 stage1_batch_size: Stage 1 批大小。 stage1_num_negatives: Stage 1 每样本的负例数量。 stage1_temperature: Stage 1 温度参数。 # Stage 2 参数 stage2_epochs: Stage 2 训练轮数。 stage2_lr: Stage 2 学习率。 stage2_batch_size: Stage 2 批大小。 stage2_num_negatives: Stage 2 每样本的负例数量。 stage2_temperature: Stage 2 温度参数。 stage2_hard_neg_update_freq: Stage 2 硬负例更新频率。 stage2_hier_loss_weight: Stage 2 层级一致性损失权重。 # Stage 3 参数 stage3_enabled: 是否启用 Stage 3。 stage3_epochs: Stage 3 训练轮数。 stage3_lr: Stage 3 学习率。 # 推理参数 coarse_top_k: 粗排候选数量。 fine_top_k: 精排返回数量。 use_bm25: 是否使用 BM25 辅助粗排。 """ # 模型参数 backbone_model: str num_heads: int hidden_dim: int dropout: float use_query_dep_weights: bool level_weight_type: str # Stage 1 stage1_epochs: int stage1_lr: float stage1_batch_size: int stage1_num_negatives: int stage1_temperature: float # Stage 2 stage2_epochs: int stage2_lr: float stage2_batch_size: int stage2_num_negatives: int stage2_temperature: float stage2_hard_neg_update_freq: int stage2_hier_loss_weight: float # Stage 3 stage3_enabled: bool stage3_epochs: int stage3_lr: float # 推理 coarse_top_k: int fine_top_k: int use_bm25: bool # --------------------------------------------------------------------------- # 辅助函数 # --------------------------------------------------------------------------- _SECTION_TO_CLASS: dict[str, type] = { "tree": TreeConfig, "embed": EmbedConfig, "llm": LLMConfig, "vlm": VLMConfig, "retriever": RetrieverConfig, "train": TrainConfig, } def _deep_merge(base: dict, override: dict) -> dict: """递归合并字典,override 优先覆盖 base。 参数: base: 基础字典。 override: 覆盖字典。 返回: 合并后的新字典。 """ merged = base.copy() for key, value in override.items(): if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): merged[key] = _deep_merge(merged[key], value) else: merged[key] = value return merged def _apply_dotpath(d: dict, key: str, value: Any) -> None: """通过点路径设置嵌套字典的值。 支持 "retriever.num_heads" 风格的路径,自动拆分并逐级写入。 参数: d: 目标字典。 key: 点分隔的路径,如 "retriever.num_heads"。 value: 要设置的值。 """ parts = key.split(".") current = d for part in parts[:-1]: if part not in current: current[part] = {} current = current[part] current[parts[-1]] = value def _coerce_value(raw: str, target_type: type) -> Any: """将 CLI 字符串值转换为目标类型。 参数: raw: 原始字符串值。 target_type: 目标 Python 类型。 返回: 转换后的值。 """ if target_type is bool: return raw.lower() in ("true", "1", "yes") if target_type is type(None): return None if raw.lower() in ("none", "null", "") else raw return target_type(raw) # --------------------------------------------------------------------------- # 顶层配置 # --------------------------------------------------------------------------- @dataclass class Config: """全局配置容器。 统一管理所有子模块配置,通过 ``Config.load()`` 加载。 属性: tree: 树索引构建参数。 embed: 嵌入模型参数。 llm: 大语言模型参数。 vlm: 视觉语言模型参数。 retriever: TRM 检索器参数。 train: 训练参数。 hier_retriever: Hierarchical Cross-Encoder 检索器参数。 """ tree: TreeConfig embed: EmbedConfig llm: LLMConfig vlm: VLMConfig retriever: RetrieverConfig train: TrainConfig hier_retriever: Optional[HierRetrieverConfig] = None @classmethod def load( cls, yaml_path: str, cli_args: Optional[dict[str, str]] = None, env_path: Optional[str] = None, ) -> "Config": """三层合并加载配置。 优先级: CLI args > .env > YAML。 参数: yaml_path: YAML 配置文件路径。 cli_args: CLI 覆盖参数,键为点路径(如 "retriever.num_heads"),值为字符串。 env_path: .env 文件路径,默认为项目根目录的 .env。 返回: 完整的 Config 实例。 异常: FileNotFoundError: YAML 文件不存在。 TypeError: YAML 中缺少必需字段。 """ # Phase 1: 读取 YAML yaml_file = Path(yaml_path) if not yaml_file.exists(): raise FileNotFoundError(f"配置文件不存在: {yaml_path}") with open(yaml_file, encoding="utf-8") as f: base_dict: dict = yaml.safe_load(f) # Phase 2: 读取 .env 覆盖敏感字段 if env_path is None: env_file = Path(yaml_path).parent.parent / ".env" else: env_file = Path(env_path) if env_file.exists(): env_vars = dotenv_values(str(env_file)) env_overrides: dict[str, dict[str, str]] = {} # .env 变量名 → (配置节, 字段名) 的映射 _ENV_MAP: dict[str, tuple[str, str]] = { "LLM_API_KEY": ("llm", "api_key"), "LLM_MODEL": ("llm", "model"), "LLM_API_URL": ("llm", "api_url"), "VLM_API_KEY": ("vlm", "api_key"), "VLM_MODEL": ("vlm", "model"), "VLM_API_URL": ("vlm", "api_url"), "EMBED_BACKEND": ("embed", "backend"), "EMBED_MODEL": ("embed", "model_name"), "EMBED_API_KEY": ("embed", "api_key"), "EMBED_API_URL": ("embed", "api_url"), } for env_name, (section, field) in _ENV_MAP.items(): if env_vars.get(env_name): env_overrides.setdefault(section, {})[field] = env_vars[env_name] base_dict = _deep_merge(base_dict, env_overrides) # Phase 3: CLI args 覆盖 if cli_args: for dotpath, value in cli_args.items(): _apply_dotpath(base_dict, dotpath, value) # Phase 4: 构造 dataclass(缺字段自动抛 TypeError) sections = {} for section_name, dc_class in _SECTION_TO_CLASS.items(): section_data = base_dict.get(section_name, {}) if not isinstance(section_data, dict): # 对于 Optional 的 section,跳过 if section_name == "hier_retriever": continue raise TypeError( f"配置节 '{section_name}' 必须是字典,实际为 {type(section_data)}" ) sections[section_name] = dc_class(**section_data) config = cls(**sections) # 校验: embed_dim 一致性 ensure( config.embed.embed_dim == config.retriever.embed_dim, f"embed.embed_dim ({config.embed.embed_dim}) 与 " f"retriever.embed_dim ({config.retriever.embed_dim}) 不一致", ) log_msg("INFO", "配置加载完成", yaml=yaml_path) return config