RAG工作流深度解析:数据摄取的艺术

我们已经从整体上了解了LlamaIndex的结构。现在是时候深入了解这个框架的细节了。随着我们的深入,技术性将会增加,但也会变得更加有趣。

准备好深入探索了吗?跟我来!

在本章中,我们将学习以下内容:

  • 使用LlamaHub连接器摄取我们的数据
  • 利用LlamaIndex中的多种文本分块工具
  • 为我们的节点注入元数据和关系
  • 保持数据隐私并节省预算
  • 创建高效且低成本的摄取管道

技术要求

你需要在你的环境中安装以下Python库,以便运行本章中的示例:

此外,还需要几个LlamaIndex集成包:

本章中的所有代码示例都可以在本书的GitHub仓库的ch4子文件夹中找到:GitHub仓库

通过LlamaHub摄取数据

正如我们在第3章《启动你的LlamaIndex之旅》中所看到的,RAG工作流的第一步之一是摄取和处理我们的专有数据。我们已经了解了文档和节点的概念,这些概念用于组织数据并为索引做准备。我还简要介绍了LlamaHub数据加载器,作为轻松将数据摄取到LlamaIndex的方法。现在是时候更详细地审视这些步骤,并逐步学习如何将LLM应用程序与我们自己的专有知识融合。不过,在继续之前,我想强调一下在这一步中遇到的一些非常常见的挑战:

  • 无论我们的RAG管道多么有效,最终结果的质量在很大程度上取决于初始数据的质量。要克服这一挑战,首先要清理数据。消除潜在的重复和错误。虽然不完全是重复信息,但冗余信息也会使你的知识库混乱,并混淆RAG系统。注意模糊、偏见、不完整或过时的信息。我见过很多结构不良和维护不足的知识库,对于寻求快速准确答案的用户来说完全没有用。问自己一个问题:如果我手动搜索这些数据,找到我需要的信息有多容易?在继续构建管道之前,认真准备数据,直到你对这个问题的答案感到满意。
  • 我们的数据是动态的。一个组织的知识库很少是静态的、永久的数据源。它随着业务的发展而演变,反映新的见解、发现和外部环境的变化。认识到这种流动性是保持系统相关性和有效性的关键。为了克服这一挑战,在生产RAG应用中,你需要实施一种系统的方法,定期审查和更新内容,确保新信息被纳入,并移除过时或错误的数据。
  • 数据有许多不同的形式、形状和大小。有时是结构化的,有时不是。一个构建良好的RAG系统应该能够正确摄取各种格式和文档类型。虽然LlamaIndex为许多不同的API、数据库和文档类型提供了大量数据加载器,但构建一个自动化摄取系统仍然可能是一个挑战。为了克服这一特定挑战,我们将在本节后面介绍LlamaParse------一种创新的托管服务,旨在自动摄取和处理来自不同数据源的数据。

既然我们知道了可能会遇到的各种问题,让我们首先讨论将数据摄取到RAG管道的最简单方法------使用现有的LlamaHub数据加载器。

LlamaHub概述

LlamaHub是一个扩展库,增强了核心框架的能力。LlamaHub包含许多集成类型,其中包括众多连接器(也称为数据读取器或数据加载器),它们专门用于允许外部数据与LlamaIndex无缝集成。目前有超过180个现成的数据读取器,涵盖广泛的数据源和格式,并且这个列表还在不断增加。

这些连接器作为标准方式摄取数据,从数据库、API、文件和网站等来源提取数据,并将其转换为LlamaIndex文档对象。这使你无需为每个新数据源编写定制的解析器和连接器。当然,如果你对现有连接器不满意,你也可以构建自己的连接器并贡献给集合。

LlamaHub使你只需几行代码就能利用多种数据源。然后,生成的文档对象可以解析为节点,并根据你的应用需求进行索引。统一的LlamaIndex文档对象输出意味着你的核心业务逻辑不必担心处理各种数据类型。框架抽象了这些复杂性。

为什么我们需要这么多集成?

在第2章《LlamaIndex:隐藏的宝石 - LlamaIndex生态系统简介》中,在"熟悉LlamaIndex代码库结构"部分,我解释了框架模块化架构背后的动机。由于这种模块化架构,LlamaIndex提供的许多RAG组件不包含在与框架其他部分一起安装的核心元素中。这意味着在首次使用任何数据加载器之前,我们必须安装相应的集成包。一旦安装了包,我们就能够将读取器导入代码并使用其功能。有些读取器还利用专门的库和工具来处理每种数据类型。例如,PDFReader利用Camelot和Tika解析PDF内容,AirbyteSalesforceReader使用Salesforce API客户端等。这使我们能够高效地适应每个来源的格式和接口,但可能需要在开发环境中安装其他包。

所有可用的读取器都列在LlamaHub网站上,通常附有详细的文档和使用示例。因此,我将简要介绍几个示例,以提供如何在应用程序中使用它们的一般概念。

我强烈建议你在构建LlamaIndex应用时,花时间浏览整个数据读取器列表,而不是花费宝贵时间从头开始构建一个读取器。很可能你会发现你只是重新发明轮子。

如果你想查看读取器的源代码,你会在Llama-index GitHub仓库的llama-index-integrations/readers子文件夹下找到它们:GitHub仓库

LlamaHub的每个数据读取器文档列出了其安装要求和使用指南,因此在尝试使用它们之前,请确保你还安装了特定连接器所需的任何附加依赖项。

使用LlamaHub数据加载器摄取内容

复制代码
pip install llama-index-readers-web

from llama_index.readers.web import SimpleWebPageReader

urls = ["https://docs.llamaindex.ai"]
documents = SimpleWebPageReader().load_data(urls)

for doc in documents:
    print(doc.text)

pip install llama-index-readers-database

from llama_index.readers.database import DatabaseReader

reader = DatabaseReader(
    uri="sqlite:///files/db/example.db"
)

query = "SELECT * FROM your_table"
documents = reader.load_data(query)

for doc in documents:
    print(doc.text)

from llama_index.core import SimpleDirectoryReader

reader = SimpleDirectoryReader(
    input_dir="files",
    recursive=True
)

documents = reader.load_data()

for doc in documents:
    print(doc.metadata)

files = ["example.pdf", "example.docx"]

from llama_parse import LlamaParse
from llama_index.core import SimpleDirectoryReader
from llama_index.core import VectorStoreIndex

parser = LlamaParse(result_type="text")
file_extractor = {".pdf": parser}

reader = SimpleDirectoryReader(
    "./files/pdf",
    file_extractor=file_extractor
)

docs = reader.load_data()
index = VectorStoreIndex.from_documents(docs)
qe = index.as_query_engine()

response = qe.query("Tell me about the content of these files.")
print(response)

除了我们在上一章讨论的Wikipedia读取器,为了更好地理解数据读取器的工作原理,让我们再看一些可以用来摄取数据的LlamaHub读取器的示例。

从网页摄取数据

SimpleWebPageReader可以从网页中提取文本内容。

要使用它,我们首先必须安装相应的集成包:

复制代码
pip install llama-index-readers-web

安装后,使用起来非常简单:

复制代码
from llama_index.readers.web import SimpleWebPageReader

urls = ["https://docs.llamaindex.ai"]
documents = SimpleWebPageReader().load_data(urls)

for doc in documents:
    print(doc.text)

这会将指定网页的文本内容加载并显示为文档。

从数据库摄取数据

使用数据库不仅是常见的做法,也是管理和检索结构化信息的高效方法。数据库提供了一个强大的平台,用于存储各种数据类型,从简单文本到复杂的实体关系,使其成为数据管理中不可或缺的资产。

DatabaseReader连接器允许查询许多数据库系统。首先,我们需要安装必要的集成包:

复制代码
pip install llama-index-readers-database

以下是如何轻松获取SQLite数据库内容的示例:

复制代码
from llama_index.readers.database import DatabaseReader

reader = DatabaseReader(
    uri="sqlite:///files/db/example.db"
)

query = "SELECT * FROM your_table"
documents = reader.load_data(query)

