在前几章中,我们的重点一直是:把预定义组件组装进 Pipeline,以实现特定目标——从数据摄取,到结合 Agent 的高级 RAG。本章标志着能力层级上的一次关键跃迁:从 Haystack 框架的使用者,转变为能够扩展它的架构师。本章的目标,是学习如何构建自定义组件,并将其集成到复杂 Pipeline 中,从而释放 Haystack 的全部潜力,以解决那些独特的、领域特定的问题。
要把 RAG 开发从一种“手艺活”提升为成熟的工程实践,就必须采用一种量化的、数据驱动的方法。本章将涵盖以下主题:
- 如何在 Haystack 中定义自定义组件
- 高级自定义组件特性的实现
- 自定义组件的测试与调试
技术要求
我们将继续使用 Jupyter Notebook 与虚拟环境,具体配置方式见代码仓库 ch5 文件夹中的 Setup Instructions 部分。关于环境配置的完整分步骤说明,可在该章节目录中找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/tree/main/ch5#setup-instructions
如何在 Haystack 中定义自定义组件
本章中,我们将重点关注自定义组件的创建,以及如何把这些自定义组件纳入 Haystack Pipeline。Haystack 中的自定义组件提供了极大的灵活性,使我们能够实现定制功能——无论是过滤结果、与外部软件交互,还是执行任何标准组件无法直接覆盖的任务。随着 Haystack 2.0 的到来,创建、复用和共享这些自定义组件都变得更加流畅,从而让你的 NLP Pipeline 拥有更强的可定制性与可扩展性。
接下来的小节中,我们将学习:构建一个可集成进 Haystack Pipeline 的自定义组件,需要满足哪些基本要求。
自定义组件的关键要求
本节将概述:如何定义一个之后可以作为 Haystack Pipeline 一部分来使用的自定义组件。要在 Haystack 中定义一个自定义组件,需要具备以下要素:
@component 装饰器:
这是将一个 Python 类注册到 Haystack 框架中的主要机制,表明该类可以被实例化,并在 Pipeline 中使用。正是这个装饰器,让一个类对 Pipeline 引擎“可见”,从而使 Haystack 能够管理它的生命周期并编排其执行。
__init__ 方法:
标准的 Python __init__ 方法充当组件的构造函数。它的主要职责是完成配置与依赖注入。那些在多次运行之间不会变化的静态参数,例如 API key、模型名称,或者与外部服务的连接,都应该在这里传入并保存为实例属性。通过 __init__ 方法让组件具备可配置性,是它能够在不同 Pipeline 和项目中复用的关键。
run() 方法:
每个组件都必须有一个 run() 方法;这是组件逻辑的入口,也是其功能的核心。该方法中包含了执行组件具体任务的代码——无论这个任务是获取数据、转换文本,还是调用 LLM。一个至关重要的规则是:run() 方法必须始终返回一个 Python 字典。这个字典的键,对应组件的输出 socket;值,则是要向下游已连接组件传递的数据。
@component.output_types 装饰器:
这个装饰器加在 run() 方法之上,用于声明组件会产出哪些输出类型和名称。这里声明的名称和类型,必须与 run() 方法返回字典中的键和值保持一致,从而保证数据流的一致性与可预测性。
基于这些要求,我们就可以把自定义组件定义为一些类,它们能够在 Pipeline 中与 Haystack 预定义组件协同工作。
Haystack 2.0 在架构上的一个最重要进步,是从 1.x 版本那种隐式的字典传递数据流,转向一种基于 “socket” 的显式、类型安全模型。这一设计选择,正是框架透明性与健壮性的基石。一个组件的数据契约(data contract)——也就是它接收什么输入、产出什么输出——会直接、显式地在代码中定义出来:
输入(Inputs):
输入 socket 的定义方式很简单:只需要把它们声明为 run() 方法签名中的带类型参数即可。Haystack 引擎会自动校验流入某个 socket 的数据,是否与该类型注解匹配,从而在早期就发现潜在的集成错误。
输出(Outputs):
输出 socket 则通过直接加在 run() 方法上方的 @component.output_types() 装饰器来定义。这个装饰器接收关键字参数,其中键是输出 socket 的名称——它必须与 run() 返回字典中的某个键一致;值则是对应的预期数据类型。
这种显式的数据契约,相较于 Haystack 1.x 中隐式传递字典的方式,是一个重大进步;后者往往很难追踪,也难以调试。它体现了更广泛的软件工程趋势:向显式接口靠拢,使得 RAG 开发不再是一种“黑盒式”的 NLP 工作,而更像一项标准的、可调试的软件工程实践。
框架中的 draw() 方法——它可以生成 Pipeline 的可视化图——正是这种设计哲学的终极体现:它把抽象代码变成了一张具体、可验证的图示。
接下来,我们就来实现第一个自定义组件,并将它集成进 Pipeline。
组件拆解:Prefixer
我们将使用一个包含自定义 Prefixer 组件的 Jupyter Notebook,它展示了构建健壮、可预测组件时的一些关键设计模式。这个组件的功能非常简单:对一组 Haystack Document 对象中的每个文档,都追加一个由用户指定的前缀。
一个用于探索简单 Prefixer 自定义组件定义方式的 Jupyter Notebook 可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/prefixed_custom-component.ipynb
下面我们来拆解它的实现逻辑。
run() 方法中的核心逻辑,会遍历输入列表中的每一个 Document 对象。对于每一个文档,它会执行两个关键动作:
不可变处理(Immutable processing):
它会创建一个新的 Document 对象。这是一种最佳实践,因为它避免了直接原地修改原始数据,而后者在复杂 Pipeline 中可能会引发不可预期的副作用。
元数据保留(Metadata preservation):
它会显式拷贝原文档的元数据到新文档中。这样就能确保诸如来源、ID 等重要信息不会在转换过程中丢失。
新文档的内容随后会被设置为格式化后的 f"{prefix}{doc.content}" 字符串,而这个新文档则会被加入 modified_documents 列表中。
在将其集成进 Pipeline 之前,Notebook 先通过独立运行该组件来验证其功能。初始化并验证这个独立组件的关键步骤如下:
实例化(Instantiate):
创建组件实例:
prefixer_instance = Prefixer()
执行(Execute):
直接调用 run() 方法:
result = prefixer_instance.run(
documents=documents, prefix="This is a prefixed document: ")
验证(Verify):
检查结果,确认它返回的是一个字典 {'documents': [...]},其中每个文档的内容都已经成功加上前缀,并且元数据保持完好。
虽然自定义组件的定义本身让我们能够创建符合自身需求的组件,但一个自定义组件真正的价值,在于它能够无缝集成到更大的 Haystack Pipeline 中。这个 Notebook 通过构建一条简单的 Pipeline 来演示这一点:把 Prefixer 组件的输出,直接送入 DocumentWriter 组件。
这条 Pipeline 会执行以下操作序列:
初始化 Pipeline 与组件
首先,初始化所需组件。这包括:我们的自定义 Prefixer、一个用于保存最终数据的 InMemoryDocumentStore,以及一个配置为使用该存储的 DocumentWriter 组件。初始化代码如下:
# Create an in-memory document store
document_store = InMemoryDocumentStore()
# Initialize our custom component
prefixer = Prefixer()
# Create a document writer component
writer = DocumentWriter(document_store=document_store)
# Create the pipeline
pipeline = Pipeline()
添加并连接组件
接下来,把组件添加到 Pipeline 中,并连接它们的 socket,以建立数据流。prefixer 自定义组件的输出 socket(名为 documents)会被连接到 writer 组件的输入 socket(同样名为 documents)。添加代码如下:
# Add components to the pipeline
pipeline.add_component("prefixer", prefixer)
pipeline.add_component("writer", writer)
# Connect the output of the prefixer to the input of the writer
pipeline.connect("prefixer.documents", "writer.documents")
执行 Pipeline
通过 run() 方法执行 Pipeline。关键在于:输入必须以一个字典形式提供,并且结构需要能正确把数据喂给 prefixer 组件。运行代码如下:
pipeline.run(
{"prefixer": {
"documents": documents,
"prefix": "This is a prefixed document: "}
}
)
这个动作会激活数据流。prefixer 接收到文档和前缀后,完成处理,再把输出结果(修改后的文档)传给 DocumentWriter,后者再将其保存进 InMemoryDocumentStore。
验证最终输出
最后,Notebook 会通过 .filter_documents() 直接查询 document_store。这一步会确认:数据库中的文档,正是那些经过我们自定义 Prefixer 组件处理后的文档,从而验证整条工作流执行成功。
在这个示例 Notebook 中,我们创建了一个能为 Haystack Document 对象追加前缀的自定义组件。接下来,我们要把注意力转向另一个问题:当自定义组件内部包含一些计算代价高昂的步骤(例如加载一个 Transformers 模型)时,如何确保它们只被加载一次。
管理状态与重资源:warm_up() 方法
在构建 AI Pipeline 时,一个常见挑战是如何管理那些代价高昂的一次性初始化任务,例如下载并加载一个数 GB 的语言模型,或者建立一个持久化数据库连接。
如果把这些任务放在 __init__ 方法中执行,会拖慢 Pipeline 的初始定义过程;而如果放在 run() 方法中执行,又会非常低效,因为资源会在每次执行时都被重新初始化。
为了解决这个问题,Haystack 提供了一个生命周期方法:warm_up()。这是一个特殊钩子,Pipeline 引擎会在某个组件实例的 run() 方法第一次被调用前,且仅调用一次。因此,它正是执行那些重型、一次性初始化任务的理想位置。
本节中,我们将通过一个简单示例,展示确保自定义组件兼容、并能够纳入 Haystack Pipeline 的最基本要求。
当你构建组件时,会面临一个关键问题:资源到底应该在哪里加载?
如果在 __init__ 构造函数中加载,那么组件初始化就会变得很慢。也就是说,哪怕只是简单地创建一个类实例(例如 my_embedder = MyEmbedder()),都会触发一段长时间的下载或模型加载流程,这既不灵活,也不高效。
而如果在 run() 方法中加载,那么组件就会极度低效。因为 run() 方法会在每一批数据到来时都被调用,这意味着你每处理一次数据,都要重新从磁盘加载整套模型,从而在每个操作上都引入巨大的延迟。
为了解决这一问题,Haystack 提供了一个标准化方案:warm_up() 方法。这个模式把组件逻辑清晰地拆分为三个阶段:
配置(Configuration)—— __init__(self, ...):
这个方法应当保持轻量、快速。它唯一的职责,就是保存配置参数,例如模型名称或 API key,而不应该加载任何重型资源。
初始化(Initialization)—— warm_up(self):
真正的“重活”应该放在这里。这个方法会在 Pipeline 启动时由 Haystack 调用。它的职责是加载模型、建立数据库连接,或完成任何其他一次性初始化工作,然后把这些资源保存到组件实例上。
处理(Processing)—— run(self, ...):
这是组件的主逻辑,在 Pipeline 执行期间会被反复调用,因此必须尽量保持快速。它会假定 warm_up() 已经执行过,从而可以直接使用预先加载好的资源。
下面我们来看一个例子:加载一个 Transformers 模型,并用它把文本转换成向量格式。
一个用于探索自定义组件定义的 Jupyter Notebook 可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/warmup_component.ipynb
该 Notebook 构建了一个这个模式的简单示例:一个名为 LocalEmbedderText 的自定义文本 embedding 组件,随后又被改造为 LocalEmbedderDocs,以便能够处理 Haystack Document 对象。该组件会加载一个 SentenceTransformer 模型来生成向量 embedding。下面我们来拆解它的“装配线”。
阶段 1 —— 配置(__init__)
首先,初始化组件。这个步骤几乎是瞬时完成的,因为它只是保存要使用的模型名称,并不会实际加载模型:
class LocalEmbedderDocs:
def __init__(
self, model_name:
str = "sentence-transformers/all-MiniLM-L6-v2"
):
self.model_name = model_name
# The model is not loaded yet
self.model: Optional = None
阶段 2 —— 初始化(warm_up)
这是真正的准备步骤。warm_up() 方法会检查模型是否已经被加载。如果还没有(即 self.model is None),它就会把体积较大的 SentenceTransformer 模型加载进内存,并赋值给 self.model。
如果 warm_up() 第二次被调用,它会发现 self.model 已经不再是 None,于是不会重复执行加载,从而避免不必要的重复开销。下面是一个示例:
def warm_up(self):
"""
Loads the SentenceTransformer model.
This is called only once before the first run.
"""
if self.model is None:
self.model = SentenceTransformer(self.model_name)
阶段 3 —— 处理(run)
这是组件真正执行“工作”的阶段。当 run() 方法接收到一组 Document 对象时,它会执行两个动作:
安全检查(Safety check):
首先,它会通过检查 self.model is None 来确认 warm_up() 是否已经执行过。如果模型尚未加载,它就会抛出一个 RuntimeError 作为警告。
执行(Execution):
它会从每个 Document 中提取文本内容,使用预先加载好的 self.model 生成 embedding,并返回一个标准输出字典。
这个模式的真正价值,会在组件被放入 Pipeline 中使用时体现出来。Notebook 通过构建一条完整的预处理工作流来展示这一点:从 PDF 文件开始,转换为 Haystack 文档,在 Pipeline 数据流中执行 document splitter,然后再把拆分后的文档送入我们的本地 embedder。
这里最关键的一点是:你不需要自己显式调用 warm_up() 。
当你执行 pipeline.run(...) 时,Haystack 会自动帮你管理这一切。在任何数据开始流动之前,Haystack 会先检查 Pipeline 中的所有组件;对于每一个实现了 warm_up() 方法的组件,它都会先调用一次这个方法。
这就确保了:当你的 PDF 数据被转换、清洗并切分完成时,LocalEmbedderDocs 组件已经完成“预热”,其模型已经加载到内存中,可以以最大速度生成 embedding。
正是这种关注点分离,使得 Haystack Pipeline 既能够保持极高的灵活性,又能拥有非常优秀的性能表现。
在本节中,我们学习了自定义组件定义的基础。接下来,我们将把这些知识应用到一个更高级的场景中:构建一组自定义组件,用于从 PDF 文件或抓取的网站中创建知识图谱,然后再利用该知识图谱从文档中生成问答对,以便后续用于评估一个 RAG 系统。
高级自定义组件特性的实现
在前面的章节中,我们已经学习了自定义组件定义的基础,包括 run() 方法,以及如何使用 warm_up() 方法来管理重型资源。现在,我们将把这些知识应用到一个更高级的场景中:构建一组自定义组件,先从文档中创建知识图谱,再利用这个图谱生成问答对,用于评估一个 RAG 系统。
在深入代码之前,必须先理解:为什么我们要把知识图谱作为中间步骤。乍看之下,似乎直接从文本 chunk 生成问题会更简单,但这种做法存在明显局限。依赖于对孤立文本块进行向量检索的标准 RAG 系统,通常很难处理复杂的多跳问题(multi-hop questions) ——也就是那些需要连接分散在多个文档或上下文中的信息,才能回答的查询。
而知识图谱恰恰擅长弥补简单向量检索的短板。它以一个网络来存储数据:节点(nodes) 代表实体(如人物、公司或概念),边(edges) 代表它们之间的关系。这种显式、连通的结构,使得复杂遍历与推理成为可能。知识图谱不仅表示“信息存在”,更表示“信息之间如何连接”;正因如此,它为生成真正能够测试 RAG 系统推理能力的问题,提供了必要的支架,使测试超越了简单事实检索。这种做法通常被称为 graph RAG,是当前构建更智能、可解释 AI 系统的先进技术。
通过先构建知识图谱,我们就能为问答数据生成过程提供一个丰富、结构化的上下文,从而使其能够生成更连贯、事实依据更强、复杂度更高的问题。这样生成出来的问题,考察的就不只是 RAG 系统能否找到某个单一事实,而是真正考察它的推理能力。正因为如此,graph RAG 被视为构建更智能系统的一种先进方法。
在本章中,我们将使用 Ragas 框架来创建知识图谱与合成问答对(synthetic question-answer pairs)。
Ragas 框架:RAG 评估的基础
当我们使用索引 Pipeline、语义 Pipeline 和混合 Pipeline 构建问答系统,并以生产化为目标时,仅仅声称某一条 Pipeline “比另一条更好”是远远不够的。我们必须从零散、经验式的 spot-checking,转向一种严格的、数据驱动的工程实践。为此,就需要一套专门工具,用来定量衡量 RAG 组件的性能。
而这,正是 Ragas 框架要解决的问题。Ragas 是一个开源 Python 库,它为评估 RAG Pipeline 提供了一整套完整工具。其完整文档可在以下链接中找到:
https://docs.ragas.io/en/stable/
它的核心设计哲学,是对 RAG 系统的两个主要部分——retriever 和 generator——既可以分别评估,也可以作为一个整体来评估。Ragas 的一个主要优势,在于它能够借助一组基于 LLM 的评估指标,提供高质量评估,而其中很多指标并不需要大量人工标注的“真实答案(ground-truth)”数据集。
虽然 Ragas 提供了很多指标,但其评估主要围绕 RAG 质量的四个关键维度展开:
Faithfulness(忠实度) :
衡量生成答案相对于检索上下文的事实一致性。这是对 LLM 幻觉程度的直接度量。
Response relevancy(回答相关性) :
评估生成出的答案与用户问题之间的相关程度。
Context precision(上下文精度) :
衡量检索上下文的“信噪比”。它评估被取回的上下文 chunk 是否真正与查询相关。
Context recall(上下文召回) :
评估检索到的上下文是否包含了回答问题所需的全部必要信息。
借助这些指标,我们就可以对 RAG Pipeline 做出客观、量化的评估。
不过,在评估一条 Pipeline 之前,我们首先需要一个高质量的测试数据集。而手工构造一组多样化、复杂、包含多跳推理的问题,是开发流程中的一个重大瓶颈。
这就引出了 Ragas 提供的第二项能力——也是对我们而言最关键的一项能力:合成测试数据生成(synthetic test data generation) 。这种 synthetic test data 由一组基于文档内容生成的问答对构成,并且会结合不同的人设(personas)与策略生成。我们 Pipeline 中的自定义组件,正是对这一高级 Ragas 方法论的直接实现。
Ragas 的 TestsetGenerator 使用了一种相当新颖的方法来解决测试数据问题:
知识图谱构建(Knowledge graph creation)
首先,它会接收一组源文档,并构建出一个知识图谱。这种基于图的表示方式,会显式刻画文本中的实体与关系,而不再停留在简单、孤立的文本 chunk 层面。
查询合成与合成数据生成(Query synthesis and synthetic data generation)
接下来,它利用这个结构化知识图谱来驱动一组查询合成器(query synthesizers) 。这些合成器被设计为能够生成不同复杂度的问题,这也是为什么最终生成的 CSV 文件中会包含一个 synthesizer_name 列。主要包括:
SingleHopSpecificQuerySynthesizer:
生成直接的、基于单一事实的问题。
MultiHopSpecificQuerySynthesizer:
生成复杂问题,这类问题需要连接文本不同部分中的具体事实才能回答。
MultiHopAbstractQuerySynthesizer:
生成高层次推理问题,这类问题需要在多个概念之间进行综合,才能得出答案。
我们将在自定义组件中模仿这一设计,并让两个自定义组件分别映射到其中的两个步骤:KnowledgeGraphGenerator 与 SyntheticTestGenerator。
通过构建 KnowledgeGraphGenerator 和 SyntheticTestGenerator 这两个组件,我们实际上是在将 Ragas 首创的这套工作流“工程化落地”。我们利用这一先进技术,来创建一个健壮、具有挑战性、并且足够多样化的评估数据集,以便真正测试我们 RAG Pipeline 的推理能力。
这个练习的完整实现,已经通过两个独立脚本放在了补充代码中。
实现 KnowledgeGraphGenerator 自定义组件
一个实现 KnowledgeGraphGenerator 自定义组件的脚本可在以下 URL 找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/scripts/knowledge_graph_component.py
KnowledgeGraphGenerator 自定义组件的工作方式,是借助 Ragas 框架,将一组扁平的文档列表转换成一个结构化、彼此连接的网络。
这个过程首先从摄取文档开始,并将它们转换为图结构中的初始节点。这里使用的关键技术是 transform,具体来说是通过 apply_transforms 方法,把这些彼此孤立的节点,升级为一张丰富的数据网络:显式识别实体,并绘制出它们之间的连接关系。这种结构之所以关键,是因为它使系统从单纯的文本存储,迈向了能够表示不同信息片段之间逻辑关联的形式,从而支持复杂推理。
kg = KnowledgeGraph()
for doc in documents:
kg.nodes.append(
Node(
type=NodeType.DOCUMENT,
properties={
"page_content": doc.page_content,
"document_metadata": doc.metadata
}
)
)
default_transforms_config = default_transforms(
documents=documents,
llm=transformer_llm,
embedding_model=embedding_model
)
apply_transforms(kg, default_transforms_config)
这个图的构建,在很大程度上依赖于组件初始化时提供的两个模型所扮演的不同角色。
LLM 充当推理引擎:它会扫描文档节点中的内容,提取出具体实体(如人物、概念或组织),并识别它们之间的关系,从而有效构建出图中的边。
与此同时,embedding 模型 则提供构建图所需的语义理解能力:它帮助系统分析文本,并衡量概念之间的相似性,从而确保形成的连接在上下文上是合理且准确的。
这两个模型协同工作,使该组件能够生成一种 graph RAG 结构,从而为后续生成复杂、多跳问题提供基础。
最终产出的是一个知识图谱。它既可以通过同一脚本中的 KnowledgeGraphSaver 自定义组件保存为 JSON 格式,也可以作为 Python 数据结构,继续传递下去,用于生成合成问题。接下来我们就来看,如何通过另一个自定义组件,基于知识图谱生成合成数据。
实现 SyntheticTestGenerator 自定义组件
一个实现 SyntheticTestGenerator 自定义组件的脚本可在以下 URL 找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/scripts/synthetic_test_components.py
SyntheticTestGenerator 自定义组件的作用,是把一个结构化知识图谱转换成一个多样化、高质量的评估数据集。它不会随机抽样文本,而是通过预先配置好的 query_distribution 来实例化特定的 synthesizer,例如 single-hop specific、multi-hop specific 和 multi-hop abstract query synthesizer。这些 synthesizer 会在 SyntheticTestGenerator 自定义组件定义中的另一个方法 create_query_synthesizers 中被创建。
这些 synthesizer 会沿着上游 KnowledgeGraphGenerator 提供的知识图谱中的节点与边进行遍历,找出那些能够用于生成复杂单跳与多跳问题的关系结构,从而真正测试 RAG 系统的推理能力。
这种架构选择确保了最终生成的测试集,不再只是一组孤立事实的集合,而是对系统“连接不同信息碎片能力”的一次全面检验。
为了确保我们的自定义组件足够稳健,我们在执行逻辑中实现了一个**首选路径 + 回退(fallback)**的设计模式。
虽然基于知识图谱生成问题是首选方法,因为这样产生的问题复杂度更高,但一个健壮的组件在图谱生成失败或结果为空时,不能直接崩溃。为此,我们把生成逻辑包裹在 try-except 结构中:如果知识图谱路径抛出异常,组件就会自动优雅降级,退回到基于文档的生成方法。这保证了即使在条件不理想的情况下,Pipeline 依然总能生成一份可用测试集,确保数据流不中断。
try:
testset = self._generate_from_knowledge_graph(knowledge_graph)
method = "knowledge_graph"
except Exception as kg_error:
logger.warning(
f"Knowledge graph generation failed: {kg_error}. "
"Falling back to document-based generation."
)
testset = self._generate_from_documents(documents)
method = "documents_fallback"
你会注意到:我们把“基于知识图谱生成测试集”和“基于文档生成测试集”的逻辑,分别封装在各自独立的方法中。这样一来,如果其中某个路径出问题,我们就能更容易地隔离与定位故障。
基于知识图谱的合成数据生成,使用的是 Ragas 的 TestSetGenerator 类。它会结合一个 LLM 和一个 embedding 模型,从图中生成问题,进而构造问答对:
def _generate_from_knowledge_graph(self, knowledge_graph: KnowledgeGraph):
"""Generate tests using a knowledge graph."""
try:
generator = TestsetGenerator(
llm=self.llm,
embedding_model=self.embeddings,
knowledge_graph=knowledge_graph
)
而我们自定义组件中的 _generate_from_documents 回退方法,也同样使用 TestSetGenerator,只不过它会直接解析文档,而不是使用知识图谱。
这里用到的 LLM 与 embedding 模型,可以来自 Haystack 支持的任意 generator。在配套 Notebook 中,我们展示了如何使用 OpenAI 和 Ollama 来生成知识图谱;但它们也都可以很容易地替换为其他 generator 和 embedding 模型。
通过这些脚本,我们将利用知识图谱组件与问答数据集生成组件,构建两条 Pipeline:一条用于 PDF 文件,另一条用于抓取的网站。这正是一个把已有 Haystack 组件与我们自定义组件结合起来的优秀示例。
接下来我们先看第一条 Pipeline。
为了完整理解这个知识图谱与合成数据 Pipeline 的架构,我们必须逐个理解每个组件是如何工作的。这个工作流,是从“Haystack 框架的使用者”迈向“架构师”的一个清晰示例。我们正在把预构建组件和自定义组件组合起来,以解决一个独特的、领域特定的问题:构建一个高质量的评估数据集。
这个 Pipeline 在逻辑上可以被划分为四个清晰阶段:
- 摄取与预处理(Ingestion and preprocessing) :把原始文件转换为标准化、干净的数据 chunk
- 格式桥接(Format bridging) :把 Haystack 原生对象转换为我们自定义逻辑所需的格式
- 知识图谱构建(Knowledge graph creation) :一个自定义组件,用于把文本 chunk 转换为结构化图谱
- 合成数据生成(Synthetic data generation) :第二个自定义组件,用于基于该图谱生成复杂问题
下面我们将遵循前述技术文档的原则与风格,更细致地拆解这个合成数据生成 Pipeline 中各组件的工作方式。
从 PDF 构建知识图谱与合成数据生成 Pipeline
下面我们来拆解这样一个过程的关键步骤:输入一组文档语料(在我们的例子中是一份 PDF),基于其内容构建知识图谱,再生成一份后续可用于评估 RAG Pipeline 的合成数据集。
我们把它分成以下关键阶段:
摄取与预处理
这一初始阶段的核心目标,是把来自不同来源的原始非结构化数据,转换为一种标准化、干净的形式,以便下游 NLP 任务使用。
PyPDFToDocument
这是入口组件。它的职责是执行最初的“获取并转换”操作。PyPDFToDocument 会读取本地 PDF 文件,并产出标准化的 List[HaystackDocument] 对象,从而让 Pipeline 后续部分不必关心内容来源本身。
DocumentCleaner
该组件执行一个关键预处理步骤:去除多余空白字符与空行。这些噪声可能会干扰 DocumentSplitter 的切分逻辑。
DocumentSplitter
这个组件被配置为 split_by="sentence" 且 split_length=5 来切分清洗后的文档。这是一个关键设计决策。通过把大文档拆分成可管理、语义连贯的 chunk,我们就能确保 KnowledgeGraphGenerator 接收到的是高质量、聚焦明确的输入,这对构建准确图谱至关重要。
这一预处理阶段确保了:我们最终生成的 synthetic data,既牢牢扎根于我们选定的原始资料,又具备较高质量。
不过,在真正开始构建知识图谱与合成数据之前,还需要再经过一个额外处理步骤。我们接下来就看它。
桥接组件(DocumentToLangChainConverter)
在构建自定义 Pipeline 时,一个关键架构要求是:保证组件之间的数据兼容性。
DocumentToLangChainConverter 是我们自己创建的一个“桥接”组件。Haystack 原生组件(例如 DocumentSplitter)输出的是 List[HaystackDocument] 对象;而驱动我们核心逻辑的 Ragas 框架,要求输入的是 List[LangChainDocument] 对象。
这个组件的 run() 方法非常直接:它遍历 Haystack 文档列表,拷贝其 .content 和 .meta 属性,并实例化新的 LangChainDocument 对象。正是这种无缝转换,让我们能够把基于 Ragas 的自定义组件顺利接入 Haystack Pipeline。也正因如此,数据在进入知识图谱构建阶段之前,已经具备了正确的数据结构形式。
核心组件拆解 —— KnowledgeGraphGenerator
这是我们的第一个高级自定义组件,它是基于本章介绍的原则构建出来的。它的设计目标,是创建一个结构化知识图谱,作为测试数据生成的基础。
该组件的 __init__ 方法保持轻量,只快速保存配置,例如 LLM、apply_transforms 标志,以及 Ragas 的包装对象。随后,主 run() 方法会接收文档,把它们作为简单、孤立节点加入一张新的知识图中。然后进入最关键的一步:如果 apply_transforms 为 True,它就会借助 Ragas 及其 LLM,构建 graph RAG 所需的智能结构——即通过处理这些节点,抽取实体、识别关系,并创建节点之间的连接(边)。最后,该组件会返回一个包含成品知识图谱的字典,供 Pipeline 下一阶段继续使用。
最后一步:利用知识图谱构建代表性 synthetic dataset
最后,我们要用这个知识图谱,生成一个由问答对构成的 synthetic dataset,并且这些问答对要遵循不同的问题生成策略。
核心组件拆解 —— SyntheticTestGenerator
该组件会消费知识图谱,并生成最终评估数据集。它展示了一种更成熟的 Pipeline 设计方式:首选路径 + fallback 机制。
其 __init__ 构造函数会完成组件配置,保存 testset_size 和 llm_model 等参数。最重要的是,它会保存 query_distribution:
query_distribution=[
("single_hop", 0.25),
("multi_hop_specific", 0.25),
("multi_hop_abstract", 0.5)
]
这一配置会直接告诉组件:应该生成哪些类型的问题,从而确保我们的测试集既多样化,又足以对 RAG 系统的推理能力形成挑战。下面我们来拆解这些不同类型的 query distribution synthesizer:
single_hop:
该 synthesizer 生成直接的事实型查询。这是最简单的一类问题,通常只需要单一信息或上下文即可回答。25% 的问题会属于这一类。
例如:
“Who is Christopher Ong in the context of ChatGPT research?”
multi_hop_specific:
该 synthesizer 生成需要连接具体事实的问题。为了回答这类问题,RAG 系统必须从源文档的多个不同部分中检索并综合信息。25% 的问题会属于这一类。
例如:
“What are the key improvements in Haystack 2.0 compared to Haystack 1.0, particularly regarding the handling of loops and customizable components?”
multi_hop_abstract:
该 synthesizer 生成需要跨多个概念进行更广泛推理的问题。这通常是最复杂的一类问题,会要求系统总结、比较,或解释一个高层主题,而这依赖于综合多个不同、甚至可能抽象的想法。50% 的问题会属于这一类。
例如:
“What are the primary functions of ChatGPT usage at work, and how does the quality of interactions reflect user satisfaction?”
这保证了我们能够构建出一个丰富的 synthetic dataset,覆盖多种不同类型的问题。
接下来我们来看这个组件的核心:run() 方法。
run() 方法被设计为能够接收来自两个上游组件的输入:KnowledgeGraphGenerator 和 DocumentToLangChainConverter。它有两种生成模式:
- 首选模式:使用现成知识图谱生成多样化测试集。因为只有基于知识图谱,才能可靠地生成复杂的多跳问题——通过分析图中的关系结构来完成。
- 回退模式:如果没有可用知识图谱,则退回到直接基于文档 chunk 生成较简单的单跳问题。
无论采用哪种方法,该组件都会把结果输出为一个 pd.DataFrame 对象,并传递给 TestDatasetSaver 组件。
这个组件正是我们 synthetic data 生成流程的核心,也是整条 Pipeline 的最后一步。
这些组件的初始化与连接方式,与我们在前几章中学到的一致。下面我们来看看:如何根据它们的输入 / 输出 socket 契约,把这些组件初始化并连接起来。
将自定义组件集成进 Pipeline
这些组件真正的价值,要在它们被纳入一个更大的 Haystack Pipeline 时才能体现出来。配套 Notebook 就展示了这一点:它构建了一条端到端工作流,用来处理 PDF、创建知识图谱、生成测试数据,并将其保存下来。
一个用于探索“从 PDF 生成知识图谱与 synthetic data”这一 Pipeline 实现的 Jupyter Notebook,可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/pdf_knowledge_graph_pipeline.ipynb
这条 Pipeline 会按以下顺序执行操作:
初始化组件
首先,初始化所需组件,包括 Haystack 内置组件以及我们新创建的自定义组件:
# Built-in Haystack components
pdf_converter = PyPDFToDocument()
doc_cleaner = DocumentCleaner(...)
doc_splitter = DocumentSplitter(...)
# Our custom components from the .py files
doc_converter = DocumentToLangChainConverter()
kg_generator = KnowledgeGraphGenerator(apply_transforms=True)
test_generator = SyntheticTestGenerator(testset_size=10, ...)
test_saver = TestDatasetSaver("data_for_eval/...")
一旦初始化完成,我们就可以像搭建普通 Haystack Pipeline 一样,把这些组件加入并连接起来。
添加并连接组件
接下来,把组件添加进 Pipeline,并按照它们的 socket 契约把它们连接起来,以定义数据流:
# Create the pipeline
pipeline = Pipeline()
# Add all components
pipeline.add_component("pdf_converter", pdf_converter)
pipeline.add_component("doc_cleaner", doc_cleaner)
pipeline.add_component("doc_splitter", doc_splitter)
pipeline.add_component("doc_converter", doc_converter)
pipeline.add_component("kg_generator", kg_generator)
pipeline.add_component("test_generator", test_generator)
pipeline.add_component("test_saver", test_saver)
# Define the data flow
pipeline.connect("pdf_converter.documents", "doc_cleaner.documents")
pipeline.connect("doc_cleaner.documents", "doc_splitter.documents")
pipeline.connect("doc_splitter.documents", "doc_converter.documents")
pipeline.connect("doc_converter.langchain_documents", "kg_generator.documents")
pipeline.connect("doc_converter.langchain_documents", "test_generator.documents")
# Connect the KG to the test generator
pipeline.connect("kg_generator.knowledge_graph", "test_generator.knowledge_graph")
# Connect the test generator to the saver
pipeline.connect("test_generator.testset", "test_saver.testset")
这种连接策略本身就是一个关键设计模式。test_generator 同时接收来自 doc_converter 的输入(用于 fallback)以及来自 kg_generator 的输入(用于首选路径),这使整条 Pipeline 更加稳健。
执行 Pipeline
最后,通过 run() 方法执行 Pipeline,并为第一个组件指定初始输入:
pdf_sources = [Path("./data_for_indexing/howpeopleuseai.pdf")]
result = pipeline.run({"pdf_converter": {"sources": pdf_sources}})
这一步会激活整条数据流。PDF 会先被转换、清洗、切分,再被转换为知识图谱;该知识图谱随后被用来生成测试集,而最终测试集会被保存为 CSV 文件。
接下来,我们来看看这条 Pipeline 所生成的 synthetic data。
分析生成出的测试数据(PDF)
在 PDF 文档上运行这条 Pipeline 后,会生成一个名为 synthetic_tests_10_from_pdf.csv 的文件,可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/data_for_eval/synthetic_tests_10_from_pdf.csv
其中包含了完整的 synthetic dataset。表 5.1 展示了其中两种查询策略生成的数据样例。需要注意的是:由于知识图谱与 synthetic data 生成过程具有非确定性,因此每次执行脚本时,具体问题都会发生变化。
表 5.1 —— synthetic data 示例:包含 question、answer 与 synthesizer
| Question | Answer (Reference) | Strategy |
|---|---|---|
| “Who is Christopher Ong in the context of ChatGPT research?” | “Christopher Ong is one of the co-authors of the NBER Working Paper No. 34255 titled 'How People Use ChatGPT.' He is affiliated with Harvard University and OpenAI.” | Single-hop-specific query synthesizer |
| “How does the usage of ChatGPT for Practical Guidance differ among users with varying education levels, particularly in relation to work-related messages and the types of requests made?” | “The usage... shows notable differences... 36% of Practical Guidance messages are requests for Tutoring or Teaching... In terms of work-related messages, 37% are sent by users with less than a bachelor’s degree, compared to... 48% for users with some graduate education.” [cite: 985, 987, 988] | Multi-hop-specific query synthesizer |
第一个问题是一个单跳查询(single-hop query) ;它只需要单一信息即可回答。第二个问题则是一个多跳查询(multi-hop query) 。它要求系统综合关于三个不同概念的信息:practical guidance 的使用情况、教育水平,以及与工作相关的消息。正是这种复杂、高价值的问题类型,才是知识图谱所要支持生成的核心目标。
接下来,我们会看到:如何非常自然地把这条 Pipeline 改造成另一种形式,用来从抓取的网站中生成 synthetic data。
从抓取的网站构建知识图谱与合成数据生成 Pipeline
这正是 Haystack 组件化架构的模块化与灵活性真正发光的地方。为了处理一个实时网页,而不是本地 PDF,我们只需要替换最前端的数据摄取组件。Pipeline 的核心逻辑——包括 DocumentCleaner、DocumentSplitter、DocumentToLangChainConverter、KnowledgeGraphGenerator 和 SyntheticTestGenerator——保持完全不变。我们对 Pipeline “头部”所做的唯一调整是:
- 移除:
PyPDFToDocument - 新增:
LinkContentFetcher(用于从 URL 抓取内容)和HTMLToDocument(用于把原始 HTML 转换为 HaystackDocument)
一个用于探索“从网站 URL 生成知识图谱与合成数据”这一 Pipeline 实现的 Jupyter Notebook,可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/web_knowledge_graph_pipeline.ipynb
这很好地展示了我们工作流的可复用性。知识图谱生成过程本身与内容来源无关;无论面对的是处理后的 Haystack 2.0 博客文章 HTML 内容,还是 PDF 内容,它都同样有效。
分析由网页生成的数据(HTML)
| Question | Answer (Reference) | Strategy |
|---|---|---|
| “What does HyDE do in Haystack?” | “HyDE is an optimization technique that can be used in Haystack pipelines with custom components.” | Single-hop-specific query synthesizer |
| “What are the key improvements in Haystack 2.0 compared to Haystack 1.0, particularly regarding the handling of loops and customizable components?” | “Haystack 2.0 introduces significant improvements... In Haystack 1.0, the pipeline graph was acyclic... Haystack 2.0 allows for cycles in the pipeline graph... Additionally, Haystack 2.0 emphasizes a technology-agnostic design... and supports the creation of custom components...” | Multi-hop-specific query synthesizer |
表 5.2 —— synthetic data 示例:包含 question、answer 与 synthesizer
和 PDF 的例子一样,这类问题依然需要综合源文本中分散在不同位置的信息。正如我们在第 4 章中学到的,我们可以通过引入 router,把来自 PDF 的文档与来自 URL 的文档统一并流。下面,我们就用一个例子来结束这一部分。
高级 Pipeline 架构:统一多种数据源
在前面的章节中,我们已经成功把自定义组件集成进单一来源的 Pipeline 中,无论来源是 PDF 还是网页。接下来,我们旅程中的最后一步,是展示 Haystack 2.0 架构真正的灵活性与模块化能力:构建一条单一的高级 Pipeline,使其能够同时摄取、处理并统一多种数据类型。
一个用于探索“同时处理 URL 与 PDF,并生成知识图谱与 synthetic dataset”这一 Pipeline 实现的 Jupyter Notebook,可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/advanced_branching_pipeline.ipynb
这个脚本构建了一条复杂的分支型(branching)Pipeline。这种设计模式对于真实世界应用至关重要,因为现实中的知识往往分散在不同的数据孤岛中(例如内部文档与公共网站)。
这种架构形成了一种“漏斗效应(funnel effect)”,可以拆分为三个阶段:
并行摄取(Parallel ingestion)
当 Pipeline 运行时,它会同时接收两条分支的输入。file_router 会把 PDF 路由给 PyPDFToDocument 转换器;与此同时,LinkContentFetcher 会把网页数据流送给 HTMLToDocument 转换器。
汇合(Unification)
DocumentJoiner 会收集来自 PDF 和网页两条分支处理后的 Document 对象。
统一处理(Unified processing)
从这一刻开始,Pipeline 与我们前面构建的简化版本完全相同。合并后的单一文档列表会依次传递给 doc_cleaner、doc_splitter,然后再进入我们的自定义 doc_converter。
下游的自定义组件 KnowledgeGraphGenerator 与 SyntheticTestGenerator,完全不需要知道这些数据原本来自两个不同来源。得益于设计中的关注点分离,它们接收到的只是一个统一的文档列表,并基于它构建一张单一、完整的知识图谱,以及一份高质量的统一测试数据集。
接下来,我们将通过一个示例来结束这一节:展示如何把这些自定义组件集成进一条同时处理 HTML 页面与 PDF 文件、并从中抽取和生成 synthetic data 的 Pipeline 中。
分析生成出的数据(HTML 与 PDF)
在 Haystack 2.0 发布博客 URL 上运行这条改造后的 Pipeline,会生成一个新的测试集 synthetic_tests_advanced_branching.csv,文件位于以下位置:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/jupyter-notebooks/data_for_eval/synthetic_tests_advanced_branching.csv
这一次,它抓取的数据来源如下:
pdf_file = Path("./data_for_indexing/howpeopleuseai.pdf")
web_urls = [
"https://www.bbc.com/news/articles/c2l799gxjjpo",
"https://www.brookings.edu/articles/how-artificial-intelligence-is-transforming-the-world/"
]
# Run pipeline with both input types
result = pipeline.run({
"file_router": {"sources": [pdf_file]},
"link_fetcher": {"urls": web_urls}
})
和之前一样,我们再次看到由不同 synthesizer 生成的、类型分布健康的问题集合,只不过这次它们来自两种不同的数据源。仍然需要注意的是:每次运行 Pipeline,最终生成的问题都会发生变化。
| Question | Answer (Reference) | Strategy |
|---|---|---|
| “How does Alexa utilize artificial intelligence in its functionality?” | “Alexa is a voice-controlled virtual assistant that uses artificial intelligence to process large amounts of data, identify patterns, and follow detailed instructions.” | Single-hop-specific query synthesizer |
| “What are the implications of legal liability in the context of improving data access for AI development?” | “The implications of legal liability in the context of improving data access for AI development are significant. A body of case law indicates that liability is determined by the facts and circumstances of a situation, which can lead to various penalties, including civil fines or imprisonment…” | Multi-hop abstract query synthesizer |
表 5.3 —— synthetic data 示例:包含 question、answer 与 synthesizer
和 PDF 示例一样,这条 Pipeline 既能成功生成简单的单跳问题,也能生成复杂的多跳问题。这里的多跳问题要求系统把 “Haystack 2.0 的改进”、“循环的处理方式” 以及 “可定制组件” 这些概念连接起来,而这些信息本身是散布在源文本不同位置上的。
通过这三个 Notebook,以及其中的知识图谱生成器与 synthetic data 生成器这两个自定义组件,我们已经展示了:如何从一个简单的 Prefixer 组件,逐步成长为能够设计一个稳健的、多源摄取与测试数据生成系统的架构师;这一切,都是通过创建并连接模块化、可复用的自定义组件实现的。
接下来,我们将通过概览一些可以用于测试自定义组件的技术,来结束本章。
自定义组件的测试与调试
在本章中,我们已经从 Haystack 框架的使用者,转变为能够扩展它的架构师。而这场转变中的关键一环——也是把 RAG 开发从“手艺活”推进到“成熟工程实践”的关键——就是具备编写健壮、可预测且可调试代码的能力。
练习为自定义组件编写测试
本章中涉及组件的一系列测试,可在以下位置找到:
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/tree/main/ch5/tests
我们的自定义组件,尤其是处于复杂多阶段 Pipeline 中时,与任何其他生产软件并无不同:它们都必须经过严格测试。
提供的测试脚本如下:
test_warmup_components.py
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/tests/test_warmup_components.pytest_synthetic_test_components.py
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/tests/test_synthetic_test_components.pytest_knowledge_graph_component.py
https://github.com/PacktPublishing/Building-Natural-Language-and-LLM-Pipelines/blob/main/ch5/tests/test_knowledge_graph_component.py
这些脚本共同展示了一种示例性的测试策略,用于验证自定义组件的每一个关键部分。下面我们来拆解其中一些核心原则,这些原则同样可以应用到你自己的组件中。
组件测试的关键原则
从这些测试文件中可以看到,我们的测试策略并不仅仅是验证“功能是否正确”。它还关心:组件是否健壮、是否高效、以及是否能与 Haystack Pipeline 正确交互。下面我们概述这一测试策略的几项关键原则。
使用 mocking 隔离组件逻辑
测试组件时最重要的原则,就是隔离要测试的逻辑单元。我们的组件往往依赖外部服务(例如 OpenAI API)或重型资源(例如加载一个数 GB 的模型)。测试不应该运行缓慢、产生额外费用,或者因为网络错误而失败。
因此,我们使用 mocking 来模拟这些依赖,从而可以在隔离环境中测试组件本身的内部逻辑。
它是如何工作的:
在 test_synthetic_test_components.py 中,@patch('synthetic_test_components.TestsetGenerator') 装饰器会把真实的 TestsetGenerator(它原本会调用 LLM)替换成一个 mock 对象。
最佳实践:
同样,在 test_synthetic_test_components.py 中,@patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key'}) 会模拟环境变量中存在 API key,从而让组件在不需要真实密钥的情况下也能初始化成功。这保证了测试既快速、可重复,也能在任意环境中运行。
借助 mocking,我们就能在不执行昂贵操作(例如真正调用 LLM)的前提下,断言组件功能是否符合预期。隔离完成后,我们就可以进一步验证组件。
验证组件生命周期与状态
正如我们在 warm_up() 方法中看到的,组件往往拥有一个清晰的生命周期:已配置、已初始化(warmed up)、运行中。测试必须覆盖这个生命周期。
test_warmup_components.py 中的测试策略,就验证了组件完整的生命周期。
首先,test_component_initialization 会确认:组件在创建时模型尚未加载(embedder.model is None)。
接着,test_run_with_valid_texts 中的安全检查会验证:如果在 warm_up() 之前就调用 run(),是否会正确抛出 RuntimeError。
随后,test_warm_up_loads_model 会验证:调用 warm_up() 后,模型确实被成功加载。
同一个测试还会验证其幂等性:再次调用 warm_up() 时,不应再次触发模型加载逻辑,从而避免低效的重复初始化。
对生命周期的验证,能够确保组件在运行过程中始终按预期工作。这也引出了下一项原则。
测试配置与初始化
组件被设计为通过 __init__ 方法进行配置,因此测试必须验证这些配置是否如预期生效。
例如,在 test_synthetic_test_components.py 中,test_component_initialization 通过两种方式验证了组件配置是否正常:
- 第一,不传参数初始化
SyntheticTestGenerator,确认默认testset_size是否正确设置为 10。 - 第二,使用自定义
testset_size=20初始化组件,确认该自定义参数是否被正确保存。
这能够保证下一阶段的执行建立在正确配置之上。
验证“桥接组件”与数据契约
在我们的高级 Pipeline 中,我们创建了一个“桥接”组件 DocumentToLangChainConverter,用于解决数据兼容性问题。这个组件唯一的职责,就是把数据从一种格式(List[HaystackDocument])转换成另一种格式(List[LangChainDocument])。
这类组件虽然逻辑简单,却极其关键,因此也必须重点测试其输入输出契约是否正确。
测试边界情况与优雅失败
一个健壮的 Pipeline,必须在面对异常输入时不至于崩溃。在我们的测试套件中,一个常见模式,就是存在如下测试:
test_empty_document_handlingtest_empty_document_list_handling
这些测试都在验证同一个关键边界情况:如果组件收到的是一个空文档列表,会发生什么?
理想情况下,组件不应该崩溃。相反,它应该优雅失败(fail gracefully) ,并返回一个可预测的空输出。这样,Pipeline 就能够继续向前推进,或者以可控方式结束,而不是因为未处理错误而整体中断。
通过贯彻这些测试原则,我们就完成了从“使用者”到“架构师”的跃迁。我们不仅构建了定制化、领域特定的组件,也落实了使这些组件变得健壮、可维护、并具备生产可用性的工程实践。
正因为如此,我们才能带着充分信心,去构建像高级分支架构那样复杂、多源的 Pipeline,并且相信其中的每一个独立部分都经得起验证。
小结
本章标志着一次关键跃迁:你不再只是 Haystack 框架的使用者,而是成为一名能够扩展它的架构师。我们已经不再停留于组装预定义组件,而是开始构建属于自己的组件,并掌握了自定义组件开发的核心原则。你学习了几个最基本的构建块:用于注册类的 @component 装饰器、用于轻量配置的 __init__ 方法,以及承载组件核心逻辑的 run() 方法。我们还引入了至关重要的生命周期方法 warm_up(),它是高效管理模型等重型资源的标准方式,能够将一次性初始化与重复执行清晰分离开来。
随后,我们将这些原则应用到了一个实用且高级的场景中,构建出一条复杂的多阶段 Pipeline:为 RAG 系统生成高质量评估数据集。你学习了如何创建 KnowledgeGraphGenerator 组件,用于从原始文档中提取结构化实体与关系。基于这张图,我们的 SyntheticTestGenerator 组件进一步生成了一组多样化的问题,其中包括复杂的多跳查询——而这类问题是简单的基于文本 chunk 的生成方式无法产出的。这种方法使我们能够真正测试 RAG 系统更深层次的推理能力,而不仅仅是检验它是否能取回单一事实。
最后,我们通过把自定义组件集成进高级 Pipeline 架构,展示了 Haystack 模块化设计的强大之处。你已经看到,如何通过构建桥接组件来保证数据兼容性,以及如何构建分支型 Pipeline,把 PDF、网站等多种数据源统一到一条无缝工作流之中。为了完成从概念到生产的最后一步,我们还建立了测试这一工程纪律。我们介绍了若干关键策略,例如对外部服务进行 mocking、验证组件生命周期,以及测试边界情况,从而确保我们的自定义组件是健壮的、可预测的、且易于维护的。
现在,我们已经成功构建了创建高质量、基于图的测试数据集所需的自定义组件,接下来就可以真正去构建并评估系统本身了。在下一章中,我们将构建一个完整的、可复现的、可扩展的 RAG 系统。我们会利用刚刚创建的数据集,借助 Ragas 框架对 RAG Pipeline 的性能进行定量评估。同时,我们还将把范围扩展到可观测性领域,结合 Weights and Biases,分析 token 使用量与成本之间的关系,并系统性评估朴素 RAG 与混合 RAG Pipeline 的表现差异。
延伸阅读
Bratanič, T. (n.d.). How to Improve Multi-Hop Reasoning With Knowledge Graphs and LLMs.
neo4j.com/blog/genai/…
Linders, J., & Tomczak, J. M. (2025). Knowledge Graph-extended Retrieval Augmented Generation for Question Answering.
Department of Mathematics and Computer Science, Eindhoven University of Technology, De Zaale, Eindhoven, the Netherlands.
arxiv.org/html/2504.0…
Bratanič, T. (n.d.). From Legal Documents to Knowledge Graphs.
neo4j.com/blog/develo…
Su, D., Xu, Y., Dai, W., Ji, Z., Yu, T., & Fung, P. (2020). Multi-hop Question Generation with Graph Convolutional Network. In Findings of the Association for Computational Linguistics: EMNLP (2020) (pp. 4636–4647). Association for Computational Linguistics.
doi.org/10.18653/v1…