Bài 37.5: Bài tập thực hành hệ thống RAG

Bài 37.5: Bài tập thực hành hệ thống RAG
Chào mừng bạn quay trở lại với chuỗi bài viết về Lập trình Web Front-end và những công nghệ đi kèm! Hôm nay, chúng ta sẽ dành thời gian để thực hành một trong những kỹ thuật AI đang rất phổ biến và mạnh mẽ: Hệ thống RAG (Retrieval Augmented Generation).
Trong bối cảnh các mô hình ngôn ngữ lớn (LLM) như GPT, Claude, Llama... ngày càng phát triển, việc tích hợp chúng vào ứng dụng web của chúng ta trở nên dễ dàng hơn. Tuy nhiên, LLM có những hạn chế cố hữu: chúng có thể "bịa chuyện" (hallucinate), kiến thức của chúng chỉ giới hạn đến thời điểm huấn luyện, và chúng không thể truy cập thông tin riêng tư, cụ thể của người dùng hoặc tổ chức bạn.
Đây chính là lúc RAG tỏa sáng. Về cơ bản, RAG giúp kết nối LLM với nguồn thông tin bên ngoài (dữ liệu của bạn, tài liệu nội bộ, thông tin thời gian thực...) để tạo ra phản hồi chính xác, dựa trên bằng chứng và phù hợp với ngữ cảnh cụ thể.
Mục tiêu của bài thực hành này không phải là xây dựng một hệ thống RAG hoàn chỉnh, sẵn sàng cho production (điều đó đòi hỏi nhiều công sức hơn), mà là để bạn hiểu rõ các thành phần chính và nắm được luồng hoạt động của một hệ thống RAG thông qua việc tự tay code một ví dụ đơn giản.
Tại sao lại cần RAG trong Web Development?
Imagine bạn đang xây dựng một trang web hỗ trợ khách hàng cho một công ty. Thay vì huấn luyện lại toàn bộ LLM trên bộ tài liệu hướng dẫn sản phẩm khổng lồ (điều này rất tốn kém và khó khăn), bạn có thể sử dụng RAG. Hệ thống RAG sẽ:
- Lấy câu hỏi của khách hàng.
- Tìm kiếm trong tài liệu hướng dẫn sản phẩm của bạn để tìm những đoạn thông tin liên quan nhất.
- Đưa những đoạn thông tin tìm được này cùng với câu hỏi ban đầu cho LLM.
- LLM sử dụng thông tin được cung cấp để tạo ra câu trả lời chính xác và đầy đủ cho khách hàng.
Kết quả? Một chatbot hỗ trợ khách hàng thông minh, trả lời đúng dựa trên tài liệu của bạn, không "nói linh tinh" và dễ dàng cập nhật kiến thức chỉ bằng cách thêm hoặc sửa đổi tài liệu nguồn. Đây chỉ là một trong rất nhiều ứng dụng tiềm năng của RAG trong phát triển web.
Các Thành phần Cốt lõi của một Hệ thống RAG (cho bài thực hành đơn giản)
Một hệ thống RAG cơ bản thường bao gồm các bước sau:
- Data Preparation (Chuẩn bị Dữ liệu):
- Thu thập các tài liệu, văn bản nguồn mà bạn muốn LLM có thể truy cập.
- Chunking: Chia các tài liệu lớn thành các đoạn nhỏ hơn (gọi là "chunks"). Điều này quan trọng vì LLM và hệ thống tìm kiếm dựa trên vector thường xử lý hiệu quả hơn với các đoạn văn bản có kích thước vừa phải.
- Embedding:
- Sử dụng một mô hình Embedding (ví dụ: Sentence-BERT, OpenAI Embeddings) để chuyển đổi mỗi "chunk" văn bản thành một vector số học (một danh sách các số). Các vector này mã hóa ý nghĩa ngữ nghĩa của đoạn văn bản.
- Các đoạn văn bản có ý nghĩa tương đồng sẽ có các vector "gần" nhau trong không gian vector.
- Vector Storage (Lưu trữ Vector):
- Lưu trữ các vector Embedding này trong một cơ sở dữ liệu chuyên biệt gọi là Vector Database (ví dụ: ChromaDB, Pinecone, Weaviate, hoặc thậm chí các thư viện tại chỗ như FAISS). Cơ sở dữ liệu này được tối ưu hóa cho việc tìm kiếm các vector "tương tự" một cách hiệu quả.
- Retrieval (Truy xuất):
- Khi nhận được câu hỏi (query) từ người dùng, chúng ta cũng chuyển đổi câu hỏi này thành một vector sử dụng cùng mô hình Embedding.
- Tìm kiếm trong Vector Database để tìm các vector văn bản (chunks) có độ tương đồng cao nhất với vector câu hỏi. Đây gọi là tìm kiếm "Nearest Neighbors" (Hàng xóm Gần nhất).
- Lấy về các đoạn văn bản (chunks) tương ứng với các vector tìm được.
- Generation (Sinh văn bản):
- Kết hợp câu hỏi ban đầu của người dùng và các đoạn văn bản liên quan vừa truy xuất được thành một prompt duy nhất.
- Đưa prompt này cho một LLM.
- LLM sử dụng thông tin trong prompt để tạo ra câu trả lời cuối cùng.
Chúng ta sẽ thực hành xây dựng luồng này bằng Python, sử dụng một vài thư viện phổ biến.
Bắt Tay vào Code: Xây dựng luồng RAG Đơn giản
Chúng ta sẽ sử dụng một số thư viện Python. Nếu bạn chưa cài đặt, hãy cài đặt chúng:
pip install langchain openai chromadb sentence-transformers pypdf
(Lưu ý: Bạn sẽ cần API Key của OpenAI hoặc sử dụng một LLM khác. Langchain hỗ trợ rất nhiều LLM và Vector DB khác nhau, đây chỉ là ví dụ)
Bước 0: Chuẩn bị Dữ liệu Nguồn
Trong thực tế, dữ liệu nguồn có thể là file PDF, trang web, cơ sở dữ liệu... Để đơn giản cho bài thực hành, chúng ta sẽ sử dụng một đoạn văn bản mẫu.
# data_source.py
text_data = """
LangChain là một framework được thiết kế để đơn giản hóa quá trình phát triển các ứng dụng sử dụng các mô hình ngôn ngữ lớn (LLM). Nó cung cấp các thành phần và công cụ để xây dựng các chuỗi xử lý (chains) phức tạp, kết nối LLM với các nguồn dữ liệu bên ngoài và các hệ thống khác.
Các thành phần chính của LangChain bao gồm:
1. Models: Giao diện chung cho các loại mô hình ngôn ngữ khác nhau.
2. Prompts: Các mẫu cho việc quản lý và tối ưu hóa các prompt cho LLM.
3. Indexes: Cấu trúc để làm việc với dữ liệu, ví dụ như trích xuất thông tin và chuẩn bị cho RAG.
4. Chains: Kết hợp nhiều thành phần lại với nhau để thực hiện một tác vụ cụ thể.
5. Agents: Sử dụng LLM để quyết định hành động nào cần thực hiện, kết nối LLM với các công cụ (tools).
ChromaDB là một cơ sở dữ liệu vector mã nguồn mở, dễ sử dụng. Nó cho phép lưu trữ và tìm kiếm các vector embeddings hiệu quả, rất phù hợp cho các ứng dụng RAG. ChromaDB có thể chạy in-memory hoặc ở chế độ client/server.
Sentence-Transformers là một thư viện Python giúp tạo ra các vector embeddings cho câu và đoạn văn. Các mô hình trong thư viện này được fine-tuned đặc biệt cho tác vụ embedding, cho kết quả tốt trong việc đo lường độ tương đồng ngữ nghĩa giữa các đoạn văn bản.
"""
Giải thích: Chúng ta định nghĩa một chuỗi văn bản (text_data
) chứa thông tin về LangChain, ChromaDB và Sentence-Transformers. Đây sẽ là "kiến thức" mà hệ thống RAG của chúng ta có thể truy cập.
Bước 1 & 2: Chunking & Embedding
Chúng ta sẽ chia đoạn văn bản thành các chunks và chuyển đổi chúng thành vector embeddings.
# rag_pipeline.py (Phần 1)
import os
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import SentenceTransformerEmbeddings
from data_source import text_data # Import dữ liệu từ file trước
# Cài đặt môi trường cho Embedding Model (nếu cần)
# os.environ["TOKENIZERS_PARALLELISM"] = "false" # Thêm dòng này nếu gặp lỗi parallelism
# 1. Chunking: Chia văn bản thành các đoạn nhỏ
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # Kích thước mỗi chunk (khoảng 200 ký tự)
chunk_overlap=40 # Lượng ký tự chồng lấn giữa các chunk
)
docs = text_splitter.create_documents([text_data])
print(f"Đã chia văn bản thành {len(docs)} chunks.")
print("-" * 20)
for i, doc in enumerate(docs[:3]): # In thử 3 chunk đầu tiên
print(f"Chunk {i+1}:\n{doc.page_content[:150]}...") # In 150 ký tự đầu
print("-" * 10)
# 2. Embedding: Chuyển chunks thành vector
# Sử dụng mô hình all-MiniLM-L6-v2 từ Sentence-Transformers
embedding_model = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
# Tạo embeddings cho các chunks
# Đây là bước chuyển đổi text -> vector
embeddings = embedding_model.embed_documents([doc.page_content for doc in docs])
print(f"\nĐã tạo {len(embeddings)} vector embeddings.")
print(f"Kích thước của mỗi vector: {len(embeddings[0])}")
Giải thích:
RecursiveCharacterTextSplitter
: Một công cụ từ LangChain giúp chia văn bản một cách thông minh, cố gắng giữ các đoạn có ý nghĩa lại với nhau.chunk_size
vàchunk_overlap
là các tham số quan trọng cần thử nghiệm để có kết quả tốt nhất.SentenceTransformerEmbeddings
: Sử dụng một mô hình từ thư việnsentence-transformers
để tạo embeddings. Mô hình"all-MiniLM-L6-v2"
là một lựa chọn phổ biến vì nó nhẹ và hiệu quả.embed_documents
: Hàm này nhận danh sách các chuỗi văn bản và trả về danh sách các vector tương ứng.
Bước 3 & 4: Lưu trữ và Truy xuất (Vector Store & Retrieval)
Chúng ta sẽ sử dụng ChromaDB (chạy in-memory cho đơn giản) để lưu trữ vector và thực hiện tìm kiếm.
# rag_pipeline.py (Phần 2)
from langchain.vectorstores import Chroma
# Tiếp tục từ đoạn code trước
# 3. Vector Storage: Lưu trữ embeddings vào Vector DB
# Sử dụng ChromaDB in-memory
vectorstore = Chroma.from_documents(
documents=docs,
embedding=embedding_model # Sử dụng lại model embedding đã tạo
)
print("\nĐã lưu trữ embeddings vào ChromaDB.")
# 4. Retrieval: Thực hiện truy xuất dựa trên câu hỏi
query = "LangChain dùng để làm gì?"
# Chuyển đổi câu hỏi thành vector
query_embedding = embedding_model.embed_query(query)
# Tìm kiếm các chunks tương đồng nhất trong Vector DB
# k=3 nghĩa là tìm 3 chunks gần nhất
retrieved_docs = vectorstore.similarity_search(query, k=3)
print(f"\nTìm kiếm cho câu hỏi: '{query}'")
print(f"Đã truy xuất được {len(retrieved_docs)} chunks liên quan:")
print("-" * 20)
for i, doc in enumerate(retrieved_docs):
print(f"Chunk {i+1}:\n{doc.page_content}")
print("-" * 10)
Giải thích:
Chroma.from_documents
: Hàm tiện lợi từ LangChain để tạo một instance ChromaDB từ danh sách các tài liệu (chunks) và mô hình embedding. Nó sẽ tự động tạo embedding và thêm vào DB.vectorstore.similarity_search
: Phương thức cốt lõi để thực hiện tìm kiếm tương đồng. Nó lấy vector củaquery
, tìm các vector gần nhất trong DB và trả về các đối tượngDocument
tương ứng.k
xác định số lượng kết quả muốn trả về.
Bạn sẽ thấy các chunks được truy xuất có chứa thông tin về định nghĩa và mục đích của LangChain, khớp với câu hỏi.
Bước 5: Generation
Bây giờ, chúng ta sẽ kết hợp câu hỏi và các chunks truy xuất được để tạo ra câu trả lời bằng LLM.
# rag_pipeline.py (Phần 3)
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema import StrOutputParser
# Tiếp tục từ đoạn code trước
# 5. Generation: Sử dụng LLM và các chunks truy xuất được để tạo câu trả lời
# Định nghĩa prompt template
template = """
Bạn là một trợ lý AI hữu ích.
Sử dụng các đoạn thông tin sau để trả lời câu hỏi của người dùng.
Nếu bạn không biết câu trả lời dựa trên thông tin được cung cấp, hãy nói rằng bạn không có đủ thông tin.
Tuyệt đối không bịa đặt thông tin.
Thông tin liên quan:
{context}
Câu hỏi:
{question}
Trả lời:
"""
prompt = ChatPromptTemplate.from_template(template)
# Khởi tạo LLM (Ví dụ: OpenAI GPT-3.5 Turbo)
# Đảm bảo bạn đã thiết lập biến môi trường OPENAI_API_KEY
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.5)
# Xây dựng chuỗi xử lý RAG (sử dụng LangChain Expression Language - LCEL)
# Đây là cách kết hợp các bước Retrieval và Generation
rag_chain = (
{"context": vectorstore.as_retriever(k=3), "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Chạy chuỗi xử lý với câu hỏi
response = rag_chain.invoke(query)
print("\n" + "=" * 30)
print("Câu trả lời từ hệ thống RAG:")
print(response)
print("=" * 30)
# Thử với một câu hỏi khác không có trong nguồn
query_unknown = "Thủ đô của Việt Nam là gì?"
print(f"\nTìm kiếm cho câu hỏi: '{query_unknown}'")
response_unknown = rag_chain.invoke(query_unknown)
print("\n" + "=" * 30)
print("Câu trả lời cho câu hỏi không có trong nguồn:")
print(response_unknown)
print("=" * 30)
Giải thích:
ChatOpenAI
: Khởi tạo một instance của mô hình chat OpenAI thông qua LangChain. Bạn cần cóOPENAI_API_KEY
trong biến môi trường.ChatPromptTemplate
: Định nghĩa cấu trúc của prompt sẽ gửi đến LLM. Chúng ta có các placeholder{context}
(chứa các chunks truy xuất được) và{question}
(câu hỏi ban đầu). Prompt này hướng dẫn LLM sử dụngcontext
để trả lờiquestion
.RunnablePassthrough
: Một thành phần của LCEL, cho phép truyền input (trong trường hợp này là câu hỏi) trực tiếp qua một phần của chuỗi.vectorstore.as_retriever(k=3)
: Chuyển đối tượng vectorstore thành mộtRetriever
có thể tích hợp vào chuỗi xử lý. Nó sẽ tự động thực hiện tìm kiếmsimilarity_search
vớik=3
khi được gọi.StrOutputParser
: Đảm bảo output từ LLM (có thể là một đối tượng tin nhắn) được chuyển đổi thành chuỗi đơn giản.|
: Toán tử trong LCEL, dùng để kết nối các thành phần lại với nhau thành một chuỗi xử lý tuần tự. Luồng ở đây là:question
->Retrieval
(lấy context) ->{context}
và{question}
đi vàoprompt
->prompt
đi vàollm
->llm
tạo ra output ->output
được parse thành chuỗi.rag_chain.invoke(query)
: Chạy toàn bộ chuỗi xử lý với câu hỏiquery
.
Khi bạn chạy đoạn code này (sau khi đã có API Key), bạn sẽ thấy:
- Với câu hỏi về LangChain, LLM sẽ đưa ra câu trả lời dựa trên nội dung từ
text_data
mà nó nhận được qua bước Retrieval. - Với câu hỏi về thủ đô Việt Nam (không có trong
text_data
), LLM sẽ trả lời rằng nó không có đủ thông tin (nhờ vào hướng dẫn trong prompt template), thay vì bịa ra thông tin hoặc trả lời sai dựa trên kiến thức chung của nó.
Thách thức và Mở rộng
Bài thực hành này chỉ là bước khởi đầu. Một hệ thống RAG thực tế sẽ phức tạp hơn nhiều:
- Nguồn dữ liệu đa dạng: Xử lý PDF, Docx, HTML, JSON...
- Chunking nâng cao: Các kỹ thuật chia chunk tối ưu hơn để giữ ngữ cảnh tốt hơn.
- Mô hình Embedding: Lựa chọn mô hình phù hợp với ngôn ngữ và lĩnh vực dữ liệu của bạn.
- Vector Database: Chọn DB phù hợp với quy mô dữ liệu, hiệu suất và tính năng (ví dụ: lọc metadata).
- Kỹ thuật Retrieval: Không chỉ
similarity_search
đơn thuần, còn có các kỹ thuật truy xuất khác nhưMMR
(Maximum Marginal Relevance), sử dụng metadata, kết hợp tìm kiếm từ khóa. - Prompt Engineering: Tối ưu hóa prompt template để LLM sử dụng thông tin truy xuất hiệu quả nhất.
- Đánh giá: Làm thế nào để đo lường chất lượng của hệ thống RAG?
- Production-Ready: Xây dựng API (dùng Flask, FastAPI, Node.js...) để frontend có thể gọi, quản lý state, caching, scaling...
Việc kết nối hệ thống RAG backend này với frontend (React, NextJs) thường thông qua các API endpoint. Frontend gửi câu hỏi đến backend API, backend thực hiện luồng RAG và trả về câu trả lời dưới dạng JSON.
Chúc mừng bạn đã hoàn thành bài thực hành về hệ thống RAG! Đây là một kỹ năng cực kỳ giá trị trong thế giới AI và Lập trình Web hiện đại.
Comments