构建自然语言与大语言模型(LLM)流水线——构建可复现、生产就绪的 RAG 系统

0 阅读28分钟

在前几章中,我们踏上了一段以实践为导向的旅程,来掌握 Haystack 框架。在第 4 章中,我们将 Haystack 的预定义组件组装成了强大的端到端 Pipeline,分别构建了一个朴素 RAG 系统和一个更高级的混合 RAG 系统。随后,第 5 章标志着能力上的一次关键跃迁:我们从框架的使用者,成长为能够扩展它的架构师,通过自定义组件,从文档中构建出一个高质量、基于图的合成测试数据集。

然而,一条“能跑通”的 Pipeline,还只是走到了中途。要构建一个生产级应用,我们必须超越零散的 spot-checking,以及“在我电脑上能跑”的经验式保证。下一步,是采用一种严格的、数据驱动的工程实践。我们将把前几章中的构建块整合起来,形成一个统一的架构模式蓝图;你可以基于这套蓝图,结合 Ragas,系统性地评估不同场景下的 Pipeline,并借助 Weights & Biases 之类的框架比较结果。

我们会先概览朴素 RAG 与混合 RAG Pipeline 的开发方式,然后将这些 Pipeline 抽象为 SuperComponent。接着,我们会使用这些 SuperComponent,并结合第 5 章生成的 synthetic data,对两类 Pipeline 的性能进行比较。

本章将涵盖以下主题:

  • 搭建一个可复现的 RAG 项目
  • 关键架构设计的论证:解释我们为何如此配置 Pipeline
  • 使用 Ragas 进行系统化 Pipeline 评估
  • 使用 Weights & Biases 增加生产级可观测性
  • 通过分析 embedding 模型,探索成本—性能权衡

到本章结束时,你将掌握如何把 Pipeline 抽象为 SuperComponent,理解如何对封装了朴素 RAG 和混合 RAG Pipeline 的 SuperComponent 做系统化评估,并学会如何借助 Ragas 与 Weights & Biases,实现客观评估与可观测性。

技术要求

为了跟上本章内容并成功实现其中讨论的概念,你需要先搭建一个合适的开发环境。本项目最核心的技术要求是 Python,它将用于管理依赖并运行项目所需代码。

本项目设计为运行在 Python 3.12 之上,因此请确保你安装的是该版本。如果尚未安装,可以考虑使用 pyenv 这类工具来管理并安装正确的 Python 版本。

关于环境配置的完整分步骤说明,可在代码仓库对应章节目录中找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/tree/main/ch6#setup-instructions

本章还会使用 Docker,因此若要完成练习,你需要安装 Docker Desktop

本章所使用架构模式的完整概览可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch6/chapter_overview.md

建议你先阅读这些概念,因为它们会贯穿整章。

本章所使用的关键架构模式包括:将前面见过的朴素 RAG 和混合 RAG Pipeline 抽象为 SuperComponent(如第 4 章所示),以及使用一个双 Docker 镜像架构,创建两个向量数据库实例,分别使用两种 embedding 模型:OpenAI 的 text-embedding-3-smalltext-embedding-3-large。虽然练习中使用的是 OpenAI 模型,但通过这些架构模式,你也可以很容易地切换模型提供商,并继续完成测试。

本章还将引入 Weights & Biases 来实现可观测性。你需要从
https://wandb.ai/
获取一个 key,不过即便不付费,也足以用来可视化 token 使用量和评估结果。

搭建一个可复现的 RAG 项目

本节中,我们将探讨如何搭建一个可复现的 RAG 项目。在构建 RAG 系统时,我们希望确保其可靠性值得信赖。如果我们提出一个问题,就希望系统给出的答案是有事实依据的,并且是可复现的。我们将通过几个 Python 脚本,来逐步完成一个包含 embedding 模型与 LLM 的完整 RAG 系统。

从图 6.1 中,我们可以识别出两条关键 Pipeline:索引 Pipeline检索 / 生成 Pipeline

image.png

图 6.1 —— 问答系统中的 Pipeline

下面我们更详细地看看这两条 Pipeline。

索引 Pipeline

索引 Pipeline 负责处理并准备数据,以便后续能够被高效查询。其涉及的步骤如下:

数据抽取(Data extraction)
从各种来源中提取原始文档或文本数据。

数据整理(Data wrangling)
对提取出的数据进行清洗、格式化和转换,去除不一致之处与无关信息。

