DLAI-Haystack-笔记-全-

DLAI Haystack 笔记(全)

001:课程介绍 🚀

在本课程中,我们将学习如何使用Haystack框架来构建人工智能应用。Haystack是一个流行的开源框架,它能帮助我们更高效地集成大型语言模型、向量数据库等多种工具,并构建复杂的AI工作流程。

为什么选择使用框架?🤔

上一节我们了解了课程目标,本节中我们来看看为什么应该使用像Haystack这样的框架,而不是完全从头开始构建应用。

完全从头开始构建应用是一次宝贵的学习经历,并能让你完全控制每一步。然而,使用框架可以帮助你更快地完成工作。框架提供了抽象概念,使你的代码更易于维护和阅读。

生成式人工智能技术发展迅速且复杂,它需要集成来自不同语言模型提供商、向量数据库、网络搜索等各种工具的API。将这些功能组合成自定义的工作流程也需要大量工作。像Haystack这样的框架可以帮助管理这种复杂性,让你能够更专注于在更高的抽象级别上开发应用程序。

例如,市场上有许多向量数据库供应商。通过使用一个共同的接口,你的代码可以更容易地适应底层数据库技术的变化,而无需对应用程序进行太多重构。

# 框架提供的抽象接口示例(伪代码)
# 无论底层是Pinecone、Weaviate还是Milvus,调用方式可能类似
results = vector_store.query(query_embedding=embedding, top_k=10)

此外,框架可以直接提供通用功能,从而加快开发过程。例如,Haystack支持管道中的分支和循环。通过分支,你的管道可以在初始步骤未能提供足够信息时,运行网络搜索等备用方案。

Haystack的管道可视化工具还能帮助你理解和优化构建大型语言模型工作流程的过程。

Haystack的设计哲学 🧩

上一节我们讨论了使用框架的优势,本节中我们来深入了解Haystack框架的核心设计思想。

本课程的讲师是Tuana Celik,她是Deepset的Haystack开发者关系负责人。Tuana一直在帮助许多开发人员使用Haystack构建自定义人工智能应用。

然而,Haystack作为一个框架,其重点不是提供你可能需要的一切,而是提供一个共同的接口和一个简单的抽象。你可以根据自己的需要扩展框架的能力。

Haystack基于两个主要元素构建:组件管道。其核心思想是拥有通过灵活管道连接的强大组件。

框架提供了许多内置组件,如嵌入器(Embedder)和生成器(Generator)。但在很多情况下,Haystack可能没有提供你所需的特定组件。例如,如果你需要从某个特定API获取数据,你可以创建自己的自定义组件来与该API交互。Haystack只要求你将其包装成一个符合其规范的组件。

课程内容概览 📚

在接下来大约一小时的视频课程中,你将学习以下内容:

以下是构成Haystack框架的核心构建块:

  • 核心概念:了解Haystack的核心抽象,包括组件、管道和文档存储。
  • 基础应用:学习如何将这些元素组合用于各种人工智能用例。

以下是你将动手构建的具体项目:

  • RAG管道:构建并自定义一个简单的检索增强生成(RAG)管道,学习如何根据特定需求调整其行为。
  • 自定义组件:通过构建一个“黑客新闻摘要器”来创建你自己的自定义组件。
  • 分支管道:创建一个带有条件路由的分支管道,当初始上下文不足时,可以回退到进行网络搜索。
  • 自我反思代理:使用Haystack的管道循环机制,构建一个能够迭代完善其响应的自我反思代理。
  • 函数调用代理:创建一个利用OpenAI函数调用能力的聊天代理,从而能够将Haystack管道作为工具提供给大型语言模型,以增强其能力。

致谢 👏

本课程的创建离不开许多人的努力。来自Deepset,我们要感谢整个Haystack团队,特别是Julian Risch、Bilge Yücel和Madhujith Kannan。此外,来自DeepLearning.AI的Eshleen Kaur和Jeff Ludwig也为本课程做出了贡献。

总结 🎯

本节课中,我们一起学习了Haystack课程的介绍。我们了解了使用AI框架(如Haystack)相对于从头开发的优势,它能够管理复杂性、提供抽象并加速开发。我们探讨了Haystack以组件管道为核心的设计哲学,并预览了课程中将涵盖的核心概念与实战项目,包括构建RAG管道、创建自定义组件以及实现分支和循环等高级工作流。期待你使用Haystack构建出许多令人兴奋的大型语言模型应用程序。

002:Haystack核心构建模块 🧱

在本节课中,我们将学习Haystack框架的核心抽象概念。你将了解什么是组件、它们如何工作、以及如何将它们组合成适用于多种AI用例的管道。我们还将介绍文档存储的概念,以及管道如何访问它们。课程将从创建一个简单的索引管道开始,然后构建一个文档搜索管道。

概述

人工智能应用通常由多个协同工作的步骤组成,以实现特定目标,例如检索增强生成。这通常包含两个步骤:检索步骤,即从数据库中查找并提取最相关的文档;以及生成步骤,即利用这些文档中的上下文来生成响应。有时,你可能需要在检索和生成之间添加额外的步骤,比如排名。在一个完整的人工智能应用中,许多较小的任务被组合成一个更大的用例。

在Haystack中,所有这些小任务都是通过组件来实现的。组件再与其他组件组合,形成一个管道。管道是实现我们想要构建的应用的实体。管道还可以访问数据库,在Haystack中,我们称之为文档存储。管道通过特定的组件来访问这些文档存储。例如,你可以将数据存储在Weaviate、Quadrant或MongoDB中,并通过组件让管道访问这些数据。

管道通过以特定方式连接组件来构建人工智能应用。一个组件可以接受任意数量的输入,也可以产生任意数量的输出。例如,句子转换器文档嵌入器组件。这个组件期望一个文档列表作为输入,并返回相同的文档列表,但其中包含了嵌入向量。它还会返回元数据。这个组件使用句子转换器嵌入模型来创建这些嵌入向量。

例如,从这个句子开始:“泰勒·斯威夫特是世界女王吗?”。我们可能有一个组件,比如一个嵌入器,它为这个查询生成一个嵌入向量。我们可能还有另一个组件,它期望一个嵌入向量或向量作为输入,然后生成一个文档。我们称这个为检索器。如果我们将这两个组件组合起来,我们就实现了一个相当准确的文档搜索管道。

Haystack提供了许多现成的组件。例如,访问不同模型提供方的生成器、执行嵌入功能的嵌入器、能够从多种数据库检索的检索器。我们还有转换器、排名器、路由器、预处理器等等。我们构建管道来创建像问答、文档搜索、聊天、问题生成、输出验证这样的应用,而且这个列表还在不断增加。

我们用Haystack构建这些管道,但Haystack管道也可以分支。这意味着我们可以有包含决策组件的应用。管道可能会变得相当复杂。Haystack管道也可以循环,即它会循环执行直到满足某个条件。最重要的是,如果Haystack没有你构建应用所需的组件,你可以构建自己的组件,并将它们放入有意义的管道中。

现在,让我们在代码中看到所有这些概念,并开始使用Haystack组件、管道和文档存储。

实践:创建和使用组件

首先,添加一行代码来抑制任何不需要的警告。

import warnings
warnings.filterwarnings("ignore")

接下来,我们将使用一个辅助函数来导入本实验所需的所有环境变量,例如OpenAI的API密钥等。

from dotenv import load_dotenv
load_dotenv()

现在,让我们开始创建第一个组件并使用它。你将使用的第一个组件是OpenAI文档嵌入器。这个组件使用OpenAI的嵌入模型为文档创建嵌入。

首先,导入文档嵌入器。

from haystack.components.embedders import OpenAIDocumentEmbedder

然后,初始化一个文档嵌入器。这里,我们将使用text-embedding-3-small作为嵌入模型。

embedder = OpenAIDocumentEmbedder(model="text-embedding-3-small")

你也可以检查这个组件期望什么样的输入和输出。例如,我们可以看到这个组件期望文档列表作为输入,它将产生文档和元数据作为输出。我们已经知道这是一个嵌入组件,所以它产生的文档也将包含嵌入。

