创建时间: 2026-08-28最后更新: 2026-08-28

1. 环境

同一份 FastAPI 代码会运行在不同环境中:

环境典型特点
development本地调试、允许 localhost、日志更详细
test使用测试数据库、假模型、固定随机性
staging接近生产,用于部署前验证
production使用正式数据库、严格来源、受控日志和真实模型

如果把每个环境都复制成一套 Python 代码,修复功能时很容易漏改。更合理的方式是保持代码一致,由配置提供环境差异。

配置来源通常不止一个:

settings-sources.txt
1
代码中的安全默认值
2
<- config.toml 中的公开默认配置
3
<- .env 中的本地覆盖值
4
<- 操作系统或部署平台环境变量
5
<- 测试或应用工厂显式传入的值

越靠近运行现场的来源,通常拥有越高优先级。例如,config.toml 默认允许本地前端,生产平台通过环境变量覆盖为正式域名。

优先级必须由代码明确,而不是依赖最后读取了哪个文件的偶然结果。文章后面的示例会采用:

priority.txt
1
显式初始化参数 > 环境变量 > .env > TOML > Python 默认值

这里的显式初始化参数主要用于测试或应用工厂,例如 Settings(app={...})。生产代码通常不手动传值,而是让部署平台环境变量覆盖公开的 TOML 配置。

2. Settings

这里的 Settings 可以理解为应用最终使用的、经过校验的配置对象。Pydantic Settings 可以把环境变量、.env 和自定义文件来源统一转换成这样的对象。项目只需要直接安装自己使用的包:

install-settings.bash
1
uv add pydantic-settings

pydantic-settings 已经包含读取 dotenv 文件所需的依赖,不需要仅为了 env_file 再单独声明 python-dotenv。接下来定义嵌套设置模型,让它与上一篇的 config.toml 结构保持一致:

app/core/settings.py
001
from pathlib import Path
002
from typing import Literal
003
004
from pydantic import (
005
BaseModel,
006
ConfigDict,
007
Field,
008
HttpUrl,
009
SecretStr,
010
model_validator,
011
)
012
from pydantic_settings import (
013
BaseSettings,
014
PydanticBaseSettingsSource,
015
SettingsConfigDict,
016
TomlConfigSettingsSource,
017
)
018
019
PROJECT_ROOT = Path(__file__).resolve().parents[2]
020
021
022
class StrictSettingsModel(BaseModel):
023
model_config = ConfigDict(
024
extra="forbid",
025
hide_input_in_errors=True,
026
)
027
028
029
class AppSettings(StrictSettingsModel):
030
name: str = Field(min_length=1)
031
environment: Literal["development", "test", "staging", "production"]
032
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
033
034
035
class CorsSettings(StrictSettingsModel):
036
allow_origins: list[str]
037
allow_methods: list[str]
038
039
040
class ServerSettings(StrictSettingsModel):
041
host: str = Field(min_length=1)
042
port: int = Field(ge=1, le=65535)
043
cors: CorsSettings
044
045
046
class ModelSettings(StrictSettingsModel):
047
provider: str = Field(min_length=1)
048
name: str = Field(min_length=1)
049
base_url: HttpUrl
050
api_key: SecretStr | None = None
051
temperature: float = Field(ge=0, le=2)
052
timeout_seconds: float = Field(gt=0)
053
max_retries: int = Field(ge=0, le=10)
054
055
056
class LangGraphSettings(StrictSettingsModel):
057
recursion_limit: int = Field(gt=0)
058
checkpoint_backend: Literal["memory", "postgres"]
059
060
061
class DatabaseSettings(StrictSettingsModel):
062
url: SecretStr | None = None
063
pool_size: int = Field(gt=0)
064
pool_timeout_seconds: float = Field(gt=0)
065
066
067
class Settings(BaseSettings):
068
app: AppSettings
069
server: ServerSettings
070
model: ModelSettings
071
langgraph: LangGraphSettings
072
database: DatabaseSettings
073
langsmith_tracing: bool = False
074
langsmith_api_key: SecretStr | None = None
075
076
model_config = SettingsConfigDict(
077
env_file=PROJECT_ROOT / ".env",
078
env_file_encoding="utf-8",
079
env_ignore_empty=True,
080
env_nested_delimiter="__",
081
toml_file=PROJECT_ROOT / "config.toml",
082
extra="forbid",
083
hide_input_in_errors=True,
084
)
085
086
@classmethod
087
def settings_customise_sources(
088
cls,
089
settings_cls: type[BaseSettings],
090
init_settings: PydanticBaseSettingsSource,
091
env_settings: PydanticBaseSettingsSource,
092
dotenv_settings: PydanticBaseSettingsSource,
093
file_secret_settings: PydanticBaseSettingsSource,
094
) -> tuple[PydanticBaseSettingsSource, ...]:
095
return (
096
init_settings,
097
env_settings,
098
dotenv_settings,
099
TomlConfigSettingsSource(settings_cls),
100
)
101
102
@model_validator(mode="after")
103
def validate_runtime(self) -> "Settings":
104
if self.langsmith_tracing and self.langsmith_api_key is None:
105
raise ValueError("启用 LangSmith 追踪时必须配置 API Key")
106
107
if self.app.environment == "production":
108
if self.model.api_key is None:
109
raise ValueError("生产环境必须配置模型 API Key")
110
if self.model.base_url.host in {"localhost", "127.0.0.1"}:
111
raise ValueError("生产模型地址不能使用本机地址")
112
113
return 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 可以写成:

