大规模 Python 工具链:LlamaIndex 的 Monorepo 全面改造
Massimiliano Pippi • 2025-05-21
当我们谈论 LlamaIndex 时,实际上指的是一个由超过 650 个 Python 包组成的生态系统,其中大部分是 Integrations 和 Packs。所有这些包共享一个 GitHub 仓库,工程师们亲切地称之为 "monorepo"。在本文中,我们将介绍 LlamaDev
,我们用于大规模管理 monorepo 的新工具,并解释我们在现有工具中遇到的挑战,从而促使我们开发了这个新工具。
挑战:650 多个依赖树
monorepo 中的每个 Python 包都发布在 PyPI 上,并带有自己的 pyproject.toml
文件。对于那些不熟悉这个生态系统的人来说,现代 Python 包的 pyproject.toml
文件是一个一站式商店,定义了包生命周期的多个方面:它的依赖项、发布版本号、支持的 Python 和操作系统版本,以及像 linters 和类型检查器这样的工具应该如何处理这个特定的包。大多数包都有测试,有些还有额外的开发依赖项列表。
Integrations 和 Packs 有一些共同的代码,这些代码通过一个名为 llama-index-core
的基础包发布,生态系统中的几乎每个包都依赖于它。此外,它们还可以相互依赖,例如 llama-index-llms-azure-openai
需要 llama-index-llms-openai
才能工作。正如你可以想象的那样,这对于测试策略有一些影响,例如:
- 如果我们在 pull request 中修改了
llama-index-core
,则需要触发 monorepo 中大多数包的测试,以确保在更改后它们仍然可以工作。 - 如果我们修改了像
llama-index-llms-openai
这样的东西,我们需要找到哪些包依赖于它,并测试它们以确保它们仍然可以工作。
开发很容易(还算容易)
在这些包中的一个中进行开发的体验各不相同。有时,我们处理的是一个独立的 Integration,除了 llama-index-core
之外没有其他的 LlamaIndex 依赖项。这是最理想的情况,我们针对特定版本的 core 包测试我们的代码,如果它有效,那就没问题了。
有时,我们处理的是一个在 monorepo 中有依赖项的包。在这种情况下,我们需要确保我们的更改对包本身有效,而不会破坏其他包。有时,我们处理的是 core 包,在这种情况下,我们需要格外小心,不要引入可能影响整个 monorepo 的破坏性更改。
最后但并非最不重要的一点是,我们会尽最大努力在我们的包中支持多个 Python 和操作系统版本,对于绝大多数情况来说,这意味着在三个操作系统(Windows、Mac 和 Linux)上,支持 Python 3.9 一直到 3.12+。
维护很难
在 monorepo 中,你首先想要的是一致性。即使测试执行方式或包的构建方式略有差异,也可能会使跨平台支持复杂化,并将跨 monorepo 的批量操作变成一场噩梦。最终得到 650 多个_略有_不同的包的风险非常高,特别是在 Python 生态系统中,因此我们引入的第一个工具是 Python 项目管理器。
Python 项目管理器通常依赖于 pyproject.toml
文件来提供诸如构建和发布包、管理依赖项和 lockfile、定义脚本和自动化任务、准备虚拟环境以便在不同的 Python 版本下隔离地工作等功能。
第一次迭代:Poetry 用于单个包
我们选择的工具是 Poetry:功能完整,在 Python 社区中非常成熟,它为我们服务了 一年多。在单个包上使用 Poetry 非常轻松:凭借其富有表现力的命令行界面,它可以设置特定的 Python 版本并构建虚拟环境,因此在迭代代码时运行测试变得非常容易。但不要太兴奋:我们处理的是一个 monorepo,即使是最容易的事情也会变得复杂。
虽然 Poetry 非常适合在单个包上独立工作,但它并不真正了解包依赖关系。例如,它无法知道依赖于我们的某个包实际上位于同一个 git 仓库的兄弟目录中。由于位于同一个仓库中,我们可以轻松地触发测试来验证我们的上游更改是否破坏了依赖项,但是 Poetry 只是没有信息来执行此操作。
第一次迭代:Pants 用于构建管理
因此,我们不得不引入另一个工具:我们需要一个构建系统,最好是专门设计用于在 monorepo 中工作的。构建系统是一种软件,它可以自动将源代码构建成 artifacts,并处理所有中间步骤。对于 Python 代码库,这意味着为特定 Python 版本设置虚拟环境,获取和安装依赖项,运行测试,构建 wheel 包,并选择性地将它们发布到像 PyPI 这样的全局注册表中。
我们选择了 Pants 来完成这项工作,而且它也为我们服务了 一年多。当上游发生更改时,Pants 显式支持 Python 项目,并且在检测要在 monorepo 中的多个包中触发哪些测试方面特别智能。它还带有一个强大的缓存系统,可以显着加快每个构建环境的设置速度。
1.0 版本运行了一年
回顾一下,在一年多的时间里,这是 LlamaIndex monorepo 中使用的技术栈:
- Poetry 用于管理单个 Python 项目。
- Pants 用于协调不同包之间的测试(我们没有使用构建功能)。
- Github actions 用于自动化 pull request 和 git push 上的 Pants 和 Poetry 运行。
我们的设置运行良好,但不可否认的是,存在一些小缺陷。小缺陷的问题在于,随着系统规模的扩大,它们往往会变得越来越不小。对单个包的单秒操作似乎并不多,但当你必须重复它 650 次时,现在自动化作业需要 10 分钟。
规模问题:构建速度和缓存服务器
让我们从 Poetry 开始。该工具运行良好,我们可以表达复杂的依赖关系需求,但是依赖关系解析器有时可能会非常慢。再加上 pip
是唯一可用的安装程序这一事实,在包上工作的开销可以用秒来衡量。同样,你可能不会注意到在单个包上工作,但是如果没有 Pants 缓存虚拟环境,则在 monorepo 上广泛运行单元测试可能需要很长时间。
说到 Pants,它也有自己的一系列缺陷。我们遇到的第一个问题是设置:虽然定义 targets 和依赖项是一项非常艰巨的任务,但至少是你做一次然后就可以受益的事情,但是缓存系统要求更高。我们不得不在 AWS 上托管一个服务,该服务位于公共地址之后,以便可以从 Github workflow 中使用它。并且缓存服务器不是你部署一次就忘记的东西。我们不得不对其进行几次扩容,在负载均衡器后面添加两个冗余实例,并多次调整存储大小。有时,Github workflow 会出现奇怪的错误而失败,直到后来才发现缓存服务器无响应或行为异常。所有这些都可以理解,但是考虑到我们团队的规模,在资源和金钱方面维护它非常昂贵。
控制问题:日志记录、不一致等等
这还不是全部:Pants 复杂的缓存带来了一个额外的代价。在构建期间使用的 Python 虚拟环境由 Pants 在内部使用 pip
管理。你对它的控制不多,环境不容易访问,这有一些影响。
- 首先,Pants 了解并且可以依赖 Poetry 配置,但是它不会直接调用它。这意味着 CI 中的测试运行与使用 Poetry 在本地调用的测试运行不同。
- 其次,Pants 日志记录非常冗长,并且与安装或依赖关系问题相关的消息不容易找到和调试。根据错误的类型,有时甚至难以在日志中找到测试失败。
- 最后,在本地运行 Pants 并不容易,我们不要求贡献者这样做,但是这意味着有时 pull request 会显示无法在本地重现的失败,从而使开发者体验非常差。
为什么我们需要 2.0 版本
回顾一下,随着 monorepo 大小的增加,我们面临着以下问题:
- 由于依赖关系管理方面的困难,迭代速度很慢。
- 维护构建系统是一种负担。
- 调试 CI 运行很困难。
- 为项目做贡献很困难。
回顾我们过去的 Slack 对话,自从我们开始摆弄用 uv
替换 pip
以便利用其在安装时带来的巨大加速的想法以来,已经有一段时间了,但是我们直到最近才有时间进行认真的尝试。以下是发生的事情。
将项目从 Poetry 迁移到 uv
最初的任务非常模糊,它基本上是这样说的:“在所有地方使用 uv
,以便我们可以缩短 CI 反馈循环”。我们不想摆脱现有的工具,只是想引入一个看起来很有希望的新工具。
Pants 在文档中有一个部分 提到了 uv,因此我们乐观地认为有可能使两者共存。这种乐观情绪持续了大约一个小时,这是浏览文档、浏览 Pants Discord 服务器中的一些线程并意识到不行的必要时间,Pants 无法在内部使用 uv
代替 pip
。有一个第三方 plugin 可以做到这一点,但是我们不想在已经几乎无法控制的东西上添加另一层复杂性,因此我们放弃了该想法。
B 计划:我们仍然可以在 Poetry 项目中调用 uv
,所以至少我们可以加快本地开发速度,不是吗?不是。Poetry 不支持 uv,并且在可预见的将来也不会。幸运的是,uv 不仅仅是 pip
的替代品,它实际上是一个非常强大的 Python 项目管理器。因此,我们尝试了一下,并迁移了一些包,以查看需要进行哪些更改。这有了一个非常好的开端。如果你安装了 uv,则迁移包实际上是在包含 pyproject.toml
文件的文件夹上运行的一个命令:
uvx migrate-to-uv
我们不会说我们对安装包的依赖关系的速度感到印象深刻,因为那是意料之中的。我们爱上的是其他一切:uv 简单、整洁、富有表现力。uv run --
非常强大,你再也不需要在你的生活中 activate
虚拟环境了。我们中的一些 Homebrew 核心用户有史以来第一次让 Python 项目管理器工具安装了 Python 发行版。我们很快就决定告别 Poetry。如果开发人员体验对我们来说如此之好,那么贡献者肯定会喜欢它。
如何在 2.0 中管理构建?
不过我们仍然不满意。CI 仍然无法从 uv 中受益,并且贡献者仍然必须在 pull request 检查中与神秘的失败作斗争。有些事情在困扰着我们,如果你不做出改变,就不会在像 LlamaIndex 这样的东西上工作。所以我们做出了改变。
我们还没有疯狂到认为我们可以重写像 Pants 这样的东西并替换它,但是我们只使用了大约 20% 的 Pants 功能,而且我们绝对比 20% 疯狂。因此,我们重写了检测 monorepo 中所有包的依赖关系图的代码,仅依赖于 pyproject.toml
文件。最重要的是,我们添加了一些代码来检测在每个 pull request 中应该测试哪些包,考虑到已更改的文件。然后是一些调用 pytest
并仅获取重要日志的代码。然后是一些避免因无法测试的包或不相关的失败而导致 CI 作业失败的逻辑。然后我们决定所有这些代码 都可以变成一个定制工具。
介绍 LlamaDev
我们将我们的新工具称为 LlamaDev,任何贡献者都可以使用 uv run llama-dev
从 monorepo 的克隆版本中运行它。
自从 我们从 Poetry+Pants 切换到 uv+LlamaDev 以来,我们引入的改进列表很长:
- 完整测试运行(不计算覆盖率)所有 600 多个包的平均时间快了约 20%,相当于减少了约 6 分钟。
- 根据具体的包,部分测试运行速度甚至更快。例如,更改一个具有许多依赖项的 Integration,如
llama-index-llms-openai
,从大约 11 分钟 降至 4 分钟。 - 现在日志更加清晰,如果检查失败,贡献者可以准确地看到发生了什么。
- 贡献者可以在本地运行
llama-dev
,如果需要进行一些调试,可以完全重现 CI 系统中所做的事情。 - 现在将 Lockfile 检查到仓库中,以获得更好的构建可重复性。
- 我们不再依赖外部资源,所有需要的计算都在 Github actions 上。
显然,这建立在 uv 为改善在任何 Python 项目上工作的日常工作所做的一切之上。
现在你可以轻松地在 LlamaIndex 和 LlamaDev 上工作
自从我们切换到 uv+LlamaDev 以来,我们引入了一些优化,例如 使用 uv 缓存,并向 LlamaDev 添加了几个 新功能,但是通过你的帮助,我们可以做得更多。如果你一直想为 LlamaIndex 做出贡献,那么现在是开始的正确时机,因为 入门从未如此简单。如果你是一位经验丰富的贡献者,我们需要你的帮助来使 LlamaDev 成为你一直想要的工具:我们将很乐意讨论功能请求并审核代码贡献。
相关文章
Introducing LlamaIndex 0.11 2024-08-22
How to train a custom GPT on your data with EmbedAI + LlamaIndex 2023-12-14
Becoming Proficient in Document Extraction 2023-11-20
Announcing LlamaIndex 0.9 2023-11-15