1. 配置层
uv 在项目中会同时读取标准 Python 配置和自己的扩展配置:
01[project]02name = "fastapi-langchain"03version = "0.1.0"04requires-python = ">=3.13"05dependencies = ["fastapi[standard]"]0607[dependency-groups]08dev = ["ruff", "pytest"]0910[tool.uv]11default-groups = ["dev"]
这三部分不能混为一谈:
| 配置 | 性质 | 其他工具是否可能理解 |
|---|---|---|
[project] | Python 标准项目元数据 | 是 |
[dependency-groups] | 标准开发依赖分组 | 是,但安装命令由工具决定 |
[tool.uv] | uv 专属设置 | 通常不会 |
uv 也支持独立的 uv.toml。在 uv.toml 中要去掉 tool.uv 前缀:
1default-groups = ["dev"]
如果同一目录同时存在 uv.toml 和 pyproject.toml,uv 会采用 uv.toml,并忽略该 pyproject.toml 中的 [tool.uv],两处设置不会合并。因此,把配置拆到 uv.toml 后,应将需要保留的 uv 设置一起迁移过去。
对于当前单体 FastAPI 项目,常用 uv 配置较短,放在 pyproject.toml 更容易统一查看。只有 uv 设置很多、需要单独分发,或不希望修改项目元数据时,才有必要拆成 uv.toml。
命令行参数、环境变量和配置文件都可能影响 uv。对于同一项设置,通常按照「命令行参数 > 环境变量 > 持久配置」的优先级生效;持久配置中又是项目级高于用户级,用户级高于系统级。排查「为什么本机与 CI 行为不同」时,应同时检查这些来源,不能只看项目中的 TOML。
如果是第一次接触 uv,可以先掌握配置层、依赖来源和 uv run。后面的环境约束与 workspace 属于进阶能力,当前单体 FastAPI 项目暂时用不到。
2. 依赖组
[dependency-groups] 负责定义组中有哪些包,[tool.uv] 可以决定 uv 默认启用哪些组:
01[dependency-groups]02lint = ["ruff", "mypy"]03test = ["pytest", "pytest-asyncio", "coverage", "httpx"]04dev = [05{ include-group = "lint" },06{ include-group = "test" },07]0809[tool.uv]10default-groups = ["dev"]
dev 本身就是 uv 默认会包含的特殊组,因此普通项目不写 default-groups 也可以。显式声明的价值在于让团队清楚默认策略,或者改成其他组合。
每个依赖组默认都要与项目的 Python 版本范围兼容。如果某个开发工具只支持更高版本,可以为组单独声明限制:
1[project]2requires-python = ">=3.11"34[dependency-groups]5dev = ["example-tool"]67[tool.uv.dependency-groups]8dev = { requires-python = ">=3.13" }
这意味着项目本身仍可运行在 Python 3.11,但使用该开发组需要 Python 3.13。只有团队确实接受这种差异时才这样配置,否则开发工具和生产代码使用同一最低版本更简单。
uv 默认会把所有依赖组一起解析进同一个锁文件,即使命令实际只安装其中一组。因此,各组通常也必须彼此兼容。如果 CPU 和 GPU 两套机器学习依赖本来就不能同时使用,可以通过 uv 的 conflicts 配置明确声明互斥关系:
01[dependency-groups]02cpu = ["example-model[cpu]"]03gpu = ["example-model[gpu]"]0405[tool.uv]06conflicts = [07[08{ group = "cpu" },09{ group = "gpu" },10],11]
声明以后,uv 可以把两套结果记录在同一个锁文件中,但同时选择 cpu 和 gpu 仍会报错。互斥配置不是用来掩盖随意的版本冲突,而是表达两套环境本来就不应同时安装。
3. 来源
正常依赖从 Python 包索引中解析。如果项目需要 Git 仓库、本地目录或指定索引,可以在 [tool.uv.sources] 中补充来源。
Git 来源
1[project]2dependencies = ["internal-agent-tools"]34[tool.uv.sources.internal-agent-tools]5git = "https://github.com/example/internal-agent-tools.git"6tag = "v1.2.0"
这里使用普通子表把多个字段分行书写。字段较少时,也可以使用更紧凑的内联表:
1[tool.uv.sources]2internal-agent-tools = { git = "https://github.com/example/internal-agent-tools.git", tag = "v1.2.0" }
TOML 1.1 已允许多行内联表,但当前 Python tomllib 仍按照 TOML 1.0 解析,许多 pyproject.toml 工具也需要兼容旧规则。为了让配置能被更多工具读取,内联表仍建议保持在同一行;字段较多时改用上面的普通子表。
可以使用 tag、branch 或 rev 选择版本。生产依赖更适合固定标签或提交,而不是长期跟随一个持续变化的分支。
本地来源
1[project]2dependencies = ["shared-agent-core"]34[tool.uv.sources]5shared-agent-core = { path = "../shared-agent-core", editable = true }
editable = true 表示本地包代码变化后无需重新安装即可生效,适合联调。部署时必须保证对应目录存在,或者改用可访问的包索引/Git 来源。
索引来源
1[[tool.uv.index]]2name = "company"3url = "https://packages.example.com/simple"4explicit = true56[tool.uv.sources]7internal-agent-tools = { index = "company" }
explicit = true 表示这个索引只服务于明确绑定的包,避免普通公共依赖意外从私有索引解析。对于内部包,这是比「让所有依赖都在所有索引中搜索」更容易控制的方式。
用户名、密码和 Token 不应直接写进 url 后提交。对于名为 company 的索引,可以通过 UV_INDEX_COMPANY_USERNAME 和 UV_INDEX_COMPANY_PASSWORD 提供凭据,并由 CI Secret 注入真实值。uv 不会把索引凭据写进 uv.lock。
添加来源时,优先使用 uv 命令生成正确结构:
1uv add git+https://github.com/example/internal-agent-tools.git2uv add --editable ../shared-agent-core
然后再检查产生的 [project].dependencies 与 [tool.uv.sources],理解声明和来源之间的分工。
还要注意,[tool.uv.sources] 只会被 uv 使用,也不会成为构建后的标准依赖元数据。pip 或其他工具只能看到 [project].dependencies 中的包名,不知道对应的 Git 地址、本地路径或 workspace 成员。准备发布包时,可以执行 uv build --no-sources,检查项目脱离这些开发来源后是否仍能正常构建。
4. 环境(进阶)
uv 会为 requires-python 覆盖的环境解析依赖。如果项目只部署在有限平台,可以使用环境标记限制求解范围:
1[tool.uv]2environments = [3"sys_platform == 'darwin'",4"sys_platform == 'linux'",5]
这表示项目不打算为 Windows 解析一套可用依赖。它不会在运行时检测操作系统并阻止 FastAPI 启动,而是在依赖解析阶段收窄目标环境。
某些包只有预编译 wheel,没有源码包。项目必须保证特定平台可解析时,可以声明 required environments:
1[tool.uv]2required-environments = [3"sys_platform == 'linux' and platform_machine == 'x86_64'",4]
这类设置通常出现在 PyTorch 等平台差异明显的项目。普通 FastAPI + LangChain 服务没有遇到真实解析问题时,不需要提前添加。
两个设置解决的问题不同:
| 设置 | 作用 |
|---|---|
environments | 限制锁文件需要为哪些环境解析依赖 |
required-environments | 要求只有 wheel 的包必须覆盖哪些环境 |
环境约束还应与容器和生产服务器一致。如果开发机是 macOS、生产环境是 Linux,锁文件至少要覆盖 Linux。不能为了让本地解析更快而把生产平台排除掉。
5. 工作区(进阶)
当一个仓库包含多个彼此关联的 Python 包时,uv workspace 可以统一管理它们:
1[tool.uv.workspace]2members = [3"packages/*",4"services/*",5]6exclude = [7"services/legacy-api",8]
工作区成员仍然各自拥有 pyproject.toml,根项目负责发现成员并共享同一个锁文件。成员之间可以使用 workspace 来源:
1[project]2dependencies = ["agent-core"]34[tool.uv.sources]5agent-core = { workspace = true }
uv run 和 uv sync 默认针对 workspace 根项目。需要操作指定成员时,可以使用 --package:
1uv run --package agent-core pytest2uv sync --package agent-core
当前 fastapi-langchain 是独立仓库,没有多个 Python 包,不需要为了「更工程化」而创建 workspace。工作区解决的是多包协作问题,不是普通目录分层问题。app/api、app/core、app/services 都属于同一个 Python 项目,不应被拆成 workspace 成员。如果多个包需要互相冲突的依赖或各自独立的虚拟环境,也更适合作为独立项目并通过路径依赖联调,而不是放进同一个 workspace。
6. 命令
用户很容易把 uv run 与前端的 npm run 混淆。两者名称相似,但语义不同:
1npm run dev:在 package.json 的 scripts 中查找 dev 字符串并执行2uv run dev:准备好 Python 项目环境,然后执行名为 dev 的命令
因此,仅仅写下 [tool.fastapi] 不会生成 dev 命令:
1[tool.fastapi]2entrypoint = "app.main:app"
这份配置支持:
1uv run fastapi dev
但不支持:
1uv run dev
uv run 不会查询任务名称。它会先创建或同步项目环境,再执行后面的命令或 Python 脚本。要让 uv run dev 成立,dev 必须能从进程的 PATH 中找到;它通常来自项目虚拟环境中的命令入口,也可能是系统已经提供的可执行文件。常见做法有三种:
- 继续使用清晰的完整命令
uv run --env-file .env fastapi dev; - 使用 Makefile、Just 或 Poe the Poet 等任务工具;
- 在
[project.scripts]中注册一个 Python 函数入口,此时项目还需要声明构建系统。
例如安装并使用 Poe the Poet:
1[dependency-groups]2dev = ["poethepoet"]34[tool.poe.tasks]5dev = "fastapi dev"6test = "pytest"7lint = "ruff check ."
执行方式是:
1uv run poe dev
任务工具适合项目确实有多条组合命令时使用。只有一个启动命令时,直接写完整命令更透明,也减少一层依赖。
uv run --env-file .env fastapi dev 中的 --env-file 是 uv 的参数,它先把文件内容加入子进程环境,再运行 FastAPI CLI。FastAPI 和应用最终读取的是环境变量,不是 uv 把 .env 转成了 TOML。同名变量已经存在于进程环境时,环境变量优先于 .env 文件;包含密钥的 .env 还应加入 .gitignore,不能提交到仓库。
维护
uv 配置越复杂,越需要控制边界:
- 包版本范围属于
[project]或依赖组; - Git、本地路径、索引绑定属于
[tool.uv.sources]; - 索引地址属于
[[tool.uv.index]]; - 索引凭据属于环境或 Secret 管理;
- FastAPI 应用入口属于
[tool.fastapi]; - 应用业务参数属于 Settings,而不是
[tool.uv]。
排查 uv 行为时,可以使用下面的顺序:
1uv --version2uv python find3uv lock --check4uv tree --locked5uv sync --locked
uv lock --check 只检查锁文件是否与项目声明一致。uv tree 默认可能先更新锁文件,因此这里加上 --locked,让锁文件过期时直接失败;uv sync --locked 会在不改写锁文件的前提下检查并同步环境。CI 使用这些命令,可以发现只改了 pyproject.toml 却忘记提交 uv.lock 的问题。
--frozen 会跳过锁文件的新旧检查,直接信任现有 uv.lock。它适合 Docker 分层构建等明确需要忽略项目元数据变化的场景,不适合用作常规 CI 一致性检查。
不要在不了解后果时复制大量 [tool.uv] 字段。默认行为已经适合大多数单体服务,配置应该用来表达真实约束,而不是展示 uv 支持多少能力。