现在,让我们看看如何运行这个组件。为此,我们创建一些示例文档。

from haystack import Document

documents = [
    Document(content="Haystack is a framework for building AI applications."),
    Document(content="You can build pipelines with Haystack components.")
]

因为我们知道嵌入器组件期望文档作为输入,我们可以用这些文档来运行它。

result = embedder.run(documents=documents)
print(result)

当我们运行这个组件时,你会注意到它产生了文档列表作为输出,并且元信息也告诉我们用于创建这些文档的是什么模型以及向量的大小(例如1536)。

构建索引管道

上一节我们介绍了如何单独使用一个组件,本节中我们来看看如何在管道中使用它。首先,我们将初始化一个文档存储,然后构建一个管道,该管道将文档及其嵌入一起写入文档存储。

目前,我们将使用内存中的文档存储。这是在Haystack中你可以使用的最简单的文档存储,使用它没有任何要求。但如果你愿意,可以将其切换为任何文档存储,如Quadrant、Weaviate、Pinecone、Chroma等。

from haystack.document_stores.in_memory import InMemoryDocumentStore

document_store = InMemoryDocumentStore()

现在我们有了一个内存中的文档存储,让我们将第一个TXT文件写入这个文档存储。

首先,导入你将用于第一个索引管道的所有组件。你将再次使用OpenAI文档嵌入器,但也将使用一些预处理程序和转换器。对于这个演示,我们将使用这些组件的所有默认变量,但你也可以改变这一点。

from haystack.components.converters import TextFileToDocument
from haystack.components.preprocessors import DocumentSplitter
from haystack.components.writers import DocumentWriter

我们将从一个转换器开始,因为我们将有一个关于达芬奇的TXT文件,我们将其写入我们的内存文档存储。

converter = TextFileToDocument()

接下来,使用文档分割器。文档分割器是一个将你的文档分块的组件。默认情况下,它按200个单词分割,我们将使用这个默认设置。然而,如果你愿意,可以改变这一点。例如,你可以决定按段落分割。

splitter = DocumentSplitter()

接下来,使用一个嵌入器。这里,我们将使用OpenAI文档嵌入器。

embedder = OpenAIDocumentEmbedder()

你将使用的最后一个组件是一个文档写入器。你已经有了一个文档存储,所以在这里你将告诉文档写入器它应该写入你的内存文档存储。

writer = DocumentWriter(document_store=document_store)

现在你有了所有的组件,接下来要做的就是创建一个管道并将这些组件添加到该管道中。你通过初始化你的管道来做到这一点,然后添加每个组件。这里重要的是,对于每个组件,你将提供一个名称。你可以给你的组件取任何你想要的名字,但之后你必须确保接下来使用这个名字。

from haystack import Pipeline

indexing_pipeline = Pipeline()
indexing_pipeline.add_component("converter", converter)
indexing_pipeline.add_component("splitter", splitter)
indexing_pipeline.add_component("embedder", embedder)
indexing_pipeline.add_component("writer", writer)

现在你已经将组件添加到你的管道中,管道可以访问这些组件,但它实际上还不知道这些组件如何相互作用。为此,Haystack使用组件连接。例如,你将从将转换器连接到分割器开始。这基本上是告诉管道转换器的输出应该交给分割器。

indexing_pipeline.connect("converter", "splitter")
indexing_pipeline.connect("splitter", "embedder")
indexing_pipeline.connect("embedder", "writer")

当你运行这个时,你也将看到你在管道中创建了什么样的连接。一旦你在管道中连接了所有组件,你还将能够观察到你的管道可以访问哪些组件,以及这些组件之间的确切连接是什么。所以我们基本上看到转换器的文档输出具体地被输入到分割器文档输入。

你会记得之前嵌入器期望文档作为输入。所以它由分割器提供文档,但它也产生文档。只是这次它有嵌入。所以嵌入器的文档输出现在被给到写入器文档输入。

Haystack提供的另一个实用程序是一种可视化这些管道的方法。你只需在你的管道上调用draw方法。

indexing_pipeline.draw("indexing_pipeline.png")

你将得到一个确切的你的管道样子的图表,包括所有的连接。在这种情况下,我们知道我们的管道从一个转换器开始,它期望的输入是源。

现在你有了你的管道并且你知道连接是准确的,你可以尝试运行它。你已经看到那个管道中的第一个组件是转换器组件,并且它期望一个源列表。对于这个实验,我们有一个关于达芬奇的TXT文件,我们将用它来索引到我们的内存文档存储中。

result = indexing_pipeline.run({"converter": {"sources": ["path/to/leonardo_da_vinci.txt"]}})
print(result)

我们运行这个管道。你会看到它用我们与OpenAI文档嵌入器一起拥有的默认嵌入模型计算嵌入。它也让我们知道它已经将一定数量的文档写入我们的内存文档存储。为了检查,你现在也可以检查你的文档存储。

documents = document_store.filter_documents()
print(f"Total documents indexed: {len(documents)}")
print(documents[5].content)

构建文档搜索管道

上一节我们构建了索引管道,本节中我们来构建一个文档搜索管道。为此,我们首先导入我们将要使用的所有组件。

from haystack.components.embedders import OpenAITextEmbedder
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever

然后我们开始创建我们想要使用的组件。这里重要的是,因为我们使用OpenAI文档嵌入器和默认嵌入模型,所以我们知道必须使用相同模型来嵌入用户传入的查询。对于这个用例,那么,我们将使用OpenAI文本嵌入器,我们称其为我们的查询嵌入器。

query_embedder = OpenAITextEmbedder()

接下来,需要一个检索器。我们使用内存中文档存储。所以对于这种情况,我们将使用内存中嵌入检索器,告诉它从我们的文档存储中检索文档。

retriever = InMemoryEmbeddingRetriever(document_store=document_store)

接下来,初始化我们的管道。我们称这个管道为文档搜索,并且我们简单地将我们拥有的组件添加到该管道中。

search_pipeline = Pipeline()
search_pipeline.add_component("query_embedder", query_embedder)
search_pipeline.add_component("retriever", retriever)

最后,就像我们之前做的那样,我们将连接我们的组件。我将从一个实际上将是不正确的连接开始,或者确切地说,连接是什么并不非常清楚。所以如果你运行这个,你会注意到你得到一个错误。这些类型的错误实际上非常有用。它告诉我们有一个管道连接错误,因为它不知道查询嵌入器应该如何确切地连接到检索器,因为这两个组件有多种连接方式。它还为我们提供了这些连接可能看起来像什么的一些建议,并让我们知道两个组件的输出和输入是什么。

为了解决这个问题,我们只需要让管道知道具体地说查询嵌入器的嵌入输出应该给予检索器的查询嵌入输入。

search_pipeline.connect("query_embedder.embedding", "retriever.query_embedding")

就是这样。我们现在有一个文档搜索管道。再次,你可以使用显示工具来确保你已经创建了你期望看到的管道。所以你可以看到第一个组件查询嵌入器期望文本。这将是例如用户正在问的问题。然后这个组件将输出嵌入到检索器,检索器然后将返回最相关的文档。

让我们运行我们的文档搜索管道。让我们从问题开始:“达芬奇去世时多大年纪?”。

query = "达芬奇去世时多大年纪?"
result = search_pipeline.run({"query_embedder": {"text": query}})
documents = result["retriever"]["documents"]
print(f"Retrieved {len(documents)} documents.")
for doc in documents:
    print(doc.content)

如你所见,这里有相当多的文档。并且因为我们使用默认变量运行我们的管道,我们有10个最相关的文档。

你现在可以做的另一件事是运行这个管道,但在运行时修改各个组件的输入。例如,与其要求10个最相关的文档,我们可以将其改为3个。我们唯一需要做的就是修改检索器的前k个输入。

result = search_pipeline.run({
    "query_embedder": {"text": query},
    "retriever": {"top_k": 3}
})
documents = result["retriever"]["documents"]
print(f"Retrieved {len(documents)} documents.")

