火柴人武林大会
156.74M · 2026-02-04
有一句话描RAG非常到位: 从本质上讲,RAG(Retrieval-Augmented Generation)是一种旨在解决大语言模型(LLM)“知其然不知其所以然”问题的技术范式。它的核心是将模型内部学到的“参数化知识”(模型权重中固化的、模糊的“记忆”),与来自外部知识库的“非参数化知识”(精准、可随时更新的外部数据)相结合。
在软件工程领域上有个专业名字叫做:Trade-off,就是我们没有办法既要又要,没有最佳方案,只有最适合的,我们要做的就是在适合的场景,选择出最贴合业务的方案。比如在llm应用开发中,如何避免大模型乱回答,成本最小的就是prompt工程,选择合适的提示字让他回答出我们想要的回答,而不一上来就换模型,prompt做不到,就回去使用RAG,给他外挂一个知识库,当前两者都无法解决问题,微调才会成为一个值得考虑的选项。
LlamaIndex是一个专注数据层的llm应用框架,其初衷就是为了简化RAG流程,更加专注于数据如何被组织、索引和检索,强调数据接入、索引结构和查询质量。
其实llm应用开发首选实际上是langchain,尤其是社区成熟度方面,langchain都不可避免的成为首选,但是做RAG的关键不是有没有接向量数据库,而是检索是否真的为模型提供了有用的上下文。LlamaIndex将RAG的复杂性前移到数据与检索层进行系统化抽象处理,因此RAG场景下LlamaIndex比langchain更自然和高效。
文档入口地址
一个完善的RAG系统可以被简化为下面这三个步骤:
数据准备与清洗
当接收到用户输入的信息时,不会立马抛给llm,而是将收集到的信息,比如PDF、word和网页等,转换成标准化文本,按照语义段落或者句子切分,生成Document或者Node,这里的Document是LlamaIndex中的一个通用容器,用来封装各种数据源的数据,本质上是一个完整的数据单元。
from llama_index.core import Document
documents = [Document(text="这是一个文档的文本。", metadata={"source": "file1.pdf"})]
实际上就是将用户的输入构建成一个LlamaIndex可以处理的对象
而Node是从Document中抽出来的块,比如一个段落、句子等。
索引构建
使用嵌入模型将文本Node转为向量,默认就是text-embedding-ada-002,转化为向量后,就可以使用向量相似度来判断语义相关性,而不是靠关键词去匹配。
有了向量后,就可以将向量存储到向量数据库中,这是为了增加检索效率,而不是每次都去遍历全部文本
检索与增强
用户在系统输入问题,LlamaIndex使用同样的模型将问题转化为向量,然后用之前构建好的索引找出与查询向量最相似的K个Node;然后对 Top-K 的结果再用轻量 LLM 或 BERT 类模型重新排序 ,来提高检索的准确性和相关性。
有了相关的Node后,结合用户的问题和系统提示来拼接成一个增强的prompt,送给llm
生成与提示工程
问题经过检索和增强提示后,得到的prompt包含原始问题、检索得到的top-k和系统角色提示,llm接收到这个增强的提示词,在上下文的基础上生成回答。
安装命令:pip install llama-index
API Key 配置。
新建.env文件在根目录,然后写上你的key
DEEPSEEK_API_KEY=xxx
from dotenv import load_dotenv
from llama_index.core import Document, VectorStoreIndex, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.deepseek import DeepSeek
# 读取 .env 中的环境变量(例如 DEEPSEEK_API_KEY)
load_dotenv()
# 显式指定 LLM 和 Embedding,避免使用默认的 OpenAI
Settings.llm = DeepSeek(
model="deepseek-chat", # 如有需要可改成其它 DeepSeek 模型
)
Settings.embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-small-zh-v1.5" # 适合中文的向量模型
)
documents = [
Document(
text="LlamaIndex 是一个用于构建 RAG 应用的数据框架。",
metadata={"source": "intro"},
),
Document(
text="RAG 的核心思想是通过检索外部知识来增强 LLM 的回答能力。",
metadata={"source": "rag"},
),
]
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("什么是 RAG?")
print(response)
这里先加载定义在环境变量中的key,显示的指定模型。然后定义了文档数据数组,由Document对象构成,前面也介绍过,Document对象就是一个基本的数据单元。VectorStoreIndex.from_documents(documents)这行代码就是构建向量索引,和用户的输入做相似度检测。
一开始我以为将文本转化成向量,是一个有一个统一的标准,后来了解才发现不是这样的,将文本转化成向量,这是一种通用的思想,用一个多维数字向量来表示这段文字,但是具体使用多少维度的向量、那些语义更加接近,这都是向量模型来决定的,比如我们这个实例中选用的就是model_name="BAAI/bge-small-zh-v1.5",还有个问题需要注意:这里的模型和我们最后的llm模型是两码事,BAAI/bge-small-zh-v1.5模型是embedding模型,将我们的用户输入和文档转换成向量,做相似度匹配的,而llm模型才是最终的推理模型。
构建好向量索引后,就可以借此来构建查询引擎,index.as_query_engine(),query_engine的职责就是将用户的输入构建成向量来和文档向量做相似度匹配,将检索到的内容和问题一起给llm总结回答。
最后调用query_engine.query来发起查询。
文档地址
加载阶段是整个RAG流程的第一步,将用户输入的数据引入系统,转化为LlamaIndex可以理解和处理的结构。这里主要有两个核心概念
Document / Node:数据容器和原子数据块的抽象。
这里前面已经介绍过了,文档也很清晰,Document本身就是一个原始的数据容器,用于存储加载的文本和内容,比如:
from llama_index import Document
doc = Document(
text="LlamaIndex 是一个连接 LLM 与外部数据的框架。",
metadata={"source": "官方文档", "type": "tutorial"}
)
而Node是从Document切分出来的最小单元信息,通常是按照段落、句子或者语义进行切分的,是向量化和索引的基本粒度。
from llama_index.node_parser import SimpleNodeParser
parser = SimpleNodeParser()
nodes = parser.get_nodes_from_documents([doc])
这里就是将Document 切分成的多个 Node
Data Connectors (LlamaHub) :数据源连接器集合。
就是一个类似于数据接口库,可以快速从各种外部数据源加载文档。有一个说法就是:LlamaHub的设计是LlamaIndex可以工程化的核心设计之一,就是因为他实现了所有的Reader都遵循统一模式:
reader = SomeReader(...)
documents = reader.load_data()
# -> List[Document]
也就是说不管读取任何数据,最终都会输出一个统一的Document的列表。
读取Notion数据库
from llama_index.readers.notion import NotionReader
reader = NotionReader(
notion_token="YOUR_TOKEN",
database_id="DATABASE_ID"
)
documents = reader.load_data()
读取word/PDF
from llama_index import SimpleDirectoryReader
reader = SimpleDirectoryReader(
input_files=["design.pdf", "api.docx"]
)
documents = reader.load_data()
读取github
from llama_index.readers.github import GithubRepositoryReader
reader = GithubRepositoryReader(
owner="openai",
repo="openai-python",
use_parser=True
)
documents = reader.load_data()
LlamaIndex用过这种设计,是的所有的Reader统一返回Document数组,使得不同的数据源能够以零适配成本接入同一RAG与Agent管道
索引的目标只有一个,即将不可检索的原始文本,转换为可高效检索的结构化表示,这一步由三个核心抽象类来协作完成:
NodeParser:负责将 Document 切分为 Node 的工具。
他的职责主要有下面几个:负责切分Document,控制chunk的大小,保留上下级关系和维护metadata中的继承关系。
看下这个实例:
from llama_index.core.node_parser import SentenceSplitter
parser = SentenceSplitter(
chunk_size=512,
chunk_overlap=50
)
nodes = parser.get_nodes_from_documents(documents)
SentenceSplitter是LlamaIndex核心库中提供基于语义、句子边界的文档分割器,而不是简单的按照字符数硬切割,他的核心特点就是优先以自然句子为分割边界,比如段落中的分隔符,句号、逗号和感叹号等,避免切割到句子中间导致语义断裂;同时还兼顾预设的分块大小限制,在语义完整和分块尺寸之间做平衡,这个也是LlamaIndex中最常用、最推荐的文档分割器。
SentenceSplitter的两个参数,chunk_size就是写个文本块(chunk)的最大令牌数限制,通常根据llm上下文窗口的大小设定的;chunk_overlap是相邻两个文本块之间的重叠令牌数量,目的就是为了保持文本上下文的连续性,避免因为分块导致的语义断层。
最终输出的nodes就是一个Node的列表
Embeddings:将文本转化为向量表示。
这里就是使用模型将Node数组转换为向量,比如之前的例子:
Settings.embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-small-zh-v1.5" # 适合中文的向量模型
)
Index:可查询的数据结构。
这里的Index是一种将Node组织为可查询结构的数据结构,LlamaIndex提供了很多种索引类型,
VectorStoreIndex 将文本块转为高维稠密向量(Embedding),基于语义相似性检索,这里的语义相似度检索就是余弦相似度检索,通过计算向量的夹角来对比匹配的相关内容,实现懂意图的语义检索。这个适合绝大多数的RAG场景,也是最通用的一个Index索引类型SummaryIndex 无复杂检索逻辑,直接将全量文档 / Node 输入 LLM 进行总结或问答 ,配置极简、无需嵌入模型、保留文本完整上下文,适合超短文档(几百字内)的场景。KeywordTableIndex 从文档中提取核心关键词,构建「关键词 - 文档块」映射表,基于字面关键词匹配检索 , 适合关键词明确的短文档检索 ,比如基于产品编号、订单号查询存储阶段是将构建好的索引数据持久化的环节,主要基于这两个类来实现的:
StorageContext:持久化索引的机制。
StorageContext是专门用于封装、管理所有索引相关存储组件的上下文容器,也是实现索引持久化、从存储中加载索引的核心机制。
看下文档中给的例子:
from llama_index.storage.storage_context import StorageContext
from llama_index.vector_stores import ChromaVectorStore
from llama_index import VectorStoreIndex
# 初始化向量存储(Chroma)
vector_store = ChromaVectorStore(collection_name="my_docs")
# 创建 StorageContext
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# 使用 StorageContext 构建 Index
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context
)
# 持久化 Index
index.storage_context.persist()
ChromaVectorStore是LlamaIndex对开源向量库Chroma的封装,ChromaVectorStore(collection_name="my_docs")就是创建一个名字叫做my_docs的集合,用来存储向量;StorageContext.from_defaults(vector_store=vector_store)就是快速创建一个StorageContext,后续在构建索引的时候,传入创建的storage_context,这样就知道向量要存储的地方;而VectorStoreIndex就是前面介绍的将文本转换为向量的Index类型, 最终返回的index 是一个可以用来 as_query_engine() / as_retriever() 的索引对象 ;index.storage_context就是前面传递的storage_context,persist()方法就是将向量存储到磁盘中;这样下次程序启动时,可以重新创建同样的ChromaVectorStore,就不用在从原始的Document中重新创建索引。
Vector Store:向量数据库
向量数据库就是用来做向量的插入和删除操作,然后持久化存储向量,后续避免重复创建,同时也会做相速度搜索。
常见的有
这个是对用户真正产生价值的阶段,就是在已有索引的基础上,检索最相关的上下文,结合用户问题,生成可控、可追溯的回答。
Retriever
根据一些指定的策略来检索相关的Node,主要有这样一些策略
Response Synthesizer
将检索到的上下文和查询结合,生成最终答案。它关注的问题是如何组织上下文,如何调用llm和如何避免幻觉。
在 LlamaIndex 中,查询阶段通过 Retriever 精准召回相关 Node,再由 Response Synthesizer 组织上下文并生成回答,实现了检索与生成的清晰解耦,是 RAG 系统可控性与质量的核心保障。
当构建一个RAG系统后,一个很核心的问题就是如何科学的评估它的表现,如何量化的追踪、迭代并提升RAG应用的性能,当系统出现幻觉时如何快速的定位问题,这都是对于开发者来说非常关键的。
文档对于RAG系统的评估主要聚焦于解决三个核心问题:
检索质量
检索是否把与问题最相关的信息找出来,有没有遗漏关键信息,有这么几个衡量指标:Recall@K,在检索返回的前k个文档块中是否包含至少一个于query相关的正确文档,衡量检索的召回能力
生成质量
生成质量主要有这么几个评估维度:回答事实一致性,主要看生成的内容是否严格基于检索上下文,而非模型自身的幻觉补全;回答相关性:看回答是否真正解决了用户的问题,是否存在看似合理但答非所问的情况;覆盖度与简洁性:是否遗漏相关信息,或者引入与问题无关的冗余内容
端到端RAG性能
端到端关注的问题是:从用户提问到最终答案,系统整体是否看起来像一个可靠的专家,这里需要关注一些最终答案的正确率、可用性,错误类型分布和响应时间与成本等。当系统出现幻觉或者错误时,可以反向拆解定位问题究竟出现在检索层、生成层,还是两者的协同策略上,从而指导下一轮的优化。
这里都是一些概念性的东西,自己看的时候也是混个眼熟,终归还是要落到代码环节。后面做一个菜谱的智能问题系统实战,应该对RAG有个更加深入的理解。这里推荐一个教程,我也是通读了一遍,但是这种好文章看几遍都不为过,应该是阿里搞得社区,很不错。