ScienceOne-AI's picture
Upload 61 files
816198f verified

配置入口说明

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. 整体工作流程

程序启动后,如果某处代码执行了:

from utils.configs import CLIENT_TIMEOUT

或者:

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,详细维护方式见后文的开发者维护指南如何判断一个新配置是否会生效
  • 配置项并不是手写白名单维护,而是自动从 utils.config.py 收集

4. 自动收集配置项的规则

当前系统不再手工维护 CONFIG_KEYS 白名单,而是从 utils/config.py 自动发现配置项。

自动发现规则如下:

一个变量会被当成“配置项”,必须同时满足:

  • 变量名是全大写,例如 CLIENT_TIMEOUT
  • 变量名不能以下划线开头,例如 _SECRET 不会被收集
  • 变量值不能是可调用对象(callable),例如函数不会被收集

当前实现等价于下面这条规则:

name.isupper() and not name.startswith("_") and not callable(value)

4.1 会被收集的例子

CLIENT_TIMEOUT = 1800
LLM_SERVER_MODEL_NAME = ["demo_model"]
USE_NLP_FORMAT_RETURN = True
CHAT_PROFILE_CONFIGS = []

4.2 不会被收集的例子

_client_timeout = 1800      # 以下划线开头,不收集
client_timeout = 1800       # 不是全大写,不收集
MixedCaseValue = 1          # 不是全大写,不收集

def BUILD_CONFIG():         # callable,不收集
    return {}

4.3 关于 isupper() 的注意事项

Python 的 str.isupper() 要求:

  • 字母必须全部是大写
  • 可以包含下划线和数字

所以这些名字都可以被收集:

MODEL_V2_NAME = "a"
API_KEY_1 = "b"
LONG_REPORT_MAX_TOKENS = 4096

而这些不会被收集:

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

示例:

export S1_DR_CONFIG_JSON=/path/to/config.json

如果给的是相对路径,例如:

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),例如:

{
  "CLIENT_TIMEOUT": 3600,
  "USE_NLP_FORMAT_RETURN": false
}

下面这种格式是错误的,因为顶层是数组:

[
  {"CLIENT_TIMEOUT": 3600}
]

如果顶层不是对象,程序会抛出异常。

JSON 中的 key 命名要求

为了让 JSON 配置真正起作用,建议:

  • key 与 utils/config.py 中的配置变量名保持完全一致
  • 推荐使用全大写的 UPPER_SNAKE_CASE

例如:

utils/config.py 中是:

MY_NEW_FLAG = "default"

那 JSON 中应写:

{
  "MY_NEW_FLAG": "json_value"
}

不要写成:

{
  "my_new_flag": "json_value"
}

后者虽然 JSON 能被读取,但因为业务代码不会访问 my_new_flag,通常不会产生你想要的效果。

5.2 第二层:环境变量

如果 JSON 没提供某个 key,系统会继续查环境变量。

对于任意配置项 KEY,系统会按顺序查找:

  1. KEY
  2. S1_DR_KEY
  3. DR_SKILLS_KEY

例如配置项为:

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,支持以下写法:

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,会执行:

int(raw_value)

例如:

export CLIENT_TIMEOUT=3600

6.3 浮点数

如果默认值是 float,会执行:

float(raw_value)

6.4 列表

如果默认值是 list,优先按 JSON 解析;如果解析后不是列表,则退化成“逗号分隔”。

例如下面两种都可以:

export LLM_SERVER_MODEL_NAME='["model_a", "model_b"]'

或者:

export LLM_SERVER_MODEL_NAME=model_a,model_b

6.5 字典

如果默认值是 dict,那么环境变量值必须是合法 JSON 对象。

例如:

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

例如:

# utils/config.py
MY_NEW_FLAG = "default"
{
  "MY_NEW_FLAG": "prod"
}
export S1_DR_CONFIG_JSON=/path/to/my_config.json
python your_eval_entry.py

7.2 适合临时调试:环境变量覆盖

例如只想临时改一个参数:

export CLIENT_TIMEOUT=7200
python your_eval_entry.py

这种方式适合:

  • 临时实验
  • CI 中临时注入参数
  • shell 启动脚本里少量覆盖

不太适合:

  • 结构复杂的长配置
  • 多 profile 管理
  • 团队共享配置模板

7.3 业务代码中的推荐 import 写法

如果你只是普通启动一次进程,下面两种都可以:

from utils.configs import CLIENT_TIMEOUT
import utils.configs as configs
print(configs.CLIENT_TIMEOUT)

但如果你希望在运行时使用 reload_config() 动态刷新,更推荐第二种

import utils.configs as configs

configs.reload_config()
print(configs.CLIENT_TIMEOUT)

因为:

from utils.configs import CLIENT_TIMEOUT

在很多情况下会把值绑定为导入时的局部变量,后面即使 reload,当前模块中的这个局部名也不一定自动变化。


8. reload_config() 的用途和边界

reload_config() 的作用是:

  • 重新读取 utils.config
  • 重新自动发现大写配置项
  • 重新扫描 JSON 配置路径
  • 重新读取环境变量
  • 刷新 utils.configs 模块中暴露的配置变量