如你所见,我们现在正在向检索器组件添加输入并调用前k为3。现在不是10个,而是3个最相关的文档。

总结

在本节课中,我们一起学习了Haystack的核心构建模块。你了解了什么是组件,如何单独运行组件,以及如何将组件组合成完整的管道。你已经构建了一个将文档索引到内存文档存储中的管道,以及一个文档搜索管道。你可以尝试围绕如何分割你的文档进行试验。例如,不是有200个单词,你可以按不同长度进行分割。你也可以尝试修改你正在问的问题、你的文档搜索管道以及检索器的前k值。在下一个实验中,你将使用这些知识来构建你的第一个检索增强生成管道,并且还将定制这些管道的行为。

003:构建自定义RAG管道 🛠️

在本节课中,我们将学习如何构建一个简单的检索增强生成(RAG)管道,并探索如何定制其行为。我们将从创建一个基础的问答RAG管道开始,然后对其进行修改,使其能够引用答案的来源。

概述:什么是RAG?

检索增强生成(RAG)主要包含两个核心步骤:

  1. 检索:接收一个查询,并从数据库中检索与该查询最相关的文档。
  2. 生成:利用检索到的文档内容来增强提示,并生成最终答案。

这个过程可以概括为以下流程:
查询 -> 检索相关文档 -> 构建增强提示 -> 生成答案

最常见的检索方式是基于嵌入的语义搜索。其核心思想是:
为查询和所有文档创建向量表示(嵌入),然后通过计算余弦相似度等方法来找出语义上最相似的文档。

在Haystack框架中,一个典型的RAG管道包含以下组件:查询嵌入器、嵌入检索器、提示构建器和生成器。


构建索引管道 📥

上一节我们介绍了RAG的基本概念,本节我们将动手构建管道。首先,我们需要一个索引管道来准备和存储文档数据。

在本实验中,我们将改变索引文档的方式。之前是将文本文件索引到内存中,这次我们将使用网络内容。具体来说,我们会使用LinkContentFetcher组件来获取URL的内容,并使用Cohere的模型来生成文档嵌入。

以下是构建索引管道所需的步骤和代码:

首先,我们初始化文档存储和各个组件。

# 初始化内存文档存储
document_store = InMemoryDocumentStore()

# 初始化用于获取URL内容的组件
fetcher = LinkContentFetcher()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_20.png)

# 初始化将HTML转换为Haystack文档格式的组件
converter = HTMLToDocument()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_22.png)

# 初始化Cohere文档嵌入器
embedder = CohereDocumentEmbedder(model=“embed-english-v3.0”)

# 初始化文档写入器
writer = DocumentWriter(document_store)

接下来,我们将这些组件添加到管道中并建立连接。

# 创建索引管道并添加组件
indexing_pipeline = Pipeline()
indexing_pipeline.add_component(“fetcher”, fetcher)
indexing_pipeline.add_component(“converter”, converter)
indexing_pipeline.add_component(“embedder”, embedder)
indexing_pipeline.add_component(“writer”, writer)

# 连接组件:fetcher -> converter -> embedder -> writer
indexing_pipeline.connect(“fetcher.streams”, “converter.sources”)
indexing_pipeline.connect(“converter”, “embedder”)
indexing_pipeline.connect(“embedder”, “writer”)

现在,我们可以运行这个索引管道,将指定URL的内容获取、转换、嵌入并存入文档库。

# 定义要索引的URL列表
urls = [
    “https://haystack.deepset.ai/integrations/anthropic”,
    “https://haystack.deepset.ai/integrations/cohere”,
    “https://haystack.deepset.ai/integrations/jina”,
    “https://haystack.deepset.ai/integrations/nvidia”
]

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_24.png)

# 运行管道,输入是URLs
indexing_pipeline.run({“fetcher”: {“urls”: urls}})

运行后,四个文档及其嵌入向量已被写入文档存储。每个文档不仅包含网页内容,还在元数据中保存了原始URL,这在后续定制中会很有用。


构建基础RAG管道 ❓

现在,我们已经有了一个包含文档的数据库,接下来可以构建检索增强生成(RAG)管道了。首先,我们需要设计一个提示模板。

Haystack使用Jinja2模板语言来构建提示,这非常灵活,允许我们使用循环、条件等逻辑。以下是一个基础的问答提示模板:

根据提供的上下文回答问题。
上下文:
{% for document in documents %}
{{ document.content }}
{% endfor %}
问题:{{ query }}
答案:

这个模板表明,提示构建器期望接收documentsquery作为输入,并将所有文档的内容循环填入“上下文”部分。

以下是构建RAG管道的代码:

首先,初始化管道所需的各个组件。

# 初始化查询嵌入器(使用与文档嵌入相同的Cohere模型)
query_embedder = CohereTextEmbedder(model=“embed-english-v3.0”)

# 初始化基于嵌入的检索器
retriever = InMemoryEmbeddingRetriever(document_store)

# 初始化提示构建器,并传入我们定义的模板
prompt_builder = PromptBuilder(template=“””根据提供的上下文回答问题。
上下文:
{% for document in documents %}
{{ document.content }}
{% endfor %}
问题:{{ query }}
答案:”””)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_34.png)

# 初始化生成器(这里使用OpenAI的GPT-3.5)
generator = OpenAIGenerator(model=“gpt-3.5-turbo”)

接着,创建管道,添加组件并连接它们。

# 创建RAG管道
rag_pipeline = Pipeline()

# 添加组件
rag_pipeline.add_component(“query_embedder”, query_embedder)
rag_pipeline.add_component(“retriever”, retriever)
rag_pipeline.add_component(“prompt_builder”, prompt_builder)
rag_pipeline.add_component(“generator”, generator)

# 连接组件:query_embedder -> retriever -> prompt_builder -> generator
rag_pipeline.connect(“query_embedder.embedding”, “retriever.query_embedding”)
rag_pipeline.connect(“retriever”, “prompt_builder.documents”)
rag_pipeline.connect(“prompt_builder”, “generator”)

现在,我们可以运行这个RAG管道进行问答了。

# 定义问题
question = “我如何使用Cohere与Haystack一起?”

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_40.png)

# 运行管道
result = rag_pipeline.run({
    “query_embedder”: {“text”: question},
    “prompt_builder”: {“query”: question},
    “retriever”: {“top_k”: 1}  # 仅检索最相关的1个文档
})

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_42.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_44.png)

# 打印生成的答案
print(result[“generator”][“replies”][0])

管道会检索与问题最相关的文档,将其内容填入提示,并调用GPT-3.5生成答案。


定制RAG管道行为 🎨

上一节我们构建了一个基础的问答管道,本节我们来看看如何通过修改提示来定制它的行为。例如,我们可以让模型在回答时引用来源URL,并指定回答的语言。

回想一下,在索引时,文档的元数据中包含了URL。我们可以在提示模板中访问这个元数据。以下是一个定制后的提示模板:

你是一个有帮助的助手。请根据以下上下文回答问题。
每个上下文片段都来自一个网页。
你的答案应该使用{{ language }}。

{% for document in documents %}
内容 {{ loop.index }}: {{ document.content }}
URL {{ loop.index }}: {{ document.meta[‘url’] }}
{% endfor %}

问题:{{ query }}
答案:

这个新模板增加了两个功能:

  1. 它期望一个额外的输入变量 language,用于指定回答语言。
  2. 它在循环中不仅输出文档内容,还输出了文档元数据中的URL。

使用新模板构建和运行管道的代码与之前类似,只需更新PromptBuilder的模板,并在运行时提供额外的language参数。

# 使用新模板初始化提示构建器
custom_prompt_builder = PromptBuilder(template=“””你是一个有帮助的助手... [上述模板内容] ...”””)

# ... (将custom_prompt_builder加入管道并连接,过程同上)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/3f9a062d201ae3d8166c2e3d753f7e8b_52.png)

# 运行定制后的管道
result = rag_pipeline.run({
    “query_embedder”: {“text”: question},
    “prompt_builder”: {“query”: question, “language”: “法语”}, # 指定法语回答
    “retriever”: {“top_k”: 1}
})

