内容参考于:图灵AI大模型全栈
LLamaIndex做RAG的步骤
如下图是LLamaIndex做RAG的步骤
1.加载文档,加载文档时识别pdf
2.创建索引
2.1索引的创建,我们使用LLamaIndex的摄取管道
2.1.1 捨取管道中可以进行文档分割,分割的方式有父子索引分割、元数据
2.1.2元数据提取
2.2.捨取管道创建完后,把捨取管道执行的结果,使用存储统一管理器,把索引存储起来,存储的格式有原文档、向量、索引
3.检索(查询相关文档)
3.1高级检索方式,如混合检索、sql检索、父子索引检索
3.2文档检索完后,进一步处理,也就是后处理器,可进行重排(如时间排序)、过滤
3.3后处理器完成后整合提示词,使用响应合成器
效果图:

如下图红框,这是索引的key值

上图索引的key值是通过下图红框的代码设置的

代码:代码运行起来很慢,也很消耗token
python
from pathlib import Path
import re
import chromadb
from base_llm import embed_model, llm
from llama_index.core import (
Settings,
StorageContext,
VectorStoreIndex,
load_index_from_storage,
)
from llama_index.core.extractors import TitleExtractor
from llama_index.core.ingestion import IngestionCache, IngestionPipeline
from llama_index.core.node_parser import (
HierarchicalNodeParser,
MarkdownElementNodeParser,
get_leaf_nodes,
)
from llama_index.core.schema import Document
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.response_synthesizers import get_response_synthesizer
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.storage.docstore.mongodb import MongoDocumentStore
from llama_index.storage.kvstore.redis import RedisKVStore
from llama_index.vector_stores.chroma import ChromaVectorStore
# 要加载的文档目录
MD_PATH = Path(
r"D:\daimacunfangdi\PythonProject\LLAmaIndex\02-高级RAG\output\2020-03-17__厦门灿坤实业股份有限公司__200512__闽灿坤__2019年__年度报告.pdf\auto\2020-03-17__厦门灿坤实业股份有限公司__200512__闽灿坤__2019年__年度报告.pdf.md"
)
# 索引的名字
INDEX_ID = "tsannkuen_2019_auto_merging"
# 该目录实际用于持久化索引元数据(如 index_store.json),向量存储在 ChromaDB(./merging_chroma),原文存储在 MongoDB
PERSIST_DIR = Path("./storage_auto_merging")
# 重排模型的目录
RERANK_MODEL_PATH = r"D:\huanjing\ai模型\LLM\Local_model\bge-reranker-large"
# 返回8个文档
SIMILARITY_TOP_K = 8
# 重排之后返回4个
RERANK_TOP_N = 4
# 0.5可以理解为百分之50,现在检索出了5个子文档,其中有3个子文档来源于同一个父文档
# 这就超过了百分之50,这时会触发合并机制,把这三个子文档使用父文档进行代替
AUTO_MERGE_RATIO = 0.5
# 逻辑:
# 通过 StorageContext.from_defaults 构建存储上下文。
# docstore 指向 MongoDB,用于持久化文档节点(父子节点全文)。
# vector_store 由 build_vector_store() 返回,使用 ChromaDB 存储向量。
# persist_dir 仅在索引已经存在(index_persisted() 返回 True)时设置为 PERSIST_DIR 的字符串路径,否则为 None。
# 当从已存在的索引加载时,需要提供 persist_dir;新建索引时通常传 None 或后续手动持久化。此处设计允许加载已有索引时使用本地持久化目录。
def build_storage_context():
# 创建存储管理器
return StorageContext.from_defaults(
# 存储到Mongodb数据库中
docstore=MongoDocumentStore.from_uri(
# Mongodb数据库地址
uri="mongodb://127.0.0.1:27017",
# 数据库名
db_name="llama_md_report",
# 命名空间名
namespace='merging_docstore',
),
# 向量存储,使用 ChromaDB数据 存储向量
vector_store=build_vector_store(),
# 索引持久化后的目录
persist_dir=str(PERSIST_DIR) if index_persisted() else None,
)
# 逻辑:
# 创建 ChromaDB 持久化客户端,数据存储于本地目录 ./merging_chroma。
# 获取或创建名为 tsannkuen_2019_leaf_nodes 的集合。
# 返回 ChromaVectorStore 对象,后续 LlamaIndex 会将其用作向量索引的存储后端。
def build_vector_store():
# 向量数据库使用chromadb,向量保存位置是merging_chroma,这里的目录是本地的
chroma_client = chromadb.PersistentClient(path="./merging_chroma")
# 设置向量数据库的数据库名
chroma_collection = chroma_client.get_or_create_collection(
name="tsannkuen_2019_leaf_nodes"
)
# 创建向量数据库存储对象
return ChromaVectorStore(chroma_collection=chroma_collection)
# 逻辑:
# 读取 Markdown 全文。
# 使用正则 r"(?=^# )" 配合 re.MULTILINE 模式,按照每行开头的 # 进行零宽分割(即保留分隔符本身)。分割后的每个段落在开头都有一个一级标题行(或没有则为空)。
# 遍历每一段:
# 去除首尾空白,跳过空段。
# 尝试匹配段首的 # 后的一级标题文本,提取标题内容作为 h1_title;若无标题则设为 "未知标题"。
# 将整段文本和元数据封装为 Document 对象,加入结果列表。
# 最终返回以一级标题划分的文档列表。
def split_by_h1():
# 读取MD文档
text = MD_PATH.read_text(encoding="utf-8")
# r"(?=^# )" 正则表达式,意思是开头是不是#号和一个空格,按照 # 号和一个空格进行分割,现在的内容假设是这样的
# "# 标题一 换行 xxxx内容 换行 # 标题二 换行 xxxx内容" 执行下方代码后就是这样的 ["# 标题一 换行 xxxx内容 换行 ","# 标题二 换行 xxxx内容"]
# 就是遇到 #和一个空格 就拆分,也就是通过一级标题进行分割
sections = re.split(r"(?=^# )", text, flags=re.MULTILINE)
results = []
# 分割后的数据
for section in sections:
# 删除前后的 空格、制表符 \t、换行符 \n 等
section = section.strip()
if not section:
continue
# 如果存在一个#和一个空格说明是一级标题
match = re.match(r"^#\s+(.+)", section)
h1_title = match.group(1).strip() if match else "未知标题"
# 如果是一级标题就在元数据中添加一个 h1_title 的内容,也就是一级标题内容
# 这里只处理的一级标题,如果需要其它级别的标题可以在这里继续添加
results.append(Document(
text=section,
metadata={"h1_title": h1_title},
))
return results
# 逻辑:
# 构建 HierarchicalNodeParser:父子节点分割器,父块 2048,子块 512,重叠 20 字符。
# 构建 MarkdownElementNodeParser:可以识别 Markdown 中的元素(表格、代码块等),并将这些元素单独解析;非元素部分交给指定的 nested_node_parser(此处传入 hierarchical_parser)进行分割。需要使用 LLM 来识别元素。
# 构建 TitleExtractor:使用 LLM 根据前 nodes=5 个节点生成标题,作为文档的标题元数据。
# 创建 IngestionPipeline:
# 变换步骤:先 markdown_parser(含元素分割及层级分割),再 title_extractor(生成标题)。
# 缓存使用 Redis,disable_cache=False 表示启用缓存。
def build_pipeline():
# 创建父子节点分割器
hierarchical_parser = HierarchicalNodeParser.from_defaults(
# 父节点2048,子节点512
chunk_sizes=[2048, 512],
# 相邻块的20个重叠字符或token
chunk_overlap=20,
)
# Markdown元素分割器
markdown_parser = MarkdownElementNodeParser(
# 设置大模型
llm=llm,
# 设置分割器,默认使用句子分割器,这里设置为父子节点分割器
nested_node_parser=hierarchical_parser,
# 2个并发线程
num_workers=2,
show_progress=True,
)
# 标题提取器,使用前 nodes 个节点做标题
# 这里也就是使用前5个节点来生成标题
title_extractor = TitleExtractor(
llm=llm,
nodes=5,
num_workers=2,
show_progress=True,
)
# 创建摄取管道
return IngestionPipeline(
transformations=[
# 先对文档进行分割
markdown_parser,
# 分割完后标题提取
title_extractor
],
# 缓存使用 redis 数据库
cache=IngestionCache(
cache=RedisKVStore(redis_uri="redis://127.0.0.1:6379"),
collection='ingestion_cache',
),
disable_cache=False,
)
# 逻辑:
# 调用 split_by_h1() 生成带一级标题元数据的 Document 列表。
# 调用 build_pipeline() 构建管道。
# 运行管道处理这些文档,返回节点列表(同时包含父节点和子节点)。
def parse_nodes():
# 分割文档,这里会把标题放到元数据中,这里只把一级标题放进了元数据中
# MarkdownElementNodeParser 会通过表格、代码块等特殊内容(元素内容)进行的分割
# 非元素部分交给嵌套解析器处理,默认是句子分割器,会丢失标题
# 这里把标题放到了元数据中,后续使用 MarkdownElementNodeParser 时就不会丢失标题了
documents = split_by_h1()
# 创建摄取管道
pipeline = build_pipeline()
return list(pipeline.run(documents=documents, show_progress=True))
# 逻辑:
# 检查持久化目录下 index_store.json 是否存在,若存在则认为索引已经持久化。
def index_persisted():
# 判断文件是否存在
return (PERSIST_DIR / "index_store.json").exists()
# 逻辑:
# 设置全局 Settings.llm 和 Settings.embed_model,供后续组件使用。
# 构建 StorageContext(含 MongoDB docstore、ChromaDB vector store 和可能的 persist_dir)。
# 检查是否存在已持久化索引(index_persisted())。若存在,使用 load_index_from_storage 加载,并返回。
# 若不存在:
# 执行 parse_nodes() 获得解析后的节点树(包含父子关系)。
# 调用 get_leaf_nodes(nodes) 提取叶子节点(即最小的子块),只有这些节点会被向量化。
# 手动将全部节点(父子全量)存入 MongoDB 的 docstore(storage_context.docstore.add_documents)。
# 用叶子节点构建 VectorStoreIndex,且设置 store_nodes_override=True 强制存储节点信息(但上面的手动添加也可省略,注释中指出这一点)。
# 设置索引 ID 并持久化到本地目录。
# 返回索引对象。
def build_or_load_index():
# 设置全局的大语言模型,这样后续不需要一个一个的传递了
Settings.llm = llm
# 设置全局的向量模型,这样后续不需要一个一个的传递了
Settings.embed_model = embed_model
# 创建存储管理,文档存储到Mongodb,向量存储到chromadb
storage_context = build_storage_context()
# 判断本地是否存在索引
if index_persisted():
print(f"加载本地 index: {PERSIST_DIR}")
# 从本地持久化目录加载索引数据,并后续通过索引去Mongodb和Chroma中找文档和向量
index = load_index_from_storage(
storage_context,
index_id=INDEX_ID,
embed_model=embed_model,
)
return index
# 如果代码执行到这里,说明本地没有索引
print("未发现本地 index,开始读取 Markdown 并摄取解析。")
# 加载文档并分割文档
nodes = parse_nodes()
# 本次使用的是父子文档,这里是获取子文档,只需要对子文档做向量
leaf_nodes = get_leaf_nodes(nodes)
# 向存储管理器中添加完整节点(父和子节点),这里是存放到Mongodb中
# 如果不执行下方的 storage_context.docstore.add_documents,它不会存储节点
storage_context.docstore.add_documents(nodes, allow_update=True)
print(f"父子全量 nodes 已写入 MongoDB: {len(nodes)}")
print(f"用于向量索引的 leaf nodes: {len(leaf_nodes)}")
# 向量索引存储器,这里就是文本转换向量、索引
index = VectorStoreIndex(
# 子节点
leaf_nodes,
# 存储位置,存储管理器
storage_context=storage_context,
# 向量模型
embed_model=embed_model,
# store_nodes_override值为True会强制存储文档
# 这里设置了,就不需要手动执行 storage_context.docstore.add_documents 这个代码了
store_nodes_override=True,
show_progress=True,
)
# 设置索引的名字
index.set_index_id(INDEX_ID)
# 持久化存储,存储到本地
index.storage_context.persist(persist_dir=str(PERSIST_DIR))
print(f"index 已持久化到: {PERSIST_DIR}")
return index
# 逻辑:
# 从索引获取基础检索器,返回 top-8 的叶子节点。
# 用 AutoMergingRetriever 包装:根据检索到的子节点集合,若来自同一父节点的子节点比例超过 simple_ratio_thresh(0.5,即 50%),则用父节点替换这些子节点(即合并为更大的父块)。
# index.storage_context 用于查找父子关系。
# 启用 verbose 输出合并细节。
def build_auto_merging_retriever(index):
# 创建检索器
vector_retriever = index.as_retriever(similarity_top_k=SIMILARITY_TOP_K)
# AutoMergingRetriever父子文档检索器
return AutoMergingRetriever(
# 检索器
vector_retriever,
# 存储器,用来找存储的原文档,通过子节点找父节点
index.storage_context,
# 子节点超过 simple_ratio_thresh 就合并(就使用父文档)
# 现在我们的 AUTO_MERGE_RATIO 值是0.5,
# 0.5可以理解为百分之50,现在检索出了5个子文档,其中有3个子文档来源于同一个父文档
# 这就超过了百分之50,这时会触发合并机制,把这三个子文档使用父文档进行代替
simple_ratio_thresh=AUTO_MERGE_RATIO,
verbose=True,
)
# 逻辑:
# 返回一个基于 SentenceTransformer 的重排后处理器,将检索结果(或合并后的节点)重新排序,最终保留 top-4 个节点。
def build_reranker():
# 创建重排后处理器
return SentenceTransformerRerank(
model=RERANK_MODEL_PATH,
top_n=RERANK_TOP_N,
)
# 逻辑:
# 创建响应合成器,采用 compact 模式(将检索到的文本尽可能压缩进上下文窗口,若仍超限会进行分块总结再合成)。
# 传入 LLM 实例,启用 verbose。
def build_response_synthesizer():
# 创建响应合成器
return get_response_synthesizer(
llm=llm,
response_mode="compact",
verbose=True,
)
# 逻辑:
# 创建重排处理器和响应合成器。
# 构建并返回 RetrieverQueryEngine,使用给定的检索器、合成器、以及后处理器列表(仅含重排器)。
def build_query_engine(retriever):
# 创建重排处理器
reranker = build_reranker()
# 创建响应合成器
response_synthesizer = build_response_synthesizer()
# 创建查询引擎
return RetrieverQueryEngine.from_args(
# 后续可以换更高级的检索器
retriever=retriever,
response_synthesizer=response_synthesizer,
node_postprocessors=[reranker],
)
# 逻辑:
# 获取/构建索引。
# 构建自动合并检索器。
# 构建查询引擎。
# 遍历预设问题列表,逐个查询并打印问答。
if __name__ == "__main__":
# 创建索引
index = build_or_load_index()
# 创建检索器
retriever = build_auto_merging_retriever(index)
# 创建查询引擎
query_engine = build_query_engine(retriever)
questions = [
"结合营业收入、净利润和现金流,评价公司2019年的经营质量。",
"公司2019年的盈利能力变动主要受哪些因素影响?",
"年报中哪些内容说明公司面临市场竞争风险?",
"请对比公司2019年和2018年的主要财务指标。",
"公司2019年的收入结构和业务重点是什么?",
"从年报看,公司未来发展的主要挑战是什么?",
]
for qa in questions:
response = query_engine.query(qa)
print('问题:', qa)
print('回答:', response)

