【学习记录】LlamaIndex 四行代码背后的完整 RAG 工作流:从文档解析到生成答案

很多人第一次接触 LlamaIndex 时,会被它的简洁 API 震撼——短短三四行代码就能搭建一个 RAG(检索增强生成)系统。但在这背后,框架默默完成了文档解析、文本分块、向量化、索引构建、检索、提示词组装、LLM 调用等一系列复杂操作。本文逐行拆解 LlamaIndex 的典型代码,揭示每一步的底层原理与数据流转。读完能真正理解 RAG 的内部机制。


📌 前置说明

本文使用的示例代码:

import os
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex

os.environ['OPENAI_API_KEY'] = 'your-api-key'

documents = SimpleDirectoryReader('data').load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("总结一下这篇文章,用中文")
print(response)

⚠️ 注意:实际运行时需设置有效的 OpenAI API 密钥(或替换为本地模型)。本文假设密钥有效,按照 LlamaIndex 正常工作流程说明。


步骤一:文档解析

documents = SimpleDirectoryReader('data').load_data()

底层做了什么?

  1. SimpleDirectoryReader('data')

    • 创建一个目录读取器实例,指向本地的 data 文件夹。
    • 内部使用 fsspecpathlib 遍历目录,获取所有文件路径。
  2. .load_data()

    • 对每个文件,根据文件扩展名自动选择合适的 BaseReader 子类:
      • .pdfPDFReader(依赖 PyPDF2pdfplumber
      • .txtTextFileReader
      • .docxDocxReader(依赖 python-docx
      • .csvCSVReader
      • 等等。
    • 调用读取器的 load_data() 方法提取文件中的纯文本元数据(如文件名、页数、创建时间等)。
    • 将所有读取结果合并成一个 Python 列表,列表元素为 Document 对象。
  3. 输出 List[Document]

    • 每个 Document 包含:
      • text:提取的文本内容(字符串)
      • metadata:字典,如 {"file_name": "a.pdf", "page_label": "1"}

💡 扩展:LlamaIndex 还支持通过 SimpleDirectoryReader(input_dir="...", recursive=True, required_exts=[".pdf"]) 等参数精细控制。


步骤二:构建索引

index = VectorStoreIndex.from_documents(documents)

这是最复杂的一步,内部包含以下子步骤:

2.1 文本分块(Node Parsing)

  • 目的:将一个长 Document 切分成多个节点(Node),每个节点长度适中。这样做可以提高检索的细粒度和相关性。
  • 默认参数chunk_size=1024(token 数),chunk_overlap=20
  • 默认解析器SimpleNodeParser
  • 底层流程
    • 遍历每个 Document,将其文本按 token 或字符切分(实际调用 tokenizer 或字符计数)。
    • 相邻块之间保留 chunk_overlap 个 token,防止信息在边界处割裂。
    • 为每个节点生成唯一 ID,并继承原 Document 的元数据(可增加 chunk_index 等字段)。

2.2 为每个节点生成嵌入向量

  • 目的:将节点文本映射到高维语义空间,使语义相似的文本向量距离更近。
  • 默认嵌入模型:OpenAI text-embedding-ada-002(维度 1536)。
  • 底层流程
    • 调用 Settings.embed_modelget_text_embedding_batch() 方法,将节点文本批量发送到 OpenAI API(或本地模型)。
    • 每个节点返回一个固定维度的浮点数向量(列表或 numpy 数组)。
    • 如果使用 OpenAI,会消耗 API 费用;本地模型(如 HuggingFaceEmbedding)则无需联网。

2.3 存储到向量数据库

  • 目的:持久化向量和节点信息,支持后续快速检索。
  • 默认向量存储SimpleVectorStore(内存中的简单索引,关机即丢失)。生产环境可换为 FAISS、Chroma、Pinecone 等。
  • 底层流程
    • 创建一个 VectorStoreIndex 实例。
    • 初始化一个 DocumentStore(默认 SimpleDocumentStore)用于存储节点文本和元数据。
    • 将节点 ID、节点向量、节点文本一并存入向量存储和文档存储。
    • 维护一个 IndexStruct,记录索引的元信息(如节点 ID 列表)。

2.4 构建索引对象

  • 返回一个 VectorStoreIndex 实例,内部持有:
    • vector_store
    • docstore
    • index_struct
  • 该索引对象可以直接用于检索和查询。

步骤三:构建查询引擎

query_engine = index.as_query_engine()

底层做了什么?

  1. 创建检索器(Retriever)

    • 默认类型:VectorIndexRetriever
    • 作用:根据用户问题从索引中检索最相关的 k 个节点(默认 k=2)。
    • 工作方式:将用户问题向量化,然后在向量存储中进行相似度搜索(余弦相似度或内积),返回 top-k 节点。
  2. 创建合成器(Response Synthesizer)

    • 默认类型:通过 get_response_synthesizer() 获得,使用 compact 模式。
    • 作用:将检索到的节点内容和用户问题组合成提示词(prompt),并调用 LLM 生成最终答案。
    • compact 模式会尽量合并节点文本,减少 token 消耗。
  3. 组装查询引擎

    • 创建一个 RetrieverQueryEngine 实例,将检索器和合成器组合在一起。
    • 返回的 query_engine 对象具有 query() 方法,用于执行查询。

步骤四:执行查询

response = query_engine.query("总结一下这篇文章,用中文")
print(response)

内部流程详解

4.1 问题向量化
  • 将用户问题 "总结一下这篇文章,用中文" 通过相同的嵌入模型(与构建索引时一致)转换为向量。
4.2 检索相关节点
  • 检索器在向量存储中进行相似度搜索,找出与问题向量最相似的 k 个节点(默认 k=2)。
  • 返回节点列表,每个节点包含 textmetadata,以及相似度分数 score
4.3 构建提示词
  • 合成器将检索到的节点文本和用户问题组合成一个结构化的提示词。典型的 compact 模式提示词格式:
    上下文信息:
    [1] 节点1的文本...
    [2] 节点2的文本...
    
    根据以上上下文信息,回答用户的问题:
    总结一下这篇文章,用中文
    
  • 还可以包含指令如“如果你不知道答案,请说‘不知道’”,具体取决于合成器配置。
4.4 调用大语言模型生成答案
  • 将提示词发送给 LLM(默认 OpenAI gpt-3.5-turbogpt-4)。
  • LLM 根据上下文生成自然语言回答。
  • 返回的 response 对象包含:
    • response:生成的答案文本(字符串)。
    • source_nodes:检索到的源节点列表,可用于溯源或展示引用。
    • metadata:其他信息(如 token 使用量、响应耗时等)。
4.5 打印答案
  • print(response) 输出答案文本。

📊 整体流程图(文本版)

data/ 目录
   │
   ▼ (SimpleDirectoryReader)
List[Document] (含文本+元数据)
   │
   ▼ (VectorStoreIndex.from_documents)
   ├─ 文本分块 → List[Node]
   ├─ 嵌入模型 → 每个Node的向量
   ├─ 存入向量存储 + 文档存储
   └─ 返回 VectorStoreIndex
   │
   ▼ (index.as_query_engine)
   ├─ 构建 Retriever (VectorIndexRetriever)
   ├─ 构建 Response Synthesizer (compact模式)
   └─ 返回 RetrieverQueryEngine
   │
   ▼ (query_engine.query("问题"))
   ├─ 问题向量化
   ├─ 检索 top-k 节点
   ├─ 组装提示词
   ├─ LLM 生成答案
   └─ 返回 Response 对象
   │
   ▼
   打印答案

⚠️ 常见问题与优化建议

问题 说明 解决方案
默认使用 OpenAI 需要有效 API 密钥,且产生费用 替换为本地模型:Settings.llm = HuggingFaceLLM(...)Settings.embed_model = HuggingFaceEmbedding(...)
分块大小不合适 默认 1024 token,太短可能丢失上下文,太长可能降低检索精度 根据文档类型调整:技术文档可用 512,长文可用 2048
检索数量 k=2 可能漏掉相关信息 增加 similarity_top_k=5(通过 as_query_engine(similarity_top_k=5)
内存向量存储不持久 SimpleVectorStore 关机即丢失 改用 ChromaVectorStoreFAISS 并持久化到磁盘
无中文优化 默认嵌入模型和 LLM 对中文效果一般 使用 BAAI/bge-large-zh 等中文嵌入模型,LLM 用 QwenChatGLM

🎯 总结

步骤 核心操作 输入 输出 关键依赖
读取本地文件 → 提取文本 目录路径 List[Document] SimpleDirectoryReader + 文件解析库
分块 → 嵌入 → 存入向量存储 List[Document] VectorStoreIndex 嵌入模型 API / 本地模型,向量数据库
组装检索器 + 合成器 VectorStoreIndex QueryEngine 默认 RetrieverQueryEngine
问题向量化 → 检索 → 提示词 → LLM 生成 问题字符串 Response 对象 嵌入模型,LLM API

LlamaIndex 用极其简洁的 API 封装了 RAG 的复杂流程,让开发者能够快速构建原型。但理解其底层原理,能帮助你在遇到性能瓶颈或定制需求时,精准地调整参数、替换组件。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