这样,模型就会用法语生成答案,并且在答案的上下文中明确指出了所引用信息的来源URL。你可以通过修改模板来尝试更多定制,例如改变指令、调整检索文档的数量(top_k)等。


总结 📝

在本节课中,我们一起学习了:

  1. RAG的核心原理:即检索与生成两阶段流程,通过语义搜索找到相关文档来增强提示。
  2. 构建索引管道:使用Haystack组件获取网页内容、转换为文档、生成嵌入并存储。
  3. 构建基础RAG管道:组合查询嵌入器、检索器、提示构建器和生成器,实现问答功能。
  4. 定制管道行为:通过灵活修改Jinja2提示模板,我们可以轻松地让RAG管道支持引用来源、指定输出语言等高级功能。

你已掌握了使用Haystack搭建和定制RAG管道的基本方法。在下一节课中,我们将探索如何创建自己的Haystack组件,以获得更大的灵活性。

004:自定义组件 - 新闻摘要器 🧩

概述

在本节课中,我们将学习如何创建自定义的Haystack组件。自定义组件是扩展Haystack功能以满足特定需求的关键。我们将从构建一个简单的问候组件开始,然后创建一个更复杂的、能够获取并总结Hacker News热门帖子的新闻摘要器组件。


什么是Haystack组件?🤔

上一节我们介绍了课程目标,本节中我们来看看Haystack组件的核心概念。

在Haystack中,组件是构建管道的基本单元。一些现成的组件包括嵌入器(Embedders)、检索器(Retrievers)、生成器(Generators)等。每个组件都期望接收特定数量的输入,并能产生特定数量的输出。

对于一个类要成为Haystack中的有效组件,需要满足几个要求:

  1. 使用 @component 装饰器装饰类。
  2. 类中需要有一个 run 方法。
  3. run 方法必须返回一个字典。
  4. 必须通过装饰器指定组件的输出类型(这在后续的管道连接和验证中非常重要)。

例如,以下是一个有效的翻译组件代码框架:

from haystack import component

@component
class Translator:
    @component.output_types(documents=List[Document])
    def run(self, from_lang: str, to_lang: str, documents: List[Document]):
        # ... 翻译逻辑 ...
        return {"documents": translated_documents}

这个组件告诉Haystack:它将输出一个类型为 List[Document]documents。其 run 方法接收三个输入参数,并返回一个包含 "documents" 键的字典。


实验目标 🎯

在本次实验中,我们将构建两个独立的管道:

  1. 对话构建器管道:使用一个自定义的“问候”组件来生成对话开头。
  2. 黑客新闻摘要器管道:创建一个自定义的“黑客新闻获取器”组件,用于获取热门帖子,并构建管道对其进行总结。

实验结束时,你将拥有一个能够提供Hacker News上Top K热门帖子摘要的功能性管道。


准备工作 ⚙️

首先,我们进行常规的警告抑制并加载环境变量(如OpenAI API密钥)。接着,导入本实验所需的所有依赖项。

import warnings
warnings.filterwarnings('ignore')
import os
from dotenv import load_dotenv
load_dotenv()

# 导入必要的Haystack及其他库
from haystack import Pipeline, component
from haystack.components.generators import OpenAIGenerator
from haystack.components.builders import PromptBuilder
# ... 其他导入 ...

完成这些步骤后,我们就可以开始编写代码了。


构建第一个自定义组件:问候器 👋

让我们从一个简单的示例开始,创建一个名为“问候器”(Greeter)的自定义组件。

以下是创建该组件的步骤:

  1. 创建一个类并使用 @component 装饰它。
  2. 使用 @component.output_types 装饰器定义其输出(这里输出一个名为 greeting 的字符串)。
  3. 在类中定义 run 方法。该方法接收一个 username 参数,并返回包含问候语的字典。
@component
class Greeter:
    @component.output_types(greeting=str)
    def run(self, username: str):
        greeting = f"Hello, {username}"
        return {"greeting": greeting}

通过 @component.output_types(greeting=str),我们告知Haystack管道:此组件输出一个字符串类型的 greeting。这确保了该组件只能连接到期望接收字符串作为输入的后续组件。


使用自定义组件构建管道 🔄

现在,让我们看看如何在管道中使用这个新创建的 Greeter 组件。我们将构建一个“对话构建器”管道。

以下是构建管道的步骤:

  1. 初始化 Greeter 组件。
  2. 创建一个提示模板,指示大语言模型根据对话开头生成剧本。
  3. 使用 PromptBuilderOpenAIGenerator(默认GPT-3.5)。
  4. 将所有组件添加到管道中并正确连接它们。
# 1. 初始化组件
greeter = Greeter()

# 2. 创建提示模板
prompt_template = """
You are given the beginning of a conversation.
Create a short play script using this as the opening.
Conversation opening: {{ conversation }}
"""
prompt_builder = PromptBuilder(template=prompt_template)

# 3. 初始化生成器
llm = OpenAIGenerator()

# 4. 构建并连接管道
conversation_builder = Pipeline()
conversation_builder.add_component("greeter", greeter)
conversation_builder.add_component("prompt_builder", prompt_builder)
conversation_builder.add_component("llm", llm)

conversation_builder.connect("greeter.greeting", "prompt_builder.conversation")
conversation_builder.connect("prompt_builder", "llm")

greeter 组件输出 greeting,而 prompt_builder 期望一个名为 conversation 的输入。通过 connect("greeter.greeting", "prompt_builder.conversation"),我们将问候语作为对话开头传递过去。

现在运行管道:

result = conversation_builder.run({"greeter": {"username": "Tejana"}})
print(result["llm"]["replies"][0])

输出将以“Hello, Tejana”开头,并由LLM生成一段简短的对话剧本。你可以更改用户名来获得不同的结果。


构建复杂组件:黑客新闻获取器 📰

你已经了解了如何创建和使用简单的自定义组件。接下来,让我们构建一个更复杂、更实用的组件——HackerNewsFetcher,用于获取Hacker News的热门帖子。

Hacker News提供了一个公共API。例如,我们可以通过以下请求获取当前最流行的帖子:

https://hacker-news.firebaseio.com/v0/topstories.json

你可以将 topstories 替换为 newstories 来获取最新帖子。API返回的是故事ID列表,我们需要进一步获取每个ID的详细信息(如标题、URL等)。


组件结构设计

我们首先搭建组件的基本框架:

@component
class HackerNewsFetcher:
    @component.output_types(articles=List[Document])
    def run(self, top_k: int):
        # 初始化一个空的文章列表
        articles = []
        # ... 获取逻辑将在这里实现 ...
        return {"articles": articles}

这个框架是有效的,但目前还不会返回任何实际内容。


实现获取逻辑

接下来,我们填充 run 方法的具体逻辑:

  1. 从API获取热门故事ID列表。
  2. 遍历前 top_k 个ID,获取每个故事的详细信息。
  3. 大多数故事有URL,我们使用一个 HTMLToDocument 管道来抓取网页内容。
  4. 少数故事只有文本,我们直接将其内容转换为Document。
  5. 将所有处理后的 Document 对象添加到 articles 列表中并返回。
import requests
from haystack.components.converters import HTMLToDocument

@component
class HackerNewsFetcher:
    def __init__(self):
        # 初始化HTML转换器,用于抓取URL内容
        self.html_converter = HTMLToDocument()

    @component.output_types(articles=List[Document])
    def run(self, top_k: int):
        articles = []
        # 1. 获取热门故事ID
        top_stories_ids = requests.get("https://hacker-news.firebaseio.com/v0/topstories.json").json()

        # 2. 遍历前 top_k 个故事
        for story_id in top_stories_ids[:top_k]:
            story_url = f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json"
            story_data = requests.get(story_url).json()

            # 3. 处理有URL的故事
            if "url" in story_data:
                # 使用HTML转换器获取网页内容
                doc = self.html_converter.run(urls=[story_data["url"]])["documents"][0]
                # 可选:将原始标题等信息加入元数据
                doc.meta["title"] = story_data.get("title", "")
                articles.append(doc)
            # 4. 处理只有文本的故事
            elif "text" in story_data:
                # 直接创建Document对象
                from haystack import Document
                doc = Document(content=story_data["text"], meta={"title": story_data.get("title", "")})
                articles.append(doc)

        return {"articles": articles}