数据切块(Data chunking)
将清洗后的数据拆分成更小的 chunk,以保证 embedding 和查询过程的高效性。

应用 embedding 模型(Application of embedding model)
将这些数据块输入 embedding 模型(例如 OpenAI embeddings),把它们转换成稠密向量表示。

向量存储(Vector storage)
将生成的向量 embedding 存入向量数据库,以便在检索阶段能够快速、准确地基于相似性查找相关信息。

检索与生成 Pipeline

当用户提出问题时,就会触发检索与生成 Pipeline。其过程包括以下步骤:

问题输入(Question input)
用户向系统提交一个问题或查询。

查询编码(Query encoding)
使用与索引 Pipeline 中相同的 embedding 模型,把问题编码为一个向量。

数据检索(Data retrieval)
将编码后的查询向量,与向量数据库中存储的文档向量进行比较,并基于向量相似性取回相关信息块。

数据排序(Data ranking)
如果系统中存在 cross-encoder ranker,则会对检索到的信息重新排序,以挑选出最相关的 chunk。

生成最终答案(Processing of final answer)
系统基于检索到的信息,为用户生成最相关、最精炼的答案。

这一系统打通了从原始文档到可执行答案的数据流。索引 Pipeline 负责准备数据,检索 Pipeline 负责依据用户查询找到相关信息,而嵌入在该 Pipeline 中的 LLM 则会把这些信息进一步加工成最终答案。整个系统高度依赖向量 embedding 与向量数据库,以确保检索高效、回答准确。

由于其结构相对简单,这一流程通常被称为 vanilla RAGnaive RAG。在本章中,我们将系统性地比较 naive RAG带 reranking 的 hybrid RAG,以展示:把稠密检索与稀疏检索结合起来,并引入 cross-encoder ranker,如何显著提升系统结构与效果。

而在做到这一点之前,我们首先要学习:如何把这些 Pipeline 拆解、保存并重用。

通往生产就绪系统的第一步,是演进代码结构。前几章中那种自包含的 Jupyter Notebook,非常适合实验和学习,但并不适合部署、版本控制,或团队协作开发。我们将采用项目 scripts 文件夹中所展示的结构化 Python 项目布局。关于每个脚本及其用途的详细说明,可在以下位置找到:

https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch6/jupyter-notebooks/scripts/README.md

这种关注点分离(也就是根据用途把 Pipeline 分开)将构成一个可部署应用的基础。项目结构位于 scripts/ 目录下,并按功能组织。贯穿这些脚本的一个关键模式,就是把 Pipeline 抽象为 Haystack SuperComponent。这是一种 Haystack 抽象,允许我们把 Pipeline 当作组件,接入更复杂的工作流中。

我们所遵循的蓝图,是建立在第 4 章与第 5 章中那些概念之上的:初始化单个组件,并把它们连接成一条 Pipeline。SuperComponent 的蓝图如下:

@super_component
class ExampleSuperComponent:
    def __init__(self, configuration_parameters):
        # Store configuration
        # Validate requirements (API keys, models, etc.)
        self._build_pipeline()

    def _build_pipeline(self):
        # 1. Initialize individual components
        # 2. Create Pipeline instance
        # 3. Add components to pipeline
        # 4. Connect components (define data flow)
        # 5. Define input/output mappings

通过这套蓝图,我们可以把单条 Pipeline 封装成一个可进一步配置的 SuperComponent。当进入实验阶段时,这会非常有用,因为它允许我们使用不同参数来初始化同一条 Pipeline。

为了简化 SuperComponent 的调用方式,我们还会定义自定义的输入 / 输出映射。这会让对 SuperComponent 的 run 调用变得更简洁。

举例来说,一个 naive RAG Pipeline 在调用 run() 方法时,原本可能要求我们显式传入诸如 text_embedder.textretriever.queryprompt_builder.question 等 socket 路径,以便从向量数据库中检索信息并回答问题。这种方式比较繁琐,也会引入不必要的复杂度。我们可以把这些 socket 路径抽象掉,用一个单独参数 query 来替代,如下所示:

# Input mapping: external inputs → internal component inputs
self.input_mapping = {
    "query": ["text_embedder.text", "retriever.query",
        "prompt_builder.question"]
}

# Output mapping: internal component outputs → external outputs
self.output_mapping = {
    "llm.replies": "replies",
    "retriever.documents": "documents"
}

