突破 OpenAI 資料限制:RAG 技術大解密!

Retrieval-Augmented Generation

Thomas
12 min readApr 2, 2024

前言

OpenAI 當道,最近花了一些時間研究一下,主要是想學習怎麼餵資料給 OpenAI,畢竟它的資料庫只更新到 2022,如果想問較新的資訊,或甚至是公司的內部資料,它似乎就沒轍了!

正確發行時間是:2023/7/1

RAG(Retrieval-Augmented Generation)

RAG 技術則可以解決我們遇到的問題,個人認為 RAG 技術的精隨是 R,也就是 Retrieval,中文是檢索的意思。

我們需要先準備好要讓 OpenAI 學習的文件,可能是 pdf 或 txt 檔,然後將這些內容切塊(chunk),透過 embedding 將其轉成向量,再存入向量 DB 中;接著將問題也轉成向量 Q_vector,查詢向量 DB 中,跟 Q_vector 最接近的幾筆資料(ex: chunk1, chunk2, chunk3),最後將問題及 chunk1, chunk2, chunk3 一同丟給 OpenAI,請它根據 chunk1, chunk2, chunk3 的內容回答問題。

Retrieval-Augmented Generation

向量 DB

向量 DB 顧名思義就是用來儲存向量的,你可以把一張圖片或一段文字轉換成一個向量,再儲存至向量 DB 中,如何轉成向量的呢?是透過 Embedding 的技術,這邊就不詳談。

向量 DB 有很多種,可以參考 Ref [1] 有很多介紹,熊貓大大最後推薦的向量 DB 是 Qdrant,因其優秀的效能,RPS 遠高於其它對手,所以我也採用 Qdrant 來進行實作。

小弟的作業系統是 Win11,Qdrant 安裝需透過 container 架起來,我是用 Ubuntu,參考 Ref [2]。之前沒接觸過 Ubuntu,也沒用過 docker,所以安裝時費了一點功夫。

臺北捷運官網有一份基北北桃都會通常見問答,後續就使用該文件,透過 Embedding 把文件向量化後,寫進 Qdrant。

PDF 切割

第一步要做的是將 pdf 內文切成小區塊(chunk),使用 langchain 來做切割

from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import CharacterTextSplitter
import re

# 將一份 pdf 切成多個 chunk
# 用 \n 分割, 每個 chunk size 上限為 256
def pdf_to_chunk(path, start_page = 1, end_page = None):
loader = PyPDFLoader(path)
pages = loader.load()
total_pages = len(pages)

text_splitter = CharacterTextSplitter(
separator = "\n",
chunk_size = 256,
chunk_overlap = 20)

if end_page is None:
end_page = len(pages)

lst_text = []
for i in range(start_page-1, end_page):
chuncks = text_splitter.split_text(pages[i].page_content)
for chunk in chuncks:
text = re.sub(r'\s+', ' ', chunk)
lst_text.append(text)

return lst_text

lst_chunk = pdf_to_chunk('基北北桃都會通常見問答.pdf')
print(lst_chunk[0])
lst_chunk[0]

Embedding

接著要將剛剛切好的每個 chunk 轉成向量,使用 Sentence-Transformers 套件,有各種 embedding 的 model 可以使用,可以參考 Ref [3],我選擇了速度快、較輕量、Performance 不錯的「all-MiniLM-L12-v2」,可以將每個 chunk 轉成 384 維的向量。

from sentence_transformers import SentenceTransformer

def chunk_to_vector(chunks): # chunks = []
model = SentenceTransformer('all-MiniLM-L12-v2')
arr_vector = model.encode(chunks)
return arr_vector # (n, 384), n 是 chunk 的數量

arr_vectors = chunk_to_vector(lst_chunk)
print(arr_vectors.shape) # (57, 384)

寫入 Qdrant

目前已經將基北北桃都會通常見問答.pdf 切成 57 個 chunk,且已經轉換成向量,接下來就是要將這些向量寫入 Qdrant,供後續使用。

from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import PointStruct

def connection(v_dim, collection): # v_dim: 向量維度, collection: 類似 DB name
client = QdrantClient("http://localhost:6333")

client.recreate_collection(
collection_name=collection,
vectors_config=models.VectorParams(
distance=models.Distance.COSINE,
size=v_dim),
optimizers_config=models.OptimizersConfigDiff(memmap_threshold=20000),
hnsw_config=models.HnswConfigDiff(on_disk=True, m=16, ef_construct=100)
)
return client

def upsert_vector(client, collection, vectors, data): # vectors = Array, data = [{}, {}, ...]
for i, vector in enumerate(vectors):
client.upsert(
collection_name=collection,
points=[PointStruct(id=i,
vector=vectors[i],
payload=data[i])]
)

lst_dic_chunk = []
for chunk in lst_chunk:
lst_dic_chunk.append({"text":chunk})

qclient = connection(v_dim=384, collection='Tpass')
upsert_vector(client=qclient, collection='Tpass', vectors=arr_vectors, data=lst_dic_chunk)

