# 配置入口说明
## 1. 整体说明
目前所有的配置项统一成了一套明确的三层配置系统,并保留了原来的 `utils.configs` 使用入口,尽量不影响现有业务代码。
特性:
- 支持统一从自定义 JSON、环境变量、`utils/config.py` 读取配置。
- 尽量兼容现有代码中 `from utils.configs import XXX` 的写法。
- 降低后续新增、删除、排查配置项时的维护成本。
当前配置优先级如下:
1. 自定义 JSON 文件
2. 环境变量
3. `utils/config.py` 中的默认值
也就是说:**越靠前优先级越高**。
---
## 2. 配置相关文件总览
本次改动后,配置系统的核心文件如下:
- `utils/config.py`
- 默认配置定义文件。
- 新增配置项时,首先应该在这里定义默认值。
- `utils/config_loader.py`
- 配置加载核心逻辑。
- 负责自动发现配置项、按优先级加载、做环境变量类型转换。
- `utils/configs.py`
- 对外兼容层。
- 旧代码仍然可以继续 `from utils.configs import XXX`。
- `utils/config/config.example.json`
- JSON 配置样例。
- `utils/config/README.md`
- 即当前文档,说明原理、使用方式和维护规范。
---
## 3. 整体工作流程
程序启动后,如果某处代码执行了:
```python
from utils.configs import CLIENT_TIMEOUT
```
或者:
```python
import utils.configs as configs
```
会触发如下链路:
1. Python 加载 `utils/configs.py`
2. `utils/configs.py` 再导入 `utils/config_loader.py`
3. `utils/config_loader.py` 在模块导入阶段创建 `_CONFIG_MANAGER = ConfigManager()`
4. `ConfigManager()` 内部加载 `utils.config`
5. 自动扫描 `utils.config` 中所有符合条件的“大写配置项”
6. 建立三层配置源:JSON、环境变量、默认 `config.py`
7. `utils.configs` 再把最终配置值暴露给业务代码
因此:
- **只要业务代码 import 了 `utils.configs`,这套配置逻辑就会启动**。
- **从使用视角看,只要在 `utils.configs` 可访问到一个新增的全大写配置变量,就可以直接通过 JSON / 环境变量配置后启动评测;按当前实现,这个变量的定义应新增在 `utils/config.py`,详细维护方式见后文的[开发者维护指南](#developer-guide)和[如何判断一个新配置是否会生效](#config-checklist)。**
- **配置项并不是手写白名单维护,而是自动从 `utils.config.py` 收集**。
---
## 4. 自动收集配置项的规则
当前系统不再手工维护 `CONFIG_KEYS` 白名单,而是从 `utils/config.py` 自动发现配置项。
自动发现规则如下:
一个变量会被当成“配置项”,必须同时满足:
- 变量名是全大写,例如 `CLIENT_TIMEOUT`
- 变量名**不能以下划线开头**,例如 `_SECRET` 不会被收集
- 变量值不能是可调用对象(callable),例如函数不会被收集
当前实现等价于下面这条规则:
```python
name.isupper() and not name.startswith("_") and not callable(value)
```
### 4.1 会被收集的例子
```python
CLIENT_TIMEOUT = 1800
LLM_SERVER_MODEL_NAME = ["demo_model"]
USE_NLP_FORMAT_RETURN = True
CHAT_PROFILE_CONFIGS = []
```
### 4.2 不会被收集的例子
```python
_client_timeout = 1800 # 以下划线开头,不收集
client_timeout = 1800 # 不是全大写,不收集
MixedCaseValue = 1 # 不是全大写,不收集
def BUILD_CONFIG(): # callable,不收集
return {}
```
### 4.3 关于 `isupper()` 的注意事项
Python 的 `str.isupper()` 要求:
- 字母必须全部是大写
- 可以包含下划线和数字
所以这些名字都可以被收集:
```python
MODEL_V2_NAME = "a"
API_KEY_1 = "b"
LONG_REPORT_MAX_TOKENS = 4096
```
而这些不会被收集:
```python
Model_Name = "a"
api_key = "b"
LongReportMaxTokens = 4096
```
**建议统一使用 `UPPER_SNAKE_CASE` 命名。**
---
## 5. 三层配置优先级详解
### 5.1 第一层:JSON 配置(最高优先级)
系统会优先尝试读取 JSON 配置。
支持两种方式指定 JSON:
#### 方式 A:通过环境变量显式指定 JSON 路径
支持以下环境变量名:
- `S1_DR_CONFIG_JSON`
- `DR_SKILLS_CONFIG_JSON`
- `CONFIG_JSON_PATH`
示例:
```bash
export S1_DR_CONFIG_JSON=/path/to/config.json
```
如果给的是相对路径,例如:
```bash
export S1_DR_CONFIG_JSON=tmp/my_config.json
```
它会被解释为**相对于仓库根目录**的路径,而不是相对于任意 shell 当前目录。
#### 方式 B:不显式指定,使用默认搜索路径
如果没有显式设置上述环境变量,系统会按顺序查找以下文件:
- `config.local.json`
- `config.json`
- `utils/config/config.local.json`
- `utils/config/config.json`
注意:
- 搜索到第一个存在的文件后就会停止。
- 不会把多个 JSON 合并。
- 当前实现是“找到第一个有效 JSON 文件并加载”。
#### JSON 文件格式要求
JSON 顶层必须是一个对象(object),例如:
```json
{
"CLIENT_TIMEOUT": 3600,
"USE_NLP_FORMAT_RETURN": false
}
```
下面这种格式是错误的,因为顶层是数组:
```json
[
{"CLIENT_TIMEOUT": 3600}
]
```
如果顶层不是对象,程序会抛出异常。
#### JSON 中的 key 命名要求
为了让 JSON 配置真正起作用,建议:
- key 与 `utils/config.py` 中的配置变量名保持**完全一致**
- 推荐使用全大写的 `UPPER_SNAKE_CASE`
例如:
`utils/config.py` 中是:
```python
MY_NEW_FLAG = "default"
```
那 JSON 中应写:
```json
{
"MY_NEW_FLAG": "json_value"
}
```
不要写成:
```json
{
"my_new_flag": "json_value"
}
```
后者虽然 JSON 能被读取,但因为业务代码不会访问 `my_new_flag`,通常不会产生你想要的效果。
### 5.2 第二层:环境变量
如果 JSON 没提供某个 key,系统会继续查环境变量。
对于任意配置项 `KEY`,系统会按顺序查找:
1. `KEY`
2. `S1_DR_KEY`
3. `DR_SKILLS_KEY`
例如配置项为:
```python
CLIENT_TIMEOUT = 1800
```
则会依次尝试:
- `CLIENT_TIMEOUT`
- `S1_DR_CLIENT_TIMEOUT`
- `DR_SKILLS_CLIENT_TIMEOUT`
先找到哪个,就使用哪个。
### 5.3 第三层:`utils/config.py` 默认值(最低优先级)
如果 JSON 和环境变量都没有给出某个配置项,就会回退到:
- `utils/config.py`
这是整个系统的默认值来源,也是自动发现配置项的来源。
---
## 6. 环境变量类型转换规则
环境变量本质上都是字符串,因此系统会根据 `utils/config.py` 里的默认值类型做自动转换。
这是一个很重要的细节:
- 类型推断**不是瞎猜**
- 而是参考 `config.py` 中该配置项的默认值类型
### 6.1 布尔值
如果默认值是 `bool`,支持以下写法:
```bash
export USE_NLP_FORMAT_RETURN=true
export USE_NLP_FORMAT_RETURN=false
export USE_NLP_FORMAT_RETURN=1
export USE_NLP_FORMAT_RETURN=0
export USE_NLP_FORMAT_RETURN=yes
export USE_NLP_FORMAT_RETURN=no
export USE_NLP_FORMAT_RETURN=on
export USE_NLP_FORMAT_RETURN=off
```
转换规则:
- `1`, `true`, `yes`, `on` -> `True`
- `0`, `false`, `no`, `off` -> `False`
大小写会先统一转成小写再判断。
### 6.2 整数
如果默认值是 `int`,会执行:
```python
int(raw_value)
```
例如:
```bash
export CLIENT_TIMEOUT=3600
```
### 6.3 浮点数
如果默认值是 `float`,会执行:
```python
float(raw_value)
```
### 6.4 列表
如果默认值是 `list`,优先按 JSON 解析;如果解析后不是列表,则退化成“逗号分隔”。
例如下面两种都可以:
```bash
export LLM_SERVER_MODEL_NAME='["model_a", "model_b"]'
```
或者:
```bash
export LLM_SERVER_MODEL_NAME=model_a,model_b
```
### 6.5 字典
如果默认值是 `dict`,那么环境变量值必须是合法 JSON 对象。
例如:
```bash
export SOME_DICT='{"a": 1, "b": 2}'
```
如果不是合法 JSON 对象,会抛出异常。
### 6.6 字符串
如果默认值不是上述类型,则保持原字符串。
---
## 7. 推荐使用方式
### 7.1 最推荐:默认值放 `config.py`,真实环境配置放 JSON
推荐原因:
- 真实环境配置可以与代码分离
- 便于本地、测试、线上使用不同 JSON
- 私有配置不必硬编码进仓库
推荐模式:
1. 在 `utils/config.py` 里定义默认值
2. 在某个 JSON 文件中写真实覆盖值
3. 通过 `S1_DR_CONFIG_JSON` 指向该 JSON
例如:
```python
# utils/config.py
MY_NEW_FLAG = "default"
```
```json
{
"MY_NEW_FLAG": "prod"
}
```
```bash
export S1_DR_CONFIG_JSON=/path/to/my_config.json
python your_eval_entry.py
```
### 7.2 适合临时调试:环境变量覆盖
例如只想临时改一个参数:
```bash
export CLIENT_TIMEOUT=7200
python your_eval_entry.py
```
这种方式适合:
- 临时实验
- CI 中临时注入参数
- shell 启动脚本里少量覆盖
不太适合:
- 结构复杂的长配置
- 多 profile 管理
- 团队共享配置模板
### 7.3 业务代码中的推荐 import 写法
如果你只是普通启动一次进程,下面两种都可以:
```python
from utils.configs import CLIENT_TIMEOUT
```
```python
import utils.configs as configs
print(configs.CLIENT_TIMEOUT)
```
但如果你希望在运行时使用 `reload_config()` 动态刷新,**更推荐第二种**:
```python
import utils.configs as configs
configs.reload_config()
print(configs.CLIENT_TIMEOUT)
```
因为:
```python
from utils.configs import CLIENT_TIMEOUT
```
在很多情况下会把值绑定为导入时的局部变量,后面即使 reload,当前模块中的这个局部名也不一定自动变化。
---
## 8. `reload_config()` 的用途和边界
`reload_config()` 的作用是:
- 重新读取 `utils.config`
- 重新自动发现大写配置项
- 重新扫描 JSON 配置路径
- 重新读取环境变量
- 刷新 `utils.configs` 模块中暴露的配置变量
典型场景:
- 你在长生命周期进程中临时改了环境变量
- 你切换了 `S1_DR_CONFIG_JSON`
- 你刚刚修改了 `utils/config.py`,想在当前解释器里重新生效
调用方式:
```python
import utils.configs as configs
configs.reload_config()
```
### 8.1 对“新增配置项”的效果
如果你新增了:
```python
NEW_SETTING = 123
```
然后执行:
```python
configs.reload_config()
```
那么新的配置项会被自动纳入配置系统。
### 8.2 对“删除配置项”的效果
如果你从 `utils/config.py` 删除了某个原有大写变量,调用 `reload_config()` 后:
- 它会从自动收集结果中消失
- `utils.configs` 中旧的同名全局变量也会被清理
- 再访问会触发 `AttributeError`
### 8.3 一个重要限制
如果其他模块已经这样写了:
```python
from utils.configs import CLIENT_TIMEOUT
```
并且这个 import 已经发生,那么该模块内的 `CLIENT_TIMEOUT` 很可能是导入时拷贝下来的值。
即使后面执行:
```python
configs.reload_config()
```
这个其他模块内部的局部变量也不一定自动更新。
这属于 Python import 机制本身的语义,不是本系统独有的问题。
所以:
- 对一次性启动的评测脚本:通常直接重启进程最稳妥
- 对需要热更新的场景:推荐统一使用 `import utils.configs as configs`
---
## 9. 常见使用示例
### 9.1 只用默认值
`utils/config.py`:
```python
MY_BATCH_SIZE = 8
```
代码:
```python
from utils.configs import MY_BATCH_SIZE
print(MY_BATCH_SIZE)
```
输出:
```python
8
```
### 9.2 用环境变量覆盖
`utils/config.py`:
```python
MY_BATCH_SIZE = 8
```
启动前:
```bash
export MY_BATCH_SIZE=32
```
程序读取到的是:
```python
32
```
### 9.3 用 JSON 覆盖
`utils/config.py`:
```python
MY_BATCH_SIZE = 8
```
JSON:
```json
{
"MY_BATCH_SIZE": 64
}
```
启动前:
```bash
export S1_DR_CONFIG_JSON=/path/to/config.json
```
程序读取到的是:
```python
64
```
### 9.4 JSON 优先级高于环境变量
如果:
```bash
export MY_BATCH_SIZE=32
export S1_DR_CONFIG_JSON=/path/to/config.json
```
并且 JSON 中:
```json
{
"MY_BATCH_SIZE": 64
}
```
最终结果是:
```python
64
```
因为 JSON 优先级更高。
---
## 10. 开发者维护指南
这一节专门从开发者视角说明:后续怎样新增、删除、重构和排查配置项。
### 10.1 新增配置项的标准流程
假设你想新增一个配置项:`MY_NEW_FLAG`
#### 第一步:在 `utils/config.py` 中定义默认值
```python
MY_NEW_FLAG = "default_value"
```
命名建议:
- 使用 `UPPER_SNAKE_CASE`
- 不以下划线开头
- 不要定义成函数
#### 第二步:如果需要,更新示例文件
建议同步更新:
- `utils/config/config.example.json`
- 如果需要,也可以补充当前文档中的例子
例如:
```json
{
"MY_NEW_FLAG": "example_value"
}
```
#### 第三步:在业务代码中读取
```python
from utils.configs import MY_NEW_FLAG
```
或者:
```python
import utils.configs as configs
print(configs.MY_NEW_FLAG)
```
#### 第四步:如果在当前进程内调试,记得 reload
```python
import utils.configs as configs
configs.reload_config()
```
#### 第五步:如果是正式脚本,最稳妥是重启进程
例如重新执行评测入口脚本,而不是依赖热更新。
### 10.2 删除配置项的标准流程
假设你想删除 `OLD_FLAG`
步骤如下:
1. 从 `utils/config.py` 中删除 `OLD_FLAG`
2. 删除 JSON 示例中对应的条目
3. 全局搜索仓库中是否还有引用:
```bash
rg "OLD_FLAG" repo/s1-dr-skills-v3
```
4. 清理这些引用
5. 重新启动相关脚本,或在调试环境里执行 `reload_config()`
注意:
- 如果业务代码里还有 `from utils.configs import OLD_FLAG`,删除后会报错
- 这是预期行为,因为该配置项已经不存在了
### 10.3 修改已有配置项类型时的注意事项
例如你原来是:
```python
MY_SETTING = "1,2,3"
```
后来改成:
```python
MY_SETTING = [1, 2, 3]
```
这会影响环境变量的解析逻辑,因为环境变量类型推断依赖默认值类型。
因此修改配置项类型时,要同步检查:
- `config.example.json` 是否仍然合理
- 启动脚本中的环境变量是否还能正确解析
- 业务代码是否仍按新类型使用
### 10.4 不建议的做法
以下做法不推荐:
#### 不建议 1:在 `config.py` 中定义大量临时大写常量
因为所有符合规则的大写变量都会自动进入配置系统。
如果某个值只是模块内部常量,不希望变成“正式配置项”,不要写成会被自动收集的形式。
比如不要这样:
```python
TEMP_DEBUG_MARKER = "abc"
```
如果它并不是你想暴露给全仓库的配置。
可以改成:
```python
_temp_debug_marker = "abc"
```
或者:
```python
temp_debug_marker = "abc"
```
#### 不建议 2:依赖拼错 key 的 JSON 配置
例如 `config.py` 中是:
```python
CLIENT_TIMEOUT = 1800
```
但 JSON 写成:
```json
{
"CLIENT-TIMEOUT": 3600
}
```
这种拼写不一致通常不会达到预期效果。
#### 不建议 3:把复杂结构随意塞进环境变量
环境变量适合少量覆盖,不太适合特别大的嵌套配置对象。
复杂对象更适合放 JSON。
### 10.5 推荐的维护原则
建议遵循以下原则:
- 默认值统一放 `utils/config.py`
- 真实环境配置优先放 JSON
- 临时实验用环境变量
- 配置名统一使用 `UPPER_SNAKE_CASE`
- 不以 `_` 开头,除非明确不想让它进入配置系统
- 修改配置项类型时,检查环境变量解析影响
- 删除配置项前,先全局搜索引用
- 需要热更新时,用 `import utils.configs as configs`
---
## 11. 如何判断一个新配置是否会生效
如果你新增了一个配置项,可以按这个 checklist 检查:
1. 它是否定义在 `utils/config.py`
2. 名字是否全大写
3. 是否没有以下划线开头
4. 是否不是函数或其他 callable
5. JSON 中的 key 是否与变量名完全一致
6. 业务代码是否真的读取了这个配置项
7. 是否是新启动的进程,或者已经执行了 `reload_config()`
只要这些条件都满足,它通常就会生效。
---
## 12. 排查问题时的建议
### 问题 1:为什么 JSON 配置没有生效?
排查顺序:
1. 是否真的设置了 `S1_DR_CONFIG_JSON`
2. 路径是否正确
3. JSON 是否是合法对象
4. JSON 中的 key 是否与 `config.py` 完全一致
5. 业务代码是否访问的是同一个名字
6. 是否其实被另一个更前面的 JSON 文件抢先匹配了
### 问题 2:为什么新增配置项没有生效?
排查顺序:
1. 变量名是不是全大写
2. 是否以下划线开头
3. 是否只是改了文件但没有重启进程
4. 是否需要调用 `reload_config()`
5. 业务代码是否真的 import/访问了该变量
### 问题 3:为什么删掉配置项后还能访问?
通常原因有两个:
1. 进程还没 reload / 重启
2. 某个模块之前已经 `from utils.configs import XXX`,把旧值绑定下来了
---
## 13. 一句话总结
当前配置系统的核心原则是:
- **配置项来源于 `utils/config.py` 中符合规则的大写变量**
- **最终取值遵循 `JSON > 环境变量 > config.py`**
- **对外统一从 `utils.configs` 访问**
- **新增/删除配置项主要维护 `utils/config.py`,并按需重启或 reload**
如果你后续继续维护这套系统,最重要的三条经验是:
1. 新配置名请用 `UPPER_SNAKE_CASE`
2. 不想暴露成配置项的变量,不要写成“全大写且不以下划线开头”
3. 动态变更配置时,优先理解 Python import 的静态绑定语义