把 Pipeline 抽象为 SuperComponent,并映射输入输出之后,就实现了以下几种好处:

  • 接口抽象:外部调用者不需要知道内部组件名称
  • 易于替换:你可以修改内部实现,而不影响外部调用方式
  • 并行处理:开发者可以把一个外部输入同时映射给多个内部组件

应用开发者可以把这些 SuperComponent 当作独立模块来导入使用,而无需理解其内部复杂的评估或监控逻辑。下面我们按照文件夹来拆解,看看如何把这一模式应用到 RAG 系统的不同阶段。

scripts/rag/

这个目录隔离出了核心应用逻辑。它包含我们在前几章中开发的关键 RAG Pipeline 的完整实现:

  • indexing.py
  • naiverag.py
  • hybridrag.py

它们构成了我们整个 RAG 工作流的基础。我们会先对一组文档(一个 PDF 和两个抓取的网站)执行索引 Pipeline,把其内容以 embedding 的形式存入向量存储;之后,就可以用 naive 或 hybrid RAG Pipeline 从向量数据库中检索信息。

接下来我们再看知识图谱与 synthetic data 生成部分。

scripts/synthetic_data_generation/

这个模块承载了第 5 章中开发的自定义组件,例如:

  • knowledge_graph_component.py
  • synthetic_test_components.py

接着,我们可以把整个知识图谱与 synthetic data 生成过程也构建成一条 Pipeline,并进一步抽象为一个 SuperComponent,正如 synthetic_data_generator_supercomponent.py 脚本中的 SDGGenerator SuperComponent 所展示的那样。

通过把知识图谱与 synthetic data 生成的四个组件放进同一条 Pipeline,再把它抽象成一个 SuperComponent,我们就把与知识图谱和 synthetic data 生成相关的所有逻辑都干净地封装了起来。这样做之后,我们就可以控制:

  • 数据来源(它们应与索引过程中使用的数据来源保持一致)
  • 测试集大小
  • query distribution
  • 构建图谱和问答对所使用的模型

当 synthetic data 生成完成后,我们就可以像第 5 章中那样,引入 Ragas,对 naive 或 hybrid RAG SuperComponent 基于 synthetic data 做评估。

scripts/ragas_evaluation/

这个目录包含测试执行逻辑,其中最重要的是 ragasevalsupercomponent.py

这个脚本包含三个组件:

  • CSVReaderComponent:读取 synthetic data,即问答对
  • RAGDataAugmenterComponent:用 naive 或 hybrid SuperComponent 的响应,对这份数据进行增强
  • RagasEvaluationComponent:使用 Ragas 指标进行评估,例如 faithfulness、context recall 和 factual correctness

这些组件可以被连接成一条 Pipeline,并进一步抽象为一个 RAGEvaluationSuperComponent SuperComponent——这实际上极大地简化了这整套工作流的复现过程,也使它能够轻松应用到不同规模的 synthetic dataset,以及不同配置的 retriever SuperComponent 上。

而这套工作流,还可以通过引入 Weights & Biases 这类平台来增强可观测性。

scripts/wandb_experiments/

这个目录隔离了我们的监控与可观测性逻辑,其中包含例如 rag_analytics.py 这样的脚本,用于更高级的成本追踪。

这种项目结构并不仅仅是文件整理方式上的便利,它本身就是一份协作蓝图。这套架构能够直接映射到 MLOps 团队中的不同角色:

  • rag/ 目录对应 NLP 工程师的职责范围(构建应用)
  • synthetic_data_generation/ragas_evaluation/ 目录对应 QA 或测试工程师的职责范围(保障质量)
  • wandb_experiments/ 目录则属于 DevOps、MLOps 或 LLMOps 工程师的职责范围(监控生产环境)

这种结构为应用的不同部分建立了清晰的归属边界与“数据契约”,而这对于同时扩展团队规模与产品规模都至关重要。

本节最后,我们再来强调一下本章中用到的一些关键软件开发工具。

本章中,我们会像前几章一样继续使用 uv,通过依赖管理来确保可复现性。前几章中,我们使用的是内存型 document store;它们对原型开发很有帮助,但在生产环境中,我们需要使用持久化 document store。因此我们将引入 Elasticsearch,并使用一个特定的 docker-compose.yml 文件来运行一个持久化的“双 document store”架构,用以保存两类 embedding:一类来自较小 embedding 模型,另一类来自较大 embedding 模型。

下面我们来看为什么要这么做。我们将使用的示例 docker-compose.yml 文件,可以在以下位置找到:

