一、直入主題
檢索增強生成(RAG)已成為將大型語言模型的專業知識、實時性與事實準確性相結合的經典架構。其核心思想直白而有力:當用户提問時,首先從一個龐大的知識庫(如公司文檔、技術手冊、最新新聞等)中檢索出最相關的信息片段,然後將這些片段與用户問題一同交給大模型,指令其基於所提供的上下文進行回答。這完美解決了大模型的幻覺問題、知識陳舊和無法溯源等痛點。
然而,一個RAG系統的性能高度依賴於一個簡單卻殘酷的準則:“垃圾進,垃圾出”(Garbage in, Garbage out)。如果我們提供給大模型的上下文材料本身就是不相關、不準確或不完整的,那麼無論後續的生成模型多麼強大,它都難以產生高質量的回答,甚至可能因為錯誤上下文而產生更危險的幻覺。
因此,召回(Retrieval) 階段,即從知識庫中精準找出相關文檔的過程,成為了整個RAG系統的基石與核心瓶頸。高效召回的目標是在毫秒級的時間內,從可能包含數百萬條文檔的知識庫中,找到真正能回答用户問題的那些黃金片段。
二、怎麼理解高效召回
通俗的理解,現在市中心發生了一起珠寶失竊案,來了一個超級偵探,非常聰明,上知天文下知地理。但凡事都有規矩,偵探破案必須基於案卷庫裏的證據,不能靠自己瞎猜。現在,來了個初級助手幫着一起來找案卷,偵探問助手:“昨天的失竊案,有什麼線索”,助手跑去巨大的檔案室,根據“盜竊”、“珠寶”、“市中心”這幾個關鍵詞,抱回來三本厚厚的、相關的案卷,於是偵探開始閲讀這些案卷,試圖找出答案。但案卷太厚了!裏面可能包含了“去年城東的失竊案”、“珠寶保養手冊”、“市中心城市規劃”等各種無關信息。偵探也要花大量時間從頭讀到尾,才能找到一點點真正有用的線索。效率極低,而且很容易被無關信息干擾,導致破案方向錯誤。
在這個故事中,超級偵探好比是大語言模型,破案就是回答問題,而案卷庫就是知識庫,查案卷就好比大模型回答問題必須基於知識庫,助手就是初級的RAG系統,檔案室就是向量數據庫,總結就是初級的RAG系統接收到問題後去向量數據庫中檢索上下文內容,結果取回了與案卷本身關聯度不高的卷宗,導致信息匹配度低,沒有得到想要的效果,對破案起不到決定性的作用,助手白忙活了一場,RAG系統也並沒有吹噓的那麼神奇高效。
至此毫無懸念的引出了高效召回,就是給偵探換一個超級聰明的得力助手。 這個新助手不會傻乎乎地抱回整本案卷,而是會用各種高級方法來找到最精煉、最相關的信息,從而達到高準確度、事半功倍的效果。
三、為什麼要做高效召回
此時相比應該都基本理解了高效召回的本質原因了,RAG系統的性能嚴重依賴於召回階段的質量,核心問題是如果檢索到的文檔片段不包含回答問題所需的信息,那麼再強大的大模型也無法生成高質量的答案,這就是開篇就提到的所謂的“垃圾進,垃圾出”。
同時,初級的RAG系統召回也會遇到很多問題和瓶頸:
- 詞彙不匹配:用户的查詢用語和知識庫中的文檔用語可能不同,但含義相似。例如,用户問“如何解決電腦無法啓動?”,而文檔中寫的是“PC開機故障排除指南”。
- 語義不匹配:查詢的意圖和文檔的側重點可能難以通過簡單嵌入對齊。
- 信息分散:答案所需的信息可能分散在多個文檔片段中,單一片段無法提供完整上下文。
- 塊大小權衡:小塊檢索精度高但上下文不足;大塊上下文豐富但檢索精度低,會引入噪聲。
因此,“高效召回”的核心目標就是:打破這些瓶頸,確保檢索系統能夠精準、全面地將最相關的信息傳遞給大模型,為生成高質量答案奠定堅實基礎。
四、典型的高效召回方法
下面我們詳細解析三種方法的概念、差異和實現邏輯。
方法一:Small-to-Big(由小到大檢索)
1. 詳細説明
在標準的RAG流程中,我們通常將文檔切分成大小均勻的片段(chunks),然後為每個片段創建向量嵌入(embeddings)。檢索時,將用户查詢也轉換為向量,並通過向量數據庫找到與查詢最相關的幾個片段,最後將這些片段連同查詢一起餵給大模型生成答案。
這是一種“分而治之”的策略。它在索引階段創建兩種顆粒度的文本塊,主要在於塊大小的權衡:
- 小塊:尺寸較小(如100-256字),用於向量檢索,檢索精度高,能更精準地定位到包含答案的文本。但上下文信息可能不足,大模型可能因為缺乏足夠的背景信息而無法生成高質量答案。其目的是精準定位,像一把手術刀,確保召回的片段與查詢高度相關。
- 大塊:尺寸較大(如512-1024字),是小塊所在的父級段落或章節。包含豐富的上下文信息,利於大模型生成,其目的是提供豐富上下文,確保大模型有足夠的背景信息來生成連貫、準確的答案,但會引入很多噪聲,降低檢索精度,因為向量檢索可能返回的是相關性不高的大塊。
關鍵機制是建立從小塊到其源大塊的映射關係,它的精髓就在於:它巧妙地規避了這個權衡,做到了魚和熊掌兼得。
2.工作流程
小檢索:
- 在索引階段,將原始文檔切分成兩種顆粒度的片段,“小片段“用於檢索尺寸較小(如100-256)的字符,旨在精準捕獲關鍵信息。“大片段”用於生成尺寸較大(如512-1024)的字符,提供充足的上下文。
- 關鍵一步:建立“小片段“到其父“大片段”的映射關係(例如,每個小片段都記錄自己是從哪個大片段中切出來的)。
- 在檢索階段,使用用户的查詢去向量數據庫中搜索最相關的 Top-K 個小片段。
大投喂:
- 獲取到Top-K個相關的小片段後,不是直接將這些小片段餵給大模型。
- 而是根據之前建立的映射關係,找到這些小片段對應的父大片段。
- 將這些大片段去重後作為上下文,與用户查詢一起組合成提示(Prompt),發送給大模型以生成最終答案。
流程總結:查詢 -> 用查詢向量檢索最相關的小塊 -> 通過映射找到這些小塊對應的大塊 -> 將大塊去重後作為上下文發送給大模型生成答案
3. 突出優勢
- 更高的檢索精度:小尺寸塊在向量空間中的表示更集中,能更精確地匹配查詢意圖,減少無關信息的干擾,從而召回更相關的內容。
- 更豐富的生成上下文:通過小塊找到父級大塊,確保了提供給大模型的上下文是完整、連貫的,包含了問題所需的背景信息和細節,極大提升了生成答案的質量。
- 降低成本和延遲:雖然存儲了兩種塊,但最終只將去重後的大塊發送給大模型。這比發送多個重疊的大塊或多個不完整的小塊更高效,減少了Token消耗和計算開銷。
- 靈活性:可以根據文檔類型和需求靈活定義“小”和“大”的尺寸,以及它們的重疊策略。
4. 使用場景
這種方法在以下場景中尤其有效:
- 長文檔問答:如技術手冊、法律合同、學術論文等,需要準確定位到某個概念(小塊),但同時需要理解其周圍的論證和解釋(大塊)。
- 複雜、多步推理:用户問題需要結合文檔中多個部分的信息進行推理。小塊找到相關點,大塊提供將這些點連接起來的完整邏輯鏈。
- 高精度要求的領域:如醫療、金融、法律等領域,答案的準確性至關重要,既不能遺漏關鍵信息,也不能缺少必要的上下文限制條件。
- 文檔結構層次分明:具有章節、段落等清晰結構的文檔,非常適合用小塊映射到大塊(如小節映射到整個章節)。
5. 案例解析
5.1 示例代碼
import requests
import json
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.schema import Document
import warnings
warnings.filterwarnings('ignore')
import os
# 1. 文檔加載和預處理
fake_document_text = """
機器學習是人工智能的一個子領域,它使計算機系統能夠從數據中學習並改進,而無需顯式編程。
機器學習算法通常分為三類:監督學習、無監督學習和強化學習。
監督學習使用標記數據來訓練模型,例如用於圖像分類。無監督學習在未標記數據中尋找隱藏模式,例如客户細分。強化學習則通過與環境交互並獲得獎勵來學習最佳策略,例如AlphaGo。
深度學習是機器學習的一個分支,它使用稱為神經網絡的多層模型。這些網絡能夠從大量數據中學習複雜的特徵層次結構。
卷積神經網絡(CNN)特別適用於圖像處理任務,而循環神經網絡(RNN)則擅長處理序列數據,如文本或時間序列。
"""
documents = [Document(page_content=fake_document_text, metadata={"source": "ml_textbook_chapter1"})]
# 2. 定義文本分割器
# 創建"大"塊的分割器
big_size = 300
big_overlap = 50
big_splitter = RecursiveCharacterTextSplitter(
chunk_size=big_size,
chunk_overlap=big_overlap,
)
# 創建"小"塊的分割器
small_size = 100
small_overlap = 20
small_splitter = RecursiveCharacterTextSplitter(
chunk_size=small_size,
chunk_overlap=small_overlap,
)
# 3. 切分文檔並建立映射關係
all_small_chunks = []
all_big_chunks = []
mapping_dict = {} # 用於存儲小塊ID到父大塊的映射
# 首先,將文檔切分成"大"塊
big_chunks = big_splitter.split_documents(documents)
for big_chunk_index, big_chunk in enumerate(big_chunks):
# 將每個"大"塊進一步切分成"小"塊
small_chunks_from_big = small_splitter.split_documents([big_chunk])
# 為每個"小"塊創建唯一ID並存儲映射關係
for small_chunk in small_chunks_from_big:
# 給小塊一個ID(這裏用內容哈希簡化演示)
small_chunk_id = hash(small_chunk.page_content)
mapping_dict[small_chunk_id] = {
"big_chunk_content": big_chunk.page_content,
"big_chunk_index": big_chunk_index
}
all_small_chunks.append(small_chunk)
all_big_chunks.append(big_chunk)
print(f"切分出 {len(all_big_chunks)} 個大塊")
print(f"切分出 {len(all_small_chunks)} 個小塊")
# 4. 為"小"塊創建向量庫(Faiss)
# 選擇嵌入模型
model_name = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
embeddings = HuggingFaceEmbeddings(model_name=model_name)
# 使用所有"小"塊構建向量索引
vector_db = FAISS.from_documents(all_small_chunks, embeddings)
# 5. 定義QWen API調用函數
def call_qwen_api(prompt, api_key, model="qwen-max", temperature=0.1):
"""
調用通義千問API
參數:
prompt: 輸入的提示文本
api_key: 你的API密鑰
model: 使用的模型名稱,默認為qwen-max
temperature: 生成温度,控制創造性
"""
url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
data = {
"model": model,
"input": {
"messages": [
{
"role": "user",
"content": prompt
}
]
},
"parameters": {
"temperature": temperature,
"top_p": 0.8,
"result_format": "text"
}
}
try:
response = requests.post(url, headers=headers, data=json.dumps(data))
response.raise_for_status()
result = response.json()
return result["output"]["text"]
except Exception as e:
print(f"API調用出錯: {e}")
return None
# 6. 檢索和生成過程
def rag_query(query, api_key, k=3):
# a) 使用查詢檢索最相關的"小"塊
retrieved_small_docs = vector_db.similarity_search(query, k=k)
print("\n--- 檢索到的最相關'小'塊 ---")
for i, doc in enumerate(retrieved_small_docs):
print(f"[Small Chunk {i+1}]: {doc.page_content}\n")
# b) 根據映射字典,找到這些"小"塊對應的父"大"塊
retrieved_big_contents = set() # 使用集合自動去重
for small_doc in retrieved_small_docs:
small_id = hash(small_doc.page_content)
if small_id in mapping_dict:
retrieved_big_contents.add(mapping_dict[small_id]["big_chunk_content"])
else:
# 如果找不到映射,使用小塊本身
retrieved_big_contents.add(small_doc.page_content)
# 將去重後的大塊內容合併為上下文
context = "\n\n---\n\n".join(retrieved_big_contents)
# c) 構建Prompt,調用QWen API生成答案
prompt_template = f"""
請根據以下上下文信息回答問題。如果上下文不包含答案,請如實告知。
上下文:
{context}
問題:{query}
請給出準確、簡潔的回答:
"""
print("\n--- 發送給QWen API的Prompt ---")
print(prompt_template)
# 調用API
answer = call_qwen_api(prompt_template, api_key)
return answer
# 7. 使用示例
if __name__ == "__main__":
# 替換為你的API密鑰
API_KEY = os.environ.get("DASHSCOPE_API_KEY", "")
# 查詢示例
query = "CNN神經網絡主要用於什麼任務?"
# 執行RAG查詢
result = rag_query(query, API_KEY)
print("\n--- 最終答案 ---")
print(result)
5.2 輸出結果
切分出 1 個大塊
切分出 4 個小塊
--- 檢索到的最相關'小'塊 ---
[Small Chunk 1]: 卷積神經網絡(CNN)特別適用於圖像處理任務,而循環神經網絡(RNN)則擅長處理序列數據,如文本或
時間序列。
[Small Chunk 2]: 深度學習是機器學習的一個分支,它使用稱為神經網絡的多層模型。這些網絡能夠從大量數據中學習複雜
的特徵層次結構。
[Small Chunk 3]: 機器學習是人工智能的一個子領域,它使計算機系統能夠從數據中學習並改進,而無需顯式編程。
機器學習算法通常分為三類:監督學習、無監督學習和強化學習。
--- 發送給QWen API的Prompt ---
請根據以下上下文信息回答問題。如果上下文不包含答案,請如實告知。
上下文:
機器學習是人工智能的一個子領域,它使計算機系統能夠從數據中學習並改進,而無需顯式編程。
機器學習算法通常分為三類:監督學習、無監督學習和強化學習。
監督學習使用標記數據來訓練模型,例如用於圖像分類。無監督學習在未標記數據中尋找隱藏模式,例如客户細分。強化學習
則通過與環境交互並獲得獎勵來學習最佳策略,例如AlphaGo。
深度學習是機器學習的一個分支,它使用稱為神經網絡的多層模型。這些網絡能夠從大量數據中學習複雜的特徵層次結構。
卷積神經網絡(CNN)特別適用於圖像處理任務,而循環神經網絡(RNN)則擅長處理序列數據,如文本或時間序列。
深度學習是機器學習的一個分支,它使用稱為神經網絡的多層模型。這些網絡能夠從大量數據中學習複雜的特徵層次結構。
深度學習是機器學習的一個分支,它使用稱為神經網絡的多層模型。這些網絡能夠從大量數據中學習複雜的特徵層次結構。
卷積神經網絡(CNN)特別適用於圖像處理任務,而循環神經網絡(RNN)則擅長處理序列數據,如文本或時間序列。
問題:CNN神經網絡主要用於什麼任務?
請給出準確、簡潔的回答:
--- 最終答案 ---
CNN神經網絡主要用於圖像處理任務。
5.3 代碼分析
- 1. 導入必要的庫
import requests # 用於發送HTTP請求到QWen API
import json # 用於處理JSON數據
from langchain.text_splitter import RecursiveCharacterTextSplitter # 文本分割工具
from langchain_community.vectorstores import FAISS # 向量數據庫
from langchain_community.embeddings import HuggingFaceEmbeddings # 文本嵌入模型
from langchain.schema import Document # 文檔數據結構
import warnings # 警告管理
warnings.filterwarnings('ignore') # 忽略警告
import os # 操作系統接口,用於讀取環境變量
- 2. 文檔加載和預處理
fake_document_text = """機器學習是人工智能的一個子領域...""" # 示例文檔內容
documents = [Document(page_content=fake_document_text, metadata={"source": "ml_textbook_chapter1"})]
這裏創建了一個包含機器學習相關內容的示例文檔,並將其包裝成 LangChain 的 Document 對象,附帶元數據標識來源。
- 3. 定義文本分割器
# 創建"大"塊的分割器 (用於生成上下文)
big_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50)
# 創建"小"塊的分割器 (用於檢索)
small_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)
這裏定義了兩個不同尺寸的文本分割器:
大塊分割器 (300字符,重疊50字符):用於生成富含上下文的文本塊
小塊分割器 (100字符,重疊20字符):用於精確檢索相關文本
- 4. 切分文檔並建立映射關係
# 首先將文檔切分成"大"塊
big_chunks = big_splitter.split_documents(documents)
# 為每個大塊創建對應的小塊,並建立映射關係
for big_chunk_index, big_chunk in enumerate(big_chunks):
small_chunks_from_big = small_splitter.split_documents([big_chunk])
for small_chunk in small_chunks_from_big:
small_chunk_id = hash(small_chunk.page_content) # 使用哈希值作為小塊ID
mapping_dict[small_chunk_id] = {
"big_chunk_content": big_chunk.page_content,
"big_chunk_index": big_chunk_index
}
all_small_chunks.append(small_chunk)
all_big_chunks.append(big_chunk)
這是 Small-to-Big 方法的核心部分:
- 先將文檔分割成大塊(用於提供豐富上下文)
- 再將每個大塊分割成小塊(用於精確檢索)
- 建立從小塊到大塊的映射關係,這樣可以通過小塊找到對應的大塊
- 5. 為"小"塊創建向量庫
# 選擇嵌入模型
model_name = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
embeddings = HuggingFaceEmbeddings(model_name=model_name)
# 使用所有"小"塊構建向量索引
vector_db = FAISS.from_documents(all_small_chunks, embeddings)
這裏使用了一個多語言句子嵌入模型,將所有小塊轉換為向量,並使用 FAISS 構建高效的向量索引,便於快速相似性搜索。
- 6. 定義 QWen API 調用函數
def call_qwen_api(prompt, api_key, model="qwen-max", temperature=0.1):
url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
data = {
"model": model,
"input": {"messages": [{"role": "user", "content": prompt}]},
"parameters": {"temperature": temperature, "top_p": 0.8, "result_format": "text"}
}
try:
response = requests.post(url, headers=headers, data=json.dumps(data))
response.raise_for_status()
result = response.json()
return result["output"]["text"]
except Exception as e:
print(f"API調用出錯: {e}")
return None
這個函數封裝了與通義千問 API 的交互,用於發送提示並獲取生成的文本響應。
- 7. 檢索和生成過程
def rag_query(query, api_key, k=3):
# a) 使用查詢檢索最相關的"小"塊
retrieved_small_docs = vector_db.similarity_search(query, k=k)
# b) 根據映射字典,找到這些"小"塊對應的父"大"塊
retrieved_big_contents = set() # 使用集合自動去重
for small_doc in retrieved_small_docs:
small_id = hash(small_doc.page_content)
if small_id in mapping_dict:
retrieved_big_contents.add(mapping_dict[small_id]["big_chunk_content"])
else:
retrieved_big_contents.add(small_doc.page_content) # 回退方案
# 將去重後的大塊內容合併為上下文
context = "\n\n---\n\n".join(retrieved_big_contents)
# c) 構建Prompt,調用QWen API生成答案
prompt_template = f"""請根據以下上下文信息回答問題..."""
# 調用API
answer = call_qwen_api(prompt_template, api_key)
return answer
這是 RAG 查詢的核心函數:
- 使用查詢在小塊向量庫中檢索最相關的小塊
- 通過映射關係找到這些小塊對應的大塊(去重)
- 將大塊內容作為上下文構建提示
- 調用 QWen API 生成最終答案
- 8. 使用示例
if __name__ == "__main__":
API_KEY = os.environ.get("DASHSCOPE_API_KEY", "") # 從環境變量獲取API密鑰
query = "CNN神經網絡主要用於什麼任務?" # 用户查詢
result = rag_query(query, API_KEY) # 執行RAG查詢
print("\n--- 最終答案 ---")
print(result)
這部分展示瞭如何使用整個系統,包括設置 API 密鑰、提出查詢並獲取答案。
方法二:索引擴展
1. 詳細説明
在標準的RAG流程中,用户的原始查詢被直接用於向量數據庫中搜索最相似的文檔片段。這種方法簡單直接,但當用户的查詢表述簡短、模糊或與文檔中的措辭差異較大時,效果會大打折扣。
索引擴展的核心思想是不直接使用原始查詢進行檢索,而是先對原始查詢進行擴展,生成多個與之相關的、從不同角度或用不同表述的查詢,然後用這一組擴展後的查詢去向量庫中檢索,最後合併所有檢索結果,剔除重複項,並將最相關的結果返回給大模型進行答案生成。
2. 工作流程
3. 常用策略
- 同義詞/近義詞擴展:使用詞庫(如WordNet)或嵌入模型為查詢中的關鍵詞生成同義詞或近義詞,組合成新的查詢。如查詢“蘋果手機” -> 擴展為["蘋果手機", "iPhone", "蘋果移動設備"]。
- 假設性文檔嵌入(HyDE):這是非常強大且具有創新性的一種策略。首先讓大模型根據原始查詢生成一個假設的答案,即使這個答案可能是錯誤的或不準確的,然後將這個假設答案的嵌入向量,注意不是原始查詢的向量用於向量數據庫檢索。
- 因為假設答案在語言風格、術語使用和文本結構上會與真實的文檔片段高度相似,從而在向量空間中的距離更近。
- 如查詢“簡述牛頓第一定律的內容”,大模型生成的假設答案:“牛頓第一定律,又稱慣性定律,指出任何物體都要保持勻速直線運動或靜止狀態,直到外力迫使它改變運動狀態為止。這意味着如果沒有外力作用,運動的物體將繼續保持勻速直線運動。”用這段生成的、富含關鍵術語(“慣性定律”、“勻速直線運動”、“外力”)的文本去檢索,比用簡短的原始查詢能找到更匹配的真實文檔。
- 大模型思維鏈擴展:指示大模型根據原始查詢,生成多個與之相關的子問題、分解問題或不同角度的思考。如“如何學習深度學習?”,大模型生成的擴展查詢:["深度學習入門教程", "深度學習需要的數學基礎", "優秀的深度學習框架對比", "深度學習實戰項目推薦"]
4. 突出優勢
- 顯著提高召回率:核心優勢。通過多路查詢,大大增加了找到所有相關文檔片段的機率,減輕了詞彙不匹配問題。
- 提升最終答案質量:檢索到更相關、更全面的上下文材料,大模型就能生成更準確、更詳盡的答案。
- 增強系統魯棒性:對於表述不完整、模糊或口語化的用户查詢,擴展方法能更好地理解其背後意圖。
- 靈活性高:可以與任何向量數據庫(如Faiss)和任何大模型結合使用,擴展策略可以自由組合和定製。
5. 使用場景
索引擴展在以下場景中尤為有效:
- 開放域問答系統:用户問題千奇百怪,表述多樣,擴展查詢能更好地覆蓋知識庫中的各種相關材料。
- 技術文檔/知識庫檢索:技術術語通常有縮寫、全稱、別名等多種形式(如“SSL”和“安全套接層”),擴展查詢能確保所有這些形式都被覆蓋。
- 長尾查詢處理:對於不常見或非常具體的查詢,直接檢索可能效果很差,通過擴展可以找到相關的上游或基礎概念文檔。
- 跨語言檢索(需配合多語言模型):用户用中文提問,知識庫有英文文檔,可以通過擴展生成英文查詢去檢索。
6. 案例解析
6.1 示例代碼
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
from typing import List
import dashscope
from dashscope import Generation
import os
# 1. 設置Key(請替換成你的實際API Key)
dashscope.api_key = os.environ.get("DASHSCOPE_API_KEY", "")
# 2. 加載嵌入模型(用於文本轉向量)
embed_model = SentenceTransformer('GanymedeNil/text2vec-large-chinese') # 一個優秀的中文嵌入模型
# 3. 假設我們有一個簡單的知識庫文檔(實際應用中應從文件加載)
knowledge_base = [
"牛頓第一定律,又稱為慣性定律,指出:任何物體在沒有外力作用時,總保持勻速直線運動狀態或靜止狀態。",
"牛頓第二定律指出,物體的加速度與所受合外力成正比,與質量成反比,公式為 F=ma。",
"牛頓第三定律,又稱作用與反作用定律,指出兩個物體之間的作用力和反作用力總是大小相等,方向相反,作用在同一直線上。",
"愛因斯坦的質能方程是 E=mc²,其中E代表能量,m代表質量,c代表光速。",
"深度學習是機器學習的一個分支,它使用名為深度神經網絡的模型。",
]
# 為知識庫生成向量並構建Faiss索引
knowledge_vectors = embed_model.encode(knowledge_base)
dimension = knowledge_vectors.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(knowledge_vectors.astype('float32'))
# 4. 定義HyDE生成函數(使用Qwen)
def generate_hyde_query(original_query: str) -> str:
"""
使用Qwen根據用户問題生成一個假設性的答案。
這個答案可能不準確,但其表述方式更接近知識庫中的文本。
"""
prompt = f"""請根據以下問題,生成一個假設性的、詳細的答案。即使你不確定正確答案,也請模仿百科知識的風格和語氣來寫。
問題:{original_query}
假設性答案:"""
response = Generation.call(
model='qwen-max',
prompt=prompt,
seed=12345,
top_p=0.8
)
hyde_text = response.output['text'].strip()
print(f"原始查詢: {original_query}")
print(f"HyDE生成: {hyde_text}")
return hyde_text
# 5. 定義檢索函數
def retrieve_with_hyde(user_query: str, top_k: int = 3) -> List[str]:
"""
1. 使用HyDE生成假設答案。
2. 將假設答案編碼為向量。
3. 用該向量在Faiss中檢索最相似的文檔。
"""
# 生成HyDE查詢
hyde_query = generate_hyde_query(user_query)
# 將HyDE查詢編碼為向量
query_vector = embed_model.encode([hyde_query])
# 在Faiss中搜索
distances, indices = index.search(query_vector.astype('float32'), top_k)
# 返回檢索到的文本
retrieved_docs = [knowledge_base[i] for i in indices[0]]
return retrieved_docs
# 6. 定義最終答案生成函數(使用Qwen)
def generate_final_answer(user_query: str, contexts: List[str]) -> str:
"""
將用户查詢和檢索到的上下文組合成Prompt,讓Qwen生成最終答案。
"""
context_str = "\n".join([f"- {doc}" for doc in contexts])
prompt = f"""請根據以下提供的上下文信息,回答用户的問題。如果上下文信息不包含答案,請直接説你不知道。
上下文信息:
{context_str}
用户問題:{user_query}
請直接給出答案:"""
response = Generation.call(
model='qwen-max',
prompt=prompt,
seed=12345,
top_p=0.8
)
final_answer = response.output['text'].strip()
return final_answer
# 7. 主流程:完整的RAG with HyDE
def rag_with_hyde(user_query: str):
# 第一步:通過HyDE檢索相關文檔
retrieved_docs = retrieve_with_hyde(user_query)
print("\n檢索到的相關文檔:")
for i, doc in enumerate(retrieved_docs):
print(f"{i+1}. {doc}")
# 第二步:合成最終答案
final_answer = generate_final_answer(user_query, retrieved_docs)
print(f"\n最終答案:\n{final_answer}")
# 8. 測試
if __name__ == "__main__":
user_question = "牛頓第一定律是什麼?"
rag_with_hyde(user_question)
6.2 輸出結果
No sentence-transformers model found with name GanymedeNil/text2vec-large-chinese. Creating a new one with mean pooling.
原始查詢: 牛頓第一定律是什麼?
HyDE生成: 牛頓第一定律,也被稱為慣性定律,是經典力學中的基礎之一。這一定律由艾薩克·牛頓在17世紀提出,並收錄於他著名的《自然哲學的數學原理》一書中。牛頓第一定律指出,在沒有外力作用的情況下,一個物體將保持其靜止狀態或勻速直線運動的狀態不變。
換句話説,如果一個物體處於靜止,則它將繼續保持靜止;若該物體正在以恆定速度沿直線移動,則它將以相同的速度繼續沿同一直線移動,除非受到外部力量的作用。這裏的“外力”指的是任何能夠改變物體當前運動狀態的力量,比如摩擦力、重力等。
牛頓第一定律揭示了自然界中物體運動的基本規律之一——慣性。慣性是指物體抵抗其運動狀態變化(即加速或減速)的一種性質。質量越大的物體,其慣性也就越大,因此需要更大的力才能改變它的運動狀態。
這條定律不僅對於理解日常生活中物體的行為至關重要,而且也是現代物理學、工程學等多個領域研究的基礎。通過牛頓的第一定律,我們可以更好地預測和解釋周圍世界的物理現象。
檢索到的相關文檔:
1. 牛頓第一定律,又稱為慣性定律,指出:任何物體在沒有外力作用時,總保持勻速直線運動狀態或靜止狀態。
2. 牛頓第二定律指出,物體的加速度與所受合外力成正比,與質量成反比,公式為 F=ma。
3. 牛頓第三定律,又稱作用與反作用定律,指出兩個物體之間的作用力和反作用力總是大小相等,方向相反,作用在同一直線上。
最終答案:
牛頓第一定律,又稱為慣性定律,指出:任何物體在沒有外力作用時,總保持勻速直線運動狀態或靜止狀態。
6.3 代碼分析
使用了一個新的詞向量模型GanymedeNil/text2vec-large-chinese,運行如果本地沒有,則會先進行下載;
- 1. 嵌入模型:我們使用 text2vec-large-chinese 來為文本生成高質量的向量表示,它適用於Faiss。
- 2. 知識庫索引:將示例知識庫文本編碼為向量並存入Faiss索引。
- 3. 通過函數generate_hyde_query執行HyDE生成 :函數調用Qwen模型,根據用户原始問題生成一段“假設答案”。
- 原始查詢: 牛頓第一定律是什麼?
- HyDE生成: 牛頓第一定律,也稱為慣性定律,是艾薩克·牛頓在1687年於《自然哲學的數學原理》中提出的三大運動定律之一。該定律表明,任何物體都會保持其靜止狀態或勻速直線運動狀態,除非有外力迫使它改變這種狀態。這一定律揭示了物體固有的慣性屬性。
- 4. 通過函數retrieve_with_hyde執行檢索:使用生成的假設答案的向量(而不是原始問題的向量)在Faiss中進行搜索,找到最相似的已知文檔片段。
- 5. 通過函數generate_final_answer最終答案生成:將檢索到的真實文檔片段和原始問題一起交給Qwen,讓它合成一個準確、基於上下文的最終答案。
- 預期最終答案:
- 根據提供的上下文信息,牛頓第一定律又稱為慣性定律,指出:任何物體在沒有外力作用時,總保持勻速直線運動狀態或靜止狀態。
方法三:雙向改寫
1. 詳細説明
傳統的RAG召回是直接將用户查詢編碼成向量,然後去向量數據庫中搜索最相似的文檔向量。但問題在於,用户的查詢通常很短、很口語化,和通常很長、很正式文檔中的語言在表達方式上存在巨大差異,這會導致即使語義相關,向量相似度也不高,從而召回失敗。這種方法的核心思想是通過改寫來彌合用户查詢(Query)和文檔(Document)之間的“語義鴻溝”,從而在向量空間中進行更精準的匹配。
2. 工作流程
查詢 -> 文檔改寫 (Query2Doc):
- 思路: 根據用户的簡短查詢,自動生成一段或幾段假想的、理想的答案文檔。
- 目的: 生成的“假文檔”會使用更豐富、更正式的語言,其表述方式與知識庫中的真實文檔風格更接近。然後用這個生成的“假文檔”去向量數據庫進行檢索,就更容易找到風格和內容都相似的真實文檔。
- 例如:
- 用户查詢:“蘋果發佈會什麼時候?”
- Query2Doc改寫:“蘋果公司的產品發佈會通常被稱為Apple Event,每年秋季(通常在9月)會舉行新品發佈會,發佈最新的iPhone等產品。春季有時也會舉行發佈會,發佈iPad、Mac等產品。”
- 用後面這段生成的文本去檢索,召回“蘋果公司發佈會時間安排”相關文檔的成功率會高得多。
文檔 -> 查詢改寫 (Doc2Query):
- 思路: 在索引構建階段(預處理階段),為知識庫中的每一篇長文檔,自動生成幾個可能的問題。
- 目的: 將這些生成的問題與原文檔關聯起來(例如,作為文檔的元數據存儲)。當用户輸入一個查詢時,系統不僅會計算查詢與原文的相似度,還會計算查詢與所有文檔對應生成的問題的相似度。相當於一篇文檔有了多個“入口”,被命中的概率大大增加。
- 例如:
- 一篇文檔內容是關於《民法典》第105條:自然人的民事權利能力一律平等。。
- Doc2Query改寫可能生成:“什麼是民事權利能力?”、“民事權利能力平等嗎?”、“民法典關於民事權利能力是如何規定的?”。
- 當用户查詢“民事權利能力是啥?”時,即使這個短查詢和法條原文的向量不相似,但它與生成的問題“什麼是民事權利能力?”高度相似,從而能成功召回這條法條文檔。
雙向:指的是這兩種方法分別從查詢端和文檔端相向而行,共同改善召回效果。
3. 突出優勢
- 顯著提升召回率: 核心優勢。通過改寫創造了更多的語義匹配路徑,尤其能召回那些與用户查詢表述方式不同但內容高度相關的長尾文檔。
- 緩解術語不匹配問題: 有效解決了用户口語化表達和文檔專業化表達之間的差異。
- 實現簡單,效果好: 相對於訓練複雜的重排序(Rerank)模型,使用現有的大模型(如ChatGLM, Qwen等)進行改寫是一種性價比極高的方案。
- 無侵入性: 特別是Doc2Query方法,是在索引階段完成的,對線上的檢索速度幾乎沒有影響,因為生成的問題可以預先計算好向量並存儲。
- 可組合性強: 可以與傳統的關鍵詞檢索、其他向量檢索方法融合,形成混合檢索,進一步提升效果。
4. 使用場景
- 開放域問答系統: 用户問題千奇百怪,知識庫文檔種類繁多,雙向改寫能極大提升泛化能力。
- 企業知識庫/客服機器人: 企業文檔通常很規範,而用户提問很隨意,如問“怎麼報銷”時需查詢的文檔標題《員工差旅費用報銷流程及規範》。
- 法律、醫療等專業領域檢索: 用户不瞭解專業術語,而文檔使用法律術語,如“被車撞了怎麼賠”和“機動車交通事故責任糾紛損害賠償”的對應。
- 學術文獻檢索: 學生用大白話檢索,而論文標題和摘要非常學術化,如“AI怎麼學”和“基於自監督學習的深度學習模型訓練策略綜述”的對應。
5. 案例解析
5.1 示例代碼
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
import requests
import json
import os
# 初始化嵌入模型
embedding_model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# Qwen API配置
QWEN_API_KEY = os.environ.get("DASHSCOPE_API_KEY", "") # 替換為您的實際API密鑰
QWEN_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
def call_qwen(prompt):
"""調用Qwen API"""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {QWEN_API_KEY}"
}
payload = {
"model": "qwen-turbo",
"input": {
"messages": [
{
"role": "user",
"content": prompt
}
]
},
"parameters": {
"temperature": 0.7
}
}
try:
response = requests.post(QWEN_API_URL, headers=headers, json=payload)
response.raise_for_status()
result = response.json()
return result['output']['text']
except Exception as e:
print(f"API調用失敗: {e}")
return None
# 1. 準備文檔數據
documents = [
"員工報銷需要提供發票和審批單,15個工作日內完成報銷。",
"請假需提前在OA系統申請,緊急情況可事後補辦手續。",
"密碼必須包含字母、數字和特殊字符,且長度至少8位。",
"新產品發佈流程包括需求評審、設計、開發、測試和發佈五個階段。"
]
# 2. 生成查詢問題 (Doc2Query)
print("為文檔生成查詢問題...")
doc_queries = []
for doc in documents:
prompt = f"請為以下文本生成3個用户可能會提出的問題:\n\n文本: {doc}\n\n生成的問題:"
response = call_qwen(prompt)
if response:
queries = [q.strip() for q in response.split('\n') if q.strip()]
doc_queries.append(queries[:3])
print(f"文檔: {doc[:20]}...")
print(f"生成的問題: {queries[:3]}")
else:
# 如果API調用失敗,使用簡單的問題
doc_queries.append([f"關於{doc[:10]}...", f"如何{doc[:10]}...", f"{doc[:10]}有什麼要求..."])
print()
# 3. 創建FAISS索引
# 合併文檔和生成的問題
all_texts = documents.copy()
for queries in doc_queries:
all_texts.extend(queries)
# 生成嵌入向量
embeddings = embedding_model.encode(all_texts)
# 創建FAISS索引
dimension = embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(np.array(embeddings).astype('float32'))
# 4. 查詢改寫函數 (Query2Doc)
def rewrite_query(query):
prompt = f"請根據以下問題生成一段詳細的答案文檔:\n\n問題: {query}\n\n生成的答案文檔:"
response = call_qwen(prompt)
return response if response else query
# 5. 檢索函數
def search(query):
print(f"原始查詢: {query}")
# 策略1: 直接檢索
query_embedding = embedding_model.encode([query])
distances, indices = index.search(np.array(query_embedding).astype('float32'), 3)
print("直接檢索結果:")
for i, idx in enumerate(indices[0]):
if idx < len(all_texts):
print(f" {i+1}. {all_texts[idx]}")
# 策略2: 查詢改寫後檢索
expanded_query = rewrite_query(query)
if expanded_query != query:
print(f"改寫後的查詢: {expanded_query}")
expanded_embedding = embedding_model.encode([expanded_query])
distances, indices = index.search(np.array(expanded_embedding).astype('float32'), 3)
print("改寫後檢索結果:")
for i, idx in enumerate(indices[0]):
if idx < len(all_texts):
print(f" {i+1}. {all_texts[idx]}")
print("-" * 50)
# 6. 測試查詢
queries = ["怎麼報銷", "如何請假", "密碼要求", "發佈流程"]
for query in queries:
search(query)
5.2 輸出結果
5.2 輸出結果
為文檔生成查詢問題...
文檔: 員工報銷需要提供發票和審批單,15個工作...
生成的問題: ['1. 員工報銷需要哪些必備的材料?', '2. 報銷流程需要多長時間?', '3. 如果超過15個工作日還沒收到報
銷款怎麼辦?']
文檔: 請假需提前在OA系統申請,緊急情況可事後...
生成的問題: ['1. 請假必須提前在OA系統申請嗎?', '2. 如果有緊急情況,是否可以先請假再補辦手續?', '3. 事後補辦
請假手續需要哪些流程?']
文檔: 密碼必須包含字母、數字和特殊字符,且長度...
生成的問題: ['1. 密碼需要滿足哪些要求?', '2. 特殊字符包括哪些類型?', '3. 如果密碼只有7位,是否符合要求?']
文檔: 新產品發佈流程包括需求評審、設計、開發、...
生成的問題: ['1. 新產品發佈流程有哪些主要階段?', '2. 需求評審在新產品發佈中起到什麼作用?', '3. 測試階段在新
產品發佈流程中的重要性是什麼?']
原始查詢: 怎麼報銷
直接檢索結果:
1. 2. 報銷流程需要多長時間?
2. 3. 如果超過15個工作日還沒收到報銷款怎麼辦?
3. 1. 員工報銷需要哪些必備的材料?
改寫後的查詢: **報銷流程説明文檔**---(中間省略8000字)---**備註:具體執行以公司最新通知為準。**
改寫後檢索結果:
1. 2. 報銷流程需要多長時間?
2. 員工報銷需要提供發票和審批單,15個工作日內完成報銷。
3. 1. 員工報銷需要哪些必備的材料?
--------------------------------------------------
原始查詢: 如何請假
直接檢索結果:
1. 3. 事後補辦請假手續需要哪些流程?
2. 2. 如果有緊急情況,是否可以先請假再補辦手續?
3. 1. 請假必須提前在OA系統申請嗎?
改寫後的查詢: **如何請假**---(中間省略8000字)---**備註**:本文檔僅供參考,具體請假流程請以所在單位或學校的規定為準。
改寫後檢索結果:
1. 3. 事後補辦請假手續需要哪些流程?
2. 請假需提前在OA系統申請,緊急情況可事後補辦手續。
3. 1. 請假必須提前在OA系統申請嗎?
--------------------------------------------------
原始查詢: 密碼要求
直接檢索結果:
1. 1. 密碼需要滿足哪些要求?
2. 密碼必須包含字母、數字和特殊字符,且長度至少8位。
3. 3. 如果密碼只有7位,是否符合要求?
改寫後的查詢: **密碼要求文檔**---(中間省略8000字)---**更新日期:2025年4月5日**
改寫後檢索結果:
1. 1. 密碼需要滿足哪些要求?
2. 密碼必須包含字母、數字和特殊字符,且長度至少8位。
3. 3. 如果密碼只有7位,是否符合要求?
--------------------------------------------------
原始查詢: 發佈流程
直接檢索結果:
1. 1. 新產品發佈流程有哪些主要階段?
2. 新產品發佈流程包括需求評審、設計、開發、測試和發佈五個階段。
3. 3. 測試階段在新產品發佈流程中的重要性是什麼?
改寫後的查詢: **發佈流程**---(中間省略8000字)---不斷優化和迭代發佈流程,以適應快速變化的市場需求。
改寫後檢索結果:
1. 新產品發佈流程包括需求評審、設計、開發、測試和發佈五個階段。
2. 1. 新產品發佈流程有哪些主要階段?
3. 3. 測試階段在新產品發佈流程中的重要性是什麼?
--------------------------------------------------
5.3 代碼分析
使用了一個新的詞向量模型BAAI/bge-small-zh-v1.5,運行如果本地沒有,則會先進行下載;
- 1.通過函數 call_qwen(prompt)封裝了對Qwen API的調用,發送提示詞並返回生成的文本響應,包括構建API請求頭和請求體、處理HTTP請求和響應、提供錯誤處理和異常捕獲以及返回API生成的文本內容或異常錯誤
- 2. Doc2Query流程 - 文檔到查詢生成,為知識庫中的每個文檔生成多個可能的用户查詢,擴展文檔的可搜索性
- 遍歷所有文檔,為每個文檔調用Qwen API
- 使用特定提示詞要求生成3個可能的用户查詢
- 處理API響應,提取和清理生成的查詢
- 提供回退機制,當API調用失敗時使用簡單生成的查詢
- 打印生成結果用於調試和驗證
- 3. 函數rewrite_query(query) 執行Query2Doc查詢改寫,將簡短的用户查詢改寫成更詳細的文檔形式,提高檢索效果
- 構建特定的提示詞,要求將查詢改寫成詳細答案文檔
- 調用Qwen API進行智能查詢改寫
- 處理API失敗情況,返回原始查詢作為回退
- 實現Query2Doc策略,彌合用户查詢與文檔內容之間的語義鴻溝
- 4. 核心檢索函數search(query),執行雙向檢索策略,結合直接檢索和改寫後檢索的結果
- 接收用户查詢作為輸入
- 實施兩種檢索策略:直接檢索,使用原始查詢進行向量相似性搜索;改寫後檢索,使用改寫後的查詢進行向量相似性搜索
- 編碼查詢為向量表示
- 使用FAISS索引進行相似性搜索
- 格式化並顯示兩種策略的檢索結果
- 提供清晰的結果展示和分隔線
- 5. FAISS索引構建流程,創建高效的向量索引,支持快速的相似性搜索,
- 合併原始文檔和生成的查詢問題,
- 使用BAAI/bge-small-zh-v1.5模型生成所有文本的嵌入向量
- 確定嵌入向量的維度,創建FAISS L2距離索引,將所有嵌入向量添加到索引中
方法對比差異
|
特性 |
Small-to-Big |
索引擴展 (HyDE) |
雙向改寫 |
|
核心思想 |
分治策略:檢索用小塊,生成用大塊 |
引導策略:用假設答案引導檢索真實答案 |
橋樑策略:讓查詢和文檔的表述更接近 |
|
主要處理階段 |
索引階段(定義塊大小和映射) |
檢索階段(生成假設文檔) |
檢索階段(查詢改寫)或索引階段(文檔增強) |
|
解決的核心問題 |
塊大小的權衡(精度 vs上下文) |
語義/詞彙不匹配、查詢信息不足 |
詞彙不匹配、查詢多樣性低 |
|
計算開銷 |
索引階段開銷大,檢索階段開銷小 |
檢索階段開銷大(每次檢索需額外調用一次LLM) |
查詢擴展:檢索開銷大;文檔增強:索引開銷大 |
|
適用場景 |
長篇、結構化文檔 |
短查詢、零樣本、冷啓動 |
搜索系統、開放域問答 |
這三種高效召回方法從不同角度突破了RAG的檢索瓶頸:
- Small-to-Big 通過改進索引結構來解決信息粒度問題。
- 索引擴展HyDE 通過利用LLM的推理能力在檢索前先“想象”答案,來彌合語義鴻溝。
- 雙向改寫 通過增加查詢和文檔的表述多樣性,來提高匹配概率。
在實際應用中,這些方法並非互斥,而是可以組合使用的。例如,可以為採用Small-to-Big策略索引的文檔,在檢索時同時採用HyDE和查詢擴展,構建一個極其強大的RAG系統。我們可以根據自己的具體場景、數據特點和性能要求,選擇合適的策略組合。
五、總結
大模型對語言都有難以跨越的鴻溝,我們總結出以下問題,從而更精細的尋找解決辦法:
- 語義鴻溝:用户提問的方式和文檔中表述的方式可能截然不同。例如,用户問“如何解決屏幕常亮”,而文檔中寫的是“禁用睡眠模式”。傳統的字面匹配方法在此失效。
- 詞彙不匹配:同一概念的不同表述、同義詞、縮寫等。如“AI”與“人工智能”,“NLP”與“自然語言處理”。
- 數據質量:知識庫本身的格式混亂、噪聲多、長度不一,都會嚴重影響檢索效果。
- 效率與精度權衡:在海量數據中實現近似最近鄰搜索(ANN)既要快,又要準,需要精巧的工程和算法設計。
儘管實現高效召回也面臨諸多挑戰,但釐清了問題的本質,瞭解其核心思想,防止那個笨助手直接抱着一堆冗長又充滿噪聲的原始材料給你,而是讓他用各種聰明的方法(Small-to-Big, HyDE, 雙向改寫)先對這些材料進行預處理、精煉和聯想,最終只把那些最核心、最相關、質量最高的內容呈到你面前。這樣一來,大模型就能更快、更準、更輕鬆地利用這些內容生成高質量的答案了,這就是高效召回的價值所在。