从 requirements.txt 到 uv:多模块 Monorepo 的依赖管理升级指南(用法、特点、区别与最佳实践 + 例子)
- [一、先把概念说清楚:依赖管理其实是 3 件事](#一、先把概念说清楚:依赖管理其实是 3 件事)
- [二、requirements.txt 工作流:用法、特点与在 monorepo 的痛点](#二、requirements.txt 工作流:用法、特点与在 monorepo 的痛点)
-
- [2.1. 最朴素的方式:只写 requirements.txt](#2.1. 最朴素的方式:只写 requirements.txt)
- [2.2. 更工程化的方式:pip-tools(requirements.in → 编译出 lock)](#2.2. 更工程化的方式:pip-tools(requirements.in → 编译出 lock))
- [2.3. dev/test 依赖:requirements 文件天生"描述不了多组"](#2.3. dev/test 依赖:requirements 文件天生“描述不了多组”)
- [2.4. 多平台:经常不得不维护多份 lock](#2.4. 多平台:经常不得不维护多份 lock)
- [三、uv 的两种用法:先兼容,再 project/workspace 化](#三、uv 的两种用法:先兼容,再 project/workspace 化)
-
- [3.1. uv 的 drop-in(兼容)用法:uv pip ...](#3.1. uv 的 drop-in(兼容)用法:uv pip ...)
-
- [✅ pip](#✅ pip)
- [✅ uv pip](#✅ uv pip)
- [3.2. uv 的 project/workspace 用法:pyproject.toml + uv.lock](#3.2. uv 的 project/workspace 用法:pyproject.toml + uv.lock)
- [四、核心区别对比:requirements vs uv(面向工程实践)](#四、核心区别对比:requirements vs uv(面向工程实践))
-
- [4.1 "事实来源(source of truth)"不同](#4.1 “事实来源(source of truth)”不同)
- [4.2 dev 依赖的表达方式:多文件 vs 依赖组(Dependency Groups)](#4.2 dev 依赖的表达方式:多文件 vs 依赖组(Dependency Groups))
- [4.3 升级策略:可控升级(只动一个包)](#4.3 升级策略:可控升级(只动一个包))
- [4.4 兼容需求:可以导出 requirements,但不建议"双源维护"](#4.4 兼容需求:可以导出 requirements,但不建议“双源维护”)
- [五、monorepo 的关键升级:uv Workspaces 是什么?](#五、monorepo 的关键升级:uv Workspaces 是什么?)
- [六、迁移示例:一个 3 模块 monorepo 从 requirements 迁到 uv workspace](#六、迁移示例:一个 3 模块 monorepo 从 requirements 迁到 uv workspace)
-
- [Step 0:在仓库根创建最小 pyproject.toml](#Step 0:在仓库根创建最小 pyproject.toml)
- [Step 1:把 workspace 配置加到根 pyproject.toml](#Step 1:把 workspace 配置加到根 pyproject.toml)
- [Step 2:给每个模块补上 pyproject.toml](#Step 2:给每个模块补上 pyproject.toml)
- [Step 3:把旧的 requirements 导入到各模块](#Step 3:把旧的 requirements 导入到各模块)
- [Step 4:声明内部库依赖(workspace = true)](#Step 4:声明内部库依赖(workspace = true))
- [Step 5:生成锁文件并安装环境](#Step 5:生成锁文件并安装环境)
- [Step 6:运行命令(推荐用 uv run)](#Step 6:运行命令(推荐用 uv run))
-
- [uv run VS uv sync](#uv run VS uv sync)
- [七、日常工作流:uv 在 monorepo 里怎么用最顺手?](#七、日常工作流:uv 在 monorepo 里怎么用最顺手?)
-
- [7.1 新增/删除依赖](#7.1 新增/删除依赖)
- [7.2 保持环境干净:uv sync 默认会删"多余包"](#7.2 保持环境干净:uv sync 默认会删“多余包”)
- [7.3 升级依赖的最佳姿势:小步升级](#7.3 升级依赖的最佳姿势:小步升级)
- [八、"最佳推荐"总结:在 monorepo 里我会这样落地](#八、“最佳推荐”总结:在 monorepo 里我会这样落地)
-
- [推荐 1:把 pyproject.toml + uv.lock 当唯一事实来源](#推荐 1:把 pyproject.toml + uv.lock 当唯一事实来源)
- [推荐 2:CI 里强制锁一致](#推荐 2:CI 里强制锁一致)
- [推荐 3:内部库用 workspace 依赖(editable),提升联调效率](#推荐 3:内部库用 workspace 依赖(editable),提升联调效率)
- [推荐 4:如果生产还必须用 requirements.txt:导出,而不是维护](#推荐 4:如果生产还必须用 requirements.txt:导出,而不是维护)
在单项目里,pip install -r requirements.txt 往往"够用";但到了多模块 monorepo(一个仓库里有多个应用/库,比如 api、worker、common-lib),你会很快遇到这些典型问题:
- 依赖文件越来越多:requirements.txt / requirements-dev.txt / requirements-win.txt ...
- 升级一个包,整套依赖跟着抖;冲突定位困难
- 环境漂移:同一个仓库,不同人装出来的环境不一致
- 内部库联调麻烦:改了 common-lib 还得发版本、改引用
uv 的 project/workspace 工作流(pyproject.toml + uv.lock)就是为了解决这类"工程化"痛点设计的。尤其在 monorepo 场景里,uv 的 workspace(工作区) 能让多个模块共享一个锁文件,统一依赖宇宙。
下面这篇博文会从你熟悉的 requirements 工作流讲起,再对比 uv 的思路,最后用一个"3 模块 monorepo"的迁移示例把流程跑通。
一、先把概念说清楚:依赖管理其实是 3 件事
不管你用什么工具,本质都在做:
-
声明(Declare):项目"需要哪些顶层依赖"(例如 fastapi, pydantic>2)
-
锁定(Lock):在当前平台/约束下,把依赖解析成"最终版本集合"(含传递依赖)
-
同步(Sync/Install):把环境安装成和锁一致,并尽量避免漂移
requirements.txt 往往把这三件事混在一个文件里;uv 则倾向于拆开:
-
pyproject.toml 专注"声明"
-
uv.lock 专注"锁定结果"
-
uv sync / uv run 专注"环境一致性"
二、requirements.txt 工作流:用法、特点与在 monorepo 的痛点
2.1. 最朴素的方式:只写 requirements.txt
典型用法:
bash
pip install -r requirements.txt
问题在于:如果你在 requirements.txt 里只写了 fastapi,并没有锁版本,那么每个人装到的版本可能不同;这也是 pip-tools 诞生的原因。
2.2. 更工程化的方式:pip-tools(requirements.in → 编译出 lock)
pip-tools 是很多公司在不引入重量级工具(如 Poetry / uv) 的情况下,用来解决 requirements.txt 依赖混乱、版本不可复现问题的"增强版 pip 工作流"。
它的核心思想很简单:
人写"想要什么",工具生成"最终安装什么"。
常见模式:
-
requirements.in:只写顶层依赖(声明)
txtfastapi requests -
执行 pip-compile 命令生成 requirements.txt:写满精确版本(锁定)
bashpip-compiletxtfastapi==0.109.0 pydantic==2.6.1 starlette==0.35.1 requests==2.31.0 -
执行 pip-sync 安装
bashpip-syncpip-sync 会:
-
安装 requirements.txt 中的
-
删除多余包
-
保证环境一致
-
uv 的迁移文档里也用同样例子解释了这点:requirements.in 里写 fastapi、pydantic>2,再编译生成 requirements.txt(精确 pin)。
2.3. dev/test 依赖:requirements 文件天生"描述不了多组"
requirements 格式一次只能表达"一套依赖集合",所以 dev/test/docs 往往要拆多个文件,例如 requirements-dev.in,并且要用 -c requirements.txt 约束版本保持一致。
这在 monorepo 里会指数级增长:每个模块都来一套 requirements + requirements-dev,维护成本非常高。
2.4. 多平台:经常不得不维护多份 lock
pip/pip-tools 编译出来的锁经常是"生成平台相关"的:Linux 和 Windows 可能锁文件不同(例如 tqdm 在 Windows 上会额外引入 colorama)。这会逼着你维护 requirements-win.txt / requirements-linux.txt 等。
小结:requirements 方案不是不能做,但 monorepo 会把它的"结构性短板"放大。
三、uv 的两种用法:先兼容,再 project/workspace 化
3.1. uv 的 drop-in(兼容)用法:uv pip ...
如果你短期还得继续交付 requirements(比如生产环境只认 requirements.txt),可以先把 pip 安装换成 uv 的 pip 接口(命令类似,但更快),再逐步迁移到 project/workspace。
这篇文章重点放在 uv 的 project/workspace(更适合 monorepo 的"终局"形态),drop-in 只作为过渡选项。
uv pip = 更快 + 更安全 + 带环境管理的 pip
✅ pip
👉 Python 官方包安装器
特点:
-
直接操作当前 Python 环境
-
Python 写的 resolver(慢)
-
串行下载
-
每次重新解析
-
不自动管理虚拟环境
-
不保证可复现
-
只负责安装
流程:
code
pip → PyPI → 安装到当前环境
✅ uv pip
👉 用 uv 引擎执行 pip 语义
它:
-
用 uv 的高速 resolver(极快)
-
用 uv 的全局缓存
-
并行下载
-
用 uv 的环境隔离
-
兼容 pip 命令习惯
流程:
code
uv → (高速 resolver + cache + venv) → 安装
3.2. uv 的 project/workspace 用法:pyproject.toml + uv.lock
uv 把"锁定/同步"做成了默认行为:
uv run:运行命令前会自动 lock + sync,确保环境始终与锁一致(可用 --locked / --frozen / --no-sync 控制)。
uv sync:显式同步环境;默认是 exact sync(会移除 lock 外的包),也可 --inexact 保留多余包;而 uv run 默认是 inexact(不删除多余包),需要严格时用 --exact。
四、核心区别对比:requirements vs uv(面向工程实践)
4.1 "事实来源(source of truth)"不同
-
requirements:经常一个文件同时承担声明+锁定+安装入口,最后变成"既有顶层依赖又混进大量传递依赖"的大清单
-
uv:
-
pyproject.toml:声明顶层依赖(uv 支持 uv add/uv remove 写入,也可以直接编辑)
-
uv.lock:锁定结果
-
uv sync:把环境同步成锁的状态
-
4.2 dev 依赖的表达方式:多文件 vs 依赖组(Dependency Groups)
uv 支持使用 [dependency-groups] 表达开发依赖,并且 dev 组默认会被安装(也可以 --no-dev 排除)。
依赖组还支持嵌套(例如 dev include lint + test)。
4.3 升级策略:可控升级(只动一个包)
uv 不会因为上游发了新版本就把 lock 判定为过期;你要升级必须显式做:
-
升全部:uv lock --upgrade
-
只升级一个:uv lock --upgrade-package
-
指定升级版本:uv lock --upgrade-package ==
这对 monorepo 非常关键:控制变更半径,避免一次升级炸全仓。
4.4 兼容需求:可以导出 requirements,但不建议"双源维护"
uv 支持把 uv.lock 导出为:
-
requirements.txt(pip 兼容)
-
pylock.toml(PEP 751)
-
CycloneDX SBOM
但 uv 官方也明确建议:一般不要同时把 uv.lock 和 requirements.txt 当成双源维护,因为 uv.lock 更强大,包含 requirements 不能表达的特性。
五、monorepo 的关键升级:uv Workspaces 是什么?
如果你把 monorepo 理解成"一个 repo,多个项目(members)",uv workspace 的定义几乎就是为它写的:
-
每个 member 都有自己的 pyproject.toml
-
整个 workspace 共享一个 lockfile(uv.lock),保证依赖一致
-
uv lock 对整个 workspace 生效;uv run / uv sync 默认在 workspace root,但都支持 --package 针对某个成员执行
-
workspace 成员之间的依赖是 editable(联调体验非常好)
什么时候不适合 workspace?
uv 也讲得很直白:如果成员之间依赖版本冲突严重、或希望每个成员一个独立虚拟环境,workspaces 不合适;可以用 path dependencies 更灵活。另外,workspace 会对整个仓库的 requires-python 取交集。
六、迁移示例:一个 3 模块 monorepo 从 requirements 迁到 uv workspace
假设你现在仓库长这样(旧世界):
bash
repo/
apps/
api/
requirements.txt
requirements-dev.txt
worker/
requirements.txt
packages/
common/
requirements.txt
你的目标是升级成(新世界):
-
每个模块一个 pyproject.toml
-
仓库根一个 uv.lock
-
依赖用 uv 维护,requirements 只在必要时导出(兼容生产)
Step 0:在仓库根创建最小 pyproject.toml
如果你不想生成 main.py/README/.python-version,用 --bare:
bash
uv init --bare
这会只生成一个最小 pyproject.toml
Step 1:把 workspace 配置加到根 pyproject.toml
workspace 主要 为了:
- 统一依赖解析
- 统一 lock
- 统一环境
根 pyproject.toml 示例(核心是 [tool.uv.workspace]):
TOML
[project]
name = "repo"
version = "0.0.0"
requires-python = ">=3.11"
[tool.uv.workspace]
members = ["apps/*", "packages/*"]
exclude = []
workspace 的成员目录必须都有 pyproject.toml
Step 2:给每个模块补上 pyproject.toml
在每个模块目录执行一次:
bash
cd apps/api
uv init --bare
cd ../worker
uv init --bare
cd ../../packages/common
uv init --bare
同样,--bare 只创建 pyproject.toml,不会动你现有代码结构。
推荐实践:至少让内部库(如 packages/common)是"可安装的 package",这样
workspace 同步时能以 editable 方式安装,跨模块 import 更稳。因为 uv 在 sync 时会把项目/成员以 editable 安装;
如果项目没有 build system,就不会被安装进环境。
Step 3:把旧的 requirements 导入到各模块
uv 支持从 requirements 文件导入依赖到 pyproject.toml
bash
# 在 root 执行,写到指定 workspace member
uv add --package api -r apps/api/requirements.txt
uv add --package worker -r apps/worker/requirements.txt
uv add --package common -r packages/common/requirements.txt
- uv add -r requirements.txt:导入 requirements 到项目依赖
- uv add --package :在 workspace 中把依赖写入指定成员的 pyproject.toml
如果你还有 requirements-dev.txt,可以把它转成 uv 的 dev 依赖组(两种方式):
方式 A:直接加到 dev 组(推荐)
bash
uv add --package api --dev -r apps/api/requirements-dev.txt
--dev 是 --group dev 的别名。
方式 B :更"uv 原生"的做法------把 dev 工具集中到根(适合 monorepo 共用 ruff/pytest)
例如在根 pyproject.toml:
TOML
[dependency-groups]
dev = ["pytest", "ruff"]
uv 默认会同步 dev 组(可用 --no-dev 排除)。
Step 4:声明内部库依赖(workspace = true)
假设 apps/api 依赖 packages/common,在 apps/api/pyproject.toml:
TOML
[project]
name = "api"
version = "0.1.0"
dependencies = ["common"]
[tool.uv.sources]
common = { workspace = true }
这表示 common 由 workspace 提供,而不是去 PyPI 拉,并且成员之间依赖是 editable。
Step 5:生成锁文件并安装环境
在根目录:
bash
uv lock
uv sync --all-packages
-
uv lock 在 workspace 下会对整个 workspace 解析并写入统一的锁文件
-
uv sync --all-packages 会把 workspace 里所有成员都同步进同一个 .venv
如果你只想安装 api(CI 常见):
bash
uv sync --package api --locked
uv sync 在默认情况下会在同步前重新 lock;加 --locked/--frozen 可以禁止自动更新 lock,适合 CI 做一致性约束。
Step 6:运行命令(推荐用 uv run)
跑测试 / 启动服务:
bash
uv run --package api pytest
uv run --package api python -m api
uv run 会在运行前自动 lock + sync(除非你用 --locked/--frozen/--no-sync 改行为)。
⭐uv run 会:
-
检查 .venv
-
检查 uv.lock
-
检查依赖是否完整
-
如果缺依赖 → 自动安装
-
然后执行命令
⭐ uv sync(强制同步)
-
强制让环境 = lock file
-
安装缺少的
-
删除多余的
-
明确依赖管理步骤
uv run VS uv sync
| 行为 | uv run | uv sync |
|---|---|---|
| 自动安装缺依赖 | ✅ | ✅ |
| 删除多余依赖 | ❌ | ✅ |
| 强制一致 | ❌ | ✅ |
| 开发体验 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| CI 推荐 | ❌ | ✅ |
七、日常工作流:uv 在 monorepo 里怎么用最顺手?
7.1 新增/删除依赖
bash
uv add --package api httpx
uv remove --package api httpx
uv 会修改对应成员的 pyproject.toml,并更新 lock 与环境(可用 --frozen 或 --no-sync 改行为)。
7.2 保持环境干净:uv sync 默认会删"多余包"
这点经常被忽略,但非常有价值:
-
uv sync 默认 exact:移除 lock 里没有的包
-
要保留多余包:uv sync --inexact
-
uv run 默认 inexact,要严格就 uv run --exact
7.3 升级依赖的最佳姿势:小步升级
-
升全部(谨慎):
bashuv lock --upgrade uv sync --all-packages -
只升级一个包(强烈推荐,变更最可控):
bash- uv lock --upgrade-package pydantic uv sync --all-packages
uv 不会因为新版本发布就认为 lock 过期,你需要显式升级。
八、"最佳推荐"总结:在 monorepo 里我会这样落地
推荐 1:把 pyproject.toml + uv.lock 当唯一事实来源
-
顶层依赖写在各模块 pyproject.toml
-
锁定结果写在根 uv.lock
-
依赖变更走 uv add/remove/lock
-
环境重建/对齐走 uv sync
推荐 2:CI 里强制锁一致
-
只装目标模块:uv sync --package api --locked
-
或跑命令时:uv run --package api --locked pytest
推荐 3:内部库用 workspace 依赖(editable),提升联调效率
workspace = true + editable 依赖,是 monorepo 生产力的关键一环。
推荐 4:如果生产还必须用 requirements.txt:导出,而不是维护
需要兼容时,从 lock 导出:
bash
uv export --format requirements.txt --output-file requirements.txt
但不要把它当"主依赖入口"长期双维护;uv 官方建议避免同时维护 uv.lock 和 requirements 作为双源。