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

1. 分工

pyproject.toml 已经是 TOML 文件,为什么 FastAPI 项目还需要 config.toml?因为它们描述的对象不同:

config-boundary.txt
1
pyproject.toml:描述 Python 项目、依赖和开发工具
2
config.toml:描述这个应用默认如何运行

例如,下面这些内容属于项目定义:

pyproject.toml
1
[project]
2
name = "fastapi-langchain"
3
requires-python = ">=3.13"
4
dependencies = ["fastapi[standard]", "langchain"]
5
6
[tool.fastapi]
7
entrypoint = "app.main:app"

下面这些内容属于应用运行配置:

config.toml
01
[app]
02
name = "FastAPI LangChain"
03
environment = "development"
04
log_level = "INFO"
05
06
[model]
07
provider = "deepseek"
08
name = "deepseek-v4-flash"
09
base_url = "https://api.deepseek.com"
10
temperature = 0.2
11
timeout_seconds = 30
12
13
[database]
14
pool_size = 10
15
pool_timeout_seconds = 30

应用配置不是 Python 标准强制规定的文件。config.toml 的文件名、表结构和字段含义都由我们的项目自己设计,因此项目也必须负责读取、校验和说明它。

对于 FastAPI + LangChain + LangGraph 服务,适合放入 TOML 的通常是:

  • 应用名称、日志级别和功能开关;
  • 默认模型名称、非敏感接口地址、超时和重试次数;
  • 数据库连接池大小、检查点保留策略;
  • CORS 来源、批处理大小、并发上限;
  • 本地、测试、生产环境都可以公开的默认值。

API Key、数据库密码、JWT 密钥不应放入会提交到 Git 的 TOML 文件。它们应由环境变量或部署平台 Secret 提供。

2. 结构

配置结构应尽量接近应用中的职责边界,而不是把所有字段放在根级:

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

其中,environment 表示当前是开发、测试还是生产环境;server.cors 决定哪些浏览器来源和请求方法可以访问接口;recursion_limit 用来限制 LangGraph 一次运行最多推进多少步;checkpoint_backend 则决定运行状态保存在内存还是持久化存储中。初学时不必一次记住所有字段,先理解每个 TOML 表对应应用中的一类职责即可。

表名应表达稳定的领域,例如 server、model、database。不要按使用文件命名为 factory_config、router_config,否则代码移动后配置名称也被迫变化。

字段名采用一致的 snake_case,与 Python 属性命名一致,可以减少转换:

access-config.py
1
model_name = config["model"]["name"]
2
pool_size = config["database"]["pool_size"]

配置值也要放在正确层级。temperature 是模型调用参数,不属于 [server];pool_size 是数据库连接池设置,不属于 [app]。层级清楚后,后续 Pydantic 模型也更容易拆分。

不要为每个 Python 模块建立一个 TOML 表。配置的目标是表达运行策略,不是复制源代码目录树。

3. 路径

最直接的读取方式如下:

naive_loader.py
1
from pathlib import Path
2
import tomllib
3
4
5
def load_config() -> dict:
6
with Path("config.toml").open("rb") as file:
7
return tomllib.load(file)

tomllib 从 Python 3.11 开始进入标准库,负责读取 TOML,但不负责写入。这里必须用 "rb" 以二进制模式打开文件,因为 tomllib.load() 接收的是二进制文件对象。

这段代码的问题是 Path("config.toml") 相对于当前工作目录,也就是执行启动命令时所在的目录。下面两种启动方式可能得到不同结果:

working-directory.bash
1
cd fastapi-langchain
2
uv run fastapi dev
3
4
cd fastapi-langchain/app
5
uv run python main.py

更稳妥的方式是从已知 Python 文件定位项目根目录。假设 config.toml 位于仓库根目录,加载器位于 app/core/config_loader.py:

app/core/config_loader.py
01
from pathlib import Path
02
import tomllib
03
04
PROJECT_ROOT = Path(__file__).resolve().parents[2]
05
DEFAULT_CONFIG_PATH = PROJECT_ROOT / "config.toml"
06
07
08
def load_toml(path: Path = DEFAULT_CONFIG_PATH) -> dict:
09
with path.open("rb") as file:
10
return tomllib.load(file)

