
第五章 配置管理:用 YAML/ENV 让项目可迁移
-
- [0. 先把概念讲清楚:什么该进 YAML?什么该进 ENV?](#0. 先把概念讲清楚:什么该进 YAML?什么该进 ENV?)
-
- [0.1 YAML:写进仓库、可版本管理的配置](#0.1 YAML:写进仓库、可版本管理的配置)
- [0.2 ENV:不进仓库、与机器/账号相关的配置](#0.2 ENV:不进仓库、与机器/账号相关的配置)
- [1. 为什么配置管理是"可迁移性"的核心](#1. 为什么配置管理是“可迁移性”的核心)
- [2. 推荐目录结构(承接上一章的黄金布局)](#2. 推荐目录结构(承接上一章的黄金布局))
- [3. YAML 规范:从一开始就"像工程"地写配置](#3. YAML 规范:从一开始就“像工程”地写配置)
-
- [3.1 一个可直接复制的 default.yaml](#3.1 一个可直接复制的 default.yaml)
- [4. ENV 规范:只放必要的"敏感/环境差异"信息](#4. ENV 规范:只放必要的“敏感/环境差异”信息)
-
- [4.1 `.env.example`(提交到仓库)](#4.1
.env.example(提交到仓库)) - [4.2 `.env`(本地实际值,不提交)](#4.2
.env(本地实际值,不提交))
- [4.1 `.env.example`(提交到仓库)](#4.1
- [5. 实现一个"统一配置入口":src/myproj/core/config.py](#5. 实现一个“统一配置入口”:src/myproj/core/config.py)
-
- [5.1 配置加载器(支持 YAML + ENV 覆盖 + 合并)](#5.1 配置加载器(支持 YAML + ENV 覆盖 + 合并))
- [6. 如何选择 dev/prod 配置(最小多环境方案)](#6. 如何选择 dev/prod 配置(最小多环境方案))
- [7. 把配置真正用起来:入口脚本示例(run_etl.py)](#7. 把配置真正用起来:入口脚本示例(run_etl.py))
- [8. 常见坑与规避策略(这是经验部分)](#8. 常见坑与规避策略(这是经验部分))
-
- [坑 1:配置键名随便改,导致项目漂移](#坑 1:配置键名随便改,导致项目漂移)
- [坑 2:ENV 覆盖过度,最后没人知道参数来自哪](#坑 2:ENV 覆盖过度,最后没人知道参数来自哪)
- [坑 3:多个 YAML 拆得太碎](#坑 3:多个 YAML 拆得太碎)
- [坑 4:运行时没有落盘 config 快照](#坑 4:运行时没有落盘 config 快照)
- [9. 本章最低交付(MDR):你必须交付什么?](#9. 本章最低交付(MDR):你必须交付什么?)
- [10. 小结:配置管理是工程化的分水岭](#10. 小结:配置管理是工程化的分水岭)
(把"写死参数"的脚本,升级为"可复现、可迁移、可交付"的工程)
如果你做过一两个数据分析/AI 项目,你一定写过这样的代码:
python
DATA_PATH = "/Users/robin/Desktop/data.xlsx"
MODEL_PATH = "model.pkl"
THRESHOLD = 0.73
API_KEY = "sk-xxxx"
它在你电脑上能跑,甚至跑得很顺。但项目一旦进入真实场景,就会迅速崩盘:
- 换机器:路径全错
- 换数据:列名不一致、阈值不适配
- 换环境:开发/测试/生产不同配置混在一起
- 交付给别人:对方不知道改哪里、改了又改坏
- 重现实验:你不知道当时用的是哪套参数
所以配置管理不是"可选优化",而是工程化的必修课。
本章我们只解决一个问题:
让同一份代码在不同环境、不同数据、不同运行方式下稳定工作。
工具也不复杂:
- 用 YAML 管"可公开、可版本化的配置"
- 用 ENV 管"敏感信息与环境差异"
- 用一个小的
config.py让加载方式标准化 - 每次运行把 config 快照落盘,形成可追溯证据链(runs)
0. 先把概念讲清楚:什么该进 YAML?什么该进 ENV?
这是新手最容易混乱的地方。我给你一个非常实用的分界线:
0.1 YAML:写进仓库、可版本管理的配置
适合放:
- 数据路径(相对路径)、文件名、schema 约束
- 清洗规则(缺失值策略、阈值、字段映射)
- 模型超参(seed、batch size、topk、chunk size)
- 输出路径(runs、reports)
- 日志级别、开关(debug、cache)
原则: YAML 里的东西应该能随代码一起进入 Git,别人拉仓库也看得懂、改得起。
0.2 ENV:不进仓库、与机器/账号相关的配置
适合放:
- API Key / Token / Password
- 数据库连接串
- 线上环境地址
- 机器差异(GPU 开关、并行度、代理)
- 任何你不想出现在 Git 历史里的东西
原则: ENV 里的东西默认不提交(.env 加入 .gitignore),只提供 .env.example 作为模板。
1. 为什么配置管理是"可迁移性"的核心
你可以用一张 Mermaid 图理解配置管理在工程中的位置:
同一份代码
不同运行场景
你的电脑
同事/评审的电脑
服务器/容器
CI测试环境
YAML:公开可版本配置
ENV:敏感与环境差异
config加载器统一入口
runs/落盘config快照
可复现 / 可迁移 / 可交付
没有配置管理,你的项目只能在一个环境"碰巧能跑";
有了配置管理,你的项目才能成为"可迁移的工程"。
2. 推荐目录结构(承接上一章的黄金布局)
本章配置管理落地的目录结构建议如下:
text
project/
configs/
default.yaml
dev.yaml
prod.yaml
src/
myproj/
core/
config.py
paths.py
.env # 不提交
.env.example # 提交
你至少要有 default.yaml。dev/prod 是否需要取决于你是否有多环境场景。
3. YAML 规范:从一开始就"像工程"地写配置
我建议你从一个结构化 YAML 开始(不要扁平化堆键值对,否则后面会失控)。
3.1 一个可直接复制的 default.yaml
yaml
# configs/default.yaml
project:
name: "myproj"
run_dir: "runs"
report_dir: "reports"
data:
input_path: "data/raw/sample.csv"
output_path: "data/processed/clean.csv"
encoding: "utf-8"
clean:
drop_duplicates: true
missing_strategy: "median" # mean/median/constant
missing_constant: 0
outlier_strategy: "clip" # clip/remove/none
outlier_clip_quantile: 0.99
train:
seed: 42
test_size: 0.2
logging:
level: "INFO"
注意这里的关键点:
- 路径尽量用相对路径(可迁移)
- 清洗策略与阈值显式化(可复现)
- 训练参数显式化(可评估)
- 输出路径显式化(可交付)
4. ENV 规范:只放必要的"敏感/环境差异"信息
4.1 .env.example(提交到仓库)
bash
# .env.example
OPENAI_API_KEY=
DB_URL=
HTTP_PROXY=
4.2 .env(本地实际值,不提交)
bash
# .env
OPENAI_API_KEY=xxxxx
DB_URL=postgresql://...
HTTP_PROXY=http://127.0.0.1:7890
并确保 .gitignore 里包含:
text
.env
这是非常基本但关键的安全纪律。
5. 实现一个"统一配置入口":src/myproj/core/config.py
这一段是本章的工程核心:让你的项目任何入口(Notebook、CLI、脚本、测试)都用同一套方式加载配置。
下面给一个够用且工程化的实现(只用标准库 + PyYAML 可选;你也可以用 yaml 包)。
说明:CSDN 文中我会给"可复制版本",你可以直接落到项目里。
5.1 配置加载器(支持 YAML + ENV 覆盖 + 合并)
python
# src/myproj/core/config.py
from __future__ import annotations
import os
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict
try:
import yaml
except ImportError:
yaml = None
def load_yaml(path: str | Path) -> Dict[str, Any]:
if yaml is None:
raise RuntimeError("PyYAML not installed. Run: pip install pyyaml")
p = Path(path)
with p.open("r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def deep_update(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
"""递归合并字典:override 覆盖 base。"""
for k, v in override.items():
if isinstance(v, dict) and isinstance(base.get(k), dict):
base[k] = deep_update(base[k], v)
else:
base[k] = v
return base
def load_env_overrides(prefix: str = "") -> Dict[str, Any]:
"""
将环境变量映射为配置覆盖。
简化策略:只读取几个常用键,避免把所有 env 映射得过复杂。
"""
overrides: Dict[str, Any] = {}
# 示例:把 OPENAI_API_KEY 注入到 cfg["secrets"]["openai_api_key"]
openai_key = os.getenv(f"{prefix}OPENAI_API_KEY")
if openai_key:
overrides.setdefault("secrets", {})["openai_api_key"] = openai_key
db_url = os.getenv(f"{prefix}DB_URL")
if db_url:
overrides.setdefault("secrets", {})["db_url"] = db_url
return overrides
@dataclass(frozen=True)
class Config:
raw: Dict[str, Any]
def get(self, *keys: str, default=None):
cur: Any = self.raw
for k in keys:
if not isinstance(cur, dict) or k not in cur:
return default
cur = cur[k]
return cur
def load_config(
base_yaml: str = "configs/default.yaml",
env_yaml: str | None = None,
use_env: bool = True,
) -> Config:
base = load_yaml(base_yaml)
# dev/prod 覆盖(可选)
if env_yaml:
env_cfg = load_yaml(env_yaml)
base = deep_update(base, env_cfg)
# ENV 覆盖(可选)
if use_env:
env_over = load_env_overrides()
base = deep_update(base, env_over)
return Config(raw=base)
def save_config_snapshot(cfg: Config, run_dir: str | Path) -> None:
"""把最终生效配置落盘到 runs,作为可复现证据链的一部分。"""
run_path = Path(run_dir)
run_path.mkdir(parents=True, exist_ok=True)
(run_path / "config_snapshot.json").write_text(
json.dumps(cfg.raw, ensure_ascii=False, indent=2),
encoding="utf-8",
)
这段代码有三点你要理解:
- 所有入口统一用 load_config
- 多环境 YAML 覆盖 + ENV 覆盖(可迁移)
- 运行时落盘 config_snapshot.json(可复现)
6. 如何选择 dev/prod 配置(最小多环境方案)
你可以约定:通过环境变量 APP_ENV 选择配置文件:
APP_ENV=dev→configs/dev.yamlAPP_ENV=prod→configs/prod.yaml- 默认 →
configs/default.yaml
示意(伪代码):
python
env = os.getenv("APP_ENV", "default")
env_yaml = None if env == "default" else f"configs/{env}.yaml"
cfg = load_config(base_yaml="configs/default.yaml", env_yaml=env_yaml)
Mermaid:配置覆盖优先级(非常重要)
default.yaml
dev/prod.yaml 覆盖
ENV 覆盖
最终生效 Config
规则只有一句:越靠后,优先级越高。
你一旦明确这一点,配置体系就不会乱。
7. 把配置真正用起来:入口脚本示例(run_etl.py)
配置管理的价值不在于"有 YAML 文件",而在于:你的脚本不再写死路径与参数。
示例入口(只展示结构,不堆业务逻辑):
python
# scripts/run_etl.py
from datetime import datetime
from pathlib import Path
from myproj.core.config import load_config, save_config_snapshot
from myproj.core.logging import setup_logging # 下一章会讲
from myproj.etl.clean import run_clean_pipeline
def main():
ts = datetime.now().strftime("%Y-%m-%d_%H%M")
run_dir = Path("runs") / ts
cfg = load_config(base_yaml="configs/default.yaml")
save_config_snapshot(cfg, run_dir)
setup_logging(run_dir=run_dir, level=cfg.get("logging", "level", default="INFO"))
input_path = cfg.get("data", "input_path")
output_path = cfg.get("data", "output_path")
run_clean_pipeline(input_path=input_path, output_path=output_path, cfg=cfg)
if __name__ == "__main__":
main()
你会发现:脚本只负责"读配置 → 调逻辑 → 落盘",业务逻辑完全在 src/。
8. 常见坑与规避策略(这是经验部分)
坑 1:配置键名随便改,导致项目漂移
策略:配置结构要稳定,键名变更属于破坏性变更,必须写 CHANGELOG。
坑 2:ENV 覆盖过度,最后没人知道参数来自哪
策略:ENV 只用于敏感信息与环境差异,不要用 ENV 管所有超参。
坑 3:多个 YAML 拆得太碎
策略:一开始只要 default.yaml,需要时再加 dev/prod,避免早期过度工程化。
坑 4:运行时没有落盘 config 快照
策略:把 save_config_snapshot 当成"交付动作",强制写入 runs。
9. 本章最低交付(MDR):你必须交付什么?
按照专栏的学习协议,本章完成后你至少交付:
configs/default.yaml(结构化配置,包含 data/clean/train/logging 等段).env.example(敏感配置模板)+.gitignore忽略.envsrc/myproj/core/config.py(统一配置入口,支持 YAML + ENV 覆盖)- 任意一个脚本入口(例如
scripts/run_etl.py)从配置读取路径/参数 - 一次运行落盘:
runs/<ts>/config_snapshot.json(可复现证据链)
你做到这五条,你的项目就具备了"可迁移性"和"可复现性"的核心能力。
10. 小结:配置管理是工程化的分水岭
从脚本到工程,配置管理是一个非常明确的分水岭。
它会把你从"靠记忆与手改跑项目",带到"靠配置与规范交付项目"。
更重要的是:当你开始这样做,你的作品集会自然变得高级------因为它不再是截图合集,而是可运行、可复现、可迁移的工程项目。
下一章我们会把日志体系补齐:logging 让排错效率翻倍,并让每一次运行都留下可追溯的线索。
如果你愿意,把你项目里目前"写死在代码里的参数"(比如路径、阈值、模型超参)列 5 条出来,我可以帮你把它们整理成一个更合理的 default.yaml 结构,并给出对应的 Config.get() 调用方式,让你直接落地到项目里。