现在,我们可以单独测试这个组件:

fetcher = HackerNewsFetcher()
results = fetcher.run(top_k=3)
for doc in results["articles"]:
    print(f"Title: {doc.meta.get('title')}")
    print(f"URL: {doc.meta.get('url', 'N/A')}")
    print(f"Content snippet: {doc.content[:200]}...\n")

组件将输出三个Document,每个都包含从Hacker News获取的帖子标题、URL和内容。


构建新闻摘要器管道 ✨

现在我们有了可用的 HackerNewsFetcher 组件,可以将其集成到一个完整的摘要管道中。

以下是构建摘要管道的步骤:

  1. 创建提示模板:指示LLM根据提供的文章内容生成摘要。
  2. 初始化组件:包括我们自定义的 fetcherprompt_builderllm
  3. 构建并连接管道:将获取的文章传递给提示生成器,再交由LLM生成摘要。
# 1. 提示模板
summary_template = """
Here are some top posts from Hacker News.
For each post, provide a brief summary if possible.

{% for article in articles %}
Post {{ loop.index }}:
Content: {{ article.content[:500] }}... (truncated)
---
{% endfor %}
"""
prompt_builder = PromptBuilder(template=summary_template)

# 2. 初始化组件
fetcher = HackerNewsFetcher()
llm = OpenAIGenerator()

# 3. 构建管道
summarizer_pipeline = Pipeline()
summarizer_pipeline.add_component("fetcher", fetcher)
summarizer_pipeline.add_component("prompt_builder", prompt_builder)
summarizer_pipeline.add_component("llm", llm)

summarizer_pipeline.connect("fetcher.articles", "prompt_builder.articles")
summarizer_pipeline.connect("prompt_builder", "llm")

运行管道,获取前3个帖子的摘要:

result = summarizer_pipeline.run({"fetcher": {"top_k": 3}})
print(result["llm"]["replies"][0])

由于Hacker News内容实时变化,你的输出将与示例不同,显示的是你运行当天热门帖子的摘要。


增强提示:包含来源URL 🔗

我们可以进一步改进提示模板,要求LLM在摘要后附上原文的URL。这利用了 HTMLToDocument 自动将源URL存入 Document 元数据的特性。

修改提示模板如下:

enhanced_summary_template = """
Here are some top posts from Hacker News.
For each post, provide a brief summary followed by the URL to the full post.

{% for article in articles %}
Post {{ loop.index }}:
Content: {{ article.content[:500] }}... (truncated)
URL: {{ article.meta['url'] }}
---
{% endfor %}
"""

使用新模板重建管道并运行,你将获得包含摘要和原文链接的更丰富结果。

enhanced_prompt_builder = PromptBuilder(template=enhanced_summary_template)

enhanced_summarizer = Pipeline()
enhanced_summarizer.add_component("fetcher", fetcher)
enhanced_summarizer.add_component("prompt_builder", enhanced_prompt_builder)
enhanced_summarizer.add_component("llm", llm)
# ... 连接组件 ...

result = enhanced_summarizer.run({"fetcher": {"top_k": 2}})
print(result["llm"]["replies"][0])

如果输出不完全符合预期,你可以继续编辑提示词使其更精确,甚至在后续实验中实现“自我反思”机制,让LLM自行改进结果。


总结 🎓

在本节课中,我们一起学习了:

  1. Haystack组件的核心要求:使用 @component 装饰器、定义 run 方法、返回字典、声明输出类型。
  2. 创建简单自定义组件:我们构建了一个 Greeter 组件,并将其成功集成到对话生成管道中。
  3. 创建复杂自定义组件:我们构建了 HackerNewsFetcher 组件,它调用外部API获取数据,并内部使用其他Haystack组件(如 HTMLToDocument)处理内容。
  4. 构建功能管道:利用自定义组件,我们创建了一个完整的新闻摘要管道,能够获取并总结网络上的热门内容。

自定义组件功能强大,它不仅让你能够接入Haystack尚未官方支持的API(如本例中的Hacker News)或模型供应商,还能实现你独有的处理逻辑。Haystack社区也贡献了许多集成组件,值得探索。在下一个实验中,我们将学习如何实现带有分支的Haystack管道,以创建更复杂的流程(如回退机制)。

005:使用分支管道实现回退机制 🛠️

在本节课中,我们将学习如何构建一个能够分支的管道,并实现一个回退到网络搜索的机制。当基于数据库的RAG管道无法回答用户查询时,系统将自动切换到网络搜索来寻找答案。

概述

我们之前提到过管道可以分支,本节课将实际构建一个分支管道。回退机制是分支管道的一个典型应用场景。我们将使用Haystack中的条件路由器组件,根据RAG管道的输出结果来决定是直接给出答案,还是转向网络搜索分支。

构建基础RAG管道

首先,我们需要构建一个基础的检索增强生成管道。以下是构建步骤:

以下是导入依赖项和初始化环境的代码:

import warnings
warnings.filterwarnings("ignore")
import os
from haystack import Pipeline
from haystack.document_stores import InMemoryDocumentStore
from haystack.nodes import BM25Retriever, PromptNode, PromptTemplate, ConditionalRouter

接下来,我们将一些虚拟文档写入内存文档存储。这些文档描述了不同的Haystack组件及其使用方法。

document_store = InMemoryDocumentStore()
documents = [
    Document(content="检索器用于为用户查询检索相关文档。"),
    Document(content="生成器基于检索到的文档和查询生成自然语言回答。"),
    Document(content="提示构建器负责组装发送给大型语言模型的提示。"),
    Document(content="文档存储用于保存和索引文档。")
]
document_store.write_documents(documents)

现在,我们开始创建RAG管道。首先定义提示模板,其中包含一个特殊指令:如果答案不在文档中,则回复“无答案”。

rag_prompt_template = PromptTemplate(
    prompt="""基于以下文档回答问题。如果答案不在文档中,请回复“无答案”。
文档:{documents}
问题:{query}
答案:"""
)

然后,我们初始化管道并添加组件:基于关键字的BM25检索器、提示构建器以及使用GPT-3的生成器。

rag_pipeline = Pipeline()
retriever = BM25Retriever(document_store=document_store)
prompt_builder = PromptNode(model_name_or_path="gpt-3.5-turbo", default_prompt_template=rag_prompt_template)
generator = PromptNode(model_name_or_path="gpt-3.5-turbo")

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/df2d29d845a86b3065b7116302337cd7_13.png)

rag_pipeline.add_node(component=retriever, name="Retriever", inputs=["Query"])
rag_pipeline.add_node(component=prompt_builder, name="PromptBuilder", inputs=["Retriever"])
rag_pipeline.add_node(component=generator, name="Generator", inputs=["PromptBuilder"])

让我们测试一下这个基础管道。询问一个已知的问题:

result = rag_pipeline.run(query="检索器是用来干什么的?")
print(result)

输出应为:检索器是用于为用户查询检索相关文档的。

再询问一个未知的问题:

result = rag_pipeline.run(query="米斯特拉尔有哪些组件?")
print(result)

由于文档中没有相关信息,模型应回复:无答案

引入条件路由器实现分支

上一节我们构建了基础的RAG管道,本节中我们来看看如何通过条件路由器为其添加分支逻辑,实现回退机制。

条件路由器是一个特殊组件,它根据预定义的条件和路线来决定管道的执行流向。其核心逻辑可以用以下伪代码描述:

if 条件1满足:
    激活分支1
elif 条件2满足:
    激活分支2
else:
    激活默认分支

在我们的用例中,条件是基于生成器的回复是否包含“无答案”。我们将创建两条路线:

  1. 路线1(网络搜索):如果回复中包含“无答案”(不区分大小写),则将查询路由到网络搜索分支。
  2. 路线2(答案):如果回复中不包含“无答案”,则直接将回复作为最终答案输出。