__file__ 是当前模块路径。调用 resolve() 后得到绝对路径。以 app/core/config_loader.py 为起点,parents[0] 是 core,parents[1] 是 app,parents[2] 才是 fastapi-langchain 项目根目录。

这段代码依赖当前目录结构,因此应该集中写在一个配置模块中,不要在各个业务模块中重复计算路径。

parents[2] 只适合上面约定好的源码目录结构,并不是定位配置文件的通用写法。项目安装为 wheel 后,仓库根目录可能根本不存在,根目录下的 config.toml 也不会自动成为包内资源。部署应用时,更常见的做法是通过启动参数或环境变量提供配置文件路径,再把这个路径传给加载器;如果要读取随 Python 包发布的内置资源,则应使用 importlib.resources。

测试时也可以根据测试文件自身的位置定位专用配置:

tests/test_config.py
1
from pathlib import Path
2
3
from app.core.config_loader import load_toml
4
5
TESTS_DIR = Path(__file__).resolve().parent
6
test_config = load_toml(TESTS_DIR / "fixtures/config.toml")

这样测试不必修改生产配置文件,也不依赖测试进程从哪个目录启动。

4. 校验

tomllib 只验证 TOML 语法,不知道 temperature 应在什么范围,也不知道 pool_size 必须大于零。下面这份文件语法完全正确,却既缺少必填配置,又包含明显不合理的数值:

invalid-business-config.toml
1
[model]
2
name = ""
3
base_url = "https://api.deepseek.com"
4
temperature = 9.5
5
timeout_seconds = -1
6
7
[database]
8
pool_size = 0

可以使用 Pydantic BaseModel 描述应用配置结构:

app/core/config_schema.py
01
from typing import Literal
02
03
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
04
05
06
class StrictConfigModel(BaseModel):
07
model_config = ConfigDict(extra="forbid")
08
09
10
class ApplicationConfig(StrictConfigModel):
11
name: str = Field(min_length=1)
12
environment: Literal["development", "test", "staging", "production"]
13
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
14
15
16
class CorsConfig(StrictConfigModel):
17
allow_origins: list[str]
18
allow_methods: list[str]
19
20
21
class ServerConfig(StrictConfigModel):
22
host: str = Field(min_length=1)
23
port: int = Field(ge=1, le=65535)
24
cors: CorsConfig
25
26
27
class ModelConfig(StrictConfigModel):
28
provider: str = Field(min_length=1)
29
name: str = Field(min_length=1)
30
base_url: HttpUrl
31
temperature: float = Field(ge=0, le=2)
32
timeout_seconds: float = Field(gt=0)
33
max_retries: int = Field(ge=0, le=10)
34
35
36
class LangGraphConfig(StrictConfigModel):
37
recursion_limit: int = Field(gt=0)
38
checkpoint_backend: Literal["memory", "postgres"]
39
40
41
class DatabaseConfig(StrictConfigModel):
42
pool_size: int = Field(gt=0)
43
pool_timeout_seconds: float = Field(gt=0)
44
45
46
class AppConfig(StrictConfigModel):
47
app: ApplicationConfig
48
server: ServerConfig
49
model: ModelConfig
50
langgraph: LangGraphConfig
51
database: DatabaseConfig

这里的模型与第 2 节 TOML 中的每个表一一对应。Literal 把字段限制在列出的字符串中;HttpUrl 要求 base_url 是 HTTP 或 HTTPS 地址;Field 则负责数值范围等规则。示例把 temperature 限制在 0 到 2,这是当前项目制定的配置契约,切换模型供应商时还要确认对方实际支持的范围。

extra="forbid" 很重要。Pydantic 默认会忽略模型中未声明的字段,配置键即使拼错也可能不报错;改为禁止额外字段后,像 timeout_second 这样的拼写错误会直接导致启动失败。

读取 TOML 后,通过 model_validate() 转换:

app/core/load_config.py
01
from pathlib import Path
02
import tomllib
03
04
from app.core.config_schema import AppConfig
05
06
07
def load_config(path: Path) -> AppConfig:
08
with path.open("rb") as file:
09
raw_config = tomllib.load(file)
10
11
return AppConfig.model_validate(raw_config)

使用方不再访问任意字典,而是获得有类型提示的对象:

use-config.py
1
config = load_config(DEFAULT_CONFIG_PATH)
2
3
print(config.model.name)
4
print(config.database.pool_size)