config.toml
01
[app]
02
name = "FastAPI LangChain"
03
environment = "development"
04
log_level = "INFO"
05
06
[server]
07
host = "127.0.0.1"
08
port = 8000
09
10
[server.cors]
11
allow_origins = ["http://localhost:3019"]
12
allow_methods = ["GET", "POST", "OPTIONS"]
13
14
[model]
15
provider = "deepseek"
16
name = "deepseek-v4-flash"
17
base_url = "https://api.deepseek.com"
18
temperature = 0.2
19
timeout_seconds = 30
20
max_retries = 2
21
22
[langgraph]
23
recursion_limit = 50
24
checkpoint_backend = "memory"
25
26
[database]
27
pool_size = 10
28
pool_timeout_seconds = 30

TOML 中没有 API Key 和数据库 URL,这两个字段会继续等待更高优先级的 .env 或环境变量提供。SecretStr | None 中的 None 只表示当前尚未配置,不是一个可用密钥。

创建 Settings 时,Pydantic 会依次收集各来源并校验:

use-settings.py
1
settings = Settings()
2
3
print(settings.app.environment)
4
print(settings.model.name)
5
print(settings.model.timeout_seconds)

3. 环境变量

env_nested_delimiter="__" 允许用双下划线覆盖嵌套字段。TOML 中的 model.name 对应:

nested-environment.bash
1
export MODEL__NAME=deepseek-v4-pro
2
export MODEL__TIMEOUT_SECONDS=60
3
uv run fastapi run

双下划线表示进入下一层。比如 APP__ENVIRONMENT 对应 settings.app.environment,SERVER__CORS__ALLOW_ORIGINS 则对应 settings.server.cors.allow_origins。

环境变量本质上都是字符串。Pydantic 会根据字段类型完成转换:

environment-conversion.txt
1
MODEL__TIMEOUT_SECONDS="60" -> float 60.0
2
MODEL__MAX_RETRIES="3" -> int 3
3
MODEL__TEMPERATURE="0.4" -> float 0.4

转换失败会在 Settings 创建阶段抛出校验错误。例如 MODEL__MAX_RETRIES=many 不能转换成整数。

数组、字典等复杂值通常需要使用 JSON 字符串:

list-environment.bash
1
export SERVER__CORS__ALLOW_ORIGINS='["https://langchainpy.usehook.cn"]'

外层单引号用于保护整段 JSON,里面的双引号属于 JSON 语法。如果团队觉得复杂 JSON 环境变量难以维护,可以把公开列表留在 TOML 中,只让环境变量覆盖少量标量和密钥。

环境变量名称建议采用大写加下划线,嵌套层级使用双下划线:

environment-names.txt
1
APP__NAME
2
APP__ENVIRONMENT
3
SERVER__CORS__ALLOW_ORIGINS
4
MODEL__NAME
5
MODEL__API_KEY
6
DATABASE__URL
7
LANGSMITH_TRACING

LANGSMITH_TRACING 对应 Settings 的根字段 langsmith_tracing,所以它不需要双下划线。不要在业务代码中到处调用 os.getenv(),否则字段类型、默认值、别名和缺失检查会分散在不同模块。统一从 Settings 读取后,业务模块只依赖明确属性。

4. dotenv

.env 是本地开发时提供环境变量的便利文件:

.env.example
1
MODEL__API_KEY=your-api-key
2
DATABASE__URL=postgresql+asyncpg://user:password@localhost/agent
3
LANGSMITH_TRACING=false
4
LANGSMITH_API_KEY=