以下是定义路线的代码:

from haystack.nodes import RouteCondition

def contains_no_answer(reply):
    # 检查回复中是否包含“无答案”,忽略大小写
    return "无答案" in reply.lower()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/df2d29d845a86b3065b7116302337cd7_27.png)

route_web = RouteCondition(
    condition=contains_no_answer,
    output="query",
    output_name="go_to_web_search"
)
route_answer = RouteCondition(
    condition=lambda reply: not contains_no_answer(reply),
    output="reply",
    output_name="answer"
)

router = ConditionalRouter(routes=[route_web, route_answer])

现在,我们将路由器添加到现有的RAG管道中,并将生成器的输出连接到路由器。

rag_pipeline.add_node(component=router, name="Router", inputs=["Generator"])

此时,管道的结构变为:Query -> Retriever -> PromptBuilder -> Generator -> Router。路由器将根据生成器的回复,输出到go_to_web_searchanswer

让我们测试一下分支逻辑。再次询问未知问题:

result = rag_pipeline.run(query="海斯塔克有哪些米斯特拉尔组件?")
print(result)

输出应为:{'go_to_web_search': {'query': '海斯塔克有哪些米斯特拉尔组件?'}}。这表明路由器已成功将查询导向网络搜索分支。

构建网络搜索分支

上一节我们成功将查询路由到了网络搜索分支,本节中我们来看看如何构建这个分支,使其能够从互联网获取信息并生成回答。

我们将使用 SerperDevWebSearch 组件进行网络搜索。该组件接收查询,在网络上搜索,并将结果作为Haystack文档返回。这样,我们就可以构建一个与之前类似的RAG管道,但文档来源是网络。

首先,定义用于网络搜索的提示模板,要求模型在答案中注明信息来源于网络搜索。

web_prompt_template = PromptTemplate(
    prompt="""基于以下从网络搜索获得的文档回答问题。你的答案应该表明信息来源于网络搜索。
网络搜索结果:{documents}
问题:{query}
基于网络搜索的答案:"""
)

现在,我们构建完整的、包含回退机制的分支管道。我们将创建一个新管道,它整合了基础的RAG管道和网络搜索分支。

from haystack.nodes import WebRetriever # 假设使用WebRetriever,实际可能是SerperDevWebSearch

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/df2d29d845a86b3065b7116302337cd7_45.png)

full_pipeline = Pipeline()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/df2d29d845a86b3065b7116302337cd7_47.png)

# 添加基础RAG组件
full_pipeline.add_node(component=retriever, name="Retriever", inputs=["Query"])
full_pipeline.add_node(component=prompt_builder, name="PromptBuilder", inputs=["Retriever"])
full_pipeline.add_node(component=generator, name="Generator", inputs=["PromptBuilder"])
full_pipeline.add_node(component=router, name="Router", inputs=["Generator"])

# 添加网络搜索分支组件
web_retriever = WebRetriever(api_key=os.getenv("SERPERDEV_API_KEY"))
web_prompt_builder = PromptNode(model_name_or_path="gpt-3.5-turbo", default_prompt_template=web_prompt_template)
web_generator = PromptNode(model_name_or_path="gpt-3.5-turbo")

full_pipeline.add_node(component=web_retriever, name="WebRetriever", inputs=["Router.go_to_web_search"])
full_pipeline.add_node(component=web_prompt_builder, name="WebPromptBuilder", inputs=["WebRetriever", "Router.go_to_web_search"])
full_pipeline.add_node(component=web_generator, name="WebGenerator", inputs=["WebPromptBuilder"])

最后,我们需要连接这些节点。关键点在于,路由器输出的查询(go_to_web_search)将同时作为 WebRetriever 的查询输入和 WebPromptBuilder 的查询输入。

# 连接网络搜索分支
full_pipeline.connect(source="Router.go_to_web_search", target="WebRetriever.query")
full_pipeline.connect(source="Router.go_to_web_search", target="WebPromptBuilder.query")
full_pipeline.connect(source="WebRetriever", target="WebPromptBuilder.documents")

现在,完整的管道已经构建完毕。其逻辑流程如下图所示(用文字描述):

  1. 用户查询进入基础RAG管道。
  2. 生成器产生回复。
  3. 条件路由器检查回复。
    • 如果不包含“无答案”,则从answer端口直接输出回复。
    • 如果包含“无答案”,则将原始查询从go_to_web_search端口输出。
  4. go_to_web_search的查询触发网络搜索。
  5. 网络搜索结果和原始查询被送入新的提示构建器和生成器,产生最终答案。

测试完整管道

让我们用几个问题来测试完整的管道。

首先,测试一个RAG管道能够回答的问题:

result = full_pipeline.run(query="生成器的用途是什么?")
print(result['answer'])

输出应为来自基础RAG管道的答案。

其次,测试一个RAG管道无法回答、需要回退到网络搜索的问题:

result = full_pipeline.run(query="法国的首都是什么?")
# 结果将来自 WebGenerator
print(result.get('WebGenerator', {}).get('replies', ['No reply'])[0])

输出应是一个基于网络搜索生成的答案,例如:“根据网络搜索结果,法国的首都是巴黎。”

你可以尝试更多问题,例如“海斯塔克有什么卡希尔组件?”(应触发网络搜索)和“提示构建器是做什么的?”(应由基础RAG回答)。

总结

在本节课中,我们一起学习了如何构建一个具有分支能力的智能管道。我们首先回顾了基础RAG管道的构建,然后引入了条件路由器组件来实现核心的分支逻辑。最后,我们构建了网络搜索分支,完成了完整的回退机制。

通过本节课,你掌握了:

  1. 条件路由器的使用方法,它通过RouteCondition对象定义分支逻辑。
  2. 构建复杂管道的能力,能够将不同的处理流程(如本地RAG和网络搜索RAG)动态连接起来。
  3. 实现一个健壮问答系统的思路:优先使用可靠的本体知识(数据库),当其不足时,智能地回退到外部知识源(互联网)。

这种模式极大地增强了AI应用的鲁棒性和实用性,使其能够应对更广泛、更开放领域的问题。

006:构建带循环的自我反思智能体 🧠

在本节课中,我们将学习如何构建一个能够进行自我反思的Haystack智能体管道。这个管道将包含一个循环机制,允许大型语言模型(LLM)评估并迭代改进自己的输出。我们将通过一个具体的例子来实现:让LLM从非结构化文本中提取命名实体,并通过自我反思来优化结果。

概述

自我反思是一种让模型自我评估和纠正的机制。通过提供反馈,模型可以生成更可靠和准确的信息。在本教程中,我们将构建一个简单的管道,其中LLM会反复检查自己提取的实体,直到它对自己的结果满意为止。我们将使用自定义组件和循环逻辑来实现这一过程。

构建自定义组件:实体验证器

首先,我们需要创建一个自定义的Haystack组件,用于验证LLM的回复。这个组件将决定LLM是否完成了任务,或者是否需要进一步反思。

以下是创建 EntityValidator 组件的代码:

from haystack import component
from typing import Dict, Any

@component
class EntityValidator:
    @component.output_types(entities_to_validate=str, entities=str)
    def run(self, replies: str):
        if "完成" in replies:
            return {"entities": replies}
        else:
            # 用红色打印需要验证的实体,便于观察循环过程
            print(f"\033[91m{replies}\033[0m")
            return {"entities_to_validate": replies}

这个组件的逻辑很简单:

  • 如果LLM的回复中包含“完成”这个词,组件就将回复作为最终“实体”输出。
  • 如果不包含“完成”,组件会用红色打印回复,并将其作为“需要验证的实体”输出,以便进行下一轮反思。

设计动态提示模板

为了让LLM能够根据上下文(首次尝试或反思后)接收不同的指令,我们需要创建一个动态的提示模板。我们将使用Jinja2模板的 if-else 语句来实现。

from haystack.components.builders import PromptBuilder

