實際應用與發展趨勢

在實際應用中,向量搜尋技術已經成為許多現代AI系統的核心元件。從人工智慧客服到內容推薦,從知識管理到搜尋引擎,向量搜尋技術無處不在。

實際應用場景

  1. 人工智慧助理的記憶系統:現代AI助理需要記住過去的對話內容,透過向量搜尋可以快速檢索相關的歷史訊息。

  2. 企業知識函式庫:將公司檔案、郵件、會議記錄等轉換為向量,員工可以使用自然語言查詢相關內容。

  3. 產品推薦系統:根據使用者的查詢或歷史行為,找到語義相似的產品進行推薦。

  4. 多模態搜尋:將文字、影像、音訊等不同模態的資料轉換為向量,實作跨模態搜尋。

在開發這些系統時,我注意到選擇合適的嵌入模型和向量資料函式庫是關鍵。對於一般用途,OpenAI的嵌入模型通常表現最佳;而在特定領域,可能需要微調或選擇專門的嵌入模型。

向量資料函式庫的選擇

除了本文介紹的ChromaDB外,還有許多其他向量資料函式庫可供選擇:

  • Pinecone:全託管的向量資料函式庫,適合大規模生產環境。
  • Weaviate:開放原始碼向量搜尋引擎,支援多模態資料。
  • Milvus:高效能向量資料函式庫,適合大規模相似性搜尋。
  • Qdrant:專注於向量相似性搜尋的開放原始碼資料函式庫。

在選擇時,需要考慮資料規模、查詢延遲要求、預算以及是否需要特殊功能(如多模態支援、過濾器等)。

最佳化技巧

在實施向量搜尋系統時,我發現以下幾點最佳化技巧特別有用:

  1. 批次處理:批次生成嵌入向量可以顯著提高效率,特別是使用API服務時。

  2. 索引最佳化:大多數向量資料函式庫支援不同型別的索引(如HNSW、IVF等),根據資料特性選擇合適的索引可以提高查詢速度。

  3. 預過濾:在進行向量相似度搜尋前,先使用傳統的過濾條件(如日期範圍、類別等)縮小搜尋範圍。

  4. 混合搜尋:結合關鍵字搜尋和向量搜尋的優點,可以提高結果的相關性和準確性。

  5. 嵌入快取:對於常用查詢或檔案,快取其嵌入向量可以減少API呼叫和計算開銷。

向量搜尋技術正在快速發展,隨著更先進的嵌入模型和更高效的向量資料函式庫的出現,我們可以期待這一領域在未來有更多創新和應用。

從基本的TF-IDF向量化到高階的神經網路嵌入,從簡單的餘弦相似度計算到專業的向量資料函式庫,向量搜尋技術已經成為連線自然語言與機器理解的關鍵橋樑。掌握這些技術,將使我們能夠構建更人工智慧、更自然的AI系統。

LangChain 開發 RAG 系統:從基礎到實作

LangChain 最初是一個專注於抽象化多種資料來源和向量儲存檢索模式的開放原始碼專案。雖然現在它已發展成為更全面的工具集,但在根本上,它仍然提供了實作檢索功能的絕佳選擇。在建構 RAG (Retrieval-Augmented Generation) 系統時,LangChain 提供了一套完整的工具和抽象層,讓開發者能夠快速實作高效的知識增強型生成系統。

RAG 系統的核心流程

LangChain 提供的檔案儲存檢索流程主要包含四個關鍵步驟:

  1. 載入 (Load) - 從各種來源匯入檔案
  2. 轉換 (Transform) - 將檔案分解成相關區塊或片段
  3. 嵌入 (Embed) - 將文字片段轉換為向量表示
  4. 儲存 (Store) - 將向量存入向量資料函式庫以供後續檢索

這些步驟對於實作記憶檢索也同樣適用。檔案檢索與記憶檢索的關鍵差異在於資料來源以及內容如何轉換。

LangChain 支援多種向量儲存選項,並提供外掛架構,支援從多種來源匯入檔案。接下來,我們將探討如何使用 LangChain 實作這些步驟,並理解其中的細節。

使用 LangChain 分割與載入檔案

檢索機制透過特定相關資訊來增強給定提示的上下文。例如,使用者可能請求關於某個本地檔案的詳細資訊。在早期的語言模型中,由於令牌限制,無法將整個檔案作為提示的一部分提交。

現在,對於許多商業 LLM(如 GPT-4 Turbo),我們可以將整個檔案作為提示請求的一部分提交。然而,這樣做的結果可能並不會更好,而與因為增加的令牌數量,成本可能會更高。因此,更好的選擇是分割檔案,並使用相關部分來提供上下文—這正是 RAG 和記憶系統所做的事情。

