danswer(Onyx)后端依赖管理实战:以 pyproject.toml 为唯一事实来源、用 uv 统一解析与锁定的完整工作流
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
本篇技术指南围绕 backend/requirements/README.md 展开,系统讲解 danswer(Onyx)AI 平台后端在 Python 依赖管理上采用的pyproject.toml + uv.lock + 导出 requirements.txt混合方案:为什么放弃手工维护多个 requirements 文件、如何划分依赖分组、如何借助 pre-commit 钩子自动化「改一处、全同步」,以及 Docker 构建与本地开发环境如何基于同一套锁定版本获得可复现的安装结果。读完你可以直接在自己的 danswer 分支上安全地增删依赖,也能把这一套「单一事实来源 + 统一锁文件 + 哈希校验导出」的工程实践迁移到其他 Python 项目中。
一、方案概览:为什么用 pyproject.toml 而非直接改 requirements.txt
backend/requirements/目录在仓库中的定位是「为兼容既有 Docker 构建而保留的遗留产物」(原文档原话:This directory is kept for backwards compatibility with existing Docker builds)。真正的依赖管理以仓库根目录的 pyproject.toml 为唯一事实来源(single source of truth),配合统一的 uv.lock 锁定所有已解析版本。
原文档明确列出了这一设计带来的收益:
- 单一事实来源:所有依赖只定义在
pyproject.toml中,一处维护; - 无重复:跨环境共享的依赖只声明一次,不随环境重复列举;
- 统一锁文件:所有版本一起解析,天然保证互相兼容;
- 快速:uv 的解析与安装速度远快于传统 pip-tools 工作流(原文档给出的量级为 10–100 倍);
- 可复现构建:锁文件钉死了全部直接与传递依赖;
- 易更新:改
pyproject.toml、提交、完成。
需要说明的是,uv 是 Astral 团队(Rust 实现的 Python 包管理与解析器)推出的工具,它同时承担 pip、pip-tools、virtualenv、poetry 等工具的职能,本仓库只把它用于依赖解析、锁定、导出与同步安装。
二、文件结构与职责边界
原文档给出的目录结构如下,这也与仓库实际布局完全一致:
pyproject.toml # 事实来源 —— 改这里! uv.lock # 统一锁文件(全部版本) backend/ └── requirements/ # 遗留 .txt 文件(兼容 Docker 构建) ├── default.txt # 共享 + backend 组 ├── dev.txt # 共享 + dev 组 ├── ee.txt # 共享 + ee 组 ├── model_server.txt # 共享 + model_server 组 └── combined.txt # 聚合其余所有文件,主要用于测试各文件职责对照仓库实况:
| 文件 | 内容来源 | 实际用途 |
|---|---|---|
| pyproject.toml | 手工维护 | 依赖声明、工具链(ruff/ty/basedpyright)配置、uv 行为配置 |
| uv.lock | uv lock生成 | 跨平台、跨 Python 版本解析后的完整版本图 |
| default.txt | uv export --group backend | backend 主镜像安装 |
| ee.txt | uv export --group ee | Enterprise Edition 附加依赖(posthog) |
| model_server.txt | uv export --group model_server | 模型服务镜像(ML 依赖) |
| dev.txt | uv export --group dev | 开发与测试工具链 |
| combined.txt | 手工-r聚合 | 测试环境一次性安装全部依赖 |
以 combined.txt 为例,它本身只包含四条-r指令:
-r default.txt -r ee.txt -r model_server.txt -r dev.txt文件头注释明确说明它「combines all the other requirements files,Primarily for testing」,并建议实际运行时按需只安装对应部分,这与原文档「按环境分组导出」的思路一脉相承。
导出文件的哈希校验特性
打开任意导出的.txt(例如 default.txt)可以看到,文件头会记录生成它的完整命令,且每个钉死的产物都带--hash=sha256:...条目:
# This file was autogenerated by uv via the following command: # uv export --no-emit-project --no-default-groups --group backend -o backend/requirements/default.txt agent-client-protocol==0.7.1 \ --hash=sha256:4ffe999488f2b23db26f09becdfaa2aaae6529f0847a52bca61bc2c628001c0f \ --hash=sha256:8d7031209e14c3f2f987e3b95e7d9c3286158e7b2af1bf43d6aae5b8a429249f # via onyx这正是原文档强调的安装安全边界:Docker 构建与 CI 使用uv pip install --require-hashes安装,任何版本号或产物哈希未出现在锁定文件中的包都会被拒绝安装,因此实际部署只能落到uv.lock解析出的版本上,杜绝了「构建时悄悄漂移到未锁定版本」的风险。
三、pyproject.toml 中的依赖分组详解
原文档指出新增依赖时应写入合适的 section,这里结合仓库根目录 pyproject.toml 的真实结构逐一说明各组语义:
[project.dependencies](共享依赖):backend 与 model_server共同使用的运行时依赖,例如fastapi==0.133.1、pydantic==2.12.5、openai==2.38.0、litellm[google]==1.93.0、sentry-sdk==2.14.0、uvicorn==0.49.0等。文件内注释还解释了某些包被放进共享组的原因,比如python-json-logger是为了让 model_server 镜像同样支持LOG_FORMAT=json结构化日志。[dependency-groups.backend](backend 独有):体量最大的一组,覆盖连接器(Slack、Confluence、Jira、Google Drive、Notion、SharePoint 等)、异步框架(aiohttp、celery)、数据库(SQLAlchemy==2.0.50、asyncpg、psycopg2-binary)、文件解析(markitdown[pdf, docx, pptx, xlsx, xls]==0.1.2,且注释提醒更新前必须了解get_markitdown_converter的补丁行为)等。组内还常见「冻结版本 + 原因注释」的写法,例如openpyxl==3.0.10因上游 issue 冻结、libpass==1.9.3替代已停止维护且不兼容 Python 3.13 的 passlib。[dependency-groups.dev](开发工具):pytest 全家桶(pytest==9.0.3、pytest-asyncio、pytest-xdist、pytest-playwright等)、ruff==0.16.1、pre-commit==3.2.2、hatchling、matplotlib,以及大量types-*类型桩。注释中强调部分版本需与.pre-commit-config.yaml中隔离安装的 hook 版本保持一致。[dependency-groups.ee](企业版特性):目前仅posthog==3.7.4,服务于企业版埋点统计。[dependency-groups.model_server](ML 依赖):accelerate、einops、numpy、sentence-transformers、torch==2.9.1、transformers==5.14.1等重 ML 包,仅随 model_server 安装。- 额外分组:
[dependency-groups.zizmor](CI 单独同步的安全审计工具,避免拖入整套 dev 工具链)、[dependency-groups.loadtest](Locust 压测运行时,刻意排除在默认组之外,避免正常uv sync拉入 gevent/flask 等重依赖)。
此外[tool.uv]一节还定义了两个对本工作流至关重要的行为:
[tool.uv] # uv 仅用于依赖管理。onyx "project" 永不构建或安装为包: # Docker 镜像拷贝源码树并从 requirements 导出安装,本地导入靠 cwd / PYTHONPATH 约定。 package = false default-groups = ["backend", "dev", "ee", "model_server"]package = false:项目只作为依赖清单使用,不会被pip install -e .安装成包(仓库根 pyproject.toml 注释中说明 Docker 镜像直接拷贝源码树并按 requirements 导出安装,本地导入依赖backend/pytest.ini的 cwd/PYTHONPATH 约定);default-groups:默认同步时会安装 backend、dev、ee、model_server 四组全部依赖,正好对应原文档「uv sync为开发常用场景」的描述。
[tool.uv]下还有override-dependencies,用于放宽 mitmproxy 等包的过紧上限(如tornado>=6.5.0附带 CVE 修复说明),保证解析器保留 backend 需要的更新版本。
四、完整工作流:从安装 uv 到日常增删依赖
1. 安装 uv
未安装 uv 时,原文档给出的官方安装方式(macOS/Linux):
curl -LsSf https://astral.py/uv/install.sh | sh安装完成后可用uv --version验证。Windows 用户可参考 uv 官方文档的 PowerShell 安装方式(仓库文档未展开,这里不做断言)。
2. 添加 / 更新依赖:只改 pyproject.toml
原文档给出了强约束:绝对不要直接编辑.txt文件。正确步骤是:
- 编辑 pyproject.toml;
- 按第二节的语义把依赖放入合适的 section:
[project.dependencies]:backend 与 model_server 共享;[dependency-groups.backend]:backend 独有;[dependency-groups.dev]:开发工具;[dependency-groups.ee]:企业版特性;[dependency-groups.model_server]:ML 包;
- 提交变更 —— 提交时 pre-commit 钩子会自动重新生成锁文件与 requirements。
仓库中.pre-commit-config.yaml的写法印证了第 3 步:它引入astral-sh/uv-pre-commit仓库的uv-sync、uv-lock与三条uv-export钩子,每个导出钩子都带有与手工命令完全一致的参数(--no-emit-project --no-default-groups --group <组名> -o backend/requirements/<对应文件>),并且只在这些文件变化时才触发(files: ^(pyproject\.toml|uv\.lock|backend/requirements/.*\.txt)$)。也就是说,一次 commit 即可完成「改声明 → 重新解析 → 重新导出哈希校验 requirements」的闭环。
3. 手动重新生成锁文件与 requirements
若需要手动触发(例如 CI 环境或临时核对),原文档给出的命令为:
uv lock uv export --no-emit-project --no-default-groups --group backend -o backend/requirements/default.txt uv export --no-emit-project --no-default-groups --group dev -o backend/requirements/dev.txt uv export --no-emit-project --no-default-groups --group ee -o backend/requirements/ee.txt uv export --no-emit-project --no-default-groups --group model_server -o backend/requirements/model_server.txt参数含义补充说明:
--no-emit-project:导出时不把项目自身(onyx)作为依赖项输出,只输出第三方包;--no-default-groups:不包含default-groups定义的那一组,从而让每条命令只导出--group指定的单一分组(这与 .pre-commit-config.yaml 中钩子参数一一对应);- 每条导出命令产出的
.txt都会包含全部共享依赖 + 该组独有依赖,这正是 default.txt 这类文件动辄上千行、且每个包都带 sha256 哈希的原因。
4. 安装依赖:uv sync 分组安装
原文档把安装命令分成了三种场景:
# 开发常用 —— 安装 共享 + backend + dev + ee uv sync # backend 生产 —— 仅 共享 + backend uv sync --no-default-groups --group backend # model server —— 仅 共享 + model_server(不含任何 backend 依赖!) uv sync --no-default-groups --group model_server其中默认uv sync的完整集合(共享 + backend + dev + ee + model_server)正是由[tool.uv] default-groups决定的。如果开启了uv-syncpre-commit 钩子,切换分支或拉取新变更时依赖会自动同步安装,无需手动干预。原文档也提示该钩子「If enabled」(由 .pre-commit-config.yaml 顶部的default_install_hook_types中包含post-checkout、post-merge、post-rewrite可知,这是 hook 安装阶段决定的行为)。
5. 升级依赖
原文档给出的升级路径依然围绕「改声明 → 交给钩子」:
- 修改
pyproject.toml中的版本约束; - 提交,pre-commit 钩子自动重新生成
uv.lock与 requirements 文件。
同时原文档特别提醒:提交前务必仔细审查变更。从源码注释可以看到仓库对此相当谨慎——多处以注释冻结版本并说明原因(如gpt4all在 Mac 与 slim-bookworm 镜像上的兼容问题、markitdown与文件抽取逻辑的耦合),说明版本升级在本项目中往往是需要人工复核正确性的操作。
五、Docker 构建中的实际落地:--require-hashes 强制校验
原文档强调「Docker builds and CI install withuv pip install --require-hashes」,这一点在 backend/Dockerfile 中得到直接印证:
COPY ./requirements/default.txt /tmp/requirements.txt COPY ./requirements/ee.txt /tmp/ee-requirements.txt # requirements/*.txt 是完全预解析的锁文件导出,--no-deps 原样安装 RUN uv pip install --system --no-cache-dir --no-deps --require-hashes \ -r /tmp/requirements.txt \ -r /tmp/ee-requirements.txt && ...关键点拆解:
--no-deps:导出文件已经是完整解析结果(含全部传递依赖),无需再次解析依赖树;--require-hashes:强制每个被安装的包都要在 requirements 中出现对应哈希,否则拒绝安装——这正是「只能安装uv.lock内版本」的安全保证;--system:backend 镜像直接在系统 Python 环境安装。
model_server 镜像的处理更有代表性。查看 backend/Dockerfile.model_server 可以看到,构建分两步:先用awk从model_server.txt中筛出torch、nvidia-*、triton等重量级 ML 包单独安装(便于分层缓存),再安装剩余全部依赖;两步都使用了--require-hashes与指向/app/.venv/bin/python的--python参数。由于导出文件把长哈希写成了\续行格式,awk 需要按行首^[[:alnum:]]判断包条目才能正确筛选——这是锁定文件格式与构建脚本耦合的一个典型细节。
六、工程实践要点小结
综合原文档与仓库源码,这套依赖管理方案的实践要点可以归纳为:
- 唯一入口:所有依赖变更只发生在 pyproject.toml,
.txt是生成物而非编辑对象; - 分组清晰:共享 / backend / dev / ee / model_server 五组职责互斥,配合
default-groups与--no-default-groups --group实现「按需安装」,避免 ML 依赖污染普通开发环境; - 钩子自动化:.pre-commit-config.yaml 中的
uv-lock、uv-export、uv-sync让锁文件与导出文件始终与声明保持一致,同时uv-sync覆盖分支切换、拉取等场景; - 哈希强校验:导出文件携带全部 sha256,Docker/CI 用
--require-hashes安装,构建结果被严格钉死在锁定版本上; - 遗留兼容:
backend/requirements/与combined.txt的存在是为了兼容历史 Docker 构建与测试场景,新代码不应再依赖手工编辑它们。
这套「单一事实来源 + 统一锁文件 + 分组导出 + 钩子自动同步 + 哈希强制校验」的组合,既保留了 uv 快速、可复现的优点,又兼容了既有镜像构建,是大型 Python 单体项目(如本仓库这种连接器众多、前后端与模型服务共存的架构)中值得参考的依赖治理模板。
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考