在前几章中,我们踏上了一段以实践为导向的旅程,来掌握 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-small 和 text-embedding-3-large。虽然练习中使用的是 OpenAI 模型,但通过这些架构模式,你也可以很容易地切换模型提供商,并继续完成测试。
本章还将引入 Weights & Biases 来实现可观测性。你需要从
https://wandb.ai/
获取一个 key,不过即便不付费,也足以用来可视化 token 使用量和评估结果。
搭建一个可复现的 RAG 项目
本节中,我们将探讨如何搭建一个可复现的 RAG 项目。在构建 RAG 系统时,我们希望确保其可靠性值得信赖。如果我们提出一个问题,就希望系统给出的答案是有事实依据的,并且是可复现的。我们将通过几个 Python 脚本,来逐步完成一个包含 embedding 模型与 LLM 的完整 RAG 系统。
从图 6.1 中,我们可以识别出两条关键 Pipeline:索引 Pipeline 与 检索 / 生成 Pipeline。
图 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 RAG 或 naive 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.text、retriever.query、prompt_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.pynaiverag.pyhybridrag.py
它们构成了我们整个 RAG 工作流的基础。我们会先对一组文档(一个 PDF 和两个抓取的网站)执行索引 Pipeline,把其内容以 embedding 的形式存入向量存储;之后,就可以用 naive 或 hybrid RAG Pipeline 从向量数据库中检索信息。
接下来我们再看知识图谱与 synthetic data 生成部分。
scripts/synthetic_data_generation/
这个模块承载了第 5 章中开发的自定义组件,例如:
knowledge_graph_component.pysynthetic_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 Precision 和 Answer 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 recall 和 factual 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 生成的输入:
queryground_truth(来自测试集的标准答案)retrieved_documentsgenerated_answer
而它的输出则是一个字典,其中包含为每个指标计算出来的 Ragas 分数。
你可以利用这些脚本与 Notebook 运行各种实验。已执行的实验,会使用 Ragas 对第 4 章中介绍的 naive RAG(稠密检索)和 hybrid RAG(稠密 + 稀疏检索)进行对比。
你也可以修改这些脚本中的 Pipeline,尝试不同的 LLM 与 embedding 提供商及其模型,或者按照脚本中的模式,开发属于你自己的 Pipeline,并把它们抽象成 SuperComponent。
下面我们进一步看看评估是如何执行的。
执行评估
评估过程会遍历我们在第 5 章中基于一个 PDF 和两个 URL 地址生成的 synthetic dataset。对于数据集中的每一行(其中包含一个问题和一个 ground_truth 答案),工作流如下:
运行第一条 Pipeline:
用这个问题执行 NaiveRAGSuperComponent。它将返回 naive_answer 与 naive_context(即检索到的文档)。
运行第二条 Pipeline:
用同样的问题执行 HybridRAGSuperComponent。它将返回 hybrid_answer 与 hybrid_context。
评估:
把这两组结果都送入 RagasEvaluationComponent。
聚合:
把两条 Pipeline 各自得到的 Ragas 分数保存下来。
当整个测试集都处理完之后,我们就会得到两组聚合后的分数,从而形成对 naive RAG 与 hybrid RAG 系统的直接、量化比较。这些结果将构成后续数据驱动分析的基础。
表 6.1 展示了一次示例运行的结果:测试集包含 10 个问题,并使用了 single-hop 与 multi-hop 的查询策略。
表 6.1 —— 基于 10 组问答对的 Ragas 评估结果
| Metric | Naive RAG | Hybrid RAG | Improvement (%) | Better system |
|---|---|---|---|---|
| Faithfulness | 0.6411 | 0.9626 | 50.16 | Hybrid RAG |
| Answer relevancy | 0.6678 | 0.7374 | 10.42 | Hybrid RAG |
| Context recall | 0.6800 | 0.7633 | 12.25 | Hybrid RAG |
| Factual correctness (F1) | 0.3556 | 0.4090 | 15.03 | Hybrid 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_ENABLED 和 WANDB_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 model | Total cost | LLM cost | Embedding cost | Average cost/query |
|---|---|---|---|---|
| Small embedding | 0.009311 | 0.008356 | 0.0009549 | 0.000931 |
| Large embedding | 0.01445 | 0.008398 | 0.006053 | 0.001445 |
现在,产品负责人或工程师就可以登录 Weights & Biases,直接查看一个实时仪表盘,从而回答一些关键业务问题:
- 昨天我们的 RAG Pipeline 总成本是多少?
- 平均每次查询的成本是多少?
- small 和 large 两条 Pipeline,哪一条在生产环境里更具成本效益?
- 我们是否观察到与某类用户查询相关的成本激增?
能够把系统性能与财务成本直接关联起来,是将 RAG 应用真正作为一门业务来运营时,最关键、但也最不显而易见的能力之一。
接下来,我们来看一个成本—性能权衡尤为常见的具体场景。
通过分析 embedding 模型来探索成本—性能权衡
借助我们的双 Elasticsearch 架构,还可以开展第二项关键分析:embedding 模型的选择。我们将比较 OpenAI 的两款主流模型:text-embedding-3-small 与 text-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 的成本对比
| Metric | Small embedding | Large embedding |
|---|---|---|
| Total cost ($) | 0.009396 | 0.014277 |
| LLM cost ($) | 0.008441 | 0.008224 |
| Embedding cost ($) | 0.000955 | 0.006053 |
| Avg cost/query ($) | 0.000940 | 0.001428 |
表 6.4 —— 大 embedding 与小 embedding 的 Ragas 指标表现对比
| Metric | Small embedding | Large embedding |
|---|---|---|
| Faithfulness | 0.804444 | 0.747778 |
| Context recall | 0.926667 | 0.946667 |
| Factual correctness | 0.599 | 0.578 |
| Response relevancy | 0.869054 | 0.868255 |
| Context entity recall | 0.356603 | 0.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…
如果你愿意,我可以继续把下一章也按这一标准接着精译。