分割檔案在將內容分解為語義相關與具體的區段方面至關重要。理想情況下,當我們將檔案分割成片段時,它們會按照相關性和語義意義進行分解。

檔案分割實作範例

讓我們看一個使用 LangChain 分割和載入檔案的例項。以下程式碼展示瞭如何處理 HTML 檔案(以 Mother Goose 童謠為例):

from langchain_community.document_loaders import UnstructuredHTMLLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 載入 HTML 檔案
loader = UnstructuredHTMLLoader("sample_documents/mother_goose.html")
data = loader.load()

# 建立文字分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=25,
    length_function=len,
    add_start_index=True,
)

# 分割檔案
documents = text_splitter.split_documents(data)
# 只取部分片段以節省成本和時間
documents = [doc.page_content for doc in documents][100:350]

# 為每個文字片段建立嵌入
embeddings = [get_embedding(doc) for doc in documents]
ids = [f"id{i}" for i in range(len(documents))]

這段程式碼首先從 HTML 檔案載入內容,然後使用 RecursiveCharacterTextSplitter 將檔案分割成 100 個字元長的片段,並設定 25 個字元的重疊部分。重疊部分確保檔案的思想不會被截斷。我們只選擇了 250 個檔案片段來降低成本並縮短執行時間。

當我們執行這段程式碼並輸入查詢時,例如「誰親吻了女孩們並讓她們哭泣?」,系統會回傳最相關的檔案片段。在這個例子中,回傳了包含相關訊息的片段:「And chid her daughter, And kissed my sister instead of me.」

使用令牌分割檔案

令牌化是將文字分解成詞令牌的過程。詞令牌代表文字中的一個簡潔元素,可以是「hold」這樣的單詞,也可以是「{」這樣的符號,取決於什麼是相關的。

使用令牌化分割檔案為文字如何被語言模型解釋以及語義相似性提供了更好的基礎。令牌化還允許移除無關字元,如空白,使檔案的相似性比對更加相關,通常提供更好的結果。

以下是使用令牌分割檔案的程式碼範例:

from langchain.text_splitter import CharacterTextSplitter

loader = UnstructuredHTMLLoader("sample_documents/mother_goose.html")
data = loader.load()

# 使用 tiktoken 編碼器建立分割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=50, chunk_overlap=10
)

# 分割檔案
documents = text_splitter.split_documents(data)
documents = [doc for doc in documents][8:94]  # 只選擇包含童謠的檔案

# 建立向量資料函式庫
db = Chroma.from_documents(documents, OpenAIEmbeddings())

def query_documents(query, top_n=2):
    # 使用資料函式庫的相似性搜尋
    docs = db.similarity_search(query, top_n)
    return docs

這段程式碼使用 CharacterTextSplitter.from_tiktoken_encoder() 方法,以令牌為基礎進行檔案分割,每塊包含 50 個令牌,重疊 10 個令牌。由於原始檔案中存在大量空白,這會導致分割出的片段大小不均等。

執行後會看到提示「Created a chunk of size 68, which is longer than the specified 50」,這表示因為空白字元的存在,分割出的片段大小不一。

當我們使用相同的查詢「誰親吻了女孩們並讓她們哭泣?」時,結果明顯優於前一個範例。系統回傳了「Georgy Porgy, pudding and pie, Kissed the girls and made them cry」,這正是我們查詢的直接答案。

更有趣的是,即使我們使用語義相似但用詞不同的查詢,如「為什麼女孩們在哭泣?」,系統仍能回傳相關結果,這表明令牌化分割方法在語義理解上更為有效。

高階檔案分割技術

檔案分割是構建適當檢索系統的最關鍵環節。我們可以使用多種方法來分割檔案,甚至可以同時使用多種方法。多種方法可以平行處理同一檔案,為相同檔案建立多種嵌入檢視。

語義分割的重要性

理想情況下,檔案分割應該根據語義相關性而非簡單的字元數量。這意味著相關的概念和想法應該保持在同一個片段中,即使它們可能跨越了多個段落或句子。

當我在設計 RAG 系統時,發現語義分割對於提高檢索精確度至關重要。傳統的固定長度分割方法經常會切斷關鍵概念,導致檢索結果不完整或不相關。使用語義感知的分割器可以顯著改善這一問題。

多種分割策略的組合

