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)