for doc in documents:
    print(doc.text)

使用SimpleDirectoryReader摄取多个数据格式

当你想快速开始或有一个简单的用例时,SimpleDirectoryReader可以派上用场。把这个读取器当作批量数据摄取的可靠工具。它易于使用,设置最小,并自动适应不同的文件类型。要加载数据,你只需将读取器指向一个文件夹或文件列表。加载包含PDF、Word文档、纯文本文件和CSV的文件夹非常简单。以下是演示:

复制代码
from llama_index.core import SimpleDirectoryReader

reader = SimpleDirectoryReader(
    input_dir="files",
    recursive=True
)

documents = reader.load_data()

for doc in documents:
    print(doc.metadata)

使用LlamaParse专业解析

虽然SimpleDirectoryReader适用于快速和简单的数据摄取,有时你需要更高级的解析功能,特别是对于复杂的文件格式。大多数时候,我们必须处理包含多种数据的复杂文件结构。例如,一个PDF文件可能包含图像、图表、代码片段、数学公式和其他元素以及其文本内容。LlamaHub集成库中包含的初级读取器将被这样的情况所困扰。它们很可能无法提取整个内容,甚至更糟的是,弄乱提取的数据,使其进一步处理更加复杂。

这就是LlamaParse的优势所在。通过LlamaCloud企业平台提供的这一读取器,通过一个先进的托管服务实现,与框架的其他组件无缝集成。它在底层使用多模态能力和LLM智能,提供业界领先的文档解析,包括对包含表格、图形和方程式的复杂PDF格式的卓越支持。

LlamaParse的一个突出特点是,它允许你通过使用parsing_instruction参数提供自然语言指令来指导解析。由于你最了解你的文档,你可以告诉LlamaParse确切需要什么样的输出以及如何从文件中提取这些信息。

例如:

复制代码
from llama_parse import LlamaParse
from llama_index.core import SimpleDirectoryReader
from llama_index.core import VectorStoreIndex

parser = LlamaParse(result_type="text")
file_extractor = {".pdf": parser}

reader = SimpleDirectoryReader(
    "./files/pdf",
    file_extractor=file_extractor
)

docs = reader.load_data()
index = VectorStoreIndex.from_documents(docs)
qe = index.as_query_engine()

response = qe.query("Explain the key points in these documents.")
print(response)

在解析技术白皮书时,你可以指示它提取所有章节标题,忽略脚注,并将任何代码片段输出为Markdown格式。LlamaParse将按照你的指示准确解析文档。

LlamaParse支持包括PDF在内的广泛且不断扩展的文件类型,包括Word文档、PowerPoint、RTF、ePub等。它提供了一个慷慨的免费层,供你开始使用。

要演示此工具的功能,我设计了一个具有更复杂结构的示例PDF,如图4.1所示:

这是一个使用LlamaParse摄取该PDF的基本代码示例:

复制代码
from llama_parse import LlamaParse
from llama_index.core import SimpleDirectoryReader
from llama_index.core import VectorStoreIndex

# 配置LlamaParse并将其作为file_extractor参数传递给SimpleDirectoryReader
parser = LlamaParse(result_type="text")
file_extractor = {".pdf": parser}

reader = SimpleDirectoryReader(
    input_dir="./files/pdf",
    file_extractor=file_extractor
)

# 摄取PDF内容并将其加载到新的Document对象中
docs = reader.load_data()

# 构建索引并对数据运行查询
index = VectorStoreIndex.from_documents(docs)
qe = index.as_query_engine()

response = qe.query("解释这些文件中的关键点。")
print(response)

该脚本的输出应类似于以下内容:

复制代码
复制代码
解释这些文件中的关键点:...

重要注意事项

使用LlamaParse等托管服务时,数据隐私是一个重要的考虑因素。在通过API提交你的专有数据之前,请务必仔细审查他们的隐私政策,以确保其符合你的数据保护要求。尽管该服务提供了强大的解析功能,但保护敏感信息至关重要。

请记住,这是一个付费服务。好消息是,你可以利用他们慷慨的免费层。对于更高的使用需求,目前的定价可以在其网站上找到。如果你想充分利用LlamaParse的全部潜力来构建高级文档检索系统或将其部署在你的私有云上以实现最大的安全性,这也是可以选择的。

对于专业的、生产就绪的应用程序,LlamaParse是一个强大的工具,能够让你完全控制数据解析,以最大限度地提高你的知识库和RAG应用程序的质量。

现在我们已经获得了数据,让我们通过将其分解为更小的部分来使其更易于处理。

将文档解析为节点

复制代码
from llama_index.core.node_parser import TokenTextSplitter

# 使用TokenTextSplitter将文档解析为节点
splitter = TokenTextSplitter(
    chunk_size=70,
    chunk_overlap=2,
    separator=" ",
    backup_separators=[".", "!", "?"]
)

nodes = splitter.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")

安装必要的库

复制代码
pip install tree_sitter
pip install tree_sitter_languages


from llama_index.core.node_parser import CodeSplitter

# 使用CodeSplitter将代码文档解析为节点
code_splitter = CodeSplitter.from_defaults(
    language='python',
    chunk_lines=5,
    chunk_lines_overlap=2,
    max_chars=150
)

nodes = code_splitter.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")

使用高级节点解析器

复制代码
from llama_index.core.node_parser import SentenceWindowNodeParser

# 使用SentenceWindowNodeParser将文档解析为节点
parser = SentenceWindowNodeParser.from_defaults(
    window_size=2,
    window_metadata_key="text_window",
    original_text_metadata_key="original_sentence"
)

nodes = parser.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")

安装Langchain库

复制代码
pip install langchain


from langchain.text_splitter import CharacterTextSplitter
from llama_index.core.node_parser import LangchainNodeParser

# 使用LangchainNodeParser将文档解析为节点
parser = LangchainNodeParser(CharacterTextSplitter())

nodes = parser.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")

其他解析器示例

复制代码
from llama_index.core.node_parser import SimpleFileNodeParser

# 使用SimpleFileNodeParser将文件解析为节点
parser = SimpleFileNodeParser()

nodes = parser.get_nodes_from_documents(documents)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")


from llama_index.core.node_parser import MarkdownNodeParser

# 使用MarkdownNodeParser将Markdown文件解析为节点
parser = MarkdownNodeParser.from_defaults()

nodes = parser.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")


from llama_index.core.node_parser import JSONNodeParser

# 使用JSONNodeParser将JSON文件解析为节点
json_parser = JSONNodeParser.from_defaults()

nodes = json_parser.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")


from llama_index.core.node_parser import HierarchicalNodeParser

# 使用HierarchicalNodeParser将文档解析为层次结构的节点
hierarchical_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[128, 64, 32],
    chunk_overlap=0,
)

nodes = hierarchical_parser.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")

节点解析器的关系设置

复制代码
from llama_index.core.schema import RelatedNodeInfo
from llama_index.core.node_parser import SentenceWindowNodeParser

node_parser = SentenceWindowNodeParser.from_defaults(
    include_prev_next_rel=True
)

# 手动设置节点关系
node1.relationships['PREVIOUS'] = RelatedNodeInfo(node_id=node0.node_id)
node2.relationships['NEXT'] = RelatedNodeInfo(node_id=node3.node_id)

正如我们在第3章《启动你的LlamaIndex之旅》中看到的,下一步是将文档拆分为节点。在许多情况下,文档往往非常庞大,因此我们需要将其拆分为称为节点的小单位。以这种细粒度级别工作可以更好地处理我们的内容,同时保持其内部结构的准确表示。这是LlamaIndex用于更轻松管理专有数据内容的基本机制。

现在是理解如何在LlamaIndex中生成节点以及沿途有哪些自定义机会的时候了。在上一章中,我们讨论了如何手动创建节点。但那只是简化解释并帮助你更好地理解其机制的一种方式。在实际应用中,我们很可能会希望使用一些自动方法从摄取的文档中生成它们。所以,这将是我们接下来的重点。

