跳转至

Settings 配置重构方案

背景

当前 app/core/config.py 把多种职责都塞进了一个很大的 Settings 类里:

  1. 声明 pydantic-settings 会读取哪些环境变量。
  2. 保存 parser、LLM、支付、对象存储、Redis、ARQ 等模块的运行默认值。
  3. 通过大量注释解释每个配置为什么存在。
  4. 兼容旧环境变量名,例如 ARQ_RETRY_DEFER_SECONDS

这样会带来几个问题:

  • 文件太长,review 成本高。
  • .env.dev.env.test、线上 JSON secrets 和真实代码行为容易漂移。
  • 很难判断某个配置到底是“必填环境变量”“可选调参项”还是“历史遗留项”。
  • 下次改配置时容易误删有效配置,或者继续保留已经没用的旧变量。

最近已经做过一轮基础清理:

  • .env.dev.env.test 已按相同分组整理。
  • 两个本地 env 的 key 集合基本一致,只保留环境本来应不同的值。
  • 已删除一批未使用的旧变量。
  • 支付配置已按最新线上 JSON 同步。
  • test 环境配置统一通过 scripts/vault-sync-env.sh 同步到 Vault,不再维护本地 secrets JSON。

下一步应该做结构性重构,而不是继续零散清理。

目标

  • 保持调用方稳定:业务代码仍然使用 settings.DATABASE_URLsettings.PARSER_EXTRACT_MODEL 等现有访问方式。
  • 按领域拆分配置声明,让后续修改更容易 review。
  • 明确哪些基础设施配置必须来自环境变量。
  • 内部调参默认值继续留在代码里,不强行全部塞进 env。
  • 保持 .env.dev.env.test 和线上 JSON secrets 的 key 集合可追踪、可校验。
  • 在线上配置迁移完成后,逐步删除旧变量别名。

非目标

  • 不把所有默认值都搬到 env。
  • 不把所有配置都改成必填。
  • 第一阶段不改变运行行为。
  • 第一阶段不要求业务代码改成 settings.parser.xxx 这类嵌套访问,除非后续明确需要。

配置分类原则

必填环境配置

这类配置通常不应该有代码默认值。缺失时应该启动失败,或者在对应服务初始化/调用时尽早失败。

示例:

DATABASE_URL: str
REDIS_URL: str
JWT_SECRET: str
PARSER_EXTRACT_API_KEY: str
PARSER_EXTRACT_BASE_URL: str
OBJECT_STORAGE_BUCKET: str
OBJECT_STORAGE_ACCESS_KEY_ID: str
OBJECT_STORAGE_ACCESS_KEY_SECRET: str

支付配置可以按“条件必填”处理:不是所有本地流程都需要支付密钥,但只要调用支付服务,就必须校验并给出清晰的配置错误。

有安全默认值的环境覆盖项

这类配置属于运行调优开关。代码里可以保留默认值,只有某个环境确实需要覆盖时,才写进 env。

示例:

AI_CHAT_TIMEOUT: float = 120.0
PARSER_QUICK_PARSE_ITEM_SSE_INTERVAL_MAX_MS: float = 260.0
ARQ_QUEUE_NAME: str = "tender_parse"

原则:env 文件只保留当前环境确实有意覆盖的值,不把所有默认项都抄一遍。

内部常量

如果某个值不打算按环境变化,就不应该放在 Settings 里。

后续可复查的候选项:

PER_PAGE_PARSE_TIMEOUT

如果确认它们只是某个模块的内部算法常量,应移动到对应模块附近。

旧变量别名

别名适合迁移期使用,但要有退出计划。

当前示例:

ARQ_DEFER = AliasChoices("ARQ_DEFER", "ARQ_RETRY_DEFER_SECONDS")
ARQ_SOFT_TIMEOUT = AliasChoices("ARQ_SOFT_TIMEOUT", "ARQ_PARSE_SOFT_TIMEOUT_SECONDS")

当本地和线上都统一使用规范名后,应在单独 cleanup 中删除旧别名。

建议目录结构

保持最终导出的 Settings 仍然是一个对象,但把字段声明拆到不同领域的 mixin 中。

app/core/settings/
  __init__.py
  project.py
  auth.py
  database.py
  redis.py
  llm.py
  parser.py
  storage.py
  payment.py

app/core/config.py 改成兼容入口:

from app.core.settings import Settings, get_settings, settings

app/core/settings/__init__.py 组合最终类:

class Settings(
    ProjectSettings,
    AuthSettings,
    DatabaseSettings,
    RedisSettings,
    LLMSettings,
    ParserSettings,
    StorageSettings,
    PaymentSettings,
    BaseSettings,
):
    model_config = SettingsConfigDict(
        env_file=os.getenv("ENV_FILE", ".env.dev"),
        env_ignore_empty=True,
        extra="ignore",
        case_sensitive=True,
    )

这样可以保留所有现有调用方式,同时让配置归属更清楚。

领域边界

Project

  • PROJECT_NAME
  • VERSION
  • DEBUG
  • CORS_ORIGINS

