RAG的工作原理
你在考试的时候有可能会因为忘记某个概念或公式而失去分数,但考试如果是开卷形式,那么你只需要找到与考题最相关的知识点,并加上你的理解就可以进行回答了。
对于大模型来说也是如此,在训练过程中由于没有见过某个知识点(比如你们公司的制度文件),因此直接向它提问相关问题会得到不准确的答案;如果在大模型生成内容时,像开卷考试一样将相关知识提供给它作为参考,那么大模型回答的质量也就会大幅提高了。
这引出了之前提到的一个核心理念:上下文工程(Context Engineering) ,专注于为大模型“上下文窗口”填充恰到好处的信息,以引导其完成特定任务。如果信息太少,模型会“不知道”;如果信息太多或无关,模型的性能会下降,成本也会增加。
而我们即将学习的RAG(Retrieval Augmented Generation,检索增强生成),是上下文工程中最重要、最有效的技术这一,专门解决大模型“知识不足”的问题,RAG应用通常包含建立索引与检索生成两部分。
建立索引
你可能会在考试前对参考资料做标记,来帮助你在考试时更容易地找到相关信息。类似的,RAG应用往往也会在回答前就已经做好了标记,这一过程叫做建立索引,建立索引包括四个步骤:
- 文档解析
就像你会将书上看到的视觉信息理解为文字信息一样,RAG应用也需要首先将知识库文档进行加载并解析为大模型能够理解的文字形式。 - 文本分段
你通常不会在某道题时把整本书都翻阅一遍,而是去查找与问题最相关的几个段落,因此你会先把参考资料做一个大致的分段。类似的,RAG应用也会在文档解析后对文本进行分段,以便于在后续能够快速找到与提问最相关的内容。 - 文本向量化
在开卷考试时,你通常会先在参考资料中寻找与问题最相关的段落,再去进行作答。在RAG应用,通常需要借助嵌入(embedding)模型分别对段落与问题进行数字化表示,在进行相似度比较后找出最相关的段落,数字化表示的过程就叫做文本向量化 - 存储索引
存储索引将向量化后的段落存储为向量数据库,这样RAG应用就无需在每次进行回复时都重复以上步骤,从而可以增加响应速度。
检索生成
检索、生成分别对应着RAG名字中的Retrieval
与Generation
两阶段。检索就开卷考试时去查找资料的过程,生成则是在找到资料后,根据参考资料与问题进行作答的过程。
-
检索生成
检索阶段会召回与问题最相关的文本段。通过embedding模型对问题进行文本向量化,并与向量数据库的段落进行语义相似度的比较,找出最相关的段落。检索是RAG应用中最重要的环节,你可以想象如果考试的时候找到了错误的资料,那么回答一定是不准确的。这个步骤完美诠释了上下文工程的精髓:从海量知识中“精准地选择相关信息”来填充上下文。找到最匹配的内容,是保证后续生成质量的第一步。为了提高检索准确性,除了使用性能强大的embedding模型,也可以做重排(Rerank)、句子窗口检索等方法。 -
生成
在检索到相关的文本段后,RAG应用会将问题与文本段通过提示词模板生成最终的提示词,由大模型生成回复,这个阶段更多是利用大模型的总结能力,而不是大模型本身具有的知识。这个提示词模板的设计,是上下文工程的另一个关键环节。我们不仅要提供检索到的“资料”,还要明确地“指导”模型如何使用这些资料来回答问题。
一个典型的提示词模板为:请根据以下信息回答用户的问题:{召回文本段}。用户的问题是:{question}。
它的整体流程图为:
创建RAG应用
一个简单的RAG应用
# 导入依赖
from llama_index.embeddings.dashscope import DashScopeEmbedding,DashScopeTextEmbeddingModels
from llama_index.core import SimpleDirectoryReader,VectorStoreIndex
from llama_index.llms.openai_like import OpenAILike# 这两行代码是用于消除 WARNING 警告信息,避免干扰阅读学习,生产环境中建议根据需要来设置日志级别
import logging
logging.basicConfig(level=logging.ERROR)print("正在解析文件...")
# LlamaIndex提供了SimpleDirectoryReader方法,可以直接将指定文件夹中的文件加载为document对象,对应着解析过程
documents = SimpleDirectoryReader('./docs').load_data()print("正在创建索引...")
# from_documents方法包含切片与建立索引步骤
index = VectorStoreIndex.from_documents(documents,# 指定embedding 模型embed_model=DashScopeEmbedding(# 你也可以使用阿里云提供的其它embedding模型:https://help.aliyun.com/zh/model-studio/getting-started/models#3383780daf8hwmodel_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2))
print("正在创建提问引擎...")
query_engine = index.as_query_engine(# 设置为流式输出streaming=True,# 此处使用qwen-plus模型,你也可以使用阿里云提供的其它qwen的文本生成模型:https://help.aliyun.com/zh/model-studio/getting-started/models#9f8890ce29g5ullm=OpenAILike(model="qwen-plus",api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",api_key=os.getenv("DASHSCOPE_API_KEY"),is_chat_model=True))
print("正在生成回复...")
streaming_response = query_engine.query('我们公司项目管理应该用什么工具')
print("回答是:")
# 采用流式输出
streaming_response.print_response_stream()
正在解析文件...
正在创建索引...
正在创建提问引擎...
正在生成回复...
回答是:
项目管理应使用项目管理软件,如Jira、Trello。这些工具可用于需求沟通、任务分配和进度跟踪,配合协作工具如Slack或Microsoft Teams,能够有效提升团队协作效率。
保存与加载索引
你可能会发现,创建索引消耗的时间比较长。如果能够将索引保存到本地,并在需要使用的时候直接加载,而不是重新建立索引,那就可以大幅提升回复的速度,LlamaIndex提供了简单易实现的保存与加载索引的方法
# 将索引保存为本地文件
index.storage_context.persist("knowledge_base/test")
print("索引文件保存到了knowledge_base/test")
存在目录列表会出现以下几个文件:
文件名 | 作用 | 说明 |
---|---|---|
default__vector.json | 文本向量存储 | 存储所有文本 chunk 的向量(embedding)及其对应的节点 ID。检索时会用这些向量做相似度搜索。 |
image__vector.json | 图片向量存储 | 如果你的文档中有图片,或者你用过图片 embedding,这里会存储图片的向量。没有图片时可能为空或不存在。 |
docstore.json | 文档存储 | 存储原始的 Document 对象(包括文本内容、元数据等),用于在检索到节点后还原原文。 |
graph_store.json | 图结构存储 | 存储索引的图结构信息(节点之间的关系、父子节点等),主要用于分层索引(TreeIndex、GraphIndex)等。 |
index_store.json | 索引元信息存储 | 存储索引的元数据(索引类型、版本、向量存储的引用等),加载索引时会先读取这个文件来恢复结构。 |
index_store.json├── 引用 → default__vector.json (文本向量)├── 引用 → image__vector.json (图片向量)├── 引用 → docstore.json (原始文档)└── 引用 → graph_store.json (节点关系)
🤔LlamaIndex 在 persist() 保存索引时,不同模型(Embedding 模型、LLM 模型)生成的文件会不会不一样?
- 核心结论
文件的种类和结构主要取决于 索引类型(VectorStoreIndex、TreeIndex、ListIndex 等)和数据类型(文本、图片、音频等),不是直接由模型决定的。
文件的内容会因为你用的模型不同而不同(尤加粗样式其是向量文件),但文件名和整体结构大体是一样的。- 为什么文件名大体固定
LlamaIndex 的持久化是基于 StorageContext 的,它会把不同类型的数据存到不同的 Store 里:
DocStore → docstore.json(原始文档)
VectorStore → default__vector.json / image__vector.json(向量数据)
GraphStore → graph_store.json(节点关系)
IndexStore → index_store.json(索引元信息)
这些 Store 的名字是固定的,所以文件名也基本固定。- 模型不同,变化在哪里?
Embedding 模型不同 → default__vector.json(或 image__vector.json)里的向量值会不同,因为不同模型生成的向量维度、数值分布不一样。
例如:
OpenAI: text-embedding-3-small → 1536 维向量
阿里云: DashScope TEXT_EMBEDDING_V2 → 1024 维向量
硅基流动: text-embedding-3-large → 3072 维向量
维度不同会直接影响向量文件的内容和大小。
是否有图片/多模态数据 → 决定是否生成 image__vector.json。
索引类型不同 → 决定是否生成 graph_store.json(比如 TreeIndex 会用到)。
LLM 模型不同 → 不会直接影响这些持久化文件,因为 LLM 主要在查询阶段用,不会存到索引文件里(除非你把 LLM 生成的内容当作文档再存)。
# 将本地索引文件加载为索引
from llama_index.core import StorageContext,load_index_from_storage
storage_context = StorageContext.from_defaults(persist_dir="knowledge_base/test")
index = load_index_from_storage(storage_context,embed_model=DashScopeEmbedding(model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2))
print("成功从knowledge_base/test路径加载索引")
LlamaIndex 会:
读取 index_store.json → 确定索引类型和引用的文件。
加载 default__vector.json(或 image__vector.json)→ 获取向量和元数据。
加载 docstore.json → 获取原文。
如果有 graph_store.json → 加载节点关系。
从本地加载索引后,你可以再次进行提问测试是否可以正常工作。
print("正在创建提问引擎...")
query_engine = index.as_query_engine(# 设置为流式输出streaming=True,# 此处使用qwen-plus模型,你也可以使用阿里云提供的其它qwen的文本生成模型:https://help.aliyun.com/zh/model-studio/getting-started/models#9f8890ce29g5ullm=OpenAILike(model="qwen-plus",api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",api_key=os.getenv("DASHSCOPE_API_KEY"),is_chat_model=True))
print("正在生成回复...")
streaming_response = query_engine.query('我们公司项目管理应该用什么工具')
print("回答是:")
streaming_response.print_response_stream()
你可以将上述代码进行封装,以便在后续持续迭代时快速使用。
from chatbot import rag# 引文在前面的步骤中已经建立了索引,因此这里可以直接加载索引。如果需要重建索引,可以增加一行代码:rag.indexing()
index = rag.load_index(persist_path='./knowledge_base/test')
query_engine = rag.create_query_engine(index=index)rag.ask('我们公司项目管理应该用什么工具', query_engine=query_engine)