在本节中,我们将探索不同的文档分块方法。我们将从理解简单的文本分块器开始------它们操作的是原始文本------然后我们将介绍更高级的节点解析器------它们能够解释更复杂的格式并在提取节点时遵循文档结构。

理解简单的文本分块器

文本分块器将文档分解为较小的片段,操作的是原始文本级别。当内容具有扁平结构且不采用特定格式时,它们非常有用。

复制代码
from llama_index.readers.flat import FlatReader

# 加载文档以进行解析
document = FlatReader(input_dir="your_directory").load_data()

你可以添加以下代码在运行解析器后查看生成的实际节点:

复制代码
for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")

文本分割器示例

SentenceSplitter

此分割器在保持句子边界的同时分割文本,提供包含句子组的节点。

TokenTextSplitter

此分割器在尊重句子边界的同时分割文本,为进一步的自然语言处理创建合适的节点。代码中的典型用法如下:

复制代码
splitter = TokenTextSplitter(
    chunk_size=70,
    chunk_overlap=2,
    separator=" ",
    backup_separators=[".", "!", "?"]
)

nodes = splitter.get_nodes_from_documents(document)

CodeSplitter

此智能分割器知道如何解释源代码。它根据编程语言分割文本,非常适合管理技术文档或源代码。在运行示例之前,请确保安装必要的库:

复制代码
pip install tree_sitter
pip install tree_sitter_languages


from llama_index.core.node_parser import CodeSplitter

code_splitter = CodeSplitter.from_defaults(
    language='python',
    chunk_lines=5,
    chunk_lines_overlap=2,
    max_chars=150
)

nodes = code_splitter.get_nodes_from_documents(document)

使用更高级的节点解析器

文本分割器仅提供用于分割文本的基本逻辑,主要使用简单规则。我们还可以使用更高级的工具将文本分块为节点。这些工具设计用于处理各种标准文件格式或更具体类型的内容。

每个解析器都有各种参数可以根据用例进行配置,但在基础上,所有解析器都有三个通用元素可以定制:

  • include_metadata:确定解析器是否应考虑元数据。默认情况下,此值为True
  • include_prev_next_rel:确定解析器是否应自动包括节点之间的前后类型关系。默认值也是True
  • callback_manager:可用于定义特定的回调函数。这些函数可用于调试、跟踪和成本分析等。我们将在第10章《提示工程指南和最佳实践》中详细讨论。

SentenceWindowNodeParser

此解析器基于简单的SentenceSplitter,将文本分割为单个句子,并在每个节点的元数据中包括周围句子的窗口。

复制代码
from llama_index.core.node_parser import SentenceWindowNodeParser

parser = SentenceWindowNodeParser.from_defaults(
    window_size=2,
    window_metadata_key="text_window",
    original_text_metadata_key="original_sentence"
)

nodes = parser.get_nodes_from_documents(document)

LangchainNodeParser

如果你更喜欢使用LangChain分割器,此解析器允许使用Langchain集合中的任何文本分割器,扩展了LlamaIndex提供的解析选项。确保安装LangChain库:

复制代码
pip install langchain


from langchain.text_splitter import CharacterTextSplitter
from llama_index.core.node_parser import LangchainNodeParser

parser = LangchainNodeParser(CharacterTextSplitter())

nodes = parser.get_nodes_from_documents(document)

其他可用解析器

SimpleFileNodeParser

此解析器基于文件类型自动决定应使用以下哪种节点解析器。

HTMLNodeParser

此解析器使用Beautiful Soup解析HTML文件并根据选定的HTML标签将其转换为节点。

MarkdownNodeParser

此解析器处理原始Markdown文本并生成反映其结构和内容的节点。

JSONNodeParser

此解析器专门处理和查询JSON格式的结构化数据。

使用关系解析器

关系解析器将信息解析为通过关系相互关联的节点。关系为我们的数据增添了一个新的维度,允许在我们的RAG工作流中使用更高级的检索技术。

HierarchicalNodeParser

此解析器将节点组织为多个层次结构。它将生成一个层次结构的节点,从较大的部分大小的顶层节点开始,到具有较小部分大小的子节点,其中每个子节点都有一个较大部分大小的父节点。

复制代码
from llama_index.core.node_parser import HierarchicalNodeParser

hierarchical_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[128, 64, 32],
    chunk_overlap=0,
)

nodes = hierarchical_parser.get_nodes_from_documents(document)

设置节点关系示例

复制代码
from llama_index.core.schema import RelatedNodeInfo
from llama_index.core.node_parser import SentenceWindowNodeParser

node_parser = SentenceWindowNodeParser.from_defaults(
    include_prev_next_rel=True
)

node1.relationships['PREVIOUS'] = RelatedNodeInfo(node_id=node0.node_id)
node2.relationships['NEXT'] = RelatedNodeInfo(node_id=node3.node_id)

以这种方式,不同节点级别可以用来调整搜索结果的准确性和深度,使用户可以在不同的粒度级别找到信息。以下是如何在代码中使用此解析器的示例:

复制代码
from llama_index.core.node_parser import HierarchicalNodeParser

# 使用HierarchicalNodeParser将文档解析为层次结构的节点
hierarchical_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[128, 64, 32],
    chunk_overlap=0,
)

nodes = hierarchical_parser.get_nodes_from_documents(document)

for node in nodes:
    print(f"Metadata: {node.metadata} \nText: {node.text}")

可自定义的两个特定参数

  • chunk_sizes:此列表中的值根据内容大小定义层次结构级别
  • chunk_overlap:定义块之间的重叠大小

UnstructuredElementNodeParser

我将这个放在最后,因为它用于更特殊的情况。有时,我们的文档可能包含文本和数据表的混合,这可能使常规方法解析效率低下。此解析器可以处理并拆分这些文档为可解释的节点,区分文本部分和嵌入的其他结构(如表格)。我们将在本章结尾详细讨论它。

对节点解析器和文本分割器的困惑?

你可能注意到我松散地使用这两个术语。将解析模块分类为这两组可能会初期造成一些困惑。为了简化,一个节点解析器比一个简单的分割器更复杂。虽然两者都具有相同的基本功能并在不同的复杂程度上操作,但它们在实现上有所不同。

  • 文本分割器(如SentenceSplitter)可以根据某些规则或限制(如chunk_sizechunk_overlap)将长文本分割为节点。这些节点可以表示行、段落或句子,并且还可以包括额外的元数据或与原始文档的链接。
  • 节点解析器更复杂,可以涉及额外的数据处理逻辑。除了简单地将文本分割为节点之外,它们还可以执行额外的任务,例如分析HTML或JSON文件的结构并生成带有上下文信息的节点。

理解chunk_sizechunk_overlap

你可能已经了解,文本分割器是一个基本但重要的组件。它们控制文档中的文本在解析过程中如何被分割成节点。对于每种文本分割器类型,LlamaIndex提供了几个参数来定制文本分割行为。

可能最重要的两个参数是chunk_sizechunk_overlap。文本分割器本身(如SentenceSplitter、TokenTextSplitter、TextSplitter等)接受chunk_sizechunk_overlap参数来控制它们在节点创建过程中如何将文本分割成较小的块。chunk_size控制节点中文本块的最大长度。这有助于确保节点不会花费太长时间进行LLM处理。请注意,在LlamaIndex中,默认的chunk_size为1024,而默认的chunk_overlap为20。

实验性方法