在實際應用中,我經常結合多種分割策略以獲得最佳效果:

  1. 根據結構的分割:利用檔案的自然結構(如標題、段落、列表)進行分割
  2. 根據語義的分割:使用語言模型識別語義邊界
  3. 重疊分割:確保相鄰片段之間有足夠的上下文重疊

以下是一個結合多種策略的分割器範例:

from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    MarkdownHeaderTextSplitter
)

# 首先根據 Markdown 標題進行分割
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
md_header_splits = markdown_splitter.split_text(markdown_text)

# 然後對每個區段進行進一步的語義分割
semantic_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n## ", "\n### ", "\n#### ", "\n", " ", ""]
)

final_docs = []
for doc in md_header_splits:
    smaller_docs = semantic_splitter.split_documents([doc])
    final_docs.extend(smaller_docs)

這段程式碼展示瞭如何結合根據結構(Markdown 標題)和語義的分割策略。首先使用 MarkdownHeaderTextSplitter 按照標題層級將檔案分割為大區塊,然後對每個區塊使用 RecursiveCharacterTextSplitter 進行更細粒度的分割。

這種方法的優勢在於它保留了檔案的層級結構,同時確保每個片段都有合適的大小和足夠的上下文。在我的實踐中,這種組合方法通常能產生更高品質的檢索結果。

向量化與儲存

一旦檔案被適當地分割,下一步就是將這些文字片段轉換為向量表示並儲存起來。LangChain 支援多種嵌入模型和向量資料函式庫,使這一過程變得簡單。

選擇合適的嵌入模型

嵌入模型的選擇對檢索品質有重大影響。OpenAI 的嵌入模型通常提供良好的效能,但在某些特定領域應用中,專門訓練的嵌入模型可能表現更佳。

以下是使用 OpenAI 嵌入模型的範例:

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma

# 初始化嵌入模型
embeddings = OpenAIEmbeddings()

# 建立向量資料函式庫
db = Chroma.from_documents(documents, embeddings)

# 執行相似性搜尋
query = "為什麼女孩們在哭泣?"
docs = db.similarity_search(query, k=3)

這段程式碼使用 OpenAI 的嵌入模型將文字片段轉換為向量表示,然後使用 Chroma 向量資料函式庫儲存這些向量。當執行查詢時,系統會計算查詢文字的向量表示,然後在資料函式庫中尋找最相似的檔案向量。

Chroma 是一個輕量級的向量資料函式庫,適合小型到中型的應用。對於大規模應用,可能需要考慮其他選項如 Pinecone、Weaviate 或 Milvus。

向量資料函式庫的選擇與設定

LangChain 支援多種向量資料函式庫,每種都有其優缺點:

  1. Chroma:輕量級,易於設定,適合開發和小型應用
  2. FAISS:Facebook AI 的高效能向量搜尋函式庫,適閤中大型應用
  3. Pinecone:全託管的向量資料函式庫服務,適合生產環境
  4. Weaviate:開放原始碼向量搜尋引擎,具有模組化結構

在選擇向量資料函式庫時,需要考慮以下因素:

  • 資料規模:預期儲存的檔案數量
  • 查詢速度要求:是否需要毫秒級的回應
  • 佈署環境:本地開發還是雲端生產
  • 預算限制:是否能承擔商業服務的費用

在我的實踐中,對於原型開發和測試,我通常選擇 Chroma 或 FAISS;而對於生產環境,則傾向於使用 Pinecone 或 Weaviate,因為它們提供了更好的可擴充套件性和管理功能。

構建完整的 RAG 系統

將前面討論的所有元素結合起來,我們可以構建一個完整的 RAG 系統。以下是一個簡化的 RAG 系統實作:

from langchain.document_loaders import UnstructuredHTMLLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI

# 1. 載入檔案
loader = UnstructuredHTMLLoader("sample_documents/knowledge_base.html")
data = loader.load()

# 2. 分割檔案
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=100, 
    chunk_overlap=20
)
documents = text_splitter.split_documents(data)

# 3. 建立向量儲存
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents, embeddings)

# 4. 建立檢索器
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 3}
)

# 5. 建立 QA 鏈
qa_chain = RetrievalQA.from_chain_type(
    llm=OpenAI(),
    chain_type="stuff",
    retriever=retriever
)

# 6. 執行查詢
query = "解釋量子計算的基本原理"
result = qa_chain.run(query)
print(result)