template = """
{% if entities_to_validate %}
你之前从以下文本中提取了实体:
文本:{{ text }}
你提取的实体:{{ entities_to_validate }}

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/932376b7d8a4ac5307efc7301474eb89_12.png)

请反思并改进你的答案。考虑以下几点:
- 确保提取了所有要求类型的实体(人物、地点、日期)。
- 如果某个类别没有实体,请返回空列表。
- 确保格式正确(键值对形式)。
如果你认为已经完善,请说“完成”。

改进后的实体:
{% else %}
请从以下文本中提取实体。
文本:{{ text }}

提取要求:
- 实体类型:人物、地点、日期。
- 格式:以键值对形式呈现,例如 {"人物": [...], "地点": [...], "日期": [...]}。
- 如果某个类别没有实体,请返回空列表。

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/932376b7d8a4ac5307efc7301474eb89_14.png)

提取的实体:
{% endif %}
"""

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/932376b7d8a4ac5307efc7301474eb89_16.png)

prompt_builder = PromptBuilder(template=template)

这个模板的核心在于:

  • 首次运行else 部分):指示LLM从给定文本中提取指定类型的实体。
  • 反思运行if 部分):向LLM展示它上一轮提取的实体,并提供具体的改进要点,引导它进行反思和修正。

组装循环管道

现在,我们将生成器、提示构建器和验证器组件连接起来,形成一个可以循环的管道。关键点在于设置循环逻辑和最大循环次数,以控制成本和防止无限循环。

from haystack import Pipeline
from haystack.components.generators import OpenAIGenerator

# 1. 初始化组件
prompt_builder = PromptBuilder(template=template)
llm = OpenAIGenerator(model="gpt-3.5-turbo") # 使用OpenAI生成器
validator = EntityValidator()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/932376b7d8a4ac5307efc7301474eb89_20.png)

# 2. 创建管道并设置循环
pipeline = Pipeline(max_loops_allowed=10) # 限制最大循环次数为10

# 3. 添加组件到管道
pipeline.add_component("prompt_builder", prompt_builder)
pipeline.add_component("llm", llm)
pipeline.add_component("validator", validator)

# 4. 连接组件
# 提示构建器接收初始文本或反馈的实体,输出给LLM
pipeline.connect("prompt_builder", "llm")
# LLM的回复交给验证器判断
pipeline.connect("llm.replies", "validator.replies")

# 5. 建立循环:如果验证器输出“需要验证的实体”,则将其反馈给提示构建器
pipeline.connect("validator.entities_to_validate", "prompt_builder.entities_to_validate")

管道的工作流程如下:

  1. prompt_builder 根据输入(初始文本或反馈实体)构建提示。
  2. llm 根据提示生成回复。
  3. validator 检查回复。
    • 若回复含“完成”,则流程结束,输出最终实体。
    • 若不含“完成”,则将回复作为 entities_to_validate 输出。
  4. 管道将 entities_to_validate 重新发送给 prompt_builder,开启新一轮的“反思-生成”循环。
  5. 循环持续,直到LLM回复“完成”或达到最大循环次数。

运行与测试

让我们用两段示例文本来测试这个自我反思智能体。

示例 1:关于城市的描述

text_1 = “””
伊斯坦布尔是土耳其最大的城市,横跨欧亚大陆。它历史上被称为拜占庭和君士坦丁堡。该市人口超过1500万。苏丹艾哈迈德清真寺和圣索菲亚大教堂是其主要地标。
“””

# 运行管道,初始输入是文本,没有需要验证的实体
result = pipeline.run({
    "prompt_builder": {
        "text": text_1,
        "entities_to_validate": None # 初始运行时为None
    }
})

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/932376b7d8a4ac5307efc7301474eb89_28.png)

# 打印最终结果
print(f"\033[92m最终实体:{result['validator']['entities']}\033[0m")

示例 2:会议记录

text_2 = “””
项目启动会议于2023年10月26日举行。与会者包括张三(项目经理)、李四(首席开发员)和王五(UI设计师)。会议在北京的办公室进行。讨论了项目使用React和Python进行开发。
“””

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/932376b7d8a4ac5307efc7301474eb89_32.png)

result = pipeline.run({
    "prompt_builder": {
        "text": text_2,
        "entities_to_validate": None
    }
})
print(f"\033[92m最终实体:{result['validator']['entities']}\033[0m")

运行过程中,控制台会以红色显示LLM在反思过程中生成的中间实体,最后以绿色显示它最终满意的实体结果。你可以观察到LLM如何从第一次可能不完整或格式不正确的输出,通过反思逐步修正为符合要求的答案。

总结

在本节课中,我们一起学习了如何构建一个带循环功能的自我反思智能体。我们实现了以下核心步骤:

  1. 创建自定义验证器:用于判断LLM的输出是否“完成”,并据此决定流程走向。
  2. 设计动态提示模板:利用条件语句为LLM提供初次指令和反思指令。
  3. 组装循环管道:将各组件连接,并通过 max_loops_allowed 参数控制循环上限,实现了“生成-验证-反馈”的闭环。

这个例子展示了实现自我反思的一种简单而有效的方法。你可以在此基础上进行扩展,例如:

  • 让模型反思并生成不同类型的输出(如摘要、情感分析)。
  • 集成更复杂的验证逻辑(如使用Pydantic验证JSON模式)。
  • 构建能够使用函数调用的更高级的聊天代理。

通过本节课,你已经掌握了在Haystack中创建具有迭代和自我改进能力AI应用的基础。

007:7.L6 使用函数调用的聊天代理 🛠️🤖

在本节课中,我们将学习如何利用OpenAI的函数调用能力,将Haystack的RAG管道和其他功能封装为“工具”,从而构建一个功能强大的聊天代理。这个代理能够理解用户意图,并动态调用合适的工具来获取信息并生成回答。


概述

函数调用是大型语言模型(LLM)领域一项激动人心的进展。它允许模型在对话中决定并调用外部函数(如API或自定义管道)来获取信息。本节课,我们将创建一个聊天代理,它能将Haystack的RAG管道和模拟的天气API作为工具来使用。


准备工作

首先,我们需要设置环境并导入必要的库。这包括抑制警告、加载环境变量以及导入Haystack和OpenAI相关的组件。

import warnings
warnings.filterwarnings('ignore')
import os
from dotenv import load_dotenv
load_dotenv()

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/a5ec60c37eb8c27d65a943f44fc9882d_7.png)

from haystack import Pipeline
from haystack.components.builders import ChatPromptBuilder
from haystack.components.generators import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
from haystack.components.joiners import BranchJoiner
from haystack.experimental.components.function_call import OpenAIFunctionCaller


第一步:创建RAG管道函数

上一节我们完成了基础设置,本节中我们来看看如何将RAG管道包装成一个可被调用的函数。

我们将创建一个标准的RAG管道,但为了演示,这里使用一些预设的“虚假”文档数据。

# 假设的文档数据
documents = [
    "马克住在柏林。",
    "乔治住在罗马。",
    "安娜住在巴黎。",
    "丽莎住在马德里。",
    "约翰住在伦敦。"
]

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/a5ec60c37eb8c27d65a943f44fc9882d_13.png)

# 创建一个简单的RAG管道(此处为概念演示,非完整代码)
# 通常包含文档存储、检索器和生成器
def create_rag_pipeline():
    # ... 初始化文档存储、检索器等组件 ...
    pipeline = Pipeline()
    # ... 添加组件并连接 ...
    return pipeline

rag_pipeline = create_rag_pipeline()

现在,我们将这个管道包装成一个Python函数,以便后续作为工具调用。

def rag_pipeline_func(query: str):
    """
    这是一个RAG管道函数。
    参数:
        query (str): 用户提出的问题。
    返回:
        str: 根据文档生成的答案。
    """
    # 在实际应用中,这里会运行完整的RAG管道
    # 为了演示,我们简单地在预设文档中查找答案
    for doc in documents:
        if query in doc:
            return doc
    return "根据现有文档,我无法找到相关信息。"

第二步:创建模拟天气函数

