80.高级RAG-LLamaIndex实际应用-金融助手

内容参考于:图灵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)

相关推荐
Clipp_Huang1 小时前
光学跟踪系统标定
人工智能·计算机视觉·重构·机器视觉
阿图灵1 小时前
Agentic AI 架构入门(三):Agent 的七大组件与 PRAL 循环
人工智能·架构·llm·rag·ai agent·智能体·agentic ai
weixin_468466851 小时前
目标检测精度上限与影响因素分析
图像处理·人工智能·目标检测·计算机视觉·图像分类·coco·检测精度
宋哥转AI1 小时前
深入理解 AI Agent · MCP 子系列 #01:MCP 协议全解—从消息格式到传输层的完整拆解
人工智能·agent·mcp
看山先生1 小时前
凌晨两点,我把一块开发板接进了自己的世界
人工智能·agent
阿源聊AI1 小时前
Claude Code 跨会话消息:并行开发终于有了信息通道
人工智能
aircrushin1 小时前
Claude 5 之后,上下文工程该做减法了
前端·人工智能·后端
Tangyuewei1 小时前
Meta Muse Code 发布:低价杀入编程
人工智能
武子康2 小时前
全双工语音 Agent 如何评测:从首音延迟到事件级验收
人工智能·llm·agent