Auth

  • JWT_SECRET
  • JWT_ALGORITHM
  • ACCESS_TOKEN_EXPIRE_MINUTES

Database

  • DATABASE_URL

SQLAlchemy engine 的行为继续放在 app/core/database.py,除非确实需要运行时配置开关。

Redis / ARQ

  • REDIS_URL
  • REDIS_DOCKER_PEER_HOST
  • ARQ_QUEUE_NAME
  • ARQ_SUBSCRIPTION_QUEUE_NAME
  • ARQ_SUBSCRIPTION_MAX_JOBS
  • ARQ_MAX_TRIES
  • ARQ_DEFER
  • ARQ_SOFT_TIMEOUT
  • ARQ_JOB_TIMEOUT_SECONDS

effective_redis_url() 应留在这个领域里。

LLM / Writing

  • 共享 LLM 账号配置。
  • AI chat 配置。
  • Generation 配置。
  • DeepSeek writing 配置。
  • Outline skeleton / children 配置。
  • Writing hint 配置。
  • 账号级并发和 429 退避配置。

Parser

  • Parser clean / extract 模型配置。
  • Quick parse 分块和 SSE 流式控制。
  • Parser scoring 动态限流。
  • PDF / DOCX 解析配置。

Storage / Vector

  • Object storage 配置。
  • 上传大小和文件名限制。
  • Cloudflare Vectorize 配置。
  • Embedding 模型配置。

Payment

  • Alipay 配置。
  • WeChat Pay 配置。
  • 订单号 / 支付策略配置。
  • 本地 mock webhook 配置。

迁移计划

Phase 1:建立基线检查

拆代码前先增加一个 settings audit 脚本或测试:

  • 加载 ENV_FILE=.env.dev
  • 加载 ENV_FILE=.env.test
  • 确认两个环境都能成功实例化 Settings
  • 确认 env 里的所有 key 都能被 Settings 接收。
  • 确认 env 没有重复 key。
  • 可选:确认 .env.test 和生成的 .secrets.test.json key 集合一致。

如果暂时不想加正式测试,可以先放一个脚本到 scripts/

Phase 2:只拆结构,不改行为

创建 app/core/settings/ 目录,把字段按领域移动到对应文件。

规则:

  • 本阶段不重命名环境变量。
  • 本阶段不删除旧别名。
  • 本阶段不改变默认值。
  • 保留 app/core/config.py 作为兼容入口。

预期结果:业务代码不用改 import,也不用改 settings.xxx 调用。

Phase 3:规范 env 暴露范围

逐个配置判断是否应该出现在 env 中:

  • 必填环境变量:保留在 .env.dev.env.test 和线上 JSON。
  • 有意覆盖项:只有本地或线上确实需要覆盖时才保留。
  • 代码默认项:如果默认值可接受,就从 env 删除。
  • 内部常量:从 Settings 移到拥有它的模块。

每次调整 .env.test 后先预览,再显式同步到 Vault:

DRY_RUN=1 ./scripts/vault-sync-env.sh
./scripts/vault-sync-env.sh

Phase 4:删除旧变量别名

确认本地和线上都使用规范名之后,再删除旧别名。

候选项:

ARQ_RETRY_DEFER_SECONDS
ARQ_PARSE_SOFT_TIMEOUT_SECONDS

这一步应和结构拆分分开做,降低风险。

Phase 5:加强校验

为各领域增加明确校验:

  • 对象存储必填字段。
  • 支付模式启用时的必填字段。
  • Redis DSN 规范化。
  • 模型名基本合法性。
  • 文件大小和文件名长度限制。

优先使用领域内 validator 或服务级校验,避免某个不相关功能的配置缺失导致整个应用无法启动。

风险点

  • Settings 多继承顺序可能影响字段解析,mixin 之间应避免重复字段名。
  • AliasChoices 必须跟最终字段声明放在一起。
  • extra="ignore" 会让线上残留旧 secret 不触发启动失败。这对兼容有好处,但也可能掩盖配置漂移。
  • WeChat Pay 多行 PEM 在 JSON/env 中应保持 \n 转义;wechat_pay_service.py 当前会把转义换行还原成真实 PEM。
  • 不要在日志、文档或对话里暴露密钥原文。

验收标准

重构后至少满足:

ENV_FILE=.env.dev  Settings() loads
ENV_FILE=.env.test Settings() loads
.env.dev has no duplicate keys
.env.test has no duplicate keys
All .env keys are accepted by Settings
No obvious dead env keys remain
Existing imports from app.core.config still work
Generated .secrets.test.json matches .env.test key set

为增强运行时信心,还应执行:

uv run python -m compileall app

如果数据库可访问,再执行:

ENV_FILE=.env.test /opt/miniconda3/bin/alembic upgrade head

建议下一步

下一次改造建议只做 Phase 1 和 Phase 2:

  1. 新增或运行 settings audit 脚本。
  2. Settings 拆成领域 mixin。
  3. 保持 app/core/config.py 作为公共入口。
  4. 验证行为不变。

确认结构稳定后,再继续做 env 暴露范围收敛和旧别名清理。