29 October, 2024
Tự chế hệ thống RAG đơn giản
Khám phá Retrieval-Augmented Generation (RAG) và tự code ra một hệ thống từ đầu để hiểu các khái niệm cơ bản.
Gần đây, khái niệm Retrieval-Augmented Generation (RAG) trở nên khá nổi trong lĩnh vực AI cùng với các mô hình ngôn ngữ lớn (LLM). RAG kết hợp việc truy vấn thông tin với việc tạo câu trả lời, nhằm nâng cao sự chính xác của câu trả lời. Phương pháp này đã cho thấy kết quả đầy hứa hẹn trong nhiều ứng dụng như trả lời câu hỏi, hệ thống đối thoại, vv.
Trong bài viết này, chúng ta sẽ khám phá RAG và xây dựng một hệ thống RAG đơn giản bằng Python và ollama. Bài viết này sẽ giúp bạn hiểu các thành phần chính của hệ thống RAG và cách chúng có thể được triển khai bằng các khái niệm lập trình cơ bản.
RAG là gì
Để bắt đầu, hãy xem xét một hệ thống chatbot đơn giản không có RAG:
Mặc dù chatbot có thể trả lời các câu hỏi phổ biến dựa trên tập dữ liệu train, nhưng nó có thể thiếu khả năng truy cập kiến thức mới nhất hoặc kiến thức chuyên ngành.
Một ví dụ thực tế là khi bạn hỏi ChatGPT "Tên mẹ tôi là gì?". ChatGPT không thể trả lời câu hỏi này vì nó không có quyền truy cập vào kiến thức bên ngoài, chẳng hạn như thông tin về các thành viên trong gia đình bạn.
Để giải quyết hạn chế này, chúng ta cần cung cấp kiến thức bên ngoài cho mô hình (trong ví dụ này là danh sách tên các thành viên trong gia đình):
RAG là viết tắt của Retrieval-Augmented Generation, tiếng Việt thì có thể hiểu nôm na là "Tìm kiếm và tạo ra câu trả lời". Một hệ thống RAG bao gồm hai thành phần chính:
- Một mô hình truy vấn (retrieval model) lấy thông tin liên quan từ nguồn kiến thức bên ngoài, có thể là database, công cụ tìm kiếm hoặc bất kỳ kho thông tin nào khác.
- Một mô hình ngôn ngữ (language model) tạo ra câu trả lời dựa trên kiến thức được trả về từ mô hình truy vấn.
Có nhiều cách để triển khai RAG, bao gồm Graph RAG, Hybrid RAG và Hierarchical RAG. Ta sẽ thảo luận thêm ở cuối bài viết này.
RAG đơn giản
Hãy tạo một hệ thống RAG đơn giản truy vấn thông tin từ một tập dữ liệu và tạo ra câu trả lời dựa trên kiến thức được trả về. Hệ thống sẽ bao gồm các thành phần sau:
- Mô hình embedding (embedding model): Một mô hình ngôn ngữ được train để chuyển đổi văn bản đầu vào thành các embedding. Có thể hiểu embeddings là các vector nắm bắt ý nghĩa ngữ nghĩa của đoạn văn bản. Các vector này sẽ được sử dụng để tìm kiếm thông tin liên quan trong tập dữ liệu.
- Vector database: Một hệ thống lưu trữ kiến thức và các embedding vector tương ứng. Mặc dù có nhiều công nghệ vector database như Qdrant, Pinecone và pgvector, chúng ta sẽ tự chế một vector database đơn giản ở phần sau.
- Chatbot: Một mô hình ngôn ngữ tạo ra câu trả lời dựa trên kiến thức được trả về từ mô hình truy vấn. Đây có thể là bất kỳ mô hình ngôn ngữ nào, như Llama, Gemma hoặc GPT.
Giai đoạn indexing
Giai đoạn indexing (lập chỉ mục) là bước đầu tiên trong việc tạo ra một hệ thống RAG. Tại đây, ta chia nhỏ tập dữ liệu (hoặc tài liệu) thành các đoạn nhỏ (chunks) và tính toán vector biểu diễn cho mỗi chunk. Sau này, ta sẽ dùng các vector này để truy vấn thông tin liên quan.
Kích thước của mỗi chunk có thể thay đổi tùy thuộc vào tập dữ liệu và ứng dụng. Ví dụ, trong hệ thống truy vấn tài liệu, mỗi chunk có thể là một đoạn văn hoặc một câu. Trong hệ thống đối thoại, mỗi chunk có thể là một lượt đối thoại.
Sau giai đoạn lập chỉ mục, mỗi chunk cùng với vector embedding tương ứng sẽ được lưu trữ trong vector database. Đây là một ví dụ về cách vector database có thể trông như thế nào sau khi đã được index:
Chunk | Vector Embedding |
---|---|
Ý và Pháp sản xuất hơn 40% tổng lượng rượu vang trên thế giới. | [0.1, 0.04, -0.34, 0.21, ...] |
Đền Taj Mahal ở Ấn Độ được làm hoàn toàn bằng đá cẩm thạch. | [-0.12, 0.03, 0.9, -0.1, ...] |
90% lượng nước ngọt trên thế giới nằm ở Nam Cực. | [-0.02, 0.6, -0.54, 0.03, ...] |
... | ... |
Các vector embedding sau đó có thể được sử dụng để truy vấn thông tin liên quan dựa trên một truy vấn nhất định. Hãy nghĩ về nó như mệnh đề WHERE
trong SQL, nhưng thay vì truy vấn bằng cách khớp văn bản chính xác, ta có thể truy vấn một tập hợp các chunks dựa trên vector embedding của chúng.
Để so sánh độ tương đồng giữa hai vector, ta có thể sử dụng cosine similarity, khoảng cách Euclidean hoặc các phép đo khoảng cách khác. Trong ví dụ này, ta sẽ sử dụng cosine similarity. Đây là công thức cho cosine similarity giữa hai vector A và B:
Đừng lo lắng nếu bạn không hiểu công thức trên, chúng ta sẽ code lại nó trong phần tiếp theo.
Giai đoạn truy vấn
Trong sơ đồ dưới đây, mình sẽ lấy một ví dụ về Truy vấn đầu vào
từ User
. Ta cần tính toán Vector truy vấn
để biểu diễn truy vấn và so sánh nó với các vector trong database để tìm các chunk liên quan nhất.
Kết quả trả về từ Vector Database
sẽ chứa N chunks liên quan nhất đến truy vấn. Những chunk này sẽ được Chatbot
sử dụng để tạo ra câu trả lời.
Bắt tay vào lập trình!
Trong ví dụ này, mình sẽ viết một triển khai đơn giản của RAG bằng Python.
Để chạy các model, mình sẽ sử dụng ollama, một công cụ dòng lệnh cho phép bạn chạy các model từ Hugging Face. Với ollama, bạn không cần phải tạo máy chủ (server) hay truy cập API bên thứ 3. Bạn có thể chạy các model trực tiếp trên máy tính của mình.
Trong hướng dẫn này, mình sẽ sử dụng các model sau:
- Embedding model: hf.co/CompendiumLabs/bge-base-en-v1.5-gguf
- Language model: hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF
Và đối với tập dữ liệu, mình sẽ sử dụng một danh sách các sự thật thú vị về loài mèo. Mỗi câu trong file này sẽ được coi là một chunk trong giai đoạn indexing.
Tải ollama và các models
Đầu tiên, hãy bắt đầu bằng cách cài đặt ollama từ web của dự án: ollama.com
Sau khi cài đặt, mở terminal và chạy lệnh sau để tải các model:
ollama pull hf.co/CompendiumLabs/bge-base-en-v1.5-gguf
ollama pull hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF
Thế này thì có nghĩa là các model đã được tải xuống thành công:
pulling manifest
...
verifying sha256 digest
writing manifest
success
Trước khi tiếp tục, để sử dụng ollama
trong python, hãy cài đặt gói ollama
:
pip install ollama
Tải file dữ liệu
Tiếp theo, tạo một file .py
và load file chứ dữ liệu. File chứa một danh sách các sự thật thú vị về loài mèo sẽ được sử dụng làm các chunk trong giai đoạn indexing.
Bạn có thể tải xuống tập dữ liệu mẫu từ đây. Ví dụ code để load file này vào Python:
dataset = []
with open('cat-facts.txt', 'r') as file:
dataset = file.readlines()
print(f'Loaded {len(dataset)} entries')
Tự chế vector database
Bây giờ, hãy tự chế một vector database để hiểu cách nó hoạt động.
Mình sẽ sử dụng embedding model từ ollama
để chuyển đổi mỗi đoạn thành một vector embedding, sau đó lưu trữ chunk và vector tương ứng của nó trong một mảng.
Đây là một ví dụ về hàm để tính toán vector embedding cho một văn bản nhất định:
import ollama
EMBEDDING_MODEL = 'hf.co/CompendiumLabs/bge-base-en-v1.5-gguf'
LANGUAGE_MODEL = 'hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF'
# Mỗi phần tử trong VECTOR_DB sẽ là một tuple (chunk, embedding)
# Embedding là một mảng các số thực, ví dụ: [0.1, 0.04, -0.34, 0.21, ...]
VECTOR_DB = []
def add_chunk_to_database(chunk):
embedding = ollama.embed(model=EMBEDDING_MODEL, input=chunk)['embeddings'][0]
VECTOR_DB.append((chunk, embedding))
Trong ví dụ này, mình sẽ coi mỗi dòng trong tập dữ liệu là một chunk để đơn giản hóa:
for i, chunk in enumerate(dataset):
add_chunk_to_database(chunk)
print(f'Added chunk {i+1}/{len(dataset)} to the database')
Code hàm truy vấn
Tiếp theo, ta cần code một hàm để lấy một truy vấn và trả về N chunk liên quan nhất dựa trên công thức tương đồng cosine (cosine similarity). Có thể hiểu nôm na rằng chúng ta đang so sánh xem 2 vector có gần nhau không. Càng gần tức là 2 chunks đang được so sánh càng liên quan đến nhau.
Đây là một ví dụ về hàm để tính toán cosine similarity giữa hai vector:
def cosine_similarity(a, b):
dot_product = sum([x * y for x, y in zip(a, b)])
norm_a = sum([x ** 2 for x in a]) ** 0.5
norm_b = sum([x ** 2 for x in b]) ** 0.5
return dot_product / (norm_a * norm_b)
Bây giờ, hãy triển khai hàm truy vấn:
def retrieve(query, top_n=3):
query_embedding = ollama.embed(model=EMBEDDING_MODEL, input=chunk)['embeddings'][0]
# danh sách tạm thời để lưu trữ các cặp (chunk, similarity)
similarities = []
for chunk, embedding in VECTOR_DB:
similarity = cosine_similarity(query_embedding, embedding)
similarities.append((chunk, similarity))
# sắp xếp theo độ tương đồng giảm dần, vì độ tương đồng cao hơn có nghĩa là các đoạn liên quan hơn
similarities.sort(key=lambda x: x[1], reverse=True)
# cuối cùng, trả về N đoạn liên quan nhất
return similarities[:top_n]
Tạo câu trả lời
Trong giai đoạn này, chatbot sẽ tạo ra câu trả lời dựa trên kiến thức được truy vấn từ bước trên. Điều này được thực hiện bằng cách đơn giản thêm các chunk đã nhận được vào prompt sẽ được đưa vào làm đầu vào cho chatbot.
Ví dụ, một prompt có thể được xây dựng như sau:
input_query = input('Ask me a question: ')
retrieved_knowledge = retrieve(input_query)
print('Retrieved knowledge:')
for chunk, similarity in retrieved_knowledge:
print(f' - (similarity: {similarity:.2f}) {chunk}')
instruction_prompt = f'''You are a helpful chatbot.
Use only the following pieces of context to answer the question. Don't make up any new information:
{'\n'.join([f' - {chunk}' for chunk, similarity in retrieved_knowledge])}
'''
Sau đó, mình sử dụng ollama
để tạo câu trả lời. Trong ví dụ này, mình sẽ sử dụng instruction_prompt
làm hướng dẫn từ hệ thống đặt cho chatbot:
stream = ollama.chat(
model=LANGUAGE_MODEL,
messages=[
{'role': 'system', 'content': instruction_prompt},
{'role': 'user', 'content': input_query},
],
stream=True,
)
# print the response from the chatbot in real-time
print('Chatbot response:')
for chunk in stream:
print(chunk['message']['content'], end='', flush=True)
Lắp ráp tất cả lại
Bạn có thể tìm thấy mã cuối cùng trong file này. Để chạy code, download demo.py
và chạy lệnh sau:
python demo.py
Bây giờ bạn có thể đặt câu hỏi cho chatbot, và nó sẽ tạo ra câu trả lời dựa trên kiến thức được truy xuất từ tập dữ liệu.
Ví dụ ở đây, mình hỏi về tốc độ mà mèo có thể chạy:
Ask me a question: tell me about cat speed
Retrieved chunks: ...
Chatbot response:
According to the given context, cats can travel at approximately 31 mph (49 km) over a short distance. This is their top speed.
Vấn đề và giải pháp
Vậy là mình đã triển khai một hệ thống RAG đơn giản sử dụng một file dữ liệu nhỏ.
Vì là đơn giản, nên cách làm này vẫn còn nhiều hạn chế:
- Nếu câu hỏi bao gồm nhiều chủ đề cùng một lúc, hệ thống có thể không đưa ra câu trả lời tốt. Điều này là do hệ thống chỉ truy vấn các đoạn dựa trên độ tương đồng của truy vấn với các đoạn, mà không xem xét ngữ cảnh của truy vấn.
Giải pháp có thể là để chatbot tự viết truy vấn của riêng nó dựa trên đầu vào của người dùng, sau đó tìm dựa trên truy vấn mới này. Ta cũng có thể sử dụng nhiều truy vấn cùng một lúc để truy vấn thông tin liên quan hơn. - N kết quả hàng đầu được trả về dựa trên cosine similarity. Điều này có thể không phải lúc nào cũng cho kết quả tốt nhất, đặc biệt là khi mỗi đoạn chứa nhiều thông tin.
Để giải quyết vấn đề này, ta có thể sử dụng reranking models để xếp hạng lại các đoạn được truy vấn dựa trên mức độ liên quan của chúng với truy vấn. - Database được lưu trữ trong bộ nhớ RAM, không thể đáp ứng được nếu các file dữ liệu lớn. Chúng ta có thể sử dụng một vector database hiệu quả hơn như Qdrant, Pinecone, pgvector
- Hiện tại, ta coi mỗi đoạn trong file là một chunk. Đối với các tác vụ phức tạp, ta có thể cần sử dụng các kỹ thuật khác để chia file thành các chunk nhỏ hơn. Chúng ta cũng có thể xử lý trước từng chunk trước khi thêm chúng vào database.
- Mô hình ngôn ngữ được sử dụng trong ví dụ này là một mô hình đơn giản chỉ có 1 tỷ tham số. Đối với các tác vụ phức tạp hơn, chúng ta có thể cần sử dụng một mô hình ngôn ngữ lớn hơn.
Các loại RAG khác
Trong thực tế, có nhiều cách để triển khai hệ thống RAG. Đây là một số loại hệ thống RAG phổ biến:
- Graph RAG: Trong loại RAG này, nguồn kiến thức được biểu diễn dưới dạng đồ thị (graph), trong đó các nút node là các kiến thức và các cạnh là mối quan hệ giữa các kiến thứ. Hiểu nôm na nó giống một mindmap. Mô hình ngôn ngữ có thể duyệt qua đồ thị để truy vấn thông tin liên quan. Có nhiều nghiên cứu hay về loại RAG này. Đây là một vài nghiên cứu về Graph RAG.
- Hybrid RAG: một loại RAG kết hợp các kỹ thuật Đồ thị Kiến thức (Knowledge Graph - KG) và vector database để cải thiện hệ thống trả lời câu hỏi. Để biết thêm, bạn có thể đọc paper tại đây.
- Modular RAG: một loại RAG vượt ra ngoài khuôn khổ "truy vấn rồi trả lời" cơ bản, sử dụng các cơ chế định tuyến (routing), lập lịch và hợp nhất để tạo ra một khung làm việc linh hoạt và có thể cấu hình lại. Thiết kế mô-đun này cho phép nhiều loại RAG khác nhau cùng hoạt động song song, cho phép các ứng dụng đòi hỏi kiến thức phức tạp và thích ứng hơn. Để biết thêm, bạn có thể đọc paper tại đây.
Đối với các loại RAG khác, bạn có thể tham khảo bài viết này của Rajeev Sharma.
Kết luận
RAG là một bước tiến quan trọng trong việc làm cho các mô hình ngôn ngữ đưa ra câu trả lời chính xác hơn. Bằng cách tự chế một hệ thống RAG đơn giản, ta đã khám phá các khái niệm cơ bản về embedding, truy vấn và tạo. Mặc dù cách làm của chúng ta còn khá căn bản, nhưng nó minh họa các nguyên tắc cốt lõi mà các hệ thống RAG phức tạp hơn sử dụng ngoài đời thực.
Khả năng mở rộng và cải thiện hệ thống RAG là rất lớn, từ việc triển khai vector database hiệu quả hơn đến khám phá các kiến trúc tiên tiến như Graph RAG và Hybrid RAG. Khi lĩnh vực này sẽ tiếp tục phát triển, RAG sẽ vẫn là một kỹ thuật quan trọng để kết nối hệ thống AI với kiến thức bên ngoài.
Tham khảo
- https://arxiv.org/abs/2005.11401
- https://aws.amazon.com/what-is/retrieval-augmented-generation/
- https://github.com/varunvasudeva1/llm-server-docs
- https://github.com/ollama/ollama/blob/main/docs
- https://github.com/ollama/ollama-python
- https://www.pinecone.io/learn/series/rag/rerankers/
- https://arxiv.org/html/2407.21059v1
- https://newsletter.armand.so/p/comprehensive-guide-rag-implementations