引言:GPT-4 很聪明,但它不认识你的数据
想象这样一个场景:
你的公司积累了 10 年的技术文档、客户案例、内部知识库,共计 10,000 份文档。某天,产品经理问你:"能不能让 GPT-4 帮我们快速查找相关案例?" 你兴冲冲地打开 ChatGPT,输入问题,然后发现——
GPT-4 完全不知道你的数据存在。
问题的本质是:
- LLM 的知识是静态的:训练数据截止日期之前的公开信息,你的私有数据它从未见过
- 上下文窗口有限:即使是 GPT-4 的 128K 上下文,也只能容纳约 50 份文档,远不够
- 直接塞入不现实:10,000 份文档直接发给 LLM?成本高昂,响应缓慢,而且绝大部分内容是无关的
你需要的是:
- 把私有数据"喂给" LLM
- 只把相关的部分发给 LLM(节省成本和时间)
- 让 LLM 能够理解、推理、回答问题
这就是 RAG(Retrieval-Augmented Generation,检索增强生成) 要解决的问题,而 LlamaIndex 正是为此而生的专业框架。
什么是 RAG?为什么需要它?
传统方案的困境
在 RAG 出现之前,让 LLM 处理私有数据有几种方案:
方案 1:Fine-tuning(微调)
- 做法:用你的数据重新训练 LLM
- 问题:
- 成本极高(数万美元起)
- 需要大量标注数据
- 数据更新需要重新微调
- 模型可能"遗忘"原有能力
方案 2:全部塞进 Prompt
- 做法:把所有文档拼接到提示词里
- 问题:
- 上下文长度限制(GPT-4 也只有 128K tokens)
- API 成本线性增长($0.03/1K tokens)
- 响应速度慢(处理时间与上下文长度成正比)
方案 3:关键词搜索 + LLM
- 做法:先用传统搜索找文档,再让 LLM 总结
- 问题:
- 关键词匹配不理解语义("苹果"公司 vs "苹果"水果)
- 找不到同义表达("机器学习" vs "AI 算法")
- 需要手动设计搜索逻辑
RAG 的核心思路
RAG = Retrieval(检索) + Augmented(增强) + Generation(生成)
用户问题:"2023 年 Q3 销售额最高的产品是什么?"
↓
[Retrieval 阶段]
向量检索找到 Top 3 相关文档:
- 2023 Q3 财报摘要.pdf(相似度 0.92)
- 产品销售数据汇总.xlsx(相似度 0.89)
- 年度业绩分析报告.docx(相似度 0.85)
↓
[Augmented 阶段]
将问题 + 检索到的文档合成 Prompt:
"根据以下文档回答问题:
[文档1内容]
[文档2内容]
[文档3内容]
问题:2023 年 Q3 销售额最高的产品是什么?"
↓
[Generation 阶段]
LLM 基于检索到的上下文生成答案:
"根据 2023 Q3 财报,销售额最高的产品是 XX,
销售额达到 $X 万,同比增长 X%。"
RAG 的优势:
- ✅ 无需微调,成本低
- ✅ 数据更新实时生效(更新索引即可)
- ✅ 只检索相关内容,节省 token 成本
- ✅ 语义理解(向量相似度匹配)
- ✅ 可解释性强(能看到引用了哪些文档)
LlamaIndex 是什么?
LlamaIndex 是一个专门为 LLM 构建 RAG 应用的数据框架。
如果把 RAG 比作"让 LLM 查资料回答问题"的过程:
- LangChain 像是"项目管理工具" → 擅长编排复杂工作流(Agent、多步推理、工具调用)
- LlamaIndex 像是"图书馆管理系统" → 擅长数据的索引、检索、结构化查询
LlamaIndex 的核心定位:
"Connect LLM with your data"(连接 LLM 和你的数据)
它提供了从数据加载、索引构建、语义检索到查询生成的完整工具链,让你用几十行代码就能搭建一个生产级的 RAG 系统。
LlamaIndex 的五大核心组件
我们通过一个具体例子来理解 LlamaIndex 的架构:
场景:构建一个企业内部知识库问答系统
- 数据源:公司内部的 PDF 文档、Word 文档、Notion 页面
- 需求:员工输入问题,自动检索相关文档并生成答案
1. Data Loaders:数据加载器
问题:如何把各种格式的数据喂给 LLM?
企业数据五花八门:PDF、Word、Excel、网页、数据库……LlamaIndex 提供了 100+ 种数据加载器(LlamaHub)。
示例:加载不同格式的文档
from llama_index.core import SimpleDirectoryReader
from llama_index.readers.file import PDFReader, DocxReader
from llama_index.readers.notion import NotionPageReader
# 方法 1:自动识别文件类型(最简单)
documents = SimpleDirectoryReader("./company_docs").load_data()
# 方法 2:指定加载器(更精确控制)
pdf_reader = PDFReader()
pdf_docs = pdf_reader.load_data("./财报.pdf")
docx_reader = DocxReader()
word_docs = docx_reader.load_data("./产品手册.docx")
# 方法 3:加载云端数据
notion_reader = NotionPageReader(integration_token="secret_xxx")
notion_docs = notion_reader.load_data(page_ids=["page_id_1", "page_id_2"])
# 合并所有文档
all_documents = pdf_docs + word_docs + notion_docs
返回的数据结构:Document 对象
from llama_index.core import Document
# 每个 Document 包含:
doc = Document(
text="这是文档的文本内容...", # 核心:文档文本
metadata={ # 元数据:用于过滤和引用
"file_name": "产品手册.docx",
"file_type": "docx",
"page_number": 1,
"author": "张三",
"created_date": "2023-10-01"
}
)
为什么需要 metadata(元数据)?
实际应用中,你经常需要:
- 过滤:"只搜索 2023 年的财报"
- 引用:"这个答案来自《产品手册》第 3 页"
- 权限控制:"这个文档只能销售部门查看"
metadata 让检索不仅基于内容相似度,还能加上业务规则。
2. Document:文档抽象
问题:文档太长怎么办?
一份 100 页的 PDF 可能包含 50,000 tokens,远超 LLM 的上下文窗口(GPT-4 是 128K tokens)。即使能塞进去,成本也高得离谱($3.75 每次查询!)。
解决方案:文档分块(Chunking)
把长文档切成小块(Chunk),每块包含 512-1024 tokens。这样:
- 检索时只需匹配相关的块(而不是整个文档)
- 发给 LLM 的上下文更聚焦
- 成本大幅下降
示例:智能分块策略
from llama_index.core.node_parser import SentenceSplitter
# 基于句子边界切块(避免切断语义)
splitter = SentenceSplitter(
chunk_size=512, # 每块最大 512 tokens
chunk_overlap=50 # 相邻块重叠 50 tokens(保持上下文连续性)
)
nodes = splitter.get_nodes_from_documents(documents)
# 查看切块结果
print(f"原始文档数:{len(documents)}") # 输出:10
print(f"切块后节点数:{len(nodes)}") # 输出:237
print(nodes[0].text) # 第一个块的内容
为什么需要 chunk_overlap(重叠)?
想象这样一个段落被切成两块:
块 1:"...我们的产品支持 Windows 和 MacOS。"
块 2:"Linux 版本将在下个季度发布。"
用户问:"产品支持哪些操作系统?"
- 如果没有重叠,两个块的信息是割裂的
- 有了 50 tokens 重叠,块 2 会包含"Windows 和 MacOS"的上下文,答案更完整
类比理解:
- 不分块:把整本百科全书给 LLM → 太长,处理不了
- 简单分块:每 10 页切一刀 → 可能把一个主题切断
- 智能分块:按章节切分,且章节之间有过渡段 → 保持语义完整
高级分块策略:
LlamaIndex 还支持根据文档结构分块:
from llama_index.core.node_parser import HierarchicalNodeParser
# 根据 Markdown 标题层级分块
parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128] # 父块 → 子块 → 孙块
)
nodes = parser.get_nodes_from_documents(documents)
# 好处:保留文档结构,支持从粗到细的检索
# 比如先匹配"第 3 章",再定位到"3.2 节",最后找到具体段落
3. Index:索引
问题:如何快速从 10,000 个块中找到相关的 3 个?
索引是 RAG 的核心。它预先计算并存储文档的结构化表示,使得检索时能在毫秒级找到相关内容。
LlamaIndex 提供了多种索引类型,适用于不同场景。
3.1 VectorStoreIndex:向量索引(最常用)
原理:将文本转换为向量,通过向量相似度匹配
用户问题:"如何提升销售额?"
↓ Embedding 模型
向量表示:[0.23, -0.15, 0.67, ..., 0.42] (1536 维)
↓ 计算相似度
文档块 1:"销售策略优化指南" → 相似度 0.89 ✅
文档块 2:"2023 年财务审计报告" → 相似度 0.34
文档块 3:"销售团队培训材料" → 相似度 0.85 ✅
完整代码示例:
from llama_index.core import VectorStoreIndex, Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
# 配置 Embedding 模型(将文本转为向量)
Settings.embed_model = OpenAIEmbedding(
model="text-embedding-3-small", # OpenAI 最新 embedding 模型
dimensions=1536 # 向量维度
)
# 配置 LLM(生成答案)
Settings.llm = OpenAI(model="gpt-4", temperature=0)
# 构建向量索引
index = VectorStoreIndex.from_documents(
documents,
show_progress=True # 显示进度条
)
# 索引会自动做这些事:
# 1. 将每个文档块转换为 1536 维向量
# 2. 存储向量和原始文本
# 3. 构建向量索引(如 FAISS、Pinecone 等)
# 持久化索引(避免每次都重新构建)
index.storage_context.persist(persist_dir="./storage")
# 下次直接加载
from llama_index.core import load_index_from_storage, StorageContext
storage_context = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(storage_context)
关键参数:similarity_top_k
query_engine = index.as_query_engine(
similarity_top_k=3 # 检索相似度最高的 3 个块
)
response = query_engine.query("如何提升销售额?")
print(response) # LLM 基于检索到的 3 个块生成的答案
你可能会问:为什么是 3 个块?
这是一个权衡:
- 太少(top_k=1):信息不完整,LLM 可能答不上来
- 太多(top_k=20):无关信息混入,干扰 LLM,且成本增加
- 经验值:3-5 个块是实践中的甜点(既有足够信息,又控制成本)
实际测试:
# 对比不同 top_k 的效果
question = "2023 年 Q3 销售额最高的产品是什么?"
# top_k = 1
engine_1 = index.as_query_engine(similarity_top_k=1)
print(engine_1.query(question))
# 可能输出:"Q3 销售数据显示..."(信息不完整)
# top_k = 3
engine_3 = index.as_query_engine(similarity_top_k=3)
print(engine_3.query(question))
# 输出:"根据 Q3 财报和销售明细,产品 A 销售额 $500 万,排名第一。"
# top_k = 10
engine_10 = index.as_query_engine(similarity_top_k=10)
print(engine_10.query(question))
# 输出可能混入 Q1、Q2 的数据,答案反而不准确
3.2 其他索引类型(适用于特殊场景)
SummaryIndex:摘要索引
from llama_index.core import SummaryIndex
# 适用场景:需要遍历所有文档(如"总结所有会议纪要")
summary_index = SummaryIndex.from_documents(documents)
# 查询时会把所有块都发给 LLM(成本高,适合小数据集)
query_engine = summary_index.as_query_engine()
response = query_engine.query("总结过去三个月的所有会议要点")
TreeIndex:树形索引
from llama_index.core import TreeIndex
# 适用场景:文档有明确层级结构(如"公司组织架构")
tree_index = TreeIndex.from_documents(documents)
# 查询时从根节点开始,逐层匹配(自顶向下检索)
query_engine = tree_index.as_query_engine()
response = query_engine.query("研发部门的项目负责人是谁?")
KeywordTableIndex:关键词索引
from llama_index.core import KeywordTableIndex
# 适用场景:精确关键词匹配(如"找所有提到'财务合规'的文档")
keyword_index = KeywordTableIndex.from_documents(documents)
# 基于关键词提取(而非语义向量)
query_engine = keyword_index.as_query_engine()
response = query_engine.query("财务合规")
如何选择索引类型?
| 索引类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| VectorStoreIndex | 通用问答、语义搜索 | 语义理解强、扩展性好 | 需要 Embedding 成本 |
| SummaryIndex | 总结性任务、小数据集 | 不遗漏信息 | 成本高(全量传输) |
| TreeIndex | 结构化文档、层级查询 | 高效的递进检索 | 需要文档有层级结构 |
| KeywordTableIndex | 精确匹配、术语查询 | 速度快、零成本 | 不理解语义 |
实践建议:90% 的场景用 VectorStoreIndex,特殊需求才考虑其他。
4. Retriever:检索器
问题:能否更精细地控制检索逻辑?
默认的 index.as_query_engine() 已经很好用,但复杂场景需要更灵活的检索策略。
4.1 混合检索:向量 + 关键词
from llama_index.core.retrievers import VectorIndexRetriever, KeywordTableRetriever
from llama_index.core.retrievers import QueryFusionRetriever
# 创建向量检索器
vector_retriever = VectorIndexRetriever(
index=vector_index,
similarity_top_k=5
)
# 创建关键词检索器
keyword_retriever = KeywordTableRetriever(
index=keyword_index,
top_k=5
)
# 融合两种检索结果(取并集 + 重新排序)
fusion_retriever = QueryFusionRetriever(
retrievers=[vector_retriever, keyword_retriever],
similarity_top_k=3, # 最终返回 3 个最相关的
num_queries=1
)
# 使用融合检索器
from llama_index.core.query_engine import RetrieverQueryEngine
query_engine = RetrieverQueryEngine(retriever=fusion_retriever)
response = query_engine.query("销售额最高的产品")
为什么需要混合检索?
-
向量检索:理解语义,但可能漏掉精确关键词
- 例:搜"AI 模型"可以匹配"机器学习算法"
- 但:搜"GPT-4"可能匹配不到"GPT-4-turbo"(版本号差异)
-
关键词检索:精确匹配,但不理解同义词
- 例:搜"CEO"找不到"首席执行官"
-
混合检索:两者优势互补
- 既能语义匹配,又不漏掉精确术语
4.2 元数据过滤:基于业务规则检索
from llama_index.core.vector_stores import MetadataFilters, ExactMatchFilter
# 场景:只搜索 2023 年的财报
filters = MetadataFilters(
filters=[
ExactMatchFilter(key="year", value="2023"),
ExactMatchFilter(key="doc_type", value="财报")
]
)
retriever = VectorIndexRetriever(
index=vector_index,
similarity_top_k=3,
filters=filters # 先过滤,再向量检索
)
response = retriever.retrieve("Q3 销售额")
# 只会从 2023 年的财报中检索
实际应用:权限控制
# 用户 ID 存储在 metadata 中
user_filter = ExactMatchFilter(key="accessible_by", value="user_123")
# 只检索该用户有权限的文档
retriever = VectorIndexRetriever(
index=vector_index,
filters=MetadataFilters(filters=[user_filter])
)
4.3 重排序(Reranking):进一步提升精度
from llama_index.core.postprocessor import SentenceTransformerRerank
# 问题:向量检索的 top_k=10 中,可能排序不够准确
# 解决:用更强的模型重新排序
# 第一步:向量检索(快速粗筛)
retriever = VectorIndexRetriever(
index=vector_index,
similarity_top_k=10 # 先取 10 个候选
)
# 第二步:重排序(精确排序)
reranker = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-6-v2", # 专门的排序模型
top_n=3 # 最终保留 3 个
)
query_engine = index.as_query_engine(
node_postprocessors=[reranker], # 添加后处理器
similarity_top_k=10
)
# 流程:检索 10 个 → 重排序 → 取 top 3 → 发给 LLM
为什么两阶段检索?
- 向量检索:速度快(毫秒级),适合处理百万级文档
- 重排序模型:精度高(理解问题和文档的交互),但速度慢(秒级)
类比:
- 向量检索 = 海选(快速筛掉 99% 无关内容)
- 重排序 = 决赛(仔细评估剩下的候选,选出最佳答案)
5. Query Engine:查询引擎
问题:如何让 LLM 基于检索结果生成答案?
前面的 Retriever 只负责"找到相关文档",Query Engine 负责"生成答案"。
5.1 基础查询引擎
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
# 最简单的用法
query_engine = index.as_query_engine()
response = query_engine.query("什么是 RAG?")
print(response)
背后发生了什么?
用户输入:"什么是 RAG?"
↓
[Step 1] 生成问题的向量表示
Embedding("什么是 RAG?") → [0.12, -0.45, ...]
↓
[Step 2] 向量检索(在索引中找相似文档)
Top 3 相关块:
- "RAG 即检索增强生成,结合了..."(相似度 0.91)
- "RAG 的优势在于无需微调..."(相似度 0.87)
- "实现 RAG 需要向量数据库..."(相似度 0.82)
↓
[Step 3] 构造 LLM Prompt
"请根据以下上下文回答问题:
上下文 1:RAG 即检索增强生成,结合了...
上下文 2:RAG 的优势在于无需微调...
上下文 3:实现 RAG 需要向量数据库...
问题:什么是 RAG?
请基于上下文回答,如果上下文中没有相关信息,请说'我不知道'。"
↓
[Step 4] 调用 LLM 生成答案
GPT-4 输出:"RAG(检索增强生成)是一种结合了检索和生成的技术..."
↓
[Step 5] 返回答案 + 引用来源
response.response = "RAG(检索增强生成)是..."
response.source_nodes = [Node(text="RAG 即检索增强...", score=0.91), ...]
查看引用来源(可解释性)
response = query_engine.query("什么是 RAG?")
# 答案
print(response.response)
# 引用的文档块
for i, node in enumerate(response.source_nodes):
print(f"\n来源 {i+1} (相似度: {node.score:.2f}):")
print(f"文件:{node.metadata.get('file_name', 'Unknown')}")
print(f"内容:{node.text[:200]}...") # 前 200 字符
输出示例:
来源 1 (相似度: 0.91):
文件:RAG技术白皮书.pdf
内容:RAG(Retrieval-Augmented Generation)即检索增强生成,是一种结合了信息检索和文本生成的混合方法。它的核心思想是在生成答案之前,先从外部知识库中检索相关信息...
来源 2 (相似度: 0.87):
文件:AI技术栈指南.docx
内容:RAG 的主要优势在于无需对 LLM 进行微调,就能让模型访问最新的、私有的知识。这在企业应用中尤为重要...
5.2 流式输出:实时显示答案
# 场景:答案很长时,希望像 ChatGPT 一样逐字输出
query_engine = index.as_query_engine(streaming=True)
response = query_engine.query("详细解释 RAG 的工作原理")
# 流式输出
for text in response.response_gen:
print(text, end="", flush=True) # 逐字打印
为什么需要流式输出?
- 用户体验:立即看到响应(而不是等待 30 秒后一次性显示)
- 适用场景:Web 应用、聊天机器人
5.3 Chat Engine:多轮对话
from llama_index.core import VectorStoreIndex
from llama_index.core.memory import ChatMemoryBuffer
# 创建聊天引擎(带有对话历史)
chat_engine = index.as_chat_engine(
chat_mode="context", # 在每轮对话中注入历史上下文
memory=ChatMemoryBuffer.from_defaults(token_limit=3000), # 保留最近 3000 tokens 的历史
system_prompt="你是一个专业的技术助手,根据提供的文档回答问题。"
)
# 第一轮对话
response1 = chat_engine.chat("什么是 RAG?")
print(response1)
# 输出:"RAG 是检索增强生成技术..."
# 第二轮对话(引用上一轮的上下文)
response2 = chat_engine.chat("它和传统搜索有什么区别?")
print(response2)
# 输出:"RAG 相比传统搜索的主要区别在于..."(LLM 知道"它"指的是 RAG)
# 第三轮对话
response3 = chat_engine.chat("给我一个代码示例")
print(response3)
# 输出:"以下是使用 LlamaIndex 实现 RAG 的代码..."
对话历史管理:
# 查看当前对话历史
print(chat_engine.chat_history)
# 重置对话(清空历史)
chat_engine.reset()
# 手动添加历史(用于导入旧对话)
chat_engine.chat_history = [
ChatMessage(role="user", content="什么是 RAG?"),
ChatMessage(role="assistant", content="RAG 是检索增强生成...")
]
5.4 自定义 Prompt 模板
from llama_index.core import PromptTemplate
# 默认的 Prompt 可能不够精确,自定义模板:
qa_prompt_tmpl = (
"我们提供了以下上下文信息:\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"请根据上下文(而非先验知识)回答问题。\n"
"如果上下文中没有相关信息,请回答'根据提供的文档,我无法回答这个问题。'\n"
"问题:{query_str}\n"
"答案:"
)
qa_prompt = PromptTemplate(qa_prompt_tmpl)
query_engine = index.as_query_engine(
text_qa_template=qa_prompt
)
response = query_engine.query("公司 CEO 是谁?")
# 如果文档中没有,会回答"根据提供的文档,我无法回答这个问题。"
为什么要自定义 Prompt?
- 控制答案格式(如"请用三个要点总结")
- 避免幻觉(强制要求"仅根据上下文回答")
- 多语言支持(翻译默认的英文 Prompt)
总结
LlamaIndex 是专门为 LLM 构建 RAG 应用的数据框架。通过五大核心组件(Data Loaders、Document、Index、Retriever、Query Engine),它将复杂的检索增强生成流程封装为简洁的 API,让你用几十行代码就能搭建问答系统。
核心价值:
- Data Loaders:支持 100+ 种数据格式,从 PDF 到数据库
- Document:智能分块策略,平衡检索精度和成本
- Index:向量索引实现语义搜索,毫秒级检索百万级文档
- Retriever:混合检索 + 元数据过滤 + 重排序,提升检索准确率
- Query Engine:流式输出、多轮对话、自定义 Prompt,灵活应对各种场景
LlamaIndex 让 RAG 从实验室走向生产,成为连接 LLM 与私有数据的最佳实践框架。