仓库应提交 .env.example,说明有哪些变量;真实 .env 应加入 .gitignore:

.gitignore
1
.env
2
.env.*
3
!.env.example

先忽略 .env 和所有带后缀的变体,再单独允许 .env.example。这样 .env.production、.env.test 等文件不会因为命名不同而被意外提交。

.env 不是加密文件。它只是把键值保存在磁盘上,仍然可能被误传、备份或日志读取。它适合本地开发,不等于生产 Secret 管理系统。

启动当前项目可以选择两种方式:

dotenv-start.bash
1
# 由 Pydantic Settings 根据 env_file 读取
2
uv run fastapi dev
3
4
# 由 uv 先把 .env 注入子进程环境
5
uv 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 + 环境变量覆盖的简单方案:

simple-environments.txt
1
config.toml # 提交:公开默认值
2
.env # 不提交:本机密钥和本地覆盖
3
生产平台变量 # 控制台或 Secret:生产覆盖

这个方案最容易理解,也适合当前项目。配置规模继续增长后,可以拆成:

default.toml所有环境共享的默认值
development.toml本地开发覆盖值
test.toml自动化测试覆盖值
production.toml生产环境公开覆盖值

应用先加载 default.toml,再根据经过校验的环境名称加载一个环境文件,最后由环境变量覆盖。合并时不能只使用普通字典的浅层 update():

shallow-merge.py
1
default = {
2
"model": {"name": "deepseek-v4-flash", "timeout": 30},
3
}
4
production = {
5
"model": {"timeout": 60},
6
}
7
8
default.update(production)
9
# model.name 也被一起丢失了

Pydantic Settings 的 toml_file 可以接收路径列表,但多个 TOML 文件默认同样是浅层合并。要保留 model.name 这样的其他子字段,需要在创建 TOML 来源时显式开启深度合并:

deep-merge-toml.py
1
model_config = SettingsConfigDict(
2
toml_file=[
3
PROJECT_ROOT / "config" / "default.toml",
4
PROJECT_ROOT / "config" / "production.toml",
5
],
6
)
7
8
# settings_customise_sources() 中
9
toml_settings = TomlConfigSettingsSource(settings_cls, deep_merge=True)

这段代码展示的是已经选定生产环境后的路径列表。真实项目需要先校验环境名称,再生成第二个文件路径;不能把未经检查的外部字符串直接拼进路径。

环境名必须先经过白名单校验,不能直接拼接任意用户输入形成文件路径:

validate-environment.py
01
ALLOWED_ENVIRONMENTS = {
02
"development",
03
"test",
04
"staging",
05
"production",
06
}
07
08
09
def validate_environment(value: str) -> str:
10
if value not in ALLOWED_ENVIRONMENTS:
11
raise ValueError(f"不支持的运行环境:{value}")
12
return value

这个值属于配置加载器的启动参数,只应在配置模块中读取一次。加载完成后,还应确认选中的环境文件与 settings.app.environment 一致,避免文件名写着 production,内容却仍声明为 development。无论采用哪种拆分方式,都要用测试固定覆盖结果。

6. 密钥

公开配置和秘密配置应从设计上分开。下面只展示公开 TOML 中与模型有关的片段:

public-config.toml
1
[model]
2
name = "deepseek-v4-flash"
3
base_url = "https://api.deepseek.com"
4
timeout_seconds = 30
secret.env
1
MODEL__API_KEY=sk-example
2
DATABASE__URL=postgresql+asyncpg://user:example-password@localhost/agent
3
LANGSMITH_API_KEY=lsv2-example

这些值都是格式示例,不能作为真实凭据。当前代码默认读取的是 .env;如果本地文件确实名为 secret.env,需要改动 env_file,或使用 uv run --env-file secret.env ... 显式加载。生产环境通常不创建这个文件,而是由部署平台注入同名变量。

在 Settings 中使用 SecretStr,可以减少意外打印:

secret-field.py
1
from pydantic import SecretStr
2
3
4
class ModelSettings(StrictSettingsModel):
5
api_key: SecretStr

打印对象时,Pydantic 会隐藏 SecretStr 的实际内容。在确认字段已经配置后,创建模型时需要显式取值:

reveal-secret.py
1
if settings.model.api_key is None:
2
raise RuntimeError("尚未配置模型 API Key")
3
4
api_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 文件全部内容;
  • 用户提交给模型的敏感对话。
正在验证登录状态
请稍候,验证完成后将继续显示文章内容