執行完成後,可以到以下路徑,就會看到 Tpass 已經被 Create 了,後續就可以開始查 Tpass 中的向量資料了!

Semantic Search

接下來關鍵的步驟是 Semantic Search,我們要從向量 DB 中查詢與「問題」最相關的內容。如前言中,我們的問題是:Tpass 月票是何時發行的。我們要從向量 DB 中找到與該問題最相關的 3 個 chuuk,最後會將這些 chunk 連同問題一併餵給 OpenAI。

from qdrant_client import QdrantClient
from qdrant_client.http import models
from sentence_transformers import SentenceTransformer
import numpy as np

qclient = QdrantClient("http://localhost:6333")
model = SentenceTransformer('all-MiniLM-L12-v2')

question = '請問台北捷運 Tpass 1200 月票是何時發行的,請用繁體中文回答'
question_emb = model.encode(question)
result = qclient.search(collection_name='Tpass', query_vector=question_emb, limit=3)

print(result)
result

問問 OpenAI

最後一步就是要將 Semantic Search 查詢回來的結果加上問題,整理一下後一併餵給 OpenAI 了!

from openai import OpenAI

def prepare_prompt(question):
prompt = ""
prompt += "請參考下列文件內容回答問題\n\n"
for i, point in enumerate(result):
prompt += "文件" + str(i+1) + "\n"
prompt += point.payload['text'] + "\n\n"

prompt += "問題:"+question
return prompt

prompt = prepare_prompt(question)
print(prompt)
prompt
client = OpenAI()

completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "你是一位聰明的 AI 助理"},
{"role": "user", "content": prompt}
]
)

answer = completion.choices[0].message.content
print(answer)
# 根據提供的文件內容,台北捷運 Tpass 1200 月票是在112年6月15日開始發行的。

登愣~最後還是答錯了XD,不過其實是因為文件中沒有很清楚的說明確切的發行時間,從找到的 3 個 chunk 中,也只有提到 7/1 會於超商販售。

以下提供幾個 Q、A 讓大家參考。由於文件中 Tpass 較常稱作「都會通」,所以用「都會通」問問題會比 Tpass 回答的較好。

Q: Tpass 可以騎 ubike 嗎
A: 根據提供的文件內容,TPASS悠遊卡可以享有YouBike站點借車前30分鐘免費的優惠,而在桃園市的YouBike站點則可享有60分鐘免費的優惠,但是YouBike 2.0E 電輔車型不適用前述優惠。TPASS悠遊卡是用來搭乘大眾運輸的卡片,並不直接提到可以用來騎YouBike,所以無法確定TPASS悠遊卡是否可以騎YouBike。如果您有更多關於TPASS悠遊卡使用YouBike的問題,建議直接向相關單位查詢以獲得確切答案。

Q: Tpass 可以搭臺鐵嗎
A: 根據提供的文件內容,Tpass 是悠遊卡的一種,可以在指定的便利商店或超商領取購卡的 100元悠遊卡加值金,並提到購買 TPASS卡且完成登錄的民眾可以領取100元加值金。然而,文件中並未提到 Tpass 可以搭乘台鐵的相關資訊,因此無法確定 Tpass 是否可以搭乘台鐵。您可以參考其他相關資訊或查詢相關單位以確認 Tpass 是否可以搭乘台鐵。

Q: 都會通可以搭臺鐵嗎
A: 根據文件4的內容,可以知道都會通可以搭乘臺鐵,但是起、訖站點均須在基北北桃四市範圍內才能啟用。如果任一起、訖站點不在範圍內,則無法啟用或使用都會通,且該趟車資將依原票價扣款。

後話

QA 的結果似乎沒有預期的好,可以再優化的地方可能是 PDF 的切割,目前是用 langchain 暴力切割;感覺如果是作有意義的切割,讓每個 chunk 都是完整的段落,效果可能會更好。

另外 Embedding 也是隨便挑一個模型「all-MiniLM-L12-v2」,或許可以試試其它模型,看會不會有更好的效果?

prompt 的部分目前是以一個 chunk 當成一個文件餵給 OpenAI,或許也可以再試試不同的 prompt 調整。

大家如果有其它更好的建議歡迎留言,或是有其它想交流的也都可以討論哦!

最後補一篇我覺得不錯的文章 Ref [4],作者有將 Semantic Search 的作法視覺化,我也是參考她的文章才完成實作的。

Refernece

[1]. https://ithelp.ithome.com.tw/users/20120030/ironman/7039?page=2

[2]. https://ithelp.ithome.com.tw/articles/10335513

[3]. https://www.sbert.net/docs/pretrained_models.html#model-overview

[4]. https://karenkaods.medium.com/%E5%B0%87rag%E8%88%87semantic-search%E8%9E%8D%E5%85%A5llm-%E6%8F%90%E5%8D%87%E6%BA%96%E7%A2%BA%E6%80%A7%E8%88%87%E6%95%88%E7%8E%87-1272759e2e19

--

--