config.model.base_url 经过校验后是 Pydantic 的 URL 对象。下游库只接受字符串时,可以使用 str(config.model.base_url) 转换。

如果配置缺失或类型错误,应用应在启动阶段失败,并给出字段路径。不要吞掉异常后悄悄使用一套不明确的默认值,否则服务表面启动成功,第一次真实请求才暴露问题。

5. 默认值

默认值可以写在 TOML,也可以写在 Pydantic 模型中。两种方式各有适用场景:

默认值位置适合
TOML运维和开发者经常调整、需要一眼看到的运行策略
Python 模型很少变化、属于代码契约的安全默认值

例如,模型名称和连接池大小适合在 TOML 中显式展示。在本章的完整示例里,所有字段都由 TOML 提供,因此 Pydantic 模型只负责声明必填项和校验规则,没有再重复设置默认值。如果希望由 Python 提供缺省日志级别,就应从 TOML 示例中删掉同一个字段,并在配置文档中说明它的默认行为。

不要在两个地方重复维护同一个值:

duplicated-default.toml
1
[model]
2
temperature = 0.2
duplicated-default.py
1
class ModelConfig(BaseModel):
2
temperature: float = 0.7

如果 TOML 一直存在,Python 中的 0.7 永远不会生效;删除 TOML 字段后,行为又突然改变。团队需要明确哪个位置是主要来源。

对必填字段,不应提供看似可用但实际上错误的默认值。例如:

required-field.py
1
class ModelConfig(BaseModel):
2
name: str
3
base_url: HttpUrl

模型名称或接口地址缺失时,启动失败比默认连接到错误服务更安全。

对于密钥,不能在 TOML 或 Python 代码中提供真实默认值。最多使用 None 表示未配置,并在真正需要创建模型时给出清晰错误。

6. 读取时机

配置通常在应用启动时读取一次,而不是每个请求都重新打开文件:

config-lifecycle.txt
1
进程启动
2
-> 定位配置文件
3
-> 解析 TOML
4
-> Pydantic 校验
5
-> 创建 AppConfig 配置对象
6
-> 初始化数据库、HTTP 客户端和模型工厂
7
-> 开始接收请求

这样做有三个好处:

  1. 配置错误会在启动阶段暴露;
  2. 请求不承担重复文件读取和校验成本;
  3. 同一进程中的组件看到一致配置。

修改 TOML 后,正在运行的进程不会自动获得新值。开发环境的代码重载可能顺带重启进程,但生产环境应把配置变更当作一次明确部署或滚动重启。

动态业务数据不应依赖修改 TOML。例如后台页面切换当前用户的模型、一次请求调整温度,这些值应该来自数据库或请求参数。TOML 更适合进程级默认策略。

如果确实需要运行时热更新,应设计独立配置服务、版本号、校验、并发控制和回滚机制,而不是让多个请求同时改写本地文件。

错误处理

一个可靠加载器至少区分三类错误:文件不存在、TOML 语法错误、字段校验错误。

app/core/config_loader.py
01
from pathlib import Path
02
import tomllib
03
04
from pydantic import ValidationError
05
06
from app.core.config_schema import AppConfig
07
08
09
class ConfigError(RuntimeError):
10
pass
11
12
13
def load_config(path: Path) -> AppConfig:
14
try:
15
with path.open("rb") as file:
16
raw_config = tomllib.load(file)
17
except FileNotFoundError as error:
18
raise ConfigError(f"配置文件不存在:{path}") from error
19
except tomllib.TOMLDecodeError as error:
20
raise ConfigError(f"TOML 语法错误:{error}") from error
21
22
try:
23
return AppConfig.model_validate(raw_config)
24
except ValidationError as error:
25
details = "; ".join(
26
f"{'.'.join(str(part) for part in item['loc'])}: {item['msg']}"
27
for item in error.errors(include_input=False)
28
)
29
raise ConfigError(f"配置字段校验失败:{details}") from error

include_input=False 会从错误详情中排除原始输入值,避免以后加入密钥配置时意外把敏感值写进日志。日志可以记录配置文件路径、当前环境和非敏感字段,但不能打印完整原始配置。

配置文件来自不可信来源时还应限制文件大小。tomllib 文档明确提醒,恶意输入可能消耗大量 CPU 或内存。普通随代码部署的小型配置文件风险较低,但上传 TOML 并在线解析属于另一种安全场景。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容