我们首次使用纯Rust进行生产级RAG开发的旅程

原文链接:https://rust-dd.com/post/our-first-production-ready-rag-dev-journey-in-pure-rust

我们涉足AI开发已有一段时日——不仅仅是那种“调用现有API”的高层次开发,而是那种必须构建无法依赖OpenAI或类似在线系统的解决方案。最近,我们面临了一项挑战:在Rust中创建我们首个生产级RAG(检索增强生成)解决方案。在这篇文章中,我将详细阐述我们所采取的步骤、遇到的困难,以及Rust社区在这一领域的现状。

我们的Rust RAG诞生记

一切始于我们在rust-dd的探索,当时我们想要构建一个抽象的RAG解决方案。这个想法很简单:我们将向量嵌入存储在某个向量数据库中,并使用标准的向量相似性搜索来为我们的本地LLM检索相关上下文。

初始技术栈

  • 编程语言:Rust(当然!)
  • 向量数据库:Qdrant —— 选择它是因为它也是用Rust编写的,并且对Rust有很好的支持。
  • LLM推理:Mistral.rs —— llama.cpp的纯Rust竞争对手。

大约一个月的时间里,这个项目完全以开源方式运行。我们构建了功能性RAG系统所需的一切:

  • 一个类似ChatGPT的前端
  • 文件上传功能,这些文件随后被嵌入到我们的向量数据库中
  • 一个本地LLM,能够利用检索到的上下文生成最佳答案

在这个阶段,我们通过官方的Qdrant fastembed Rust库使用纯文本来生成嵌入。初步测试看起来非常有希望,然后……真正的挑战开始了。

试点请求:压力骤增

我们收到了一个工业解决方案的试点请求,期限为1.5个月。这个RAG必须处理未知的数据源,因此我们不能依赖ChatGPT或其他大型成熟模型。我们的资源有限,这意味着我们实际上只能使用14–32B参数的模型——大多数是4位或8位量化形式。

PDF的烦恼与多语言混乱

我们无法分享所有细节,但可以说,我们必须回答来自用多种语言编写的PDF文件的查询,包括一种特别奇特的语言。任何尝试处理过PDF的人都知道,这是一个由来已久的难题:

  • 格式是非结构化的(几十年的难题)。
  • 将其转换为文本会变得更加诡异。

我们测试了几种策略,最终选择了:

  1. 将PDF转换为Markdown(我们尝试了基于AI的PDF转换器,如Marker、Docling等,但最终选择了PyMuPDF以确保可靠性)。
  2. 使用Rust文本分割库(例如text-splitter或类似工具)进行语义分块。

由于文档是专业化的,较小的LLM没有足够的内置领域知识。在许多方面,将数据上传到Qdrant比从LLM中获得正确答案更具挑战性。许多博客文章警告说,在RAG解决方案中使用PDF文件有点像是噩梦——我们完全同意!如果你对自己的数据很了解,并且不需要通用解决方案,你可以通过蛮力找到一种不错的方法。但要小心:

  • 页眉和页脚:如果它们被嵌入,可能会严重误导你的向量搜索。
  • 分块大小:如果分块太大,而嵌入模型不支持长序列长度,某些数据可能永远无法正确嵌入。
  • 嵌入向量维度:如果你的分块对于较小的嵌入维度来说太大,你会失去保真度。

突然之间,我们面临着一个关于分块大小、嵌入限制以及寻找完美平衡点的无尽难题。

多模型Embeddings

由于我们需要处理多种语言和专业化文本,我们最终使用了不同的模型,所有这些模型都来自Hugging Face。我们还引入了一个基于BERT的模型,甚至是一个生成稀疏向量的模型。结果是,我们构建了一个比最初预期更复杂的嵌入管道,但这是捕捉这些专业化文本细微差别的必要之举。

遇到障碍:Mistral.rs与CUDA

随着截止日期的临近,更多问题出现了:

  • 我们在Mistral.rs中遇到了设备映射问题(这意味着模型数据映射到GPU内存的方式出现了问题)。
  • 我们也遇到了一些与CUDA相关的错误。

我们不是CUDA开发专家,也没有时间自己修复这些问题,但非常感谢Mistral.rs的维护者Eric Blueher,他已经解决了这些问题。不幸的是,由于时间紧迫,我们转向了Ollama-rs以获取更稳定的框架,而Ollama运行顺利,没有进一步的GPU问题。我们仍然打算最终回归Mistral.rs,因为我们希望完全控制每个组件,而Mistral.rs提供了许多低层配置的可能性。

Python的渗透:Rust-Bert与PyO3

我们还不得不在Rust中使用Rust-Bert处理基于BERT的模型,但它底层使用了LibTorch。这带来了一些依赖关系上的摩擦。这是我们第一次感觉到,无论好坏,我们现在根本无法避免使用Python。

因此,我们采取了务实的方法:

  • 编写了一些快速的Python脚本
  • 使用PyO3从Rust调用它们

这为我们提供了所需的嵌入,并且我们可以继续冲刺到终点线。

最后的冲刺

我们只剩下不到一个月的时间。最大的挑战是弄清楚如何将最佳的向量数据库结果返回给LLM。如果你有巨大的文档和小的分块大小,你可以保持文档的连贯性,但可能会错过大局上下文。如果你有巨大的分块大小,你可能会失去细节或使搜索变得不那么相关。我们尝试了许多“常识性”的解决方案,但结果并不如我们希望的那么好。

最终,我们不得不退一步,真正思考底层数学如何塑造我们的数据。通常,你不仅仅需要遵循最佳实践;你需要深入了解模型的结构以及向量相似性是如何计算的。但时间紧迫,所以我们决定为演示选择我们拥有的最佳解决方案。

演示与成功

终于到了展示我们解决方案的日子。当一切顺利运行时,我们感到无比轻松——甚至有点惊讶:
系统在85-90%的时间内给出了正确的英文答案,几乎没有出现幻觉。

由于整个管道是英文的,其他语言的准确性稍差,但也不落后太多。
总的来说,我们自信地认为,我们已经在紧迫的期限内实现了交付基于Rust的生产级RAG的目标。

下一步是什么?

我们已经期待提高解决方案的准确性并扩展到更多语言。那些许多个月——以及熬夜的日子——绝对值得。我们的计划是:

  • 重新审视Mistral.rs以获得更多控制权。
  • 调整我们的分块策略,以更优雅地处理巨大文档。
  • 更多地尝试不同的嵌入模型,以支持真正的多语言和特定领域的用例。

这就是我们在Rust中成功构建RAG的故事——我们希望这只是众多成功故事中的第一个!

最后感想

在Rust中为RAG解决方案工作非常有成就感,但也充满了陷阱。无论是嵌入管道的复杂性、GPU的问题,还是不得不集成Python,构建一个稳健的RAG都是一个多层次的难题。但如果你对Rust和AI充满热情,毫无疑问,社区正在突破界限。我们很高兴能成为这段旅程的一部分——并迫不及待地想看看它接下来会走向何方。

如果你有任何问题或见解(特别是如果你正在处理Rust、GPU推理和复杂嵌入),请随时联系或留言。让我们继续用Rust构建AI的未来!