https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch6/docker-compose.yml

它将帮助我们模拟一个双 embedding document store 配置。当然,这个配置也可以修改,以适配其他 document store。下一节中,我们会更详细地讨论背后的原因。

关键架构论证:解释我们为何如此配置 Pipeline

本项目架构中还包含若干关键但不那么显而易见的工程决策。接下来这些论证,就是我们之所以如此搭建系统的“why”,也是构建健壮、优化良好的 RAG 系统所必需的。

向量空间奇点:为什么一个 embedding 模型必须统治一切

RAG 架构中有一条核心且不可妥协的规则:所有相关阶段必须使用同一个 embedding 模型

  • 索引阶段:为文档创建向量表示
  • synthetic data 生成阶段:为“ground truth”答案生成 embedding(如第 5 章所示)
  • 查询阶段:在运行时为用户问题生成 embedding

Haystack 文档本身就对此给出了明确警告:如果在索引时使用了诸如 sentence-transformers/all-MiniLM-L6-v2 这样的模型,那么用户查询也必须使用同一个模型来生成 embedding。

原因并不只是“最佳实践”层面的偏好,而是一个数学事实。一个 embedding 模型会创造出一个独特的高维宇宙,也就是所谓的向量空间(vector space) 。当我们为文档建立索引时,本质上是在把每一个文档 chunk 的语义位置,绘制到由那个特定向量空间定义出来的“地图”上。而当用户提出问题时,我们再次使用 embedding 模型,来确定这个问题在该空间中的坐标。这个 query vector,就相当于我们的“指南针”。

向量搜索的工作机制,就是在文档向量(地图上的位置)中,找到那些最接近查询向量(指南针位置)的点。如果“地图”和“指南针”不是在同一个宇宙里校准出来的,那么整个过程就会彻底失效。

在查询阶段使用与索引阶段不同的模型,就像某篇分析中所说的那样: “拿着巴黎的地图去东京找路。” 查询向量本身在它自己的向量空间中当然是有意义的,但在文档存储所在的向量空间中,它却在数学上几乎是随机且无意义的。

这种 “向量空间不匹配(vector space mismatch)” 会导致“完全无关、毫无意义的结果”,而这也是 RAG 应用中最常见、也最致命的失败模式之一。

在我们的实验里,由大 embedding 模型生成的向量,与由小 embedding 模型生成的向量,会被分别存入不同的向量存储中。接下来我们就来看原因。

解耦 document store:双 Elasticsearch 架构

我们项目中的一个显著特征,是在 docker-compose.yml 中定义的双 Elasticsearch 架构

所谓双 Elasticsearch 架构,是指部署两个彼此独立的 Elasticsearch 集群,以处理数据 Pipeline 的不同部分。这并不是一种冗余部署;而是一种特定的架构模式,用于支持 A/B 测试、独立资源分配,以及成本—性能优化。在这里,这种双架构将帮助我们研究:在使用不同 embedding 模型时,RAG Pipeline 的性能差异。

Haystack 的设计把 DocumentStore 视为一个灵活接口,因此这种模式在技术上是可行的。框架的模块化设计完全支持同时接入多个不同的 document store,甚至还支持诸如多 embedding 检索这样的高级策略。

这种双 store 架构带来了两项主要优势:

成本—性能 A/B 测试的蓝图

这套项目配置并不是为单条 Pipeline 服务的;它本质上是一套用于比较两条并行 Pipeline 的框架:一条 naive RAG Pipeline,以及一条 hybrid RAG Pipeline。正如我们后面将详细分析的那样,本章中的关键问题之一,就是比较小 embedding 模型与大 embedding 模型的成本收益比。而这套架构,正是用来在真实生产环境中开展这种分析的机制:

  • Pipeline A(small) 可以使用 ES_SMALL_URL,并用 text-embedding-3-small 生成的向量填充
  • Pipeline B(large) 可以使用 ES_LARGE_URL,并用 text-embedding-3-large 生成的向量填充

然后,我们就可以把同一个用户问题同时发送给两条 Pipeline,获取两份答案,并分别记录其性能(通过 Ragas)和每次查询成本(通过 rag_analytics.py)。

这样一来,我们就得到了一套实时、持续的 A/B 测试机制,用于找出最适合业务场景的成本—性能平衡点。

独立资源分配

small 实例通常优化为 512 MB 到 1 GB heap,而 large 实例则通常优化为 2 到 4 GB。这直接来自 embedding 模型本身的差异。