這段程式碼實作了一個完整的 RAG 系統,包含六個主要步驟:

  1. 載入檔案:從 HTML 檔案中載入知識函式庫內容
  2. 分割檔案:使用根據令牌的分割器將檔案分割成適當大小的片段
  3. 建立向量儲存:將文字片段轉換為向量表示並儲存到 Chroma 資料函式庫
  4. 建立檢索器:設定檢索方法(這裡使用相似性搜尋)和引數
  5. 建立 QA 鏈:將語言模型與檢索器結合,建立一個問答鏈
  6. 執行查詢:處理使用者查詢並回傳結果

這個系統使用 “stuff” 鏈型別,意味著它會將所有檢索到的檔案直接插入到提示中。對於較長的檔案或大量檢索結果,可能需要考慮其他鏈型別如 “map_reduce” 或 “refine”。

RAG 系統的最佳化與最佳實踐

構建高效的 RAG 系統不僅是實作基本功能,還需要關注系統的最佳化和最佳實踐。

檔案分割的最佳實踐

  1. 片段大小平衡:太小的片段可能缺乏足夠上下文,太大的片段可能包含太多無關訊息
  2. 適當的重疊量:通常設定為片段大小的 10-20%,確保上下文連續性
  3. 根據內容結構分割:利用檔案的自然結構(標題、段落、章節)進行分割
  4. 測試不同分割策略:針對特定使用案例評估不同分割方法的效果

提高檢索品質的技巧

  1. 查詢重寫:使用 LLM 將使用者查詢重寫為更適合向量檢索的形式
  2. 混合檢索:結合關鍵字搜尋和向量搜尋的結果
  3. 多重查詢生成:從單一使用者問題生成多個查詢,然後合併結果
  4. 查詢擴充套件:使用同義詞或相關概念擴充套件原始查詢

以下是一個實作查詢重寫的範例:

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

# 查詢重寫提示範本
query_rewrite_template = """
你的任務是將使用者的問題重寫為更適合向量檢索的形式。
保留所有重要的關鍵字和概念,但移除不必要的詞語。

原始問題: {query}

重寫後的查詢:"""

query_rewrite_prompt = PromptTemplate(
    input_variables=["query"],
    template=query_rewrite_template
)

# 建立查詢重寫鏈
llm = OpenAI(temperature=0.0)
query_rewriter = LLMChain(llm=llm, prompt=query_rewrite_prompt)

# 重寫查詢
original_query = "我想知道為什麼小女孩們在遇到那個男孩後都哭了?"
rewritten_query = query_rewriter.run(original_query)
print(f"原始查詢: {original_query}")
print(f"重寫後的查詢: {rewritten_query}")

這段程式碼展示瞭如何使用 LLM 將使用者的自然語言查詢重寫為更適合向量檢索的形式。查詢重寫可以顯著提高檢索效果,特別是當使用者使用口語化或含糊不清的表達時。

在這個例子中,原始查詢「我想知道為什麼小女孩們在遇到那個男孩後都哭了?」可能被重寫為「男孩 女孩們 哭泣 原因」,這樣的形式更適合向量檢索。

系統評估與監控

建立有效的評估機制對於持續改進 RAG 系統至關重要。以下是一些評估方法:

  1. 人工評估:讓人類評估者對系統回答進行評分
  2. 自動評估:使用預定義的問答對來測試系統效能
  3. 相關性指標:測量檢索檔案與查詢的相關性
  4. 回答品質指標:評估生成答案的準確性、完整性和連貫性

以下是一個簡單的自動評估範例:

from langchain.evaluation import QAEvalChain

# 預定義的評估資料集
eval_dataset = [
    {"query": "誰親吻了女孩們?", "answer": "Georgy Porgy親吻了女孩們。"},
    {"query": "女孩們為什麼哭泣?", "answer": "因為Georgy Porgy親吻了她們。"},
    # 更多評估問題...
]

# 使用RAG系統生成答案
predictions = []
for example in eval_dataset:
    result = qa_chain.run(example["query"])
    predictions.append({"query": example["query"], "result": result})

# 建立評估鏈
eval_chain = QAEvalChain.from_llm(OpenAI())
graded_outputs = eval_chain.evaluate(eval_dataset, predictions)

# 計算準確率
correct = sum(1 for output in graded_outputs.values() if output["score"] == "CORRECT")
accuracy = correct / len(eval_dataset)
print(f"系統準確率: {accuracy:.2f}")

這段程式碼展示瞭如何使用預定義的問答對來評估 RAG 系統的效能。評估鏈會比較系統生成的答案與預期答案,並計算準確率。

這種自動評估方法可以快速檢測系統效能的變化,特別是在更新或修改系統元件後。然而,它也有侷限性,因為自然語言可以有多種表達相同意思的方式。因此,通常需要結合人工評估來獲得更全面的效能評估。