1. 环境
同一份 FastAPI 代码会运行在不同环境中:
| 环境 | 典型特点 |
|---|---|
| development | 本地调试、允许 localhost、日志更详细 |
| test | 使用测试数据库、假模型、固定随机性 |
| staging | 接近生产,用于部署前验证 |
| production | 使用正式数据库、严格来源、受控日志和真实模型 |
如果把每个环境都复制成一套 Python 代码,修复功能时很容易漏改。更合理的方式是保持代码一致,由配置提供环境差异。
配置来源通常不止一个:
1代码中的安全默认值2<- config.toml 中的公开默认配置3<- .env 中的本地覆盖值4<- 操作系统或部署平台环境变量5<- 测试或应用工厂显式传入的值
越靠近运行现场的来源,通常拥有越高优先级。例如,config.toml 默认允许本地前端,生产平台通过环境变量覆盖为正式域名。
优先级必须由代码明确,而不是依赖最后读取了哪个文件的偶然结果。文章后面的示例会采用:
1显式初始化参数 > 环境变量 > .env > TOML > Python 默认值
这里的显式初始化参数主要用于测试或应用工厂,例如 Settings(app={...})。生产代码通常不手动传值,而是让部署平台环境变量覆盖公开的 TOML 配置。
2. Settings
这里的 Settings 可以理解为应用最终使用的、经过校验的配置对象。Pydantic Settings 可以把环境变量、.env 和自定义文件来源统一转换成这样的对象。项目只需要直接安装自己使用的包:
1uv add pydantic-settings
pydantic-settings 已经包含读取 dotenv 文件所需的依赖,不需要仅为了 env_file 再单独声明 python-dotenv。接下来定义嵌套设置模型,让它与上一篇的 config.toml 结构保持一致:
001from pathlib import Path002from typing import Literal003004from pydantic import (005BaseModel,006ConfigDict,007Field,008HttpUrl,009SecretStr,010model_validator,011)012from pydantic_settings import (013BaseSettings,014PydanticBaseSettingsSource,015SettingsConfigDict,016TomlConfigSettingsSource,017)018019PROJECT_ROOT = Path(__file__).resolve().parents[2]020021022class StrictSettingsModel(BaseModel):023model_config = ConfigDict(024extra="forbid",025hide_input_in_errors=True,026)027028029class AppSettings(StrictSettingsModel):030name: str = Field(min_length=1)031environment: Literal["development", "test", "staging", "production"]032log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]033034035class CorsSettings(StrictSettingsModel):036allow_origins: list[str]037allow_methods: list[str]038039040class ServerSettings(StrictSettingsModel):041host: str = Field(min_length=1)042port: int = Field(ge=1, le=65535)043cors: CorsSettings044045046class ModelSettings(StrictSettingsModel):047provider: str = Field(min_length=1)048name: str = Field(min_length=1)049base_url: HttpUrl050api_key: SecretStr | None = None051temperature: float = Field(ge=0, le=2)052timeout_seconds: float = Field(gt=0)053max_retries: int = Field(ge=0, le=10)054055056class LangGraphSettings(StrictSettingsModel):057recursion_limit: int = Field(gt=0)058checkpoint_backend: Literal["memory", "postgres"]059060061class DatabaseSettings(StrictSettingsModel):062url: SecretStr | None = None063pool_size: int = Field(gt=0)064pool_timeout_seconds: float = Field(gt=0)065066067class Settings(BaseSettings):068app: AppSettings069server: ServerSettings070model: ModelSettings071langgraph: LangGraphSettings072database: DatabaseSettings073langsmith_tracing: bool = False074langsmith_api_key: SecretStr | None = None075076model_config = SettingsConfigDict(077env_file=PROJECT_ROOT / ".env",078env_file_encoding="utf-8",079env_ignore_empty=True,080env_nested_delimiter="__",081toml_file=PROJECT_ROOT / "config.toml",082extra="forbid",083hide_input_in_errors=True,084)085086@classmethod087def settings_customise_sources(088cls,089settings_cls: type[BaseSettings],090init_settings: PydanticBaseSettingsSource,091env_settings: PydanticBaseSettingsSource,092dotenv_settings: PydanticBaseSettingsSource,093file_secret_settings: PydanticBaseSettingsSource,094) -> tuple[PydanticBaseSettingsSource, ...]:095return (096init_settings,097env_settings,098dotenv_settings,099TomlConfigSettingsSource(settings_cls),100)101102@model_validator(mode="after")103def validate_runtime(self) -> "Settings":104if self.langsmith_tracing and self.langsmith_api_key is None:105raise ValueError("启用 LangSmith 追踪时必须配置 API Key")106107if self.app.environment == "production":108if self.model.api_key is None:109raise ValueError("生产环境必须配置模型 API Key")110if self.model.base_url.host in {"localhost", "127.0.0.1"}:111raise ValueError("生产模型地址不能使用本机地址")112113return self
嵌套的 AppSettings、ModelSettings 等类只描述数据结构,所以继承 BaseModel;最外层 Settings 还要主动读取环境变量和配置文件,因此继承 BaseSettings。
SettingsConfigDict 只是在声明配置。如果没有把 TomlConfigSettingsSource 放进 settings_customise_sources(),toml_file 不会凭空自动加入默认来源。
返回元组的顺序就是优先级顺序,前面的来源优先级更高。TOML 放在最后,意味着它提供公开基础值,而环境变量可以只覆盖其中一两个字段,其他字段继续使用 TOML 中的值。
方法签名中的 file_secret_settings 是 Pydantic Settings 提供的默认来源参数,但这份示例没有配置 secrets_dir,所以没有把它放进返回元组。以后如果要直接读取 Docker 或 Kubernetes 挂载的 Secret 文件,需要先确定目录和文件命名方式,再显式加入对应来源。
嵌套模型使用 extra="forbid",根设置也禁止未声明字段。这样 TOML 或 .env 中的变量名拼错时会在启动阶段报错,而不是被静默忽略。因此,由 Pydantic 直接读取的 .env 也只应包含 Settings 已声明的变量。env_ignore_empty=True 会忽略 .env 中的空值,例如 LANGSMITH_API_KEY= 会继续使用字段默认的 None,不会被误认为有效密钥;hide_input_in_errors=True 则避免校验错误文本直接带出原始输入值。
对应 TOML 可以写成:
01[app]02name = "FastAPI LangChain"03environment = "development"04log_level = "INFO"0506[server]07host = "127.0.0.1"08port = 80000910[server.cors]11allow_origins = ["http://localhost:3019"]12allow_methods = ["GET", "POST", "OPTIONS"]1314[model]15provider = "deepseek"16name = "deepseek-v4-flash"17base_url = "https://api.deepseek.com"18temperature = 0.219timeout_seconds = 3020max_retries = 22122[langgraph]23recursion_limit = 5024checkpoint_backend = "memory"2526[database]27pool_size = 1028pool_timeout_seconds = 30
TOML 中没有 API Key 和数据库 URL,这两个字段会继续等待更高优先级的 .env 或环境变量提供。SecretStr | None 中的 None 只表示当前尚未配置,不是一个可用密钥。
创建 Settings 时,Pydantic 会依次收集各来源并校验:
1settings = Settings()23print(settings.app.environment)4print(settings.model.name)5print(settings.model.timeout_seconds)
3. 环境变量
env_nested_delimiter="__" 允许用双下划线覆盖嵌套字段。TOML 中的 model.name 对应:
1export MODEL__NAME=deepseek-v4-pro2export MODEL__TIMEOUT_SECONDS=603uv run fastapi run
双下划线表示进入下一层。比如 APP__ENVIRONMENT 对应 settings.app.environment,SERVER__CORS__ALLOW_ORIGINS 则对应 settings.server.cors.allow_origins。
环境变量本质上都是字符串。Pydantic 会根据字段类型完成转换:
1MODEL__TIMEOUT_SECONDS="60" -> float 60.02MODEL__MAX_RETRIES="3" -> int 33MODEL__TEMPERATURE="0.4" -> float 0.4
转换失败会在 Settings 创建阶段抛出校验错误。例如 MODEL__MAX_RETRIES=many 不能转换成整数。
数组、字典等复杂值通常需要使用 JSON 字符串:
1export SERVER__CORS__ALLOW_ORIGINS='["https://langchainpy.usehook.cn"]'
外层单引号用于保护整段 JSON,里面的双引号属于 JSON 语法。如果团队觉得复杂 JSON 环境变量难以维护,可以把公开列表留在 TOML 中,只让环境变量覆盖少量标量和密钥。
环境变量名称建议采用大写加下划线,嵌套层级使用双下划线:
1APP__NAME2APP__ENVIRONMENT3SERVER__CORS__ALLOW_ORIGINS4MODEL__NAME5MODEL__API_KEY6DATABASE__URL7LANGSMITH_TRACING
LANGSMITH_TRACING 对应 Settings 的根字段 langsmith_tracing,所以它不需要双下划线。不要在业务代码中到处调用 os.getenv(),否则字段类型、默认值、别名和缺失检查会分散在不同模块。统一从 Settings 读取后,业务模块只依赖明确属性。
4. dotenv
.env 是本地开发时提供环境变量的便利文件:
1MODEL__API_KEY=your-api-key2DATABASE__URL=postgresql+asyncpg://user:password@localhost/agent3LANGSMITH_TRACING=false4LANGSMITH_API_KEY=
仓库应提交 .env.example,说明有哪些变量;真实 .env 应加入 .gitignore:
1.env2.env.*3!.env.example
先忽略 .env 和所有带后缀的变体,再单独允许 .env.example。这样 .env.production、.env.test 等文件不会因为命名不同而被意外提交。
.env 不是加密文件。它只是把键值保存在磁盘上,仍然可能被误传、备份或日志读取。它适合本地开发,不等于生产 Secret 管理系统。
启动当前项目可以选择两种方式:
1# 由 Pydantic Settings 根据 env_file 读取2uv run fastapi dev34# 由 uv 先把 .env 注入子进程环境5uv run --env-file .env fastapi dev
两种方式同时使用通常没有必要。它们的差异是:
- Pydantic
env_file只影响 Settings; - uv
--env-file会把变量提供给整个子进程,包括 LangChain、LangSmith 和其他库; - 已存在的操作系统环境变量会优先于 uv 或 Pydantic 读取的
.env值。
例如,只依靠 Pydantic 的 env_file 时,settings.langsmith_tracing 可以读到 .env,但 .env 中的值不会因此自动写入 os.environ,直接读取进程环境的 LangSmith SDK 不一定能看到它。如果希望第三方库也读取同一份文件,应使用 uv 注入;生产环境则优先使用部署平台环境变量。项目自身字段仍然通过 Settings 校验。
5. 多环境
多环境配置可以采用一个公共 TOML + 环境变量覆盖的简单方案:
1config.toml # 提交:公开默认值2.env # 不提交:本机密钥和本地覆盖3生产平台变量 # 控制台或 Secret:生产覆盖
这个方案最容易理解,也适合当前项目。配置规模继续增长后,可以拆成:
应用先加载 default.toml,再根据经过校验的环境名称加载一个环境文件,最后由环境变量覆盖。合并时不能只使用普通字典的浅层 update():
1default = {2"model": {"name": "deepseek-v4-flash", "timeout": 30},3}4production = {5"model": {"timeout": 60},6}78default.update(production)9# model.name 也被一起丢失了
Pydantic Settings 的 toml_file 可以接收路径列表,但多个 TOML 文件默认同样是浅层合并。要保留 model.name 这样的其他子字段,需要在创建 TOML 来源时显式开启深度合并:
1model_config = SettingsConfigDict(2toml_file=[3PROJECT_ROOT / "config" / "default.toml",4PROJECT_ROOT / "config" / "production.toml",5],6)78# settings_customise_sources() 中9toml_settings = TomlConfigSettingsSource(settings_cls, deep_merge=True)
这段代码展示的是已经选定生产环境后的路径列表。真实项目需要先校验环境名称,再生成第二个文件路径;不能把未经检查的外部字符串直接拼进路径。
环境名必须先经过白名单校验,不能直接拼接任意用户输入形成文件路径:
01ALLOWED_ENVIRONMENTS = {02"development",03"test",04"staging",05"production",06}070809def validate_environment(value: str) -> str:10if value not in ALLOWED_ENVIRONMENTS:11raise ValueError(f"不支持的运行环境:{value}")12return value
这个值属于配置加载器的启动参数,只应在配置模块中读取一次。加载完成后,还应确认选中的环境文件与 settings.app.environment 一致,避免文件名写着 production,内容却仍声明为 development。无论采用哪种拆分方式,都要用测试固定覆盖结果。
6. 密钥
公开配置和秘密配置应从设计上分开。下面只展示公开 TOML 中与模型有关的片段:
1[model]2name = "deepseek-v4-flash"3base_url = "https://api.deepseek.com"4timeout_seconds = 30
1MODEL__API_KEY=sk-example2DATABASE__URL=postgresql+asyncpg://user:example-password@localhost/agent3LANGSMITH_API_KEY=lsv2-example
这些值都是格式示例,不能作为真实凭据。当前代码默认读取的是 .env;如果本地文件确实名为 secret.env,需要改动 env_file,或使用 uv run --env-file secret.env ... 显式加载。生产环境通常不创建这个文件,而是由部署平台注入同名变量。
在 Settings 中使用 SecretStr,可以减少意外打印:
1from pydantic import SecretStr234class ModelSettings(StrictSettingsModel):5api_key: SecretStr
打印对象时,Pydantic 会隐藏 SecretStr 的实际内容。在确认字段已经配置后,创建模型时需要显式取值:
1if settings.model.api_key is None:2raise RuntimeError("尚未配置模型 API Key")34api_key = settings.model.api_key.get_secret_value()
SecretStr 不是加密存储。Python 进程仍然能取得明文,它只是在日志和调试输出中降低误泄漏概率。生产密钥仍应由云平台 Secret、容器 Secret、Kubernetes Secret 或专用密钥服务提供。
健康检查接口不应返回密钥,也不应返回完整数据库 URL。最多返回「已配置」「连接正常」等状态。
启动校验
配置问题越早失败,越容易处理。第 2 节 Settings 中的 validate_runtime() 使用 model_validator(mode="after"),它会在所有来源合并且字段校验完成后运行,因此可以检查启用追踪时必须有 LangSmith API Key、生产环境必须有模型 API Key 这类跨字段关系。
不要把所有连通性检查都塞进字段校验。Pydantic 适合验证结构和业务约束;数据库是否可连接、模型接口是否可用,需要在 FastAPI lifespan 中执行异步预检查,并设置明确超时。
日志可以安全记录:
- 当前环境名称;
- 配置文件路径;
- 模型名称和非敏感
base_url; - 是否启用追踪;
- 数据库是否连接成功。
日志不应记录:
- API Key、密码、Authorization 请求头;
- 带密码的完整数据库 URL;
.env文件全部内容;- 用户提交给模型的敏感对话。