更大、更强的 embedding 模型,例如 text-embedding-3-large,会生成更高维的向量(3072 维),而小模型只生成 1536 维向量。这些更大的向量需要更多存储空间,而更关键的是,在向量数据库中执行高效最近邻搜索时,它们还需要更多 RAM。

通过把 document store 解耦,我们就可以据此合理配置基础设施。ES_LARGE_URL 对应的实例可以部署在高内存机器上,而 ES_SMALL_URL 则可以运行在成本更低的标准机器上。这样就能避免对“小模型 Pipeline”进行过度资源配置,从而节省大量基础设施成本。

有了可复现的项目结构,以及合理的架构基础之后,我们现在就可以进入定量评估阶段了。

使用 Ragas 进行系统化 Pipeline 评估

在第 5 章中,我们使用
advanced_branching_pipeline.ipynb
这个 Notebook,以及自定义的 KnowledgeGraphGenerator,构建了一份“黄金标准(golden)”测试集,其中包含问题及其 ground-truth 答案。现在,我们将利用这份测试集来评估两个 RAG SuperComponent:

  • NaiveRAGSuperComponent
  • HybridRAGSuperComponent

当我们比较这两个 SuperComponent 的 Ragas 聚合分数时,几乎肯定会观察到:混合 RAG Pipeline 表现出可验证的优势,尤其是在 Context PrecisionAnswer Relevancy 这两个指标上。这种差异,正是由第 4 章中分析过的架构差别直接导致的:

Naive RAG

它由一条单路径的稠密(语义)retriever 构成(ElasticsearchEmbeddingRetriever)。它非常擅长处理那些更加宽泛、概念性的问题——在这类问题中,用户意图比具体措辞更重要。

但它在关键词敏感型查询上存在根本性弱点。它会受到**词汇不匹配问题(vocabulary mismatch problem)**的影响,往往无法有效取回那些包含特定错误码、产品 SKU 或专业术语的文档,尤其当这些内容在 embedding 空间中表达不足时更是如此。

Hybrid RAG

它是一个更稳健的多路径系统。它会并行运行两个 retriever:

  • ElasticsearchBM25Retriever(稀疏、词法检索)
  • ElasticsearchEmbeddingRetriever(稠密、语义检索)

随后,它再通过一个 DocumentJoiner 组件,把两边结果合并起来。这样一来,它就能把两种方法互补的优势“融合”起来:既找到那些在概念上接近的文档,也找到那些包含精确关键词的文档。

在检索流程中,我们还可以在 chunk 被取回之后、最终答案生成之前,再增加一层增强机制:reranker

Reranker(通常是一个 cross-encoder 模型)会接收 hybrid retrieval 融合后的候选列表,并对其做最后一轮高精度重排,“显著提升召回性能”,从而确保只有最相关的文档会被传递给 LLM。在示例代码中,HybridRAGSuperComponent 默认已经加入了 SentenceTransformersSimilarityRanker 组件,用于对检索到的 chunk 进行排序。

当 naive 和 hybrid RAG 的 SuperComponent 初始化完成后,我们就可以系统化地运行评估了。这里使用的工具是 Ragas,它已经成为业界评估 RAG 的标准框架之一。

我们将重点关注该框架提供的四个关键指标,它们分别衡量 RAG 系统的两大核心部分:检索生成

Faithfulness(忠实度)
衡量生成答案与检索上下文之间的事实一致性。分数较低通常说明 LLM 出现了“幻觉”,即编造了上下文中不存在的信息。

Context precision(上下文精度)
衡量检索上下文的“信噪比”。它评估被检索到的文档 chunk 是否真的与问题相关。

Context recall(上下文召回)
评估 retriever 是否找到了完整回答问题所需的全部必要信息。

Answer relevancy(答案相关性)
一个端到端指标,用来评估最终生成的答案与原始用户问题之间的相关程度。

本章代码仓库中的 Jupyter Notebook,可以作为完整教程,帮助你上手如何使用 Ragas 评估 naive / hybrid RAG,并结合 Weights & Biases 引入可观测性。

一个用于演示 Ragas 初始配置的 Jupyter Notebook 可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch6/jupyter-notebooks/get_started_rag_evaluation_with_ragas.ipynb

在这个 Notebook 中,你将学到如何使用第 5 章中构建的 synthetic data generation 自定义组件,结合 Ragas 的 faithfulness、answer relevancy、context recallfactual correctness 等指标,对我们的 HybridRAGSuperComponent 进行评估。