除了RAG管道,我们还可以为代理提供其他工具。接下来,我们创建一个模拟的天气查询函数。

以下是模拟的天气数据:

weather_data = {
    "柏林": "天气以晴朗为主,气温7°C。",
    "巴黎": "天气多云,气温10°C。",
    "罗马": "天气晴朗,气温15°C。",
    "马德里": "天气以晴朗为主,气温12°C。",
    "伦敦": "天气多云,气温8°C。"
}

然后定义天气函数:

def get_current_weather(location: str):
    """
    获取指定城市的当前天气。
    参数:
        location (str): 城市名称。
    返回:
        str: 该城市的天气描述。
    """
    # 查找城市天气,如果未找到则返回默认值
    return weather_data.get(location, "天气晴朗,气温21.8°C。")

现在,我们有了两个可用的工具:一个用于回答居住地问题的RAG函数,另一个用于查询天气的函数。


第三步:将函数描述为OpenAI工具

为了让OpenAI模型理解并调用我们的函数,我们需要按照OpenAI API的规范来描述这些工具。

以下是描述工具的方法:

tools = [
    {
        "type": "function",
        "function": {
            "name": "rag_pipeline_func",
            "description": "在文档中搜索信息以回答关于人物居住地的问题。",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "在搜索中使用的查询,应从用户消息中推断。"
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "获取指定城市的当前天气信息。",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "需要查询天气的城市名称。"
                    }
                },
                "required": ["location"]
            }
        }
    }
]

第四步:初始化聊天生成器与函数调用器

有了工具描述,我们就可以配置聊天生成器了。OpenAIChatGenerator组件可以接收这些工具列表。

# 初始化聊天生成器,并为其提供工具
chat_generator = OpenAIChatGenerator(model="gpt-3.5-turbo", generation_kwargs={"tools": tools})

接下来,我们需要一个组件来实际执行模型要求调用的函数。我们将使用Haystack实验包中的OpenAIFunctionCaller

# 初始化函数调用器,并为其提供可用的函数字典
available_functions = {
    "rag_pipeline_func": rag_pipeline_func,
    "get_current_weather": get_current_weather
}
function_caller = OpenAIFunctionCaller(functions=available_functions)

第五步:构建聊天代理管道

现在,我们将各个组件组合成一个完整的聊天代理管道。这个管道需要处理消息的流转、工具的调用以及响应的生成。

以下是构建管道的步骤:

  1. 消息收集器:使用BranchJoiner来合并来自用户和函数调用器的消息,形成一个连贯的消息历史。
  2. 聊天生成器:接收消息历史,决定是否需要调用工具,并生成回复。
  3. 函数调用器:如果生成器决定调用工具,则由它来执行具体的函数,并将结果返回。
# 创建管道
chat_agent = Pipeline()

# 添加组件
chat_agent.add_component("message_collector", BranchJoiner())
chat_agent.add_component("generator", chat_generator)
chat_agent.add_component("function_caller", function_caller)

# 连接组件
# 消息收集器的输出作为生成器的输入
chat_agent.connect("message_collector", "generator.messages")
# 生成器的输出传递给函数调用器
chat_agent.connect("generator.replies", "function_caller.messages")
# 函数调用器的输出返回到消息收集器,形成循环,以便模型能将函数结果转化为自然语言回复
chat_agent.connect("function_caller.replies", "message_collector")

# 可视化管道(可选)
# chat_agent.draw("./chat_agent_pipeline.png")


第六步:与聊天代理交互

管道构建完成后,我们可以创建一个简单的交互循环来测试它。

首先,初始化消息队列,并可以加入一个系统消息来指导模型行为。

messages = [ChatMessage.from_system("如果需要,请将用户问题分解为更简单的问题。不要对要插入函数的值进行假设。")]

然后,启动一个交互循环:

print("聊天代理已启动。输入‘退出’或‘离开’来结束对话。")
while True:
    user_input = input("\n你: ")
    if user_input.lower() in ["退出", "离开"]:
        print("再见!")
        break

    # 将用户消息加入队列
    messages.append(ChatMessage.from_user(user_input))

    # 运行管道,从`message_collector`组件输入当前消息
    result = chat_agent.run({"message_collector": {"value": messages}})

    # 获取最新的助手回复
    # 回复可能来自`generator`(直接回答)或`function_caller`(函数调用后的结果)
    latest_reply = result.get("generator", {}).get("replies", []) or result.get("function_caller", {}).get("replies", [])
    if latest_reply:
        assistant_message = latest_reply[-1]
        print(f"助理: {assistant_message.content}")
        # 将助理的回复也加入消息历史,以便进行多轮对话
        messages.append(assistant_message)

第七步:创建Gradio Web界面(可选)

为了使聊天代理更易于使用,我们可以使用Gradio快速创建一个Web界面。

import gradio as gr

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/a5ec60c37eb8c27d65a943f44fc9882d_41.png)

# 重用之前的消息列表和管道
demo_messages = [ChatMessage.from_system("如果需要,请将用户问题分解为更简单的问题。不要对要插入函数的值进行假设。")]

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/a5ec60c37eb8c27d65a943f44fc9882d_43.png)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/a5ec60c37eb8c27d65a943f44fc9882d_45.png)

def chat_function(message, history):
    """Gradio聊天函数"""
    demo_messages.append(ChatMessage.from_user(message))
    result = chat_agent.run({"message_collector": {"value": demo_messages}})
    latest_reply = result.get("generator", {}).get("replies", []) or result.get("function_caller", {}).get("replies", [])
    if latest_reply:
        assistant_message = latest_reply[-1]
        demo_messages.append(assistant_message)
        return assistant_message.content
    return "抱歉,我没有收到回复。"

# 创建Gradio界面
demo = gr.ChatInterface(
    fn=chat_function,
    title="Haystack 智能聊天代理",
    examples=["马克住在哪里?", "那里的天气怎么样?", "他今天适合穿什么衣服?"]
)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-haystk/img/a5ec60c37eb8c27d65a943f44fc9882d_47.png)

# 启动应用
if __name__ == "__main__":
    demo.launch()

总结

在本节课中,我们一起学习了如何构建一个使用函数调用的聊天代理。我们主要完成了以下工作:

  1. 创建工具函数:将Haystack RAG管道和模拟天气API封装成可调用的Python函数。
  2. 描述工具:按照OpenAI的规范,将函数描述为模型可以理解的“工具”。
  3. 配置核心组件:使用OpenAIChatGenerator来集成工具,使用OpenAIFunctionCaller来执行函数调用。
  4. 构建代理管道:通过Pipeline连接消息收集、生成、函数调用等环节,形成一个可以循环对话的智能体。
  5. 实现交互:通过命令行循环和Gradio Web界面两种方式与代理进行交互。

通过本节课,你掌握了利用Haystack和OpenAI函数调用构建复杂AI应用代理的核心方法。你可以尝试用真实的API替换模拟的天气函数,或者接入更复杂的自定义管道,从而扩展聊天代理的能力。

008:8.课程总结 🎉

在本课程中,我们学习了如何使用Haystack框架构建和定制复杂的人工智能应用程序。

课程回顾

上一节我们介绍了应用程序的部署与优化,本节我们将对整个课程内容进行总结。

恭喜你完成本课程。在本课程中,你学习了如何构建和定制复杂的人工智能应用程序。

不仅如此,你还学习了如何专门根据你的需求扩展框架的能力。

核心收获

以下是本课程涵盖的核心技能:

  • 构建复杂AI应用:掌握了使用Haystack框架搭建端到端AI应用流程的方法。
  • 定制化开发:学会了根据特定业务需求调整和定制AI模型与组件。
  • 扩展框架能力:理解了如何通过添加新组件或集成外部工具来扩展Haystack框架的功能。

总结

本节课中,我们一起学习了使用Haystack构建人工智能应用的全过程。从基础概念到复杂应用的定制与扩展,你已经掌握了利用这一强大框架解决实际问题的关键技能。希望你能将这些知识应用于未来的项目中,构建出更智能、更高效的应用程序。

posted @ 2026-03-26 08:11  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报