典型场景:

  • 你在长生命周期进程中临时改了环境变量
  • 你切换了 S1_DR_CONFIG_JSON
  • 你刚刚修改了 utils/config.py,想在当前解释器里重新生效

调用方式:

import utils.configs as configs
configs.reload_config()

8.1 对“新增配置项”的效果

如果你新增了:

NEW_SETTING = 123

然后执行:

configs.reload_config()

那么新的配置项会被自动纳入配置系统。

8.2 对“删除配置项”的效果

如果你从 utils/config.py 删除了某个原有大写变量,调用 reload_config() 后:

  • 它会从自动收集结果中消失
  • utils.configs 中旧的同名全局变量也会被清理
  • 再访问会触发 AttributeError

8.3 一个重要限制

如果其他模块已经这样写了:

from utils.configs import CLIENT_TIMEOUT

并且这个 import 已经发生,那么该模块内的 CLIENT_TIMEOUT 很可能是导入时拷贝下来的值。

即使后面执行:

configs.reload_config()

这个其他模块内部的局部变量也不一定自动更新。

这属于 Python import 机制本身的语义,不是本系统独有的问题。

所以:

  • 对一次性启动的评测脚本:通常直接重启进程最稳妥
  • 对需要热更新的场景:推荐统一使用 import utils.configs as configs

9. 常见使用示例

9.1 只用默认值

utils/config.py

MY_BATCH_SIZE = 8

代码:

from utils.configs import MY_BATCH_SIZE
print(MY_BATCH_SIZE)

输出:

8

9.2 用环境变量覆盖

utils/config.py

MY_BATCH_SIZE = 8

启动前:

export MY_BATCH_SIZE=32

程序读取到的是:

32

9.3 用 JSON 覆盖

utils/config.py

MY_BATCH_SIZE = 8

JSON:

{
  "MY_BATCH_SIZE": 64
}

启动前:

export S1_DR_CONFIG_JSON=/path/to/config.json

程序读取到的是:

64

9.4 JSON 优先级高于环境变量

如果:

export MY_BATCH_SIZE=32
export S1_DR_CONFIG_JSON=/path/to/config.json

并且 JSON 中:

{
  "MY_BATCH_SIZE": 64
}

最终结果是:

64

因为 JSON 优先级更高。


10. 开发者维护指南

这一节专门从开发者视角说明:后续怎样新增、删除、重构和排查配置项。

10.1 新增配置项的标准流程

假设你想新增一个配置项:MY_NEW_FLAG

第一步:在 utils/config.py 中定义默认值

MY_NEW_FLAG = "default_value"

命名建议:

  • 使用 UPPER_SNAKE_CASE
  • 不以下划线开头
  • 不要定义成函数

第二步:如果需要,更新示例文件

建议同步更新:

  • utils/config/config.example.json
  • 如果需要,也可以补充当前文档中的例子

例如:

{
  "MY_NEW_FLAG": "example_value"
}

第三步:在业务代码中读取

from utils.configs import MY_NEW_FLAG

或者:

import utils.configs as configs
print(configs.MY_NEW_FLAG)

第四步:如果在当前进程内调试,记得 reload

import utils.configs as configs
configs.reload_config()

第五步:如果是正式脚本,最稳妥是重启进程

例如重新执行评测入口脚本,而不是依赖热更新。

10.2 删除配置项的标准流程

假设你想删除 OLD_FLAG

步骤如下:

  1. utils/config.py 中删除 OLD_FLAG
  2. 删除 JSON 示例中对应的条目
  3. 全局搜索仓库中是否还有引用:
rg "OLD_FLAG" repo/s1-dr-skills-v3
  1. 清理这些引用
  2. 重新启动相关脚本,或在调试环境里执行 reload_config()

注意:

  • 如果业务代码里还有 from utils.configs import OLD_FLAG,删除后会报错
  • 这是预期行为,因为该配置项已经不存在了

10.3 修改已有配置项类型时的注意事项

例如你原来是:

MY_SETTING = "1,2,3"

后来改成:

MY_SETTING = [1, 2, 3]

这会影响环境变量的解析逻辑,因为环境变量类型推断依赖默认值类型。

因此修改配置项类型时,要同步检查:

  • config.example.json 是否仍然合理
  • 启动脚本中的环境变量是否还能正确解析
  • 业务代码是否仍按新类型使用

10.4 不建议的做法

以下做法不推荐:

不建议 1:在 config.py 中定义大量临时大写常量

因为所有符合规则的大写变量都会自动进入配置系统。

如果某个值只是模块内部常量,不希望变成“正式配置项”,不要写成会被自动收集的形式。

比如不要这样:

TEMP_DEBUG_MARKER = "abc"

如果它并不是你想暴露给全仓库的配置。

可以改成:

_temp_debug_marker = "abc"

或者:

temp_debug_marker = "abc"

不建议 2:依赖拼错 key 的 JSON 配置

例如 config.py 中是:

CLIENT_TIMEOUT = 1800

但 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 的静态绑定语义