这个入门 Notebook 本质上展示的是:如何评估一条单独的 Pipeline(在这里是被抽象为 SuperComponent 的 hybrid RAG Pipeline),并让它对着通过知识图谱和 synthetic data generator 生成的问答对进行测试。核心步骤包括:

  • 加载带有问题与答案的 synthetic data
  • 使用 HybridRAGSuperComponent 回答同样的问题
  • 再利用 Ragas 指标,以及一个充当“裁判”的 LLM,把 synthetic dataset 中的答案与 HybridRAGSuperComponent 给出的答案进行比较

随后,我们就可以把这一初始流程,进一步抽象成一种更系统化的方法:识别出评估中的关键步骤,将其封装为自定义组件,再把这些组件连接成一条评估 Pipeline。

练习把 Ragas 工作流抽象为 SuperComponent

一个用于比较 naive RAG 与 hybrid RAG,并借助组件与 SuperComponent 完成评估的 Jupyter Notebook,可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch6/jupyter-notebooks/ragas_evaluation_with_custom_components.ipynb

在这个 Notebook 中,我们将构建一条 Haystack 元 Pipeline(meta-pipeline) ——也就是一条专门用于运行并评估其他 Pipeline 的 Pipeline,而其核心就是 RagasEvaluationComponent

这个组件把 Ragas 框架的复杂逻辑封装在一个简单的 Haystack 接口之后。它的 run() 方法期望接收由我们 RAG Pipeline 生成的输入:

  • query
  • ground_truth(来自测试集的标准答案)
  • retrieved_documents
  • generated_answer

而它的输出则是一个字典,其中包含为每个指标计算出来的 Ragas 分数。

你可以利用这些脚本与 Notebook 运行各种实验。已执行的实验,会使用 Ragas 对第 4 章中介绍的 naive RAG(稠密检索)和 hybrid RAG(稠密 + 稀疏检索)进行对比。

你也可以修改这些脚本中的 Pipeline,尝试不同的 LLM 与 embedding 提供商及其模型,或者按照脚本中的模式,开发属于你自己的 Pipeline,并把它们抽象成 SuperComponent。

下面我们进一步看看评估是如何执行的。

执行评估

评估过程会遍历我们在第 5 章中基于一个 PDF 和两个 URL 地址生成的 synthetic dataset。对于数据集中的每一行(其中包含一个问题和一个 ground_truth 答案),工作流如下:

运行第一条 Pipeline:
用这个问题执行 NaiveRAGSuperComponent。它将返回 naive_answernaive_context(即检索到的文档)。

运行第二条 Pipeline:
用同样的问题执行 HybridRAGSuperComponent。它将返回 hybrid_answerhybrid_context

评估:
把这两组结果都送入 RagasEvaluationComponent

聚合:
把两条 Pipeline 各自得到的 Ragas 分数保存下来。

当整个测试集都处理完之后,我们就会得到两组聚合后的分数,从而形成对 naive RAG 与 hybrid RAG 系统的直接、量化比较。这些结果将构成后续数据驱动分析的基础。

表 6.1 展示了一次示例运行的结果:测试集包含 10 个问题,并使用了 single-hop 与 multi-hop 的查询策略。

表 6.1 —— 基于 10 组问答对的 Ragas 评估结果

MetricNaive RAGHybrid RAGImprovement (%)Better system
Faithfulness0.64110.962650.16Hybrid RAG
Answer relevancy0.66780.737410.42Hybrid RAG
Context recall0.68000.763312.25Hybrid RAG
Factual correctness (F1)0.35560.409015.03Hybrid RAG

表 6.1 中所体现出的显著提升,说明带 reranking 的 hybrid 方法威力极大。借助这一工作流,我们就能对 Pipeline 和 SuperComponent 进行客观评估。至此,我们已经准备好进入下一步:加入可观测性。

使用 Weights & Biases 增加生产级可观测性

到目前为止,我们已经对 Pipeline 的质量进行了静态评估。而一个生产就绪系统的最后一块拼图,是在真实环境中进行动态、持续的监控

这里首先必须区分评估(evaluation)可观测性(observability)

评估(Ragas)
这是一次静态、一次性的测试,类似单元测试或集成测试。它针对一份“黄金标准”数据集执行,以便在部署前获得系统质量的一个快照。

