1 LlamaIndex 简要介绍

LlamaIndex 是连接私有数据与大型语言模型的桥梁,核心目标是弥合私有数据与大型语言模型(如 GPT、Claude 或本地模型)之间的鸿沟,它能够轻松构建基于检索增强生成 (RAG)的强大应用。

1.1 核心价值

  • 数据接入:无缝连接各种私有或专有数据源(如 PDF、文档、数据库、API、云存储),让 LLM 能够访问和处理这些 LLM 本身无法看到的信息。
  • 弥补 LLM 固有缺陷
    • 实时信息缺失:LLM 训练数据通常滞后,LlamaIndex 可以利用最新数据。
    • 无法访问私有数据:使 LLM 可以基于您的私有信息进行推理和生成。
    • 幻觉问题:通过提供事实性基础数据,减少 LLM 产生无根据信息的风险。
  • 构建上下文感知应用:基于您的特定数据构建问答系统、智能聊天机器人、代理助手和知识管理系统。

1.2 核心组件

  1. 数据连接器(Ingestion/Data Loaders):
    • 支持从 150+ 种数据源加载数据(本地文件、S3、Notion、数据库等)。
  2. 索引(Indexing):
    • 将原始数据处理成优化的数据结构,便于快速查询检索。
    • 常用索引类型
      • 向量存储索引(VectorStoreIndex:基于语义嵌入(Embeddings)进行相似性搜索(最常见)。
      • 摘要索引(SummaryIndex:提取文本摘要,适合摘要型查询。
      • 树索引(TreeIndex:构建树状结构,实现多级查询(如摘要查询后下钻)。
      • 关键词表索引(KeywordTableIndex:基于关键词匹配进行搜索(稀疏检索)。
      • 知识图谱索引(KnowledgeGraphIndex:构建实体关系图进行结构化查询(较少用)。
  3. 检索器(Retrievers):
    • 根据查询,从索引中高效、准确地检索出最相关的上下文片段。
    • 支持高级检索技术:混合搜索(Hybrid Search)、重排序(Re-ranking)、元数据过滤(Metadata Filtering)等,提升召回率和精准度。
  4. 响应合成器(Response Synthesis):
    • 将检索到的上下文片段输入给 LLM,指导 LLM 生成最终的自然语言响应。
    • 提供多种合成策略(如 refine, compact, tree_summarize),平衡响应质量和成本。

1.3 核心流程

RAG一般分成两个阶段:1. 知识库构建阶段:构建知识库;2. 查询阶段:从知识库检索相关上下文信息,以辅助 LLM 回答问题。

  1. 知识库构建阶段
    知识库构建阶段有几个关键步骤:文档解析、文档切分、文本嵌入(文本块向量化)、保存为本地知识库。在LlamaIndex中,流程如下:

在这里插入图片描述
Data Source 是数据源,可以是本地文档,也可以是数据库,或者从API接口中获得的数据;Data Connectors 是数据连接器,它负责将来自不同数据源的不同格式的数据解析出来,并转换为 LlamaIndex 支持的文档(Document)表现形式(不管什么数据进来,都会转化为这个格式),其中包含了文本和元数据;元数据经过 Embedding 模型进行文本向量化,并保存到向量数据库(例如ChromaDB、FAISS等)中,称为知识库(KnowledgeBase)。

  1. 查询阶段
    查询阶段的几个步骤:提示词文本向量化(和构建知识库时使用同一个 Embedding model)、知识库检索、检索结果后处理(重排序)、模型响应。在 LlamaIndex中,过程示意图如下:

在这里插入图片描述
图中 Retrievers 是检索器,它定义如何高效地从知识库检索相关上下文信息;Node Postprocessors 是 Node后处理器,它对一系列文档节点(Node)实施转换、过滤或排名;Response Synthesizers 是响应合成器,它将用户的提示词和一组检索到的文本块合并形成上下文,然后利用 LLM 生成响应。

1.4 为什么要用 LlamaIndex?

  • 高效智能检索:自动化处理数据块切分(Chunking)、嵌入计算和高效检索,避免您重复造轮子。
  • 高度模块化与灵活
    • 兼容多种 LLM 提供商(OpenAI、Anthropic、Hugging Face 等)。
    • 支持主流向量数据库(Pinecone、Chroma、Qdrant、Milvus、FAISS 等)。
    • 可与 LangChain 等框架结合使用。
  • 功能先进强大
    • 支持多文档/多步骤复杂查询(代理、多跳问答)。
    • 提供工具增强代理(Agent Tooling)。
    • 集成评估和微调功能(Fine-tuning)。
  • 降低成本:通过优化上下文检索和合成策略,有效减少向 LLM 发送的 token 数量。
  • 专注于核心任务:抽象化底层复杂逻辑,让开发者聚焦于应用构建本身。

1.5 典型应用场景

  • 基于私有文档(手册、合同、报告)的 问答系统
  • 企业知识助手,让员工轻松访问内部知识库。
  • 数据感知型聊天机器人,能回答关于公司特定数据的问题。
  • 自动化研究工具,整合多个来源的信息。
  • 定制化业务分析,对报告数据进行自然语言查询。

1.6 与类似工具对比

工具 核心侧重点 最适合场景
LlamaIndex 端到端 RAG 流程 需要复杂上下文检索、连接私有数据与 LLM 的应用
LangChain 通用 LLM 应用开发编排 需要链接多个组件(模型、工具、记忆)的复杂流程
FAISS 纯向量相似度搜索库 仅需底层高效向量检索功能
Chroma 开源向量数据库 需要轻量级向量数据库进行本地嵌入存储和检索

关键洞察LlamaIndex 精于高效、结构化的上下文检索,并将其融入 LLM 生成流程(RAG)。LangChain 则擅长编排复杂的、多步骤的 LLM 任务链(例如使用工具、管理记忆)。两者功能重叠,可结合使用(LlamaIndex 常作为 LangChain 的检索模块)。

1.7 安装

新建一个 python 3.12 的环境

conda create -n llama_index python=3.12 -y
pip install llama-index		# 不要带版本号,我们直接安装最新的

上面只是基本安装了核心包,具体使用的时候,还需要安装很多附加包。

1.8 学习资源

  • 官方文档 (英文,最权威)
  • GitHub 仓库
  • 搜索中文社区(如知乎、微信公众号)中的相关教程和案例。

总结: LlamaIndex 是简化构建私有数据驱动的 LLM 应用的关键利器,尤其擅长解决 RAG 中的高效上下文检索与整合问题。它让您充分利用大模型能力,同时避免信息孤岛和不准确信息的问题。

2 文档解析与 Document 对象

2.1 示例文件与代码

以读取单个文本为例,假设在 data 目录下有一个名为 report_with_table.pdf 的文件,文件中只有一页,内容如下:

在这里插入图片描述

运行如下代码:

from llama_index import SimpleDirectoryReader

# 读取单个文件,需要将文件路径放到列表里,然后传给 input_files 
reader = SimpleDirectoryReader(
    input_files=["data/report_with_table.pdf"]
)

docs = reader.load_data()       # 返回的是一个列表,该列表只有一个 Document 对象
print(f"Loaded {len(docs)} docs")
print(docs)
print('-'*80)
print(docs[0])

输出:

Loaded 1 docs
[Document(id_='64f93aff-d379-4357-bc42-a994e0b5e144', embedding=None, metadata={'page_label': '1', 'file_name': 'report_with_table.pdf', 'file_path': 'data/report_with_table.pdf', 'file_type': 'application/pdf', 'file_size': 58537, 'creation_date': '2025-06-18', 'last_modified_date': '2025-06-18'}, excluded_embed_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], excluded_llm_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], relationships={}, metadata_template='{key}: {value}', metadata_separator='\n', text_resource=MediaResource(embeddings=None, data=None, text='# 季度销售报告(示例内容)\n## 销售业绩\n本季度总销售额达到$1.2M,环比增长 15%。主要增长来自亚太地区...\n## 数据明细\n地区 Q1 销售额 Q2 销售额 增长率\n北美 $400K $420K 5%\n欧洲 $300K $330 10%\n亚太 $350K $450K 28.6%', path=None, url=None, mimetype=None), image_resource=None, audio_resource=None, video_resource=None, text_template='{metadata_str}\n\n{content}')]
--------------------------------------------------------------------------------
Doc ID: 64f93aff-d379-4357-bc42-a994e0b5e144
Text: # 季度销售报告(示例内容) ## 销售业绩 本季度总销售额达到$1.2M,环比增长 15%。主要增长来自亚太地区... ##
数据明细 地区 Q1 销售额 Q2 销售额 增长率 北美 $400K $420K 5% 欧洲 $300K $330 10% 亚太 $350K
$450K 28.6%

在 LlamaIndex 框架中,Document 对象是一个核心数据结构,它表示被处理数据的最小逻辑单元,完整类名为 llama_index.core.schema.Document

我们来解析 Document 对象的结构:

# 解析 Document 对象的结构
for key, field in docs[0].__dict__.items():
    print(f"{key} ====>", field)

输出

id_ ====> 64f93aff-d379-4357-bc42-a994e0b5e144
embedding ====> None
metadata ====> {'page_label': '1', 'file_name': 'report_with_table.pdf', 'file_path': 'data/report_with_table.pdf', 'file_type': 'application/pdf', 'file_size': 58537, 'creation_date': '2025-06-18', 'last_modified_date': '2025-06-18'}
excluded_embed_metadata_keys ====> ['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date']
excluded_llm_metadata_keys ====> ['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date']
relationships ====> {}
metadata_template ====> {key}: {value}
metadata_separator ====> 

text_resource ====> embeddings=None data=None text='# 季度销售报告(示例内容)\n## 销售业绩\n本季度总销售额达到$1.2M,环比增长 15%。主要增长来自亚太地区...\n## 数据明细\n地区 Q1 销售额 Q2 销售额 增长率\n北美 $400K $420K 5%\n欧洲 $300K $330 10%\n亚太 $350K $450K 28.6%' path=None url=None mimetype=None
image_resource ====> None
audio_resource ====> None
video_resource ====> None
text_template ====> {metadata_str}

{content}

从 Document 对象的 text_resource 可以看到原始文本

2.2 Document 对象的核心特性

  1. 文本内容容器

    • 包含从原始文档中解析出的所有文本内容(包含表格转换后的文本)
    • 示例:print(docs[0].text[:200]) 会显示 PDF 前 200 个字符
  2. 元数据存储

    • 包含文件信息的字典:document.metadata
    • 自动填充的关键信息:
      {
      	'page_label': '1',	# 页数
          'file_path': 'data/report_with_table.pdf',
          'file_name': 'report_with_table.pdf',
          'file_type': 'application/pdf',		# 文件类型
          'file_size': 58537,  # 字节大小
          'creation_date': '2023-01-15', # 创建日期
          'last_modified': '2023-10-01'  # 最后修改日期
      }
      
  3. 唯一标识符

    • 自动生成的文档 ID:document.id_
    • 用于在索引中唯一标识该文档

2.3 在 RAG 工作流程中的角色

原始文件
Document 对象
索引处理
节点 Node
文本块+元数据+嵌入向量
查询检索
  1. 数据准备阶段

    • 每个文件被转换为一个 Document 对象,即使 PDF 有多页也属于同一 Document
    • PDF/Word/HTML 等格式都会被解析为统一文本格式,图片/图表等非文本内容默认会被忽略(若不想被忽略则需要 OCR 扩展)
  2. 索引构建阶段

    • Document 被分割为更小的 Node (节点) 对象,每个对象对应一个文本块
  3. 元数据继承

    • 所有从 Document 分割出的 Node 都会继承原始元数据
    • 支持后续的元数据过滤查询:

总结:Document 对象是 LlamaIndex 数据处理流程中的原子单位,作为连接原始数据与向量索引的关键桥梁,封装了内容、元数据和身份信息,为后续的检索增强生成(RAG)提供结构化数据基础。

2.4 多文件解析与专业解析工具

2.2.1的示例只解析了一个文件,假如我有多个文件,该如何解析?

假设 /data/coding/llama-index/data 目录下的文档有6个,包含 txt、json、markdown、docx、pdf 等格式,如下图所示:

在这里插入图片描述

# 读取多个文件,此时写入文件夹路径
reader = SimpleDirectoryReader("/data/coding/llama-index/data")	

docs = reader.load_data()       # 返回的是由 Document 构成的列表
print(f"Loaded {len(docs)} docs")

输出:

Loaded 6 docs

上面的示例表明,Simple Directory Reader 能解析大部分常用文档,但从 2.2.1 的示例来看,对PDF中的表格解析的很粗糙:

地区 Q1 销售额 Q2 销售额 增长率\n北美 $400K $420K 5%\n欧洲 $300K $330 10%\n亚太 $350K $450K 28.6%

这个结果很粗糙,真给模型用,它未必能读出结果来。

其实,针对不同格式的文件,推荐使用专业的解析工具,比如PDF,可以使用 pdfplumber

import pdfplumber
from pathlib import Path
from llama_index.core.schema import Document  # 注意核心 Document 类的导入路径

def extract_text_with_pdfplumber(pdf_path: str) -> str:
    """使用 pdfplumber 提取 PDF 文本内容(含表格)"""
    full_text = ""
    
    with pdfplumber.open(pdf_path) as pdf:
        for page in pdf.pages:
            # 提取普通文本
            page_text = page.extract_text()
            if page_text:
                full_text += page_text + "\n\n"
            print(full_text)  
            print('-'*80)
            
            # 提取表格数据(转换为文本表格)
            tables = page.extract_tables()
            for table in tables:
                print("\n表格内容:")
                for row in table:
                    print(row)
            print('-'*80)
    
    return full_text.strip()

# 使用 pdfplumber 处理 PDF
pdf_path = "data/report_with_table.pdf"
pdf_content = extract_text_with_pdfplumber(pdf_path)

# 构建元数据字典
pdf_metadata = {
    "file_path": pdf_path,
    "file_name": Path(pdf_path).name,
    "file_type": "application/pdf",
    "file_size": Path(pdf_path).stat().st_size,
}

# 创建 LlamaIndex 的 Document 对象
document = Document(
    text=pdf_content,
    metadata=pdf_metadata,
    # 以下为可选参数
    id_=f"pdfplumber_{Path(pdf_path).stem}",  # 自定义文档ID
    excluded_embed_metadata_keys=["file_size"],  # 元数据中不参与嵌入的字段
)

# 现在可以用于构建索引
documents = [document]  # LlamaIndex 需要文档列表

print(f"文档文本前200字符: {document.text[:200]}...")
print(f"元数据: {document.metadata}")

输出:

# 季度销售报告(示例内容)
## 销售业绩
本季度总销售额达到$1.2M,环比增长15%。主要增长来自亚太地区...
## 数据明细
地区 Q1销售额 Q2销售额 增长率
北美 $400K $420K 5%
欧洲 $300K $330 10%
亚太 $350K $450K 28.6%


--------------------------------------------------------------------------------

表格内容:
['地区', 'Q1销售额', 'Q2销售额', '增长率']
['北美', '$400K', '$420K', '5%']
['欧洲', '$300K', '$330', '10%']
['亚太', '$350K', '$450K', '28.6%']
--------------------------------------------------------------------------------
文档文本前200字符: # 季度销售报告(示例内容)
## 销售业绩
本季度总销售额达到$1.2M,环比增长15%。主要增长来自亚太地区...
## 数据明细
地区 Q1销售额 Q2销售额 增长率
北美 $400K $420K 5%
欧洲 $300K $330 10%
亚太 $350K $450K 28.6%...
元数据: {'file_path': 'data/report_with_table.pdf', 'file_name': 'report_with_table.pdf', 'file_type': 'application/pdf', 'file_size': 58537}

2.5 在线寻找解析工具

对于网页、数据库、API数据源的解析,可以在 LlamaIndex 官网寻找解析工具,假如我们想解析一个网页,内容如下:
在这里插入图片描述

下面讲解步骤:

先进入官网
在这里插入图片描述

点击最上方的 Examples 选项卡:
在这里插入图片描述

在侧边栏找到 Data Connectors,展开就是 llama-index 官方针对各种数据源提供的解析工具
在这里插入图片描述
在这里插入图片描述

我们找到 Web Page Reader:
在这里插入图片描述

进去可以看到用法和示例,我尝试了一下示例,使用下面的代码提取信息:

from llama_index.readers.web import SimpleWebPageReader
from IPython.display import Markdown, display

documents = SimpleWebPageReader(html_to_text=True).load_data(
    ["http://paulgraham.com/worked.html"]
)

display(Markdown(f"<b>{documents[0].text_resource.text}</b>"))

上面的程序在控制台打印不出来,最好在 jupyter 中打印,效果很明显:

在这里插入图片描述
在这里插入图片描述

最简单、最基础的是 Simple Directory Reader,对于纯文档(没有表格、流程图、图片等)非常合适:

在这里插入图片描述

3 文档切分与 Node 对象

一个文档可能会非常大,比例硕士博士的毕业论文,动辄几十上百页,必须对齐进行分块处理。

3.1 常用分块方式

分块的常用方法有以下几种:
(1)按固定字数/token数分块,这种一般用于格式化数据,例如古诗词、对联;
(2)按段落分块,对于每个自然段落的含义相对独立的文章,这种分块方式很适合;
(3)按语义分块,这是使用文本嵌入模型,让模型计算每条句子的语义,然后与前一句进行相似度比较,如果低于阈值就作为新的一块开始,这种分块方式较优,是最常用的方式,但计算资源消耗较大;
(4)按业务逻辑分块,若上述分块方式都无法对文档进行合理分块,那么只能根据业务逻辑,自己设计一种分块方式;
(5)人工分块,对于无法使用固定规则处理的文档,那么只能采用人工了,比如,一些散文、歌词等,你很难用程序去划分,因为划分的结果总少了那么一丝“感觉”,但这种感觉又很难用规则描述出来,这个时候只能上人工。

总的来说,文档切分是为了更好的适配后续的检索,要尽量做到每个分块的含义相对完整且只包含一个知识点,避免知识点被打断,没有固定的方案,都是要结合项目需求来设计切分方案。

此外,对于小说、新闻等内容,切分的时候要保留每个分块的上下文关系,这样AI才能理解块与块之间的顺序关系,因此需要一定的重叠度,即第一块和第二块有10%~20%的字数重叠,第二块和第三块有10%~20%的字数重叠,以此类推。分块是否需要重叠,关键是要看块与块之间是否需要包含上下文关系,即是否需要有关联。

3.2 示例

假设现有一篇文章(文件名为 ai.txt),内容如下:

人工智能的发展史是一个充满探索与突破的历程,以下按照时间线为您梳理其关键阶段和里程碑事件:
1. 起源阶段(20世纪50年代)
1950年:英国数学家艾伦·图灵(Alan Turing)提出“图灵测试”,这是首次尝试定义机器智能的标准,标志着人工智能概念的萌芽。
1956年:美国达特茅斯学院召开第一次人工智能研讨会,正式提出“人工智能”这一术语,这被认为是人工智能作为一门学科的诞生标志。
早期研究:这一时期的研究主要集中在符号主义和逻辑推理,例如机器定理证明和跳棋程序等。
2. 第一次低谷(20世纪70年代)
原因:由于技术限制和资金不足,人工智能研究未能达到预期的高期望,导致第一次“人工智能寒冬”。
影响:研究重点转向更具体的应用领域,如专家系统和自然语言处理。
3. 复苏与初步应用(20世纪80-90年代)
1980年代:专家系统(如医疗诊断系统)开始被广泛应用,人工智能在商业领域初露锋芒。
1997年:IBM的“深蓝”计算机在国际象棋比赛中战胜世界冠军加里·卡斯帕罗夫,这是人工智能在复杂决策任务中的重大突破。
4. 第二次低谷(20世纪90年代末至21世纪初)
原因:尽管有“深蓝”等成功案例,但人工智能的整体发展仍受限于计算能力和数据规模,导致第二次“人工智能寒冬”。
转折点:随着互联网的普及和大数据技术的发展,人工智能研究逐渐复苏。
5. 深度学习与复兴(21世纪初至今)
2006年:加拿大科学家杰弗里·辛顿(Geoffrey Hinton)提出深度学习理论,为人工智能的突破奠定了基础。
2012年:深度学习在图像识别任务中首次超越人类表现,标志着人工智能进入实用化阶段。
2016年:谷歌的AlphaGo击败围棋世界冠军李世石,展示了人工智能在复杂策略游戏中的强大能力。
2020年代:大型语言模型(如GPT-3、GPT-4)的出现,使人工智能在自然语言处理、代码生成等领域取得突破性进展。
6. 当前与未来展望
当前应用:人工智能已广泛应用于医疗、交通、金融、教育等领域,例如自动驾驶、智能客服和个性化推荐系统。
未来趋势:随着算力的提升和算法的优化,人工智能有望在更多复杂任务中实现突破,同时伦理与安全问题也成为研究重点。

现在要将其分块,我们这里演示一下固定token数分块和语义分块。

固定token数分块

from llama_index.core import SimpleDirectoryReader
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# 加载文档
documents = SimpleDirectoryReader(input_files=["/data/coding/llama-index/data/test.txt"]).load_data()

# 案例1:固定token数分块 
from llama_index.core.node_parser import TokenTextSplitter 
fixed_splitter = TokenTextSplitter(chunk_size=200, chunk_overlap=20) 
fixed_nodes = fixed_splitter.get_nodes_from_documents(documents)  # 返回的是一个 TextNode 对象构成的列表
print("固定分块示例:", [len(n.text) for n in fixed_nodes]) 

输出:

固定分块示例: [85, 155, 97, 127, 159, 123, 62, 117]

LlamaIndex 的分割器使用 tiktoken 等 tokenizer,其规则复杂(如合并空格、分割词缀),这里输出中的 [85, 155, 97, …] 是字符数统计,与 token 数无关,每个块的实际 token 数会接近 200,但字符数取决于文本密度(英文平均 1 token ≈ 4 字符,中文 ≈ 2 字符)。如果还是搞不明白,那就先忽略,因为实际项目中很少会按固定 token 数来分割文档。

我们来看看每个分块的内容:

for n in fixed_nodes:
    print(n.text)
    print('-'*80)

输出

人工智能的发展史是一个充满探索与突破的历程,以下按照时间线为您梳理其关键阶段和里程碑事件:
1. 起源阶段(20世纪50年代)
1950年:英国数学家艾伦·图灵(Alan
--------------------------------------------------------------------------------
Turing)提出“图灵测试”,这是首次尝试定义机器智能的标准,标志着人工智能概念的萌芽。
1956年:美国达特茅斯学院召开第一次人工智能研讨会,正式提出“人工智能”这一术语,这被认为是人工智能作为一门学科的诞生标志。
早期研究:这一时期的研究主要集中在符号主义和逻辑推理,例如机器定理证明和跳棋程序等。
2.
--------------------------------------------------------------------------------
第一次低谷(20世纪70年代)
原因:由于技术限制和资金不足,人工智能研究未能达到预期的高期望,导致第一次“人工智能寒冬”。
影响:研究重点转向更具体的应用领域,如专家系统和自然语言处理。
3.
--------------------------------------------------------------------------------
复苏与初步应用(20世纪80-90年代)
1980年代:专家系统(如医疗诊断系统)开始被广泛应用,人工智能在商业领域初露锋芒。
1997年:IBM的“深蓝”计算机在国际象棋比赛中战胜世界冠军加里·卡斯帕罗夫,这是人工智能在复杂决策任务中的重大突破。
4.
--------------------------------------------------------------------------------
第二次低谷(20世纪90年代末至21世纪初)
原因:尽管有“深蓝”等成功案例,但人工智能的整体发展仍受限于计算能力和数据规模,导致第二次“人工智能寒冬”。
转折点:随着互联网的普及和大数据技术的发展,人工智能研究逐渐复苏。
5. 深度学习与复兴(21世纪初至今)
2006年:加拿大科学家杰弗里·辛顿(Geoffrey
--------------------------------------------------------------------------------
...
6. 当前与未来展望
当前应用:人工智能已广泛应用于医疗、交通、金融、教育等领域,例如自动驾驶、智能客服和个性化推荐系统。
未来趋势:随着算力的提升和算法的优化,人工智能有望在更多复杂任务中实现突破,同时伦理与安全问题也成为研究重点。
--------------------------------------------------------------------------------

语义分块
这里我们使用 paraphrase-multilingual-MiniLM-L12-v2 作为文本嵌入模型:

from llama_index.core.node_parser import SemanticSplitterNodeParser

# 初始化模型和解析器
embed_model = HuggingFaceEmbedding(
    #指定了一个预训练的sentence-transformer模型的路径
    model_name="/data/coding/model_weights/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
)

semantic_parser = SemanticSplitterNodeParser(
    buffer_size=1,
    breakpoint_percentile_threshold=90,
    embed_model=embed_model
)

# 执行语义分割
semantic_nodes = semantic_parser.get_nodes_from_documents(documents)	# 分割的结果是由 TextNode 对象构成的列表

# 打印结果
print(f"语义分割节点数: {len(semantic_nodes)}")
for i, node in enumerate(semantic_nodes[:2]):  # 只打印前两个节点
    print(f"\n节点{i+1}:\n{node.text}")
    print("-"*50)

输出:

语义分割节点数: 2

节点1:
人工智能的发展史是一个充满探索与突破的历程,以下按照时间线为您梳理其关键阶段和里程碑事件:
1. 起源阶段(20世纪50年代)
1950年:英国数学家艾伦·图灵(Alan Turing)提出“图灵测试”,这是首次尝试定义机器智能的标准,标志着人工智能概念的萌芽。
1956年:美国达特茅斯学院召开第一次人工智能研讨会,正式提出“人工智能”这一术语,这被认为是人工智能作为一门学科的诞生标志。
早期研究:这一时期的研究主要集中在符号主义和逻辑推理,例如机器定理证明和跳棋程序等。
2. 第一次低谷(20世纪70年代)
原因:由于技术限制和资金不足,人工智能研究未能达到预期的高期望,导致第一次“人工智能寒冬”。
影响:研究重点转向更具体的应用领域,如专家系统和自然语言处理。
3. 复苏与初步应用(20世纪80-90年代)
1980年代:专家系统(如医疗诊断系统)开始被广泛应用,人工智能在商业领域初露锋芒。
1997年:IBM的“深蓝”计算机在国际象棋比赛中战胜世界冠军加里·卡斯帕罗夫,这是人工智能在复杂决策任务中的重大突破。
4. 第二次低谷(20世纪90年代末至21世纪初)
原因:尽管有“深蓝”等成功案例,但人工智能的整体发展仍受限于计算能力和数据规模,导致第二次“人工智能寒冬”。
转折点:随着互联网的普及和大数据技术的发展,人工智能研究逐渐复苏。
5. 
--------------------------------------------------

节点2:
深度学习与复兴(21世纪初至今)
2006年:加拿大科学家杰弗里·辛顿(Geoffrey Hinton)提出深度学习理论,为人工智能的突破奠定了基础。
2012年:深度学习在图像识别任务中首次超越人类表现,标志着人工智能进入实用化阶段。
2016年:谷歌的AlphaGo击败围棋世界冠军李世石,展示了人工智能在复杂策略游戏中的强大能力。
...
6. 当前与未来展望
当前应用:人工智能已广泛应用于医疗、交通、金融、教育等领域,例如自动驾驶、智能客服和个性化推荐系统。
未来趋势:随着算力的提升和算法的优化,人工智能有望在更多复杂任务中实现突破,同时伦理与安全问题也成为研究重点。
--------------------------------------------------

上面的程序中,出现了 TextNode 对象,它的全称是 llama_index.core.schema.TextNode,稍后会详细介绍。

3.2 TextNode 对象

在 LlamaIndex 中,TextNode 对象是构建索引的基本单元,它是 Document 对象经过分割处理后生成的更小文本块。以下是 TextNode 的详细介绍:

3.2.1 TextNode 的核心特性和结构

class TextNode(BaseNode):
	id_: str				# 节点的ID,是节点的唯一识别号
    text: str               # 节点实际的文本内容
    embedding: List[float]  # 文本对应的嵌入向量
    metadata: Dict[str, Any] # 元数据字典
    excluded_embed_metadata_keys: List[str] # 不参与嵌入计算的元数据键
    excluded_llm_metadata_keys: List[str]  # 不传递给LLM的元数据键
    relationships: Dict[NodeRelationship, RelatedNodeInfo] # 节点间关系
    hash: str               # 内容哈希值
    class_name: str         # 类名标识(固定为"TextNode")

3.2.2 关键属性详解

  1. text (str)

    • 当前节点包含的实际文本内容(通常是200-500个token)
    • 从原始文档分割出来的连贯语义片段
    • 示例:"LlamaIndex 是一个开源的 Python 框架,用于..."
  2. embedding (List[float])

    • 文本对应的向量表示(128-1536维浮点数)
    • 用于相似性搜索的数学表示
    • 默认情况下这个属性是空的,需要调用嵌入模型生成
  3. metadata (Dict[str, Any])

    • 对于从父Document切割而来的TextNode,则它们的metadata继承自父Document的元数据
    • 自动包含的属性:
      {
          'file_path': '/data/coding/llama-index/data/ai.txt',
          'file_name': 'ai.txt',
          'document_id': 'd758a8a4-fc0b...', # 指向父Document
          'page_label': '1',               # 如果文档分页
          'chunk_size': 200,                # 分块大小
          'chunk_overlap': 20               # 重叠大小
      }
      
    • 实际项目中,经常需要手动设定元数据需要包含的内容(后面会在项目中演示)。
  4. relationships (节点关系)

    • 定义节点间的逻辑关系:
      {
          NodeRelationship.PREVIOUS: RelatedNodeInfo(node_id="node1"),
          NodeRelationship.NEXT: RelatedNodeInfo(node_id="node3"),
          NodeRelationship.PARENT: RelatedNodeInfo(node_id="doc_root"),
          NodeRelationship.SOURCE: RelatedNodeInfo(node_id="doc_id")
      }
      
    • 维护文本块的先后顺序和文档结构
  5. excluded_xxx_metadata_keys

    • 精细化控制元数据的使用:
    • excluded_embed_metadata_keys:指定哪些元数据不参与嵌入向量计算
    • excluded_llm_metadata_keys:指定哪些元数据不传递给语言模型

3.2.3 节点处理流程

Document
文本分割器
TextNode 1
TextNode 2
TextNode ...n
嵌入模型
向量存储

3.2.4 实际使用示例

# 查看分割后的第一个节点
first_node = fixed_nodes[0]		# first_node 是按固定token数分块得到的结果

print(f"节点ID: {first_node.id_}")
print(f"文本内容: {first_node.text[:50]}...")
print(f"文本长度: {len(first_node.text)}字符")
print(f"元数据: {first_node.metadata}")
print(f"来源文档ID: {first_node.ref_doc_id}")

# 手动设置嵌入向量(通常在索引时自动完成)
embed_model = HuggingFaceEmbedding(model_name="/data/coding/model_weights/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
first_node.embedding = embed_model.get_text_embedding(first_node.text)
print(f"嵌入向量维度: {len(first_node.embedding)}")

输出:

节点ID: 5695e0c9-9080-4fde-a158-71eca5ee48a4
文本内容: 人工智能的发展史是一个充满探索与突破的历程,以下按照时间线为您梳理其关键阶段和里程碑事件:
1....
文本长度: 85字符
元数据: {'file_path': '/data/coding/llama-index/data/ai.txt', 'file_name': 'ai.txt', 'file_type': 'text/plain', 'file_size': 2504, 'creation_date': '2025-06-21', 'last_modified_date': '2025-06-18'}
来源文档ID: 92509469-6d0c-4f24-a101-151cfcf662ca
嵌入向量维度: 384

3.2.5 节点与文档的关系

Document
Node 1
Node 2
Node 3
嵌入向量
嵌入向量
嵌入向量
向量数据库

3.2.6 高级应用技巧

  1. 自定义元数据

    for node in fixed_nodes:
        node.metadata["custom_tag"] = "important"
        node.excluded_embed_metadata_keys = ["file_size"]  # 文件大小不参与向量计算
    
  2. 关系维护

    # 建立双向链接
    for i in range(len(fixed_nodes)-1):
        fixed_nodes[i].relationships[NodeRelationship.NEXT] = fixed_nodes[i+1].id_
        fixed_nodes[i+1].relationships[NodeRelationship.PREVIOUS] = fixed_nodes[i].id_
    
  3. 混合节点类型

    from llama_index.core.schema import ImageNode
    # 可以与图像节点等组成混合索引
    
  4. 精细检索控制(了解即可)

    query_engine = index.as_query_engine(
        node_postprocessors=[
            MetadataReplacementPostProcessor(
                target_metadata_key="custom_tag"
            )
        ]
    )
    

3.2.7 为什么需要TextNode?

  1. 粒度控制:相比整个文档,节点级检索更精准
  2. 上下文管理:解决LLM的上下文长度限制
  3. 效率优化:避免每次查询处理整个文档
  4. 关系建模:保持文本块的逻辑顺序
  5. 多模态扩展:统一处理文本、图像等多类型内容

TextNode 是 LlamaIndex 架构中的原子操作单元,它使大文档处理、精准检索和高效生成成为可能,构成了 RAG(检索增强生成)应用的核心基础结构。

4 文本嵌入与向量数据库

文档切分之后,就是输入到文本嵌入模型中,将其转成固定长度的向量,然后存入向量数据库中。

4.1 文本嵌入模型

文本嵌入模型都可以理解成 BERT,用于对文本进行编码,将长度变化的文本转换成固定长度的向量。转成固定长度向量的目的,一是为了方便存储,二是为了方便进行文本相似度计算。

目前用的最多的三个向量维度是 384、768、1024,维度越大,进行嵌入计算时消耗的算力就越大。

文本嵌入模型的选型上,一般只需要考虑两个方面,语言和维度。如果纯中文或者中文占主导地位,那就用中文语料训练出来的,如果是多语言场景,那就用多语言的模型;需要检索精度高就选1024,需要速度快就选384或者768。

工业上用的比较多的嵌入模型有:BGE系列(例如合同/政策文件等,选择BGE-M3)、 M3E-base(社交媒体分析)、paraphrase-multilingual-MiniLM-L12-v2(多语言)。

可以在魔搭上,根据 句子相似度 和 文本向量 去找文本嵌入模型:
在这里插入图片描述

4.2 ChromaDB简介

目前 RAG 存储向量数据库,用的最多的就是 ChromaDB,所以我们这里不介绍其他向量数据库了。

Chroma 是一款开源的向量数据库,专为高效存储和检索高维向量数据设计。其核心能力在于语义相似性搜索,支持文本、图像等嵌入向量的快速匹配,广泛应用于大模型上下文增强(RAG)、推荐系统、多模态检索等场景。与传统关系型数据库不同,Chroma 基于向量距离(如余弦相似度、欧氏距离)衡量数据关联性,而非关键词匹配。

核心优势

  • 轻量易用:以 Python/JS 包形式嵌入代码,无需独立部署,适合快速原型开发。
  • 灵活集成:支持自定义嵌入模型(如 OpenAI、HuggingFace),兼容 LangChain 等框架。
  • 高性能检索:采用 HNSW 算法优化索引,支持百万级向量毫秒级响应。
  • 多模式存储:内存模式用于开发调试,持久化模式支持生产环境数据落地。

安装

pip install chromadb

4.3 ChromaDB基本使用

ChromaDB 只需要记住几个操作:创建客户端与集合,还有对数据库增删改查,以及统计条目数。

下面是创建客户端与集合,以及数据库的增加与查询操作:

import chromadb
from sentence_transformers import SentenceTransformer

# chromadb 现在已经不支持直接加载 SentenceTransformer 模型了,因此需要用 __call__ 函数封装
# 但 llama-index 已经将 chromadb 和 SentenceTransformer 做了集成,不需要我们自己写 __call__ 函数了
class SentenceTransformerEmbeddingFunction:
    def __init__(self, model_path: str, device: str = "cuda"):
        self.model = SentenceTransformer(model_path, device=device)
    
    def __call__(self, input: list[str]) -> list[list[float]]:
        if isinstance(input, str):
            input = [input]
        return self.model.encode(input, convert_to_numpy=True).tolist()

# 创建/加载集合(含自定义嵌入函数)
embed_model = SentenceTransformerEmbeddingFunction(
    model_path="/data/coding/EmotionalDialogue/model_weights/sungw111/text2vec-base-chinese-sentence",
    # model_path="/data/coding/model_weights/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
    device="cuda"  # 无 GPU 改为 "cpu"
)

# 创建客户端和集合
client = chromadb.Client()  # 临时存放在内存中(内存模式)
# client = chromadb.PersistentClient(path="/path/to/save") # 数据保存至本地目录(持久化模式)
collection = client.create_collection("my_knowledge_base",
                                      metadata={"hnsw:space": "cosine"},    # 当前特征嵌入空间中的数据,使用余弦相似度进行搜索
                                      embedding_function=embed_model)

# 添加文档
collection.add(
    documents=["RAG是一种检索增强生成技术", "向量数据库存储文档的嵌入表示","三英战吕布"],       # 文档
    metadatas=[{"source": "tech_doc"}, {"source": "tutorial"}, {"source": "tutorial1"}],    # 元数据,给每个文档取一个元数据的名称(名称要唯一),方便后续查询
    ids=["doc1", "doc2","doc3"]     # 向量的索引名称
)
# 存的时候就会去衡量向量与向量之间的相似度,把相关性高的向量放到了一起,以提高后续检索速度

# 查询相似文档
results = collection.query(
    query_texts=["什么是RAG技术?"],
    n_results=3     # 返回最相似的3个向量
)

print(results)

输出

{'ids': [['doc1', 'doc2', 'doc3']], 'embeddings': None, 'documents': [['RAG是一种检索增强生成技术', '向量数据库存储文档的嵌入表示', '三英战吕布']], 'uris': None, 'included': ['metadatas', 'documents', 'distances'], 'data': None, 'metadatas': [[{'source': 'tech_doc'}, {'source': 'tutorial'}, {'source': 'tutorial1'}]], 'distances': [[0.09666752815246582, 0.2053605318069458, 0.2142857313156128]]}

结果中 ids 是与待查寻文本最接近的三个文档的索引名称,distance 是三个文档与待查文本的距离(不是相似度)。

接下来是修改、删除、统计条目:

# 修改更新
collection.update(
    ids=["doc1"],  # 使用已存在的ID
    documents=["更新后的RAG技术内容"]
)

# 查看更新后的内容 - 方法1:获取特定ID的内容
updated_docs = collection.get(ids=["doc1"])
print("更新后的文档内容:", updated_docs["documents"])

# 查看更新后的内容 - 方法2:查询所有文档
all_docs = collection.get()
print("集合中所有文档:", all_docs["documents"])

# 删除内容
collection.delete(ids=["doc1"])
all_docs = collection.get()
print("集合中所有文档:", all_docs["documents"])

#统计条目
print(collection.count())

输出:

更新后的文档内容: ['更新后的RAG技术内容']
集合中所有文档: ['更新后的RAG技术内容', '向量数据库存储文档的嵌入表示', '三英战吕布']
集合中所有文档: ['向量数据库存储文档的嵌入表示', '三英战吕布']
2

5 检索与模型响应

5.1 检索

检索就是从知识库里找到与用户问题相关的文本,目前用的最多的就是语义相似度检索(也称向量检索):使用嵌入模型把用户问题也转成文本向量,然后与向量数据库中的文本向量逐个计算相似度,找出最接近的几个,与用户问题合并之后喂给大模型。

文本相似度计算一般都是用余弦相似度,因为余弦相似度自带归一化,取值范围为 [-1, 1],当然,一般情况下,检索的时候向量数据库都会对负值进行处理。例如,ChromaDB 评估两个文本的相似度的方式是通过距离来计算的,即 distance = 1 - cosine_similarity(a, b),这样就避开了负数的情况,两个向量距离越近,则说明对应的文本越相似;另外,也可以通过元数据过滤排除负相关文档,代码如下:

# 添加元数据标识
collection.add(
    embeddings=[[0.8, 0.6], [-0.8, -0.5]],
    metadatas=[{"polarity":"pos"}, {"polarity":"neg"}],
    ids=["pos", "neg"]
)

# 只检索正向相关文档
results = collection.query(
    query_embeddings=[[0.9, 0.5]],
    where={"polarity": {"$eq": "pos"}}  # 元数据过滤
)

除了语义相似度检索,还有一种检索方式是混合检索,它其实是在语义相似度的基础上增加了关键词检索,示意图如下:

用户问题
关键词搜索
语义检索
初步结果
合并去重

关键词搜索其实就是关键词匹配,使用的时候,先从用户的问题中解析出关键词,然后再去知识库中匹配包含关键词的文本。

增加关键词检索理论上能提升召回率,较少漏检的可能,但关键词有效的前提是,用户得规范提问,问题中得包含正确的关键词(即知识库中所包含的关键词),这点并不是所有人都能做到,另一方面,增加关键词检索会增加检索时间。因此,混合检索用的是比较少的。

5.2 模型响应

这是RAG的最后一步,实际上就是把检索结果合并,然后一起喂给模型,获得模型的回复。我们用一个程序示例来讲解检索得到向量后如何获得响应。

文档为 Xtuner 在Github上的说明文档(即readme),下面是程序:

from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import Settings,SimpleDirectoryReader,VectorStoreIndex
from llama_index.llms.huggingface import HuggingFaceLLM

#初始化一个HuggingFaceEmbedding对象,用于将文本转换为向量表示,
# 在此之前需要安装 llama_index.embeddings.huggingface,pip install llama_index.embeddings.huggingface
embed_model = HuggingFaceEmbedding(
    #指定一个预训练的sentence-transformer模型的路径,只能加载 sentence-transformer 的模型,其他模型都不行
    model_name="/data/coding/model_weights/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" # L12-V2 模型
)

#将创建的嵌入模型赋值给全局设置的embed_model属性,这样在后续的索引构建过程中,就会使用这个模型
Settings.embed_model = embed_model

#使用HuggingFaceLLM加载本地大模型
llm = HuggingFaceLLM(model_name="/data/coding/model_weights/Qwen/Qwen1.5-1.8B-Chat",
               tokenizer_name="/data/coding/model_weights/Qwen/Qwen1.5-1.8B-Chat",
               model_kwargs={"trust_remote_code":True},
               tokenizer_kwargs={"trust_remote_code":True})
# model_name 和 tokenizer_name 是本地模型的路径,model_kwargs 和 tokenizer_kwargs 这两个参数先不管

#设置全局的llm属性,这样在索引查询时会使用这个模型。
Settings.llm = llm

#读取文档,将数据加载到内存,会转化为 document 对象,这里需要安装 Markdown 的解析工具
documents = SimpleDirectoryReader(input_files=["/data/coding/llama-index/data/README_zh-CN.md"]).load_data()  
# documents是一个列表,里面只有一个 Document 对象,这个对象没有被切分,在后续创建索引的时候,默认只有一个 TextNode

#创建一个VectorStoreIndex,并使用之前加载的文档来构建向量索引
#此索引将文档转换为向量,并存储这些向量(内存)以便于快速检索
index = VectorStoreIndex.from_documents(documents)

#创建一个查询引擎,这个引擎可以接收查询并返回相关文档的响应。
query_engine = index.as_query_engine()          # 通过 index 构建一个查询引擎
rsp = query_engine.query("xtuner是什么?")      # 通过查询引擎来查询用户的问题,语义检索
# 查询的时候,会根据向量索引做一个相似度匹配,匹配的结果再扔给大模型,最后生成响应
# 如果问题与知识库有相关性,则答复的质量会非常高

print(rsp)
# 怎么判断模型生成的内容是幻觉?你得对这个领域有所了解,但不需要很深入,这样就有一定的判别能力

输出:

问题描述中的 "XTuner" 是一个工具,用于高效地调整大型语言模型 (LLM) 的超参数,该工具具有以下特点:
1. 自动分配高性能计算单元 (如 FlashAttention、Triton kernels 等) 加速模型训练。
2. 兼容 DeepSpeed 平台,可以轻松应用于各种 ZeRO 训练优化策略。
3. 支持灵活且多样的大规模语言模型,如 InternLM、Mixtral-8x7B、Llama 2、ChatGLM、Qwen 和 Baichuan。
4. 支持多种文本图模型 LLaVA 的预训练与微调,通过 XTuner 训得模型 LLaVA-InternLM2-20B 表现优异。
5. 设计了数据管道,可以支持任意数据格式,并具有开源数据或自定义数据资源快速获取的能力。
6. 支持增量预训练、指令微调与 Agent 微调,以及预先定义的对话模板和大规模评测工具库。
7. 支持多种微调算法,如 ZeRO-1、ZeRO-2、ZeRO-3 等,可根据实际需求进行最佳微

作为对比,我们来看看不带知识库时,模型的回答:

from llama_index.core.llms import ChatMessage
from llama_index.llms.huggingface import HuggingFaceLLM

#使用HuggingFaceLLM加载本地大模型,需要先安装 llama_index.llms.huggingface
llm = HuggingFaceLLM(model_name="/data/coding/model_weights/Qwen/Qwen1.5-1.8B-chat",
               tokenizer_name="/data/coding/model_weights/Qwen/Qwen1.5-1.8B-chat",
               model_kwargs={"trust_remote_code":True},
               tokenizer_kwargs={"trust_remote_code":True})

#借助llama-index调用本地大模型进行提问
#调用模型chat引擎得到回复,消息必须封装成 llama-index 的 message,即 ChatMessage
rsp = llm.chat(messages=[ChatMessage(content="xtuner是什么?")])    # 提问的时候,不需要自己指定角色
print(rsp)  # 你问了就会给你答案,假如模型不知道,也会给你要给答案,即幻觉

输出:

assistant: "xtuner"是一个中文词语,原意是指"音源搜索器""音乐搜索器",通常用于在线音乐播放平台或其他音频应用程序中。在实际应用中,xtuner通常指的是一个能够自动发现和推荐音乐的系统,它使用机器学习、深度学习等技术,根据用户的听歌历史、喜好、行为习惯等多种因素,为用户推荐最符合其口味的新歌曲或专辑。

以下是一个简单的 xtuner 的基本工作原理:

1. 数据收集:xtuner 首先需要从各种来源获取用户的音乐数据,如 Spotify 等音乐平台的歌曲库、在线流媒体服务(如 Apple Music、Tidal、YouTube Music)的歌曲列表、电台节目推荐等。

2. 建立模型:利用深度学习、自然语言处理(NLP)等技术,建立一个音乐情感分析模型。该模型可以识别用户喜欢的音乐类型、情感色彩(如愉悦、悲伤、平静)、歌手、流派等特征,并将这些信息编码成特征向量。

3. 用户画像构建:基于用户的音乐历史、行为习惯等数据,通过机器学习算法对用户进行聚类,创建个性化的用户画像。这包括用户的听歌

Qwen1.5 在预训练的时候,数据集中没有关于 Xtuner 的资料,所以 Qwen1.5 不知道这个工具,而模型获得输入之后就必然会有输出,所以它就根据自己的理解(或想象)进行回答,这个回答就是幻觉,通俗点说,就是一本正经的胡说八道。

6 官方文档的使用

假如我想看看LlamaIndex是如何集成 ChromaDB 的,可以按照下面的步骤:

  1. 点击右上角的搜索框
    在这里插入图片描述
    2.搜索

在这里插入图片描述
3.点击chromadb

在这里插入图片描述

  1. 可以看到LlamaIndex中如何chromadb的文档了,往下翻能看到示例

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

7 总结

本文介绍了RAG的各个过程及相关的工具,目前只剩下检索结果后处理(重排序)没有专门介绍,这个我们在后面的项目中会专门讲。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