LlamaIndex 概念解析
LLamaIndex 采用一种被称为“逐步揭示复杂性“(progressive disclosure of complexity)的设计原则。基于这个原则,完成一个任务,只需要短短的几行代码就能够实现。然而,如果需要对任务进行一些配置,或者实现一些更深入、更细致的功能,就需要对组件进行自定义,或者添加更深入的配置。此时,对于相关概念和组件的理解,就显得尤为重要。因为LlamaIndex涉及了相当多的概念,针对这些概念官方站点上又提供了上百的示例程序,所以想要深入理解和掌握仍是有一定难度的。本文将从定义和作用两个方面,对LLamaIndex的核心概念进行阐述。本文的内容,大体上是:官方文档、AI模型问答、《Building Data-Driven Applications with LlamaIndex》,以及我个人理解的综合。
RAG 处理管线
RAG 的作用就是为大模型引入本地知识,使得大模型不再只能基于预训练的数据进行回答,而是能够将本地私有知识库考虑在内。RAG 的处理过程,大体分为下面几个步骤:
- 对本地文档构建向量数据库
- 用户提问,根据提问问题,在向量数据库中检索出合适的段落作为上下文
- 对用户提问和本地数据进行装饰,添加提示语
- 将整理后的内容(包含了用户问题、本地相关的上下文、提示语)一起发送给大模型
- 大模型进行处理后,将结果返回给用户
文档(document)
文档是一个通用的容器,适用于任何数据源——例如,PDF、API 输出或从数据库中检索的数据。文档可以手动构建,也可以通过数据加载器自动创建。默认情况下,文档存储文本及其他一些属性。文档比较重要的几个属性是:
- text:文档的文本内容
- metadata:可以附加到文档上的注释字典;包含关于文档的描述性信息,比如文件名、创建时间、作者、标签、分类等。这些元数据在检索和过滤时非常有用
- relationships:定义文档/节点间关系(如父子文档、前后顺序),用于构建图结构索引
- id_:文档的唯一ID,用于在索引中区分不同的文档
数据连接器(Data Connector)
我们可以在代码中手动创建文档对象(Documment),但是更多的情况下,数据原本就存在于各式各样的文档中,例如:PDF、网页、文本文件、CSV、Word文档、API、数据库 等等。数据连接器 负责从各种数据源中提取和加载数据,将异构数据统一转换为框架可处理的 Document 对象。
数据连接器的主要作用如下:
- 数据提取:从各种格式和来源的数据中提取文本内容,包括文件、数据库、API、网页等。
- 格式解析:处理不同的文件格式,如 PDF、Word、Excel、Markdown、HTML 等,将其转换为纯文本或结构化数据。
- 元数据提取:自动提取文件属性、创建时间、作者信息等元数据,并将其附加到生成的 Document 中。
- 批量处理:支持一次性处理多个文件或数据源,提高数据加载的效率。
常见的数据连接器例如:
文件型:
- SimpleDirectoryReader:读取目录中的各种文件
- PDFReader:专门处理 PDF 文档
- DocxReader:处理 Word 文档
- CSVReader:读取 CSV 文件
数据型:
- DatabaseReader:连接各种 SQL 数据库
- MongoReader:连接 MongoDB
- PineconeReader:从 Pinecone 向量数据库读取
网页型:
- WebPageReader:抓取网页内容
- YoutubeTranscriptReader:获取 YouTube 视频字幕
节点(Nodes)
LlamaIndex 中的节点(Nodes)是系统的核心数据结构之一,它比 Document 更加细粒度,是实际参与索引构建和检索的基本单元。和Document类似,可以在代码中手动构建Node,但Node通常是通过将 Document 进行分块(chunking)后生成的。它代表文档分割后的语义片段,直接影响检索精度与效率。节点是文档被分割后的最小语义单元,通常对应文本段落、表格或图像片段。
节点的属性包括:
- 节点ID(node_id):每个 Node 都有唯一的标识符,用于在索引中进行引用和管理。
- 文本内容(text):原始数据的语义片段(如一个自然段)。这段文本通常是语义相对完整的片段,比如几个句子或一个段落。
- 元数据(metadata):继承自父文档的属性(如文件名、来源 URL)或自定义属性(如作者、时间戳)
- 关系(relationships):定义节点与其他节点或者文档之间的关系(如父子关系、前后顺序),用于构建图结构索引
- 向量嵌入(embedding):Node 的向量表示,用于语义检索和相似性计算。
节点的核心作用:
- 粒度控制:将大文档拆解为小单元,适配 LLM 的上下文长度限制
- 精准检索:通过语义单元匹配问题,避免返回无关内容。
节点的生成过程:
- 文档分块:将原始 Document 按照一定策略分割成多个较小的文本片段。分块策略可以基于字符数、token 数、句子边界或语义边界。
- 节点创建:为每个文本片段创建对应的 Node 对象,继承来源文档的元数据并添加位置信息。
- 关系建立:在节点之间建立必要的关系链接,保持上下文的连贯性。
节点解析器(Node Parser)
前面有说,节点通常是将文档进行分块(chunking)后生成的。而 节点解析器 就是负责将文档分块,并创建节点的组件。换言之,处理流程类似这样:document --> node parser --> nodes。换言之:节点是一个结果,Node Parser则负责转换和生成节点。
NodeParser的核心功能
- 文本解析:处理输入的原始文本,将其拆分成适合索引的节点单元。解析可以基于句子、段落或其他语义结构。
- 元数据提取:从原始文档提取相关的元信息(例如标题、作者、日期等),并将其包含在生成的节点中。
- 格式适配:处理不同类型的文档格式(如 PDF、HTML、Markdown),确保文本的正确解析和节点的生成。
- 节点创建:为解析后的文本生成 Node 对象,完成从原始数据到节点的转换过程。
- 关系构建:自动建立节点间关系(如顺序关系),支持图结构索引。
关键参数
对于处理文本的解析器,常见的关键参数包括:
- chunk_size:每个节点的最大长度(如字符数、token 数),用于控制节点的粒度。
- chunk_overlap:节点之间的重叠长度,用于确保上下文的连续性。
- separators:用于定义文本分割的边界,如句号、换行符等。
- metadata_extractors:用于从文档中提取自定义的元数据(如标题、作者、日期等)。
常见的解析器
显然,不同的文档结构完全不同。例如:普通的文本、代码文件(python、go)、HTML文件 结构不同。因此,需要选择合适的解析器进行处理,常见的解析器如下:
- SentenceSplitter:基于句子边界进行解析
- SimpleParser:简单的文本解析器,按行分割
- CodeSplitter:用于解析代码文件,支持多种编程语言(如 Python、Java、C++ 等)
- MarkdownParser:用于解析 Markdown 文件
- HTMLNodeParser :用于解析 HTML 文件
这里类型有些是以Parser结尾,有些是以Splitter结尾,但它们均继承自 NodeParser 基类,核心任务是将原始文档(Document)分割为节点(Nodes)。Parser强调的是将文档解析为节点,而 Splitter 是通用切分策略,解决文本物理切割的边界问题。两者在LlamaIndex中可以结合使用。
向量(Vector)和嵌入(Embedding)
向量
向量是数学中的基本概念,在计算机科学中用来表示多维数据。向量是一个有序的数字列表,例如:[0.2, -0.5, 0.8, 0.1, -0.3]。在LlamaIndex中,向量的作用如下:
- 数据表示: 将复杂的信息(如文字、图片、音频)转换为计算机能理解的数字形式
- 相似性计算: 通过计算向量间的距离来判断相似程度
- 数学运算: 支持各种数学操作,如加法、减法、点积等
嵌入
嵌入是将离散的、高维的或复杂的数据转换为连续的、低维的向量表示的过程。嵌入是过程,向量是结果。
原始数据 -> 嵌入过程 -> 向量 苹果 -> Embedding -> [0.2, -0.5, 0.8, 0.1, -0.3]
数据向量化之后,最大的好处就是可以进行相似度运算:
# 用户查询 查询: "如何做红烧肉?" 查询向量: [0.1, 0.7, -0.2, 0.4, ...] # 文档库中的文档 文档1: "红烧肉的制作方法" → [0.2, 0.6, -0.1, 0.5, ...] 文档2: "计算机编程教程" → [-0.5, 0.1, 0.8, -0.3, ...] # 通过向量相似性找到最相关的文档 文档1 相似度: 0.92 ← 最相关 文档2 相似度: 0.15 ← 不相关
嵌入模型(embedding-model)
具体执行 embedding 这一过程,将文本进行向量化的工具与算法,就是嵌入模型(embedding-model)。嵌入模型的输入是一段文本,输出是一个向量。
使用大语言模型来执行embedding,是因为大语言模型具有强大的文本理解和表示能力。
索引(Indexing)
索引由nodes构建,用于快速获取和用户提问相关的上下文。简单来说,就是从所有的nodes中找出和用户提问最相关的节点(Nodes)。索引由下面几部分构成:
- 文档存储(Document Store):存储实际的文档数据或节点对象。文档是用户输入的原始数据,索引会基于这些文档创建数据结构。
- 索引元数据(Index Metadata):包含关于索引的描述信息,如索引的类型、版本、创建时间、更新记录等。这些元数据有助于索引的管理和维护。
- 向量存储(Vector Store):存储与文档相关的向量表示(embeddings)。这些向量通常基于文档的内容通过某种嵌入算法生成,便于进行相似性计算和高效检索。
- 属性图存储(Property Graph Store,若适用):如果索引用于构建知识图谱,属性图存储会保存图结构和节点属性信息,使得查询和分析图数据成为可能。
常见的索引类型
索引类型 | 简要说明 |
---|---|
VectorStoreIndex(向量存储索引) | 基于向量相似性检索,查询时通过计算向量相似度找到最相关内容 |
ListIndex(列表索引) | 按顺序存储文档节点;查询时会考虑所有或大部分节点;不依赖向量相似性 |
TreeIndex(树形索引) | 自底向上构建摘要树; 支持层级式信息检索; 可以获得不同粒度的信息 |
DocumentSummaryIndex(文档摘要索引) | 预生成文档摘要; 基于摘要进行初步筛选; 结合摘要和原文内容 |
不同索引类型的工作流程
ListIndex
用户查询 → 遍历所有节点 → 将相关节点发送给LLM → 生成回答
VectorStoreIndex
用户查询 → 向量化 → 向量相似性搜索 → 返回top-k相似节点 → 发送给LLM → 生成回答
TreeIndex
构建时: 文档 → 分块成多个节点(叶子节点) → 对叶子节点进行分组 → LLM 为每组生成摘要(父节点) → 递归向上构建,直到根节点 → 存储完整的树形结构 查询时: 用户查询 → 与根节点摘要比较相关性 → 选择最相关的子节点路径 → 递归向下遍历子树 → 到达最相关的叶子节点 → 将相关节点内容发送给LLM → 生成最终回答
DocumentSummaryIndex
构建时: 文档 → 分块成节点(按文档分组) → LLM 为每个完整文档生成摘要 → 建立摘要到原文档节点的映射关系 → 存储摘要索引和原文档内容 查询时: 用户查询 → 与所有文档摘要进行比较 → 根据相关性排序选择top-k文档 → 获取选中文档的完整原文内容 → 将相关文档内容发送给LLM → 基于原文生成详细回答
存储上下文(StorageContext)
如果文档内容比较多,构建索引不仅是一个耗时的过程,而且还是一个花钱的事情(调用LLM接口的费用)。在默认情况下,索引构建完成后,存在于内存当中。但是,LlamaIndex也支持将索引保存在外部(磁盘、数据库),再下一次使用时,直接从外部进行读取就可以了。
StorageContext就是统一管理数据存储组件的核心容器,负责协调索引构建与查询所需的各类存储后端。除了索引以外,还细分成了下面几种数据的存储:
- 文档存储(Document Store):保存原始文档分块后的 Node 对象(含文本和元数据)
- 索引存储(Index Store):记录索引结构元数据(如向量索引的节点关系)
- 向量存储(Vector Store):存储文本嵌入向量,支持语义检索
- 图存储(Graph Store):存储知识图谱的三元组(用于 KnowledgeGraphIndex)
- 对话存储(Chat Store):管理多轮对话历史消息
检索器(Retriever)
检索器具体负责根据用户查询从索引中检索相关信息的核心组件。它是连接用户查询和文档内容之间的桥梁,决定了哪些文档片段会被送给LLM进行最终的答案生成。当需要基于特定情况对查询结果进行过滤,例如根据权限进行访问控制的时候,就可以通过自定义检索器来完成。
Index是数据的组织和存储方式,关注的是”数据怎么存“;Retriever是从Index中检索数据的执行机制,关注的是”数据怎么取“。Retriever的主要作用是:从节点中筛选出预查询最相关的内容,从而减少发送给LLM的无关信息,提高响应质量。
不同的索引(index),对应着不同的检索器(Retriever)。例如下面的检索器:
- VectorIndexRetriever:向量检索器
- ListIndexRetriever:列表检索器
- TreeIndexRetriever:树形检索器
- DocumentSummaryIndexRetriever:文档摘要检索器
后处理器(Postprocessor)
Postprocessor(后处理器) 是 LlamaIndex 中用于对检索结果进行二次处理和优化的组件。它在 Retriever 检索出候选节点后、将结果传递给 LLM 生成答案前发挥作用。其简要流程如下:
用户查询 → Retriever检索 → Postprocessor后处理 → 优化后的节点 → LLM生成答案
其核心作用,包含了下面几个方面:
- 质量筛选:过滤低质量或不相关的检索结果
- 结果排序:重新排列检索结果的优先级
- 内容增强:为检索结果添加额外信息或上下文
- 结果去重:移除重复或高度相似的内容
常见后处理器的类型:
- SimilarityPostprocessor(相似度后处理器):根据内容相似度过滤结果
- KeywordNodePostprocessor(关键词后处理器):根据关键词过滤结果
- LLMRerank(LLM 排序后处理器):根据 LLM 打分结果排序
- DuplicateRemoverNodePostprocessor(重复节点移除后处理器):移除高度相似的节点
- EmbeddingSimilarityPostprocessor(嵌入相似度后处理器):根据嵌入向量相似度过滤结果
响应合成器(Response Synthesizer)
用户的一个问题,可能关联多个节点(Nodes),而这些节点的长度,超过了LLM模型的最大上下文长度。此时,一个合理的方案就是:1、分批次将用户查询和节点发送给LLM;2、再将每次LLM的返回的结果进行合并,最终构成给用户显示的完整回复。
这就是 响应合成器 的主要作用。所有与LLM的交互均由响应合成器发起和调度,用户仅发起一次查询,但 响应合成器 可能进行多次LLM调用(不同模式策略决定)。简言之,它的职责如下:
- 决定如何组织和分批发送上下文
- 控制与 LLM 的交互策略
- 合并多次 LLM 调用的结果
查询引擎(Query Engine)
查询引擎是连接 用户查询 与 数据 的主要接口。用户发起查询,查询引擎负责将自然语言问题转换为结构化检索任务,协调检索器、后处理器和响应合成器,最终返回给用户一个完整的答案。
简单理解:查询引擎相当于是对底层组件的一个封装,相当于一个更高级的接口,且直接面向用户,接受用户的查询。
查询引擎适合处理单次问询(一问一答),它不具备记忆功能。
聊天引擎(Chat Engine)
聊天引擎 是 查询引擎(Query Engine)的“有状态”版本。通过持续追踪对话历史,系统能够结合上下文语境给出更为精确的答案。聊天引擎 底层依赖 查询引擎执行单轮检索与生成,但在此基础上添加了对话状态管理。聊天引擎 可以将复杂对话拆解成为子问题,调用 查询引擎 获取分步答案。
它的核心能力有下面几项:
- 上下文记忆:保留对话历史,理解用户后续问题的上下文关联
- 多种对话模式:提供 best、condense_question 等模式,适应不同对话场景
- 流式输出:支持逐词生成响应,提升交互体验
感谢阅读,希望这篇文章能给你带来帮助!