可观测性(Weights & Biases)
这是对生产环境中真实运行的 Pipeline 或 SuperComponent 进行持续、实时的监控。它对于理解真实用户查询、检测性能漂移、监控成本,以及追踪失败原因,都是必不可少的。

我们提供了一个示例 Jupyter Notebook,可用于在运行过程中通过 Weights & Biases 加入可观测性:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch6/jupyter-notebooks/add_observability_with_wandb.ipynb

在这个 Notebook 中,关键组件是 Haystack 的 WeaveConnector——这是一个面向 Weights & Biases 的集成组件。

这个组件展示了 Haystack 中一种很特别的模式:它不需要任何 .connect() 调用。相反,它通过环境变量(例如 HAYSTACK_CONTENT_TRACING_ENABLEDWANDB_API_KEY),自动拦截 Pipeline 中所有其他组件的 trace(包括输入、输出和元数据),并把这些信息发送到 Weights & Biases 仪表盘。它被加入 Pipeline 的方式如下:

pipe.add_component("weave", connector)

这个 Notebook 还使用了另一个附加脚本:
rag_analytics.py
它负责提供增强型分析能力。该脚本会记录 token 数量,并结合诸如 gpt-4o-mini 以及 embedding 模型的定价信息,计算每一次 Pipeline 运行的美元成本。

这项增强,让 Weights & Biases 从一个单纯的机器学习监控工具,升级成了一个 FinOps(财务运营)仪表盘。一个简单示例如图 6.2 和表 6.2 所示。

图 6.2 —— 示例仪表盘:跟踪两种 embedding 模型下的成本使用情况

表 6.2 —— 大 embedding 模型与小 embedding 模型的使用成本

Embedding modelTotal costLLM costEmbedding costAverage cost/query
Small embedding0.0093110.0083560.00095490.000931
Large embedding0.014450.0083980.0060530.001445

现在,产品负责人或工程师就可以登录 Weights & Biases,直接查看一个实时仪表盘,从而回答一些关键业务问题:

  • 昨天我们的 RAG Pipeline 总成本是多少?
  • 平均每次查询的成本是多少?
  • small 和 large 两条 Pipeline,哪一条在生产环境里更具成本效益?
  • 我们是否观察到与某类用户查询相关的成本激增?

能够把系统性能与财务成本直接关联起来,是将 RAG 应用真正作为一门业务来运营时,最关键、但也最不显而易见的能力之一。

接下来,我们来看一个成本—性能权衡尤为常见的具体场景。

通过分析 embedding 模型来探索成本—性能权衡

借助我们的双 Elasticsearch 架构,还可以开展第二项关键分析:embedding 模型的选择。我们将比较 OpenAI 的两款主流模型:text-embedding-3-smalltext-embedding-3-large,重点考察它们在性能(准确性)成本之间的权衡。

Massive Text Embedding Benchmark(MTEB) 上,text-embedding-3-large(平均得分 64.6%)始终且明显优于 text-embedding-3-small(平均得分 62.3%)。从定性研究来看,大模型在处理上下文理解与模糊问题时也表现更强,因为它额外的向量维度能够捕捉更细微的语义关系。

但这种更高性能也伴随着显著更高的成本:

  • text-embedding-3-small 的价格是 每 100 万 token 0.02 美元
  • text-embedding-3-large 的价格是 每 100 万 token 0.13 美元

这意味着,仅在 embedding 这一步上,大模型的成本就是小模型的 6.5 倍。这组数据呈现出一个非常典型的**边际收益递减(diminishing returns)**问题:我们必须判断,在 MTEB 上获得 2.3 个百分点(约 3.7% 的相对提升)的收益,是否值得付出 6.5 倍的成本增长。这一点也会进一步影响向量存储本身的成本。

对于绝大多数通用型 RAG 应用而言,text-embedding-3-small 都是更清晰的胜者,也是最佳的起点选择。它在性能与成本之间取得了极好的平衡:不仅明显优于旧的 text-embedding-ada-002,而且成本极低,因此是最具成本效益的方案。

text-embedding-3-large 更像是一种专用工具。它应当主要保留给那些高风险、垂直化的领域使用,例如法律、金融或医疗 RAG,在这些场景中,它对于复杂上下文的细腻理解能力,或许足以支撑其指数级上升的成本。

下面的表 6.3 和表 6.4 展示了使用我们 Notebook 跑出的一次示例结果,用来说明这种性能—成本之间的权衡。