在构建RAG系统时,chunk大小是一个重要的设置。如果块太小,可能会丢失重要的上下文,导致LLM响应质量下降。另一方面,大块会增加提示的大小,从而增加计算成本和响应生成时间。默认值是通过实验方法选择的: [实验性方法。

chunk_overlap通过重新包含前一个节点中的一些标记来创建重叠节点。这有助于提供上下文,以便LLM在处理相邻节点时可以理解思想的连续性。

下图提供了这一概念的视觉表示:

这个概念类似于 SentenceWindowNodeParser 的工作方式------它为每个句子提取一个上下文窗口。例如,假设我们有以下文本,chunk_size=100 和 chunk_overlap=10:

复制代码
Gardening is not only a relaxing hobby but also an art form. Cultivating plants, designing landscapes, and nurturing nature bring a sense of accomplishment. Many find it therapeutic and rewarding, especially when they see their garden flourish.

将会被分割为以下区域:

  • 节点 1(前 100 个字符):"Gardening is not only a relaxing hobby but also an art form. Cultivating plants, designing landscapes, an"
  • 节点 2(从第 75 个字符开始,接下来的 100 个字符):"designing landscapes, and nurturing nature bring a sense of accomplishment. Many find it therapeutic and re"
  • 节点 3(从第 150 个字符到文本结束):"Many find it therapeutic and rewarding, especially when they see their garden flourish."

在这种设置中,节点 1 和节点 2 之间的重叠是"designing landscapes, an",而节点 2 和节点 3 之间的重叠是"Many find it therapeutic and re"。

这些重叠意味着一个节点重新包含前一个节点的部分。这种机制确保了块之间的连续性和上下文,使每个部分在顺序阅读时更有意义。当然,选择这些参数的正确值非常重要。最大的影响将在创建向量索引时出现。我们稍后在第五章《使用 LlamaIndex 建立索引》中将讨论它们。

接下来,让我们快速概览一下节点关系。

包含关系 (include_prev_next_rel)

让我们谈谈另一个可以决定解析器行为的重要参数:include_prev_next_rel 选项。当设置为 True 时,此选项会使解析器自动在连续节点之间添加 NEXT 和 PREVIOUS 关系。下面是一个示例:

复制代码
from llama_index.core.schema import RelatedNodeInfo
from llama_index.core.node_parser import SentenceWindowNodeParser

node_parser = SentenceWindowNodeParser.from_defaults(
    include_prev_next_rel=True
)

node1.relationships['PREVIOUS'] = RelatedNodeInfo(node_id=node0.node_id)
node2.relationships['NEXT'] = RelatedNodeInfo(node_id=node3.node_id)

这有助于捕获节点之间的顺序关系。然后,在查询时,您可以选择性地使用诸如 PrevNextNodePostprocessor 之类的功能检索前一个或下一个节点以获取更多上下文。更多内容将在第六章《查询我们的数据,第一部分------上下文检索》中讨论。

这些关系被添加到每个节点的 .relationships 字典中。

例如,节点 1 现在如下:

复制代码
Node ID: 0715876a-61e6-4e77-95ba-b93e10de1c67
Text: Sentence 1.
{'ContextWindow': 'Sentence 1. Sentence 2.', 'node_text': 'Sentence 1.'}

节点 2 如下:

复制代码
Node ID: 8fcbda35-1d4d-40d3-89bc-ff1ffbbd7568
Text: Sentence 2.
{'ContextWindow': 'Sentence 1. Sentence 2. Sentence 3.', 'node_text': 'Sentence 2.'}

捕获这些顺序有助于在长文档中提供上下文连续性,并带来我在上一章中详细列出的许多其他好处。

其中一个好处是,拥有前后关系可以实现集群检索:您可以通过遵循关系获取附近连接的节点,从而获得一个相关节点的集群。这提供了更集中的上下文,而不是随机分散的节点。保持故事或对话中的叙述连续性是建立这些节点关系的另一个好理由。

接下来,让我们看看如何在工作流中使用这些解析器和分割器。

实际使用这些节点创建模型的方法

您在代码中实现节点解析器或文本分割器的方式取决于您希望自定义流程的程度,但最终,这一切归结为三种主要选项:

  1. 独立使用它们

可以通过调用 get_nodes_from_documents() 来使用它们,例如:

复制代码
from llama_index.core import Document
from llama_index.core.node_parser import SentenceWindowNodeParser

doc = Document(
    text="Sentence 1. Sentence 2. Sentence 3."
)
parser = SentenceWindowNodeParser.from_defaults(
    window_size=2,
    window_metadata_key="ContextWindow",
    original_text_metadata_key="node_text"
)
nodes = parser.get_nodes_from_documents([doc])

这段代码将生成三个节点。如果我们查看第二个节点,例如,通过运行 print(nodes[1]),我们将得到以下输出:

复制代码
Node ID: 8fcbda35-1d4d-40d3-89bc-ff1ffbbd7568
Text: Sentence 2.

可以看到,解析器提取了第二个句子并为节点分配了一个随机 ID。但是,如果我们通过运行 print(nodes[1].metadata) 查看节点的元数据,还可以看到它收集的上下文,使用我们指定的键:

复制代码
{'ContextWindow': 'Sentence 1. Sentence 2. Sentence 3.', 'node_text': 'Sentence 2.'}

这些元数据可以在构建查询时使用,为每个句子提供更多上下文并改善LLM的响应。

我们将在第六章《查询我们的数据,第一部分------上下文检索》中更详细地探讨这一点。

  1. 在设置中配置它们

    from llama_index.core import Settings, Document, VectorStoreIndex
    from llama_index.core.node_parser import SentenceWindowNodeParser

    doc = Document(
    text="Sentence 1. Sentence 2. Sentence 3."
    )
    text_splitter = SentenceWindowNodeParser.from_defaults(
    window_size=2,
    window_metadata_key="ContextWindow",
    original_text_metadata_key="node_text"
    )
    Settings.text_splitter = text_splitter
    index = VectorStoreIndex.from_documents([doc])

第二种选择在需要为应用程序中的多个用途自动使用相同的解析器时更加通用和方便:

这次,在定义和配置自定义 text_splitter 之后,我们将其预加载到 Settings 中。从此之后,无论何时调用任何依赖于文本分割的函数,我们的自定义 text_splitter 将默认使用。

当然,这个例子有点过头了。您可能注意到我使用了一个节点解析器代替简单的文本分割器。我们用节点构建的索引不会从解析器创建的额外上下文元数据中受益。我只是想强调之前关于解析器和分割器的观点。

  1. 将解析器定义为摄取管道中的转换步骤

摄取管道是一种自动化和结构化的摄取数据的方法。它将数据按顺序通过一系列步骤(称为转换)运行。

我将在本章稍后的《使用摄取管道提高效率》部分解释其工作原理及其用途。您还将看到将解析器作为管道中转换的实现代码。

接下来,我们将讨论元数据以及如何使用元数据来改进我们的RAG应用程序。

利用元数据改进上下文

什么是元数据?它只是我们可以附加到文档和节点上的附加信息。这些额外的上下文帮助 LlamaIndex 更好地理解我们的数据。它提供了关于数据的附加上下文,并且可以在可见性和格式方面进行自定义。

例如,假设你已经将一些 PDF 报告作为文档摄取。你可以简单地添加一些元数据,如下所示:

复制代码
document.metadata = {
    "report_name": "Sales Report April 2022",
    "department": "Sales",
    "author": "Jane Doe"
}

这些元数据在以后查询数据时提供了重要的线索。在这个例子中,我们可以用它来按部门或作者定位报告。你可以将任何有用的信息存储为元数据------类别、时间戳、位置等。

这里有一个巧妙的技巧------你在文档上设置的任何元数据都会自动流向子节点!因此,如果我在文档上设置了一个 author 字段,从该文档派生的所有节点将继承 author 元数据。这种传播节省了时间,并防止在节点间重复元数据。

有多种定义元数据的方法:

  1. 直接在文档构造函数中设置元数据值:

    Document(
    text="",
    metadata={"author": "John Doe"}
    )

  2. 在文档创建后添加元数据:

    document.metadata = {"category": "finance"}

  3. 在使用数据连接器进行摄取时自动设置元数据,如 SimpleDirectoryReader

    def set_metadata(filename):
    return {"file_name": filename}

    documents = SimpleDirectoryReader(
    "./data",
    file_metadata=set_metadata("file1.txt")
    ).load_data()

  4. 使用 LlamaIndex 提供的独立专用提取器。元数据提取器是一种强大的工具,可以利用 LLM 的力量从文本中生成相关元数据。提取的元数据可以附加到文档和节点上,以提供附加的上下文。

  5. 将提取器定义为摄取管道中的转换步骤。就像节点解析器一样,提取器也可以成为管道的一部分。我们将在本章的《使用摄取管道提高效率》部分中讨论这种方法。

但首先,让我们详细了解这些专用元数据提取器,了解它们的工作原理。

在继续之前,如果你想运行以下代码示例,请确保在代码开头包括必要的导入、文档摄取和节点解析逻辑,添加以下行:

复制代码
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter

reader = SimpleDirectoryReader('files')
documents = reader.load_data()
parser = SentenceSplitter(include_prev_next_rel=True)
nodes = parser.get_nodes_from_documents(documents)

这些样板代码准备好你的数据------从 files 子文件夹摄取,并将你需要的一切放入 Nodes 中。我们将在一个名为 metadata_list 的变量中存储元数据。我在每个示例的末尾添加了 print(metadata_list),这样我们就能看到提取的元数据的输出。除了描述它们的逻辑,我还强调了每个提取器的实际用途。

SummaryExtractor

此提取器生成节点包含文本的摘要。可选地,它可以生成前一个和下一个相邻节点的摘要。下面是一个示例:

复制代码
from llama_index.core.extractors import SummaryExtractor

summary_extractor = SummaryExtractor(summaries=["prev", "self", "next"])
metadata_list = summary_extractor.extract(nodes)
print(metadata_list)

此提取器为每个节点或相邻节点生成简明摘要。这些在 RAG 架构中的检索阶段至关重要。这确保搜索可以考虑文档的摘要,而不必处理它们的全部内容。

实际用例

想象一下一个客户支持知识库,SummaryExtractor 可以提供客户问题和解决方案的摘要。然后,当有新的支持请求进来时,我们的应用程序可以检索最相关的过去案例,以帮助生成详细且有上下文的解决方案。

你可以通过在 summaries 列表中设置值以及在 prompt_template 参数中定义将用于 LLM 的实际提示,来自定义生成的摘要类型。

QuestionsAnsweredExtractor

此提取器生成节点文本可以回答的指定数量的问题。以下示例应提供使用指南:

复制代码
from llama_index.core.extractors import QuestionsAnsweredExtractor

qa_extractor = QuestionsAnsweredExtractor(questions=5)
metadata_list = qa_extractor.extract(nodes)
print(metadata_list)

此提取器识别文本可以回答的问题,使检索过程能够专注于明确回答特定查询的节点。

实际用例

对于 FAQ 系统,此提取器识别文章回答的独特问题,使得更容易找到用户查询的精确答案。

你可以自定义生成的问题数量,还可以通过设置 prompt_template 参数自定义将用于 LLM 的实际提示。如果将 embedding_only 布尔参数设置为 True,则该元数据将仅对嵌入可用。更多内容将在第五章《使用 LlamaIndex 建立索引》中讨论。

TitleExtractor

此提取器从文本中提取标题。下面是一个示例:

复制代码
from llama_index.core.extractors import TitleExtractor

title_extractor = TitleExtractor()
metadata_list = title_extractor.extract(nodes)
print(metadata_list)

TitleExtractor 专门从较大的文本中提取有意义的标题,帮助快速识别和检索文档。在数字图书馆中,例如,TitleExtractor 可以帮助通过从无标题文本中提取标题来对文档进行分类,从而提高使用标题作为搜索关键词时的检索效率。有几个参数你可以调整:

  • nodes:设置用于标题提取的节点数
  • node_template:更改用于提取标题的默认提示模板
  • combine_template:更改用于在文档范围内合并多个节点级标题的提示模板

EntityExtractor

此提取器使用 span-marker 包从节点文本中提取实体,如人物、地点、组织等。此包与 EntityExtractor 集成一起自动安装,因此无需额外安装。它能够执行命名实体识别(NER),并依赖于自然语言工具包(NLTK)提供的分词器。

关于 NER 的简短说明

NER 是一种计算机用于识别和标记文本中特定实体的技术,例如人名、公司名、地点和日期。这有助于计算机更好地理解内容,并在 RAG 场景中提供有用的上下文。

以下是使用此提取器的代码示例:

复制代码
from llama_index.core.extractors import EntityExtractor

entity_extractor = EntityExtractor(
    label_entities=True,
    device="cpu"
)
metadata_list = entity_extractor.extract(nodes)
print(metadata_list)

提取器识别文本中的命名实体,标记它们,并将它们添加到元数据中,使检索系统能够专注于包含特定引用的节点。

实际用例

想象一下法律文件档案中每个节点都有此元数据。此提取器可以简化检索提到特定人物、地点或组织的文件,从而为我们的查询提供最佳上下文。

KeywordExtractor

此提取器从文本中提取重要的关键词。让我们看看一个示例:

复制代码
from llama_index.core.extractors import KeywordExtractor

key_extractor = KeywordExtractor(keywords=3)
metadata_list = key_extractor.extract(nodes)
print(metadata_list)

此提取器识别重要的词语或短语,是根据用户查询检索最相关节点的宝贵工具。

实际用例

将 KeywordExtractor 集成到内容推荐引擎中可以显著提高其效果。通过对齐从内容节点中提取的关键词与用户搜索中使用的术语,引擎可以更准确地匹配和推荐与用户兴趣相关的内容。这种基于关键词的匹配确保推荐不仅相关,还能针对用户探索的具体查询或主题进行量身定制。

你可以通过更改 keywords 参数自定义生成的关键词数量。

PydanticProgramExtractor

此提取器使用 Pydantic 结构提取元数据。请看这里以获取使用此提取器的完整示例:docs.llamaindex.ai/en/stable/e...

这个多功能工具使我们能够通过使用 Pydantic 模型的单个 LLM 调用创建复杂和结构化的元数据架构。与其他提取器相比,它的主要优势之一是它可以通过单个 LLM 调用提取多个字段的数据,使其成为提取元数据的非常高效的方法。这些数据将被很好地组织在我们设计的模型中。

MarvinMetadataExtractor

此提取器使用 Marvin AI 工程框架提取元数据(www.askmarvin.ai/)。利用 Marvin AI 工程框架的优势,此提取器能够进行可信和可扩展的元数据提取和增强。其复杂性在于提供类型安全的文本架构------类似于 Pydantic 模型------但也支持业务逻辑转换。你可以在这里找到详细示例:docs.llamaindex.ai/en/stable/e...

定义你的自定义提取器

如果这些现成的提取器都不能满足你的需求,你可以始终定义自己的提取器函数。下面是一个简单的示例,说明如何定义自定义提取器:

复制代码
from llama_index.core.extractors import BaseExtractor
from typing import List, Dict

class CustomExtractor(BaseExtractor):
    async def aextract(self, nodes) -> List[Dict]:
        metadata_list = [
            {
                "node_length": str(len(node.text))
            }
            for node in nodes
        ]
        return metadata_list

这个基本的提取器测量每个节点的字符长度并将这些值保存到元数据中。当然,你可以用任何应用程序需要的逻辑来替换它。

拥有这么多可用的工具和方法是一件好事。但随之而来的是一个新问题:我们真的需要那么多元数据吗?让我们找出答案。

拥有所有这些元数据总是好事吗?

不一定。一个关键的细节是元数据会被注入到发送给 LLM 和嵌入模型的文本中。这可能会在模型中引入一些偏差。这意味着有时你可能不希望所有元数据都是可见的。例如,文件名可能对嵌入有帮助,但可能会分散 LLM 的注意力,因为 LLM 可能不会将它们理解为文件名,而是作为其他实体,此外文件名在提示的上下文中可能没有关联。你可以使用以下命令选择性地隐藏元数据:

复制代码
document.excluded_llm_metadata_keys = ["file_name"]

这会从 LLM 中隐藏 file_name。如果需要,你也可以隐藏嵌入中的元数据:

复制代码
document.excluded_embed_metadata_keys = ["file_name"]

此外,你可以这样自定义元数据格式:

复制代码
document.metadata_template = "{key}::{value}"

这里有一个处理元数据模式的专业提示。LlamaIndex 有一个名为 MetadataMode 的枚举,控制元数据的可见性:

  • MetadataMode.ALL:显示所有元数据
  • MetadataMode.LLM:仅对 LLM 可见元数据
  • MetadataMode.EMBED:仅对嵌入可见元数据

你可以使用以下命令测试元数据的可见性:

复制代码
print(document.get_content(metadata_mode=MetadataMode.LLM))

总之,元数据为你的数据提供了急需的上下文。你可以完全控制其格式和对不同模型的可见性。这些自定义让你可以根据你的用例调整元数据!

讨论完这个话题,现在是时候谈谈钱的问题了。

估算使用元数据提取器的潜在成本

在使用 LlamaIndex 中的各种元数据提取器时,一个关键考虑因素是相关的 LLM 计算成本。如前所述,这些提取器中的大多数依赖 LLM 来分析文本并生成描述性元数据。

反复调用 LLM 处理大量文本可能会迅速增加费用。例如,如果你使用 SummaryExtractorKeywordExtractor 从成千上万的文档节点中提取摘要和关键词,那么这些持续的 LLM 调用将带来显著的成本。

遵循以下简单的最佳实践以尽量减少成本

让我们讨论一些常见的最佳实践,以尽量减少你的 LLM 成本:

  1. 将内容批量处理成更少的 LLM 调用,而不是对每个节点进行单独调用。 这可以摊销开销,因为与多个单独调用相比,你消耗的标记更少。使用 Pydantic 提取器非常有用,因为它可以在单个 LLM 调用中生成多个字段。
  2. 使用计算要求较低的便宜 LLM 模型,如果不需要完全的准确性。 但是要小心------你可能会在数据中引入错误,而这些错误会在后续处理中传播和放大。
  3. 缓存以前的提取结果,避免每次都重新调用 LLM。 我将在本章后面的《使用摄取管道提高效率》部分中向你展示如何实现这一点。
  4. 仅限于对关键节点的选择性元数据提取,而不是全面覆盖。 这在自动化场景中可能难以实现。
  5. 考虑离线 LLM 以消除云成本。 这取决于你的硬件,这可能是也可能不是一个解决方案。

虽然这些指南应该有助于大大减少提取成本,但在处理大数据集之前,最好确保先进行一些估算。

在实际运行提取器之前估算你的最大成本

以下是如何在运行真正的提取器之前使用 MockLLM 估算 LLM 成本的基本示例:

复制代码
from llama_index.core import Settings
from llama_index.core.extractors import QuestionsAnsweredExtractor
from llama_index.core.llms.mock import MockLLM
from llama_index.core.schema import TextNode
from llama_index.core.callbacks import (
    CallbackManager,
    TokenCountingHandler
)

llm = MockLLM(max_tokens=256)
counter = TokenCountingHandler(verbose=False)
callback_manager = CallbackManager([counter])

Settings.llm = llm
Settings.callback_manager = CallbackManager([counter])

sample_text = (
    "LlamaIndex is a powerful tool used "
    "to create efficient indices from data."
)
nodes = [TextNode(text=sample_text)]

extractor = QuestionsAnsweredExtractor(show_progress=False)
Questions_metadata = extractor.extract(nodes)

print(f"Prompt Tokens: {counter.prompt_llm_token_count}")
print(f"Completion Tokens: {counter.completion_llm_token_count}")
print(f"Total Token Count: {counter.total_llm_token_count}")

你会注意到我们使用了一些专用工具来进行实际的估算。MockLLM------顾名思义------是一个模拟 LLM 的替代品,它模拟 LLM 的行为,而无需进行任何实际的 API 调用。

max_token 参数的作用是什么?

目标是预测最坏情况下的情景,但你的实际成本将根据 LLM 响应的大小有所不同,并且在大多数常规场景下应该低于 max_tokens 值。这仍然是一个非常有用的工具,因为它帮助你了解不同的元数据提取策略应用于不同数据集时如何影响总成本。对于元数据提取,总成本将取决于提示和响应大小乘以提取器执行的调用总数。

CallbackManager 是 LlamaIndex 中实现的调试机制,我们将在第十章《提示工程指南和最佳实践》中详细介绍。在我们的示例中,CallbackManagerTokenCountingHandler 模块结合使用,该模块专门用于计算与 LLM 相关的各种操作使用的标记数。当定义 TokenCountingHandler 时,你还可以指定一个 tokenizer 参数。

什么是 tokenizer 以及为什么需要它?

tokenizer 负责将文本标记化,即将其转换为标记,因为 LLM 使用标记进行工作并且也使用标记来测量其使用情况。当为特定提示运行成本预测时,重要的是使用与特定 LLM 兼容的 tokenizer。每个 LLM 通常与特定的 tokenizer 一起训练,这决定了文本如何被分割成标记。如果你想进行更准确的成本预测,使用正确的 tokenizer 非常重要。默认情况下,LlamaIndex 使用 CL100K tokenizer,这是 GPT-4 的特定 tokenizer。因此,如果你计划使用其他 LLM,可能需要自定义 tokenizer。更多关于这个主题以及如何优化 RAG 应用的成本的内容将在第十章《提示工程指南和最佳实践》中讨论。

回到我们的示例,当我们运行提取器时,它使用 MockLLM------因此,一切都在本地进行。然后,TokenCountingHandler 拦截这个 MockLLM 的提示和响应,并计算实际使用的标记数。

我们将在第 5 章和第 6 章讨论类似的机制,用于估算生成某些类型索引和运行查询的成本。

在这个示例中,我展示了如何估算仅一种提取器 QuestionsAnsweredExtractor 的成本。如果你需要在同一次运行中估算多种提取器的单个成本,你可以使用 token_counter.reset_counts() 方法在下一轮提取之前将计数器重置为零。

本节的主要教训

虽然丰富的元数据解锁了许多功能,但不经过优化的过度使用可能会对运营成本产生负面影响并破坏你的计划。确保考虑到这一点。应用最佳实践以尽量减少成本,并始终在大数据集上运行提取器之前进行估算。

接下来,让我们讨论另一个非常重要的考虑因素------数据隐私。

保护隐私不仅仅是使用元数据提取器

复制代码
pip install llama-index-llms-huggingface


from llama_index.core.postprocessor import NERPIINodePostprocessor
from llama_index.llms.huggingface import HuggingFaceLLM
from llama_index.core.schema import NodeWithScore, TextNode

original = (
    "Dear Jane Doe. Your address has been recorded in "
    "our database. Please confirm it is valid: 8804 Vista "
    "Serro Dr. Cabo Robles, California(CA)."
)
node = TextNode(text=original)
processor = NERPIINodePostprocessor()
clean_nodes = processor.postprocess_nodes(
    [NodeWithScore(node=node)]
)
print(clean_nodes[0].node.get_text())

输出结果:

复制代码
Dear [PER_5]. Your address has been recorded in our database. Please confirm it is valid: 8804 [LOC_95] Dr. [LOC_111], [LOC_124]([LOC_135]).

利用 LLM 增强你自己的数据------在许多情况下,这些数据可能属于你的客户------在数据隐私方面可能是一项具有挑战性的任务。虽然基于云的 LLM 解决方案可以丰富你的专有数据并提供众多优势,但与外部方共享数据的不受控制的行为可能很快就会变成法律、安全和合规方面的噩梦。

尽管在索引和查询的情况下,数据隐私问题更为突出,但使用元数据提取器也可能引发潜在的隐私问题。因此,我认为在这里已经需要一个简要的警告。

由于大多数提取器依赖 LLM 处理内容以生成元数据,这意味着你的实际数据会被传输到外部云服务并进行分析。

存在泄露或误用数据中任何个人或机密信息的风险,无论是由于安全漏洞、LLM 供应商的内部风险,还是恶意活动。

这不仅仅是我们的隐私问题

谈到隐私问题,记得我们之前讨论过的 LlamaHub 连接器示例吗?使用 DiscordReader 获取消息会将数据从 Discord 服务器传输出来。鉴于 Discord 消息可能包含私人对话,如果不考虑 Discord 的服务条款和消息发送者的期望,这可能会带来隐私问题。因此,如果你的数据包含私人身份、医疗细节、财务信息等,允许不受限制的提取工作流可能会带来问题。

这里有一些减轻隐私风险的方法:

  • 在将数据引入 LlamaIndex 之前,使用例如 PIINodePostprocessor 结合本地 LLM 清理个人数据。下一节将为此选项提供一个简单的实施指南。
  • 将元数据提取限制在仅非敏感的节点子集上。当然,这需要手动分类每个节点的敏感性。对于自动化处理管道来说,这将是不切实际的。
  • 在可能的情况下,本地运行 LLM 以限制外部曝光。当然,这取决于你的硬件和模型选择。
  • 如果某些 LLM 供应商提供了启用加密机制的功能,则启用它们。如果隐私是你实现中的一个重要问题,你可能想考虑并阅读更多关于完全同态加密(FHE)的内容:huggingface.co/blog/encryp...

这些担忧和最佳实践适用于与任何 LLM 的交互。这一主题已在许多可用的讲座和文章中进行了讨论和分析,因此我在这里不会详细介绍。但这并不意味着它不重要!

关键信息

你应该明白,使用 LLM 已经对你的数据构成了隐私风险。通过一个像 LlamaIndex 这样的额外框架增强该 LLM 也意味着增强涉及的隐私风险。

从本质上讲,当处理私人数据时,需要额外的谨慎,以确保便利性不会凌驾于安全要求之上。

清理个人数据和其他敏感信息

在一个充满好奇旁观者和数据规则书的世界中,小心谨慎地处理你的数据是至关重要的。好消息是,有一些确保隐私的解决方案。LlamaIndex 框架已经提供了一个方便的方法。

节点后处理器可以为我们解决这个问题。

在前一章中,我们发现节点后处理器在查询引擎中使用。它们被应用于从检索器返回的节点,在响应合成步骤之前,对节点或节点数据本身进行不同的转换。这至少是它们最常见的用例。

但还有另一个使用它们的理由

事实证明,我们也可以在查询引擎之外使用节点处理器。它们可以用于在使用外部 LLM 提取元数据之前清理任何敏感数据。

有两种可用的方法:PIINodePostprocessorNERPIINodePostprocessor。第一种方法旨在与任何你手头的本地 LLM 配合使用,而另一种方法则专门用于使用专门的 NER 模型。如果你不熟悉这个缩写,PII 代表个人身份信息。

这里有一个使用 NERPIINodePostprocessor 清理数据的简单示例。此方法使用 Hugging Face 的 NER 模型来完成这项工作。因为我想保持简单,所以没有指定特定的模型。因此,你可能会收到一个警告,HuggingFaceLLM 可能会默认使用 dbmdz/bert-large-cased-finetuned-conll03-english 模型,如此处所述:huggingface.co/dbmdz/bert-...

确保首先安装相应的集成包:

复制代码
pip install llama-index-llms-huggingface

此外,在第一次运行时,代码将从 Hugging Face 下载模型,你需要确保机器上至少有 1.5 GB 的可用空间。

以下是代码:

复制代码
from llama_index.core.postprocessor import NERPIINodePostprocessor
from llama_index.llms.huggingface import HuggingFaceLLM
from llama_index.core.schema import NodeWithScore, TextNode

original = (
    "Dear Jane Doe. Your address has been recorded in "
    "our database. Please confirm it is valid: 8804 Vista "
    "Serro Dr. Cabo Robles, California(CA)."
)
node = TextNode(text=original)
processor = NERPIINodePostprocessor()
clean_nodes = processor.postprocess_nodes(
    [NodeWithScore(node=node)]
)
print(clean_nodes[0].node.get_text())

输出结果应类似于:

复制代码
Dear [PER_5]. Your address has been recorded in our database. Please confirm it is valid: 8804 [LOC_95] Dr. [LOC_111], [LOC_124]([LOC_135]).

查看结果,我们可以看到名字已被占位符替换,因此现在可以安全地将数据传递给任何外部 LLM。这种方法的妙处在于,在返回时,答案可以重新处理,占位符可以替换为原始数据,从而实现无缝的用户体验。

实际的占位符与真实数据之间的映射将存储在 clean_nodes[0].node.metadata 中。此元数据不会发送给 LLM,之后可以用于在响应合成期间生成原始名称。

接下来,我们将讨论如何提高摄取管道的效率。

使用摄取管道来提高效率

复制代码
from llama_index.core import SimpleDirectoryReader
from llama_index.core.extractors import SummaryExtractor, QuestionsAnsweredExtractor
from llama_index.core.node_parser import TokenTextSplitter
from llama_index.core.ingestion import IngestionPipeline, IngestionCache
from llama_index.core.schema import TransformComponent

class CustomTransformation(TransformComponent):
    def __call__(self, nodes, **kwargs):
        # 在这里运行任何节点转换逻辑
        return nodes

reader = SimpleDirectoryReader('files')
documents = reader.load_data()

try:
    cached_hashes = IngestionCache.from_persist_path("./ingestion_cache.json")
    print("找到缓存文件。使用缓存运行")
except:
    cached_hashes = ""
    print("未找到缓存文件。无缓存运行")

pipeline = IngestionPipeline(
    transformations = [
        CustomTransformation(),
        TokenTextSplitter(
            separator=" ",
            chunk_size=512,
            chunk_overlap=128
        ),
        SummaryExtractor(),
        QuestionsAnsweredExtractor(
            questions=3
        )
    ],
    cache=cached_hashes
)

nodes = pipeline.run(
    documents=documents,
    show_progress=True,
)

pipeline.cache.persist("./ingestion_cache.json")
print("所有文档已加载")

from llama_index.core import Settings

Settings.transformations = [
    CustomTransformation(),
    TokenTextSplitter(
        separator=" ",
        chunk_size=512,
        chunk_overlap=128
    ),
    SummaryExtractor(),
    QuestionsAnsweredExtractor(
        questions=3
    )
]

从版本 0.9 开始,LlamaIndex 框架引入了一个非常巧妙的概念:所谓的摄取管道。

一个简单的类比

摄取管道有点像工厂里的传送带。在 LlamaIndex 的上下文中,它是一个将你的原始数据准备好以集成到你的 RAG 工作流程中的设置。它通过将数据逐步运行------称为转换------逐一完成。关键思想是将摄取过程分解为一系列可重用的转换,并应用于输入数据。这有助于标准化和定制不同用例的摄取流程。可以将转换视为传送带上的不同工作站。当你的原始数据经过时,它会碰到不同的站点,在那里发生特定的事情。例如,它可能在一个站点被分割成句子------这就是你的 SentenceSplitter------并在另一个站点提取标题------例如使用 TitleExtractor。

如果工厂的默认工作站不完全适合你,没问题!假设你有一个特殊工具想要用于你的原始数据。LlamaIndex 使得将你的自定义工具轻松插入管道变得容易。只需说明你的工具做什么------例如,使用字典将缩写替换为完整名称------LlamaIndex 将愉快地将其添加到你的管道中。图 4.4 提供了一个摄取管道的示意图:

摄取管道最重要的事情是它记住了已经处理过的数据。

它对每个节点数据和每次运行的转换组合运行一个哈希函数。在未来的任何运行相同节点的相同转换时,哈希值将相同,因此将使用缓存的、已经处理过的数据,而不是重新运行转换。

这对我意味着什么?

如果你再次将相同的文档发送到管道,就像有一个快速通道,它跳过了队列,因为它已经被处理过了。这很酷,因为它节省了你的时间和金钱,避免了无用的多次处理相同数据。

默认情况下,缓存存储在本地,但你可以自定义存储选项并使用任何你喜欢的外部数据库提供商。让我们看一个如何实现管道的示例。我将逐节解释代码,以便更容易理解。

让我们从代码的第一部分开始:

在处理完必要的导入之后,为了向你展示如何定制你的管道,我定义了一个名为 CustomTransformation 的类。这将稍后被馈送到管道中。在我的示例中,没有实际处理发生,因此这将返回未更改的节点。

继续第二部分:

上述代码负责将 files 子文件夹的所有内容获取为文档。接下来,代码检查缓存文件是否已经存在,并尝试将其加载到内存中。记住,缓存文件包含以前运行生成的哈希值和结果。第一次运行代码时,将没有文件,因此代码不会加载任何缓存值。

移动到第三部分:

这是我们定义管道的部分。如你所见,它将包含四个转换。第一个是我们的 CustomTransformation,接着是 TokenTextSplitter,负责将每个文档分割成更小的块并生成节点。第三个转换提取摘要元数据,最后一个提取每个节点可以回答的一组问题。

如果你想查看结果,可以在整个脚本的末尾添加 print(nodes[0])。注意,在缓存参数中,我们还指定了管道的缓存来源。如果为空,它将被忽略;否则,将使用它来避免任何不必要的处理,通过从缓存中检索值。

最后一部分:

这是我们运行管道并将 show_progress 选项设置为 True 的地方。这将使管道的进度可见,并帮助你更好地了解后台发生的事情。最后,我们将结果保存到缓存文件中,以避免在下次运行时重新处理。

一个快速的旁注:

即使你保存了一个缓存文件,你在管道逻辑中所做的任何更改都不会被缓存,必须在下次运行时处理。

你还应该知道,还有一种替代方法可以避免每次想摄取更多数据时手动定义和运行管道。就像节点解析器一样,我们可以在 Settings 中定义转换,如下所示:

复制代码
from llama_index.core import Settings

Settings.transformations = [    CustomTransformation(),    TokenTextSplitter(        separator=" ",        chunk_size=512,        chunk_overlap=128    ),    SummaryExtractor(),    QuestionsAnsweredExtractor(        questions=3    )]

总之,摄取管道是一种超高效的方法,可以通过运行自定义的一系列转换来自动准备和优化你的数据,直到它适合你的应用或数据库。

在构建 PITS 辅导应用程序时,我们将利用摄取管道,你将有机会更多地体验这个概念。

接下来,让我们讨论更复杂的场景。

处理包含文本和表格数据的文档

数据并不总是简单的。许多现实世界中的文档,如研究论文、财务报告等,包含结构化表格数据以及非结构化文本。在摄取这些异构文档时,面临的一个额外挑战是我们不仅需要提取文本,还需要识别、解析和处理嵌入在文本中的表格数据。因为有时你会得到表格,有时是文本,有时则是两者的混合。

LlamaIndex 提供了 UnstructuredElementNodeParser,用来处理包含自由格式文本和表格等结构化元素的文档。它利用 Unstructured 库来分析文档布局,划分文本部分和表格。

此解析器专门用于 HTML 文件,可以提取两种类型的节点:

  • 文本节点:包含文本块
  • 表格节点:包含表格数据和元数据(例如坐标)

将这些元素存储为单独的节点可以在 RAG 工作流程中实现更模块化和有意义的处理。文本可以与关键字等元素一起进行正常索引和搜索。表格可以加载到 pandas DataFrame 或任何结构化数据库中,以进行基于 SQL 的访问。因此,在涉及混合数据类型的复杂情况下,在摄取之前利用 UnstructuredElementNodeParser 可以实现更好的数据组织。

在官方 LlamaIndex 文档中可以找到使用 UnstructuredElementNodeParser 的完整演示:docs.llamaindex.ai/en/stable/e...

实践操作------将学习资料导入我们的 PITS

是时候进行一些实践操作了。我们现在拥有继续构建项目所需的一切。让我们编写 document_uploader.py 模块。 该模块将负责导入和准备我们现有的学习材料。用户可以上传任何可用的书籍、技术文档或现有文章,为我们的导师提供更多的上下文。

首先,我们需要导入相关的模块:

复制代码
from global_settings import STORAGE_PATH, CACHE_FILE
from logging_functions import log_action
from llama_index import SimpleDirectoryReader, VectorStoreIndex
from llama_index.ingestion import IngestionPipeline, IngestionCache
from llama_index.text_splitter import TokenTextSplitter
from llama_index.extractors import SummaryExtractor
from llama_index.embeddings import OpenAIEmbedding

接下来,我们必须定义负责处理导入过程的主函数。你会注意到,它使用了一个摄取管道来简化代码并利用缓存:

复制代码
def ingest_documents():
    documents = SimpleDirectoryReader(
        STORAGE_PATH,
        filename_as_id=True
    ).load_data()
    for doc in documents:
        print(doc.id_)
        log_action(
            f"File '{doc.id_}' uploaded by user",
            action_type="UPLOAD"
        )

该函数加载了 global_settings.py 中定义的 STORAGE_PATH 中的所有可读文档。 对于处理的每个文档,使用 logging_functions.py 中的 log_action 在日志文件中存储一个新事件。

接下来,函数检查是否有已缓存的管道数据可用:

复制代码
    try:
        cached_hashes = IngestionCache.from_persist_path(CACHE_FILE)
        print("Cache file found. Running using cache")
    except:
        cached_hashes = ""
        print("No cache file found. Running without cache")

下一步是定义并运行管道。如果缓存文件中的哈希值对应,则不应处理任何操作,而应直接从缓存中加载值:

复制代码
    pipeline = IngestionPipeline(
        transformations=[
            TokenTextSplitter(
                chunk_size=1024,
                chunk_overlap=20
            ),
            SummaryExtractor(summaries=['self']),
            OpenAIEmbedding()
        ],
        cache=cached_hashes
    )
    nodes = pipeline.run(documents=documents)
    pipeline.cache.persist(CACHE_FILE)
    return nodes

我们在管道中运行三个转换:

  1. 使用 TokenTextSplitter 进行基本的分块。
  2. 使用元数据提取器总结每个节点。
  3. 使用 OpenAIEmbedding 生成嵌入。现在不用担心这一步,我会在第5章"使用 LlamaIndex 进行索引"中详细解释。

最后,函数将当前数据保存在缓存文件中,并返回处理后的节点。

就这样,现在我们已经上传并准备好了学习材料以供将来处理。我们将在下一章继续处理索引部分。

总结

LlamaHub 提供了多种预构建的数据加载器,简化了将数据从各种来源导入为文档的过程。这消除了为不同数据格式创建独特解析器的需要。

在数据导入后,它会进一步处理成节点,我们讨论了各种可用的自定义选项。

有很多元数据提取的选项,解析过程可以根据特定需求进行定制。

开发数据摄取管道是提高我们 RAG 应用效率的无价工具,不论是在成本还是时间方面。考虑隐私问题也至关重要。

随着数据摄取的完成,让我们继续探索 LlamaIndex 的索引功能。

如何学习AI大模型?

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;

第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;

第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;

第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;

第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;

第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;

第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。

👉学会后的收获:👈

• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

1.AI大模型学习路线图
2.100套AI大模型商业化落地方案
3.100集大模型视频教程
4.200本大模型PDF书籍
5.LLM面试题合集
6.AI产品经理资源合集

👉获取方式:

😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓

相关推荐
Arbori_262151 分钟前
获取oracle表大小
数据库·oracle
掘金一周2 分钟前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
王强你强8 分钟前
MySQL 高级查询:JOIN、子查询、窗口函数
数据库·mysql
草巾冒小子9 分钟前
brew 安装mysql,启动,停止,重启
数据库·mysql
用户62799471826215 分钟前
南大通用GBase 8c分布式版本gha_ctl 命令-HI参数详解
数据库
白雪讲堂18 分钟前
AI搜索品牌曝光资料包(精准适配文心一言/Kimi/DeepSeek等场景)
大数据·人工智能·搜索引擎·ai·文心一言·deepseek
斯汤雷24 分钟前
Matlab绘图案例,设置图片大小,坐标轴比例为黄金比
数据库·人工智能·算法·matlab·信息可视化
ejinxian30 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
SQLplusDB31 分钟前
Oracle 23ai Vector Search 系列之3 集成嵌入生成模型(Embedding Model)到数据库示例,以及常见错误
数据库·oracle·embedding
葡萄成熟时_35 分钟前
【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】【代码篇】A题解题全流程(持续更新)
人工智能·数据挖掘