表 6.3 —— 大 embedding 与小 embedding 的成本对比

MetricSmall embeddingLarge embedding
Total cost ($)0.0093960.014277
LLM cost ($)0.0084410.008224
Embedding cost ($)0.0009550.006053
Avg cost/query ($)0.0009400.001428

表 6.4 —— 大 embedding 与小 embedding 的 Ragas 指标表现对比

MetricSmall embeddingLarge embedding
Faithfulness0.8044440.747778
Context recall0.9266670.946667
Factual correctness0.5990.578
Response relevancy0.8690540.868255
Context entity recall0.3566030.415104

这也正是为什么我们的解耦双存储架构如此强大:它赋予了我们选择权。我们可以把小模型作为主应用中具有成本效益的默认方案,同时只在某些特定的高价值 Pipeline 中部署大模型,以利用其更先进的性能——而这一切,都发生在同一个统一项目之内。

小结

在本章中,我们完成了从 RAG 开发者RAG 架构师 的关键跃迁。我们不只是构建了一条 Pipeline;我们构建的是一个可复现、生产就绪的系统

我们首先采用了一种专业、模块化的项目结构,并为这种设计给出了论证,把它视为团队协作的一份蓝图。随后,我们为保证系统稳健性所依赖的核心架构决策进行了辩护:一方面,是要求始终使用同一种 embedding 模型的“向量空间奇点”;另一方面,是支持资源优化与实时 A/B 测试的解耦双数据库架构。

在这套坚实基础之上,我们借助 Ragas 构建了一条定量评估 Pipeline,使用第 5 章生成的 synthetic dataset,对 naive 与 hybrid RAG 实现进行了严格打分。借助这些数据,我们得以开展一场细致且数据驱动的成本—收益分析:一方面衡量 hybrid RAG 相较 naive RAG 在准确率上的提升是否值得其额外延迟;另一方面评估 text-embedding-3-large 相比 text-embedding-3-small 所带来的边际性能收益,是否足以支撑其 6.5 倍的成本。最后,我们又补上了一个生产系统最重要的“封顶石”:通过 Weights & Biases 实现持续可观测性,尤其是成本追踪能力。

整章内容,其实都是在为部署做最后准备。现在,我们的项目已经:

  • 通过 Docker 实现容器化
  • 通过 uv 锁定依赖
  • 将逻辑封装进可导入的 Python 脚本
  • 拥有健壮且可重复的评估系统(Ragas)
  • 拥有健壮且可重复的监控系统(Weights & Biases)

我们已经不再只是开发者,而是真正的架构师。至此,我们终于拥有了一整套完整的、端到端的系统,也终于准备好迈出最后一步:将 Haystack Pipeline 部署为一个可扩展、生产可用的 API

延伸阅读

Haystack. (n.d.). Tutorial: Creating Your First QA Pipeline with Retrieval-Augmentation.
haystack.deepset.ai/tutorials/2…

Jabloun, M. (n.d.). Don’t Break Your RAG: Why You Must Use the Same Embedding Model for Retrieval and Indexing.
medium.com/@mariem.jab…

DeconvoluteAI. (n.d.). Understanding Failures and Mitigation Strategies in RAG Pipelines.
deconvoluteai.com/blog/rag/fa…

Milvus. (n.d.). How do I use Haystack with different types of document stores?
milvus.io/ai-quick-re…

Haystack. (n.d.). Retrievers - Haystack Documentation.
docs.haystack.deepset.ai/v2.9/docs/r…

Madan, P. (n.d.). Evaluating Naive and Hybrid RAG using Weaviate and Athina.
medium.com/athina-ai/e…

Paul, K. (n.d.). Advanced RAG: From Naive Retrieval to Hybrid Search and Re-ranking.
dev.to/kuldeep_pau…

Superlinked. (2024). Optimizing RAG with Hybrid Search & Reranking | VectorHub.
superlinked.com/vectorhub/a…

Pinecone. (n.d.). Rerankers and Two-Stage Retrieval.
www.pinecone.io/learn/serie…

Tiger Data. (n.d.). Evaluating Open-Source vs. OpenAI Embeddings for RAG: A How-To Guide.
www.tigerdata.com/blog/open-s…

Enterprise Bot. (n.d.). Choose the best embedding model for your Retrieval-augmented generation (RAG) system.
www.enterprisebot.ai/blog/choose…

如果你愿意,我可以继续把下一章也按这一标准接着精译。