# Xây Dựng RAG Đơn Giản Từ Đầu ## Tổng Quan Kiến Trúc ``` Tài liệu → Chunking → Embedding → Vector Store ↓ Câu hỏi → Embedding → Tìm kiếm tương đồng → Context → LLM → Câu trả lời ``` ## Cài Đặt Thư Viện ```bash pip install numpy openai tiktoken ``` ## Code Hoàn Chỉnh ```python import numpy as np import json import os from typing import List, Dict, Tuple from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) # ============================================================ # 1. CHUNKING - Chia nhỏ tài liệu # ============================================================ def chunk_text(text: str, chunk_size: int = 200, overlap: int = 50) -> List[str]: """ Chia văn bản thành các đoạn nhỏ có overlap. Args: text: Văn bản đầu vào chunk_size: Số từ mỗi chunk overlap: Số từ overlap giữa các chunk """ words = text.split() chunks = [] start = 0 while start < len(words): end = start + chunk_size chunk = " ".join(words[start:end]) chunks.append(chunk) # Di chuyển với overlap start += chunk_size - overlap if start >= len(words): break return chunks # ============================================================ # 2. EMBEDDING - Chuyển text thành vector # ============================================================ def get_embedding(text: str, model: str = "text-embedding-3-small") -> List[float]: """Tạo embedding vector cho một đoạn text.""" text = text.replace("\n", " ") response = client.embeddings.create(input=[text], model=model) return response.data[0].embedding def get_embeddings_batch(texts: List[str], model: str = "text-embedding-3-small") -> List[List[float]]: """Tạo embeddings cho nhiều text cùng lúc (hiệu quả hơn).""" texts = [t.replace("\n", " ") for t in texts] response = client.embeddings.create(input=texts, model=model) return [item.embedding for item in response.data] # ============================================================ # 3. VECTOR STORE - Lưu trữ và tìm kiếm vector # ============================================================ class SimpleVectorStore: """ Vector store đơn giản sử dụng numpy. Trong thực tế có thể dùng: Pinecone, Weaviate, ChromaDB, FAISS... """ def __init__(self): self.vectors: List[List[float]] = [] self.documents: List[str] = [] self.metadata: List[Dict] = [] def add_documents(self, chunks: List[str], embeddings: List[List[float]], metadata: List[Dict] = None): """Thêm tài liệu và embeddings vào store.""" self.documents.extend(chunks) self.vectors.extend(embeddings) if metadata: self.metadata.extend(metadata) else: self.metadata.extend([{}] * len(chunks)) print(f"✅ Đã thêm {len(chunks)} chunks vào vector store") print(f"📚 Tổng số chunks: {len(self.documents)}") def cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float: """Tính cosine similarity giữa 2 vector.""" v1 = np.array(vec1) v2 = np.array(vec2) dot_product = np.dot(v1, v2) norm1 = np.linalg.norm(v1) norm2 = np.linalg.norm(v2) if norm1 == 0 or norm2 == 0: return 0.0 return dot_product / (norm1 * norm2) def search(self, query_embedding: List[float], top_k: int = 3) -> List[Tuple[str, float, Dict]]: """ Tìm kiếm top_k chunks tương đồng nhất. Returns: List of (document, similarity_score, metadata) """ if not self.vectors: return [] # Tính similarity với tất cả vectors similarities = [ self.cosine_similarity(query_embedding, vec) for vec in self.vectors ] # Lấy top_k indices có similarity cao nhất top_indices = np.argsort(similarities)[::-1][:top_k] results = [] for idx in top_indices: results.append(( self.documents[idx], similarities[idx], self.metadata[idx] )) return results def save(self, filepath: str): """Lưu vector store ra file.""" data = { "vectors": self.vectors, "documents": self.documents, "metadata": self.metadata } with open(filepath, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) print(f"💾 Đã lưu vector store tại: {filepath}") def load(self, filepath: str): """Tải vector store từ file.""" with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) self.vectors = data["vectors"] self.documents = data["documents"] self.metadata = data["metadata"] print(f"📂 Đã tải {len(self.documents)} chunks từ: {filepath}") # ============================================================ # 4. RAG PIPELINE - Kết hợp tất cả lại # ============================================================ class RAGPipeline: """Pipeline RAG hoàn chỉnh.""" def __init__(self, chunk_size: int = 200, overlap: int = 50, top_k: int = 3): self.vector_store = SimpleVectorStore() self.chunk_size = chunk_size self.overlap = overlap self.top_k = top_k # --- INDEXING PHASE --- def index_document(self, text: str, source: str = "unknown"): """ Xử lý và index một tài liệu. Args: text: Nội dung tài liệu source: Tên/nguồn tài liệu """ print(f"\n📄 Đang index tài liệu: {source}") # Bước 1: Chunking chunks = chunk_text(text, self.chunk_size, self.overlap) print(f"✂️ Đã chia thành {len(chunks)} chunks") # Bước 2: Tạo embeddings print("🔢 Đang tạo embeddings...") embeddings = get_embeddings_batch(chunks) # Bước 3: Lưu vào vector store metadata = [{"source": source, "chunk_id": i} for i in range(len(chunks))] self.vector_store.add_documents(chunks, embeddings, metadata) def index_documents(self, documents: Dict[str, str]): """Index nhiều tài liệu cùng lúc.""" for source, text in documents.items(): self.index_document(text, source) # --- RETRIEVAL PHASE --- def retrieve(self, query: str) -> List[Tuple[str, float, Dict]]: """ Tìm kiếm các chunks liên quan đến query. Returns: List of (chunk, score, metadata) """ # Tạo embedding cho query query_embedding = get_embedding(query) # Tìm kiếm trong vector store results = self.vector_store.search(query_embedding, self.top_k) return results # --- GENERATION PHASE --- def generate(self, query: str, context_chunks: List[Tuple[str, float, Dict]]) -> str: """ Tạo câu trả lời dựa trên query và context. Args: query: Câu hỏi của người dùng context_chunks: Các chunks liên quan từ retrieval """ # Xây dựng context từ các chunks context_parts = [] for i, (chunk, score, meta) in enumerate(context_chunks, 1): source = meta.get("source", "unknown") context_parts.append(f"[Nguồn {i}: {source} | Độ liên quan: {score:.3f}]\n{chunk}") context = "\n\n---\n\n".join(context_parts) # Tạo prompt system_prompt = """Bạn là trợ lý AI hữu ích. Hãy trả lời câu hỏi DỰA TRÊN context được cung cấp. Quy tắc: - Chỉ sử dụng thông tin từ context - Nếu context không đủ thông tin, hãy nói rõ - Trả lời ngắn gọn, chính xác - Trích dẫn nguồn khi cần thiết""" user_prompt = f"""Context: {context} Câu hỏi: {query} Trả lời:""" # Gọi LLM response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.1, # Thấp để câu trả lời nhất quán max_tokens=500 ) return response.choices[0].message.content # --- FULL RAG QUERY --- def query(self, question: str, verbose: bool = True) -> str: """ Thực hiện full RAG pipeline: Retrieve → Augment → Generate. Args: question: Câu hỏi của người dùng verbose: Hiển thị thông tin chi tiết """ if verbose: print(f"\n{'='*60}") print(f"❓ Câu hỏi: {question}") print(f"{'='*60}") # Bước 1: RETRIEVE if verbose: print("\n🔍 Đang tìm kiếm context liên quan...") relevant_chunks = self.retrieve(question) if verbose: print(f"\n📋 Top {len(relevant_chunks)} chunks liên quan:") for i, (chunk, score, meta) in enumerate(relevant_chunks, 1): print(f"\n [{i}] Score: {score:.4f} | Source: {meta.get('source', 'N/A')}") print(f" {chunk[:100]}...") # Bước 2: AUGMENT + GENERATE if verbose: print("\n🤖 Đang tạo câu trả lời...") answer = self.generate(question, relevant_chunks) if verbose: print(f"\n💬 Câu trả lời:\n{answer}") print(f"\n{'='*60}") return answer # ============================================================ # 5. DEMO - Chạy thử # ============================================================ def main(): # Dữ liệu mẫu documents = { "AI_History.txt": """ Trí tuệ nhân tạo (AI) được Alan Turing đề xuất lần đầu vào năm 1950 với bài báo "Computing Machinery and Intelligence". Ông đặt ra câu hỏi "Máy móc có thể suy nghĩ không?" và đề xuất Turing Test. Năm 1956, John McCarthy tổ chức Hội nghị Dartmouth - sự kiện đánh dấu sự ra đời chính thức của AI như một lĩnh vực nghiên cứu. McCarthy cũng là người đặt ra thuật ngữ "Artificial Intelligence". Giai đoạn 1956-1974 được gọi là "Mùa xuân AI" với nhiều tiến bộ vượt bậc. Tuy nhiên, sau đó AI trải qua "Mùa đông AI" (1974-1980) do thiếu tài nguyên tính toán và kỳ vọng quá cao. Deep Learning bùng nổ từ năm 2012 khi AlexNet giành chiến thắng tại ImageNet competition với độ chính xác vượt trội. Đây là bước ngoặt quan trọng cho Computer Vision và AI hiện đại. """, "LLM_Overview.txt": """ Large Language Models (LLM) là các mô hình ngôn ngữ lớn được huấn luyện trên lượng dữ liệu văn bản khổng lồ. GPT (Generative Pre-trained Transformer) của OpenAI là một trong những LLM nổi tiếng nhất. GPT-3 ra mắt năm 2020 với 175 tỷ tham số, tạo ra cuộc cách mạng trong xử lý ngôn ngữ tự nhiên. ChatGPT ra mắt tháng 11/2022 đã thu hút 100 triệu người dùng chỉ trong 2 tháng. Transformer architecture, được giới thiệu trong paper "Attention Is All You Need" năm 2017 bởi nhóm Google Brain, là nền tảng của hầu hết các LLM hiện đại. RAG (Retrieval-Augmented Generation) là kỹ thuật kết hợp LLM với cơ sở kiến thức bên ngoài, giúp mô hình trả lời chính xác hơn và giảm hallucination. """, "Vietnam_Tech.txt": """ Việt Nam đang nổi lên như một trung tâm công nghệ tại Đông Nam Á. Năm 2023, Việt Nam có hơn 4000 doanh nghiệp công nghệ thông tin. VinAI Research là tổ chức nghiên cứu AI hàng đầu Việt Nam, được thành
Gần đây, **Retrieval-Augmented Generation (RAG)** đã nổi lên như một mô hình mạnh mẽ trong lĩnh vực AI và Mô hình Ngôn ngữ Lớn (LLMs). RAG kết hợp truy xuất thông tin với sinh văn bản để nâng cao hiệu suất của các mô hình ngôn ngữ bằng cách tích hợp các nguồn kiến thức bên ngoài. Phương pháp này đã cho thấy kết quả đầy hứa hẹn trong nhiều ứng dụng khác nhau, chẳng hạn như hỏi đáp, hệ thống hội thoại và sinh nội dung.
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 từ đầu bằng **Python** và **ollama**. Dự án 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.
Để 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 thông thường dựa trên tập dữ liệu huấn luyện, nhưng nó có thể thiếu quyền truy cập vào 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à 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):
Một hệ thống RAG bao gồm hai thành phần chính:
Có một số cách để triển khai RAG, bao gồm Graph RAG, Hybrid RAG và Hierarchical RAG, mà chúng ta sẽ thảo luận ở cuối bài viết này.
Hãy cùng tạo một hệ thống RAG đơn giản có khả năng truy xuất thông tin từ một tập dữ liệu được định nghĩa sẵn và sinh ra các phản hồi dựa trên kiến thức đã truy xuất được. Hệ thống sẽ bao gồm các thành phần sau:
Giai đoạn lập chỉ mục là bước đầu tiên trong việc tạo một hệ thống RAG. Nó bao gồm việc chia tập dữ liệu (hoặc các tài liệu) thành các **chunk** nhỏ và tính toán biểu diễn vector cho mỗi chunk để có thể tìm kiếm hiệu quả trong quá trình sinh văn bản.
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 xuất 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 hội thoại, mỗi chunk có thể là một lượt hộ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 cơ sở dữ liệu vector. Dưới đây là ví dụ về cách cơ sở dữ liệu vector có thể trông như thế nào sau khi lập chỉ mục:
Các vector embedding sau đó có thể được sử dụng để truy xuất thông tin liên quan dựa trên một truy vấn cho trước. Hãy nghĩ về nó như mệnh đề `WHERE` trong SQL, nhưng thay vì truy vấn bằng khớp văn bản chính xác, chúng ta có thể truy vấn một tập hợp các chunk dựa trên biểu diễn vector của chúng.
Để so sánh độ tương đồng giữa hai vector, chúng ta có thể sử dụng cosine similarity, khoảng cách Euclidean hoặc các độ đo khoảng cách khác. Trong ví dụ này, chúng ta sẽ sử dụng cosine similarity. Dưới đây là công thức tính cosine similarity giữa hai vector A và B:
Đừng lo lắng nếu bạn chưa quen với công thức trên, chúng ta sẽ triển khai nó trong phần tiếp theo.
Trong sơ đồ bên dưới, chúng ta sẽ lấy ví dụ về một **Truy vấn Đầu vào** từ **Người dùng**. Sau đó chúng ta tính toán **Vector Truy vấn** để biểu diễn truy vấn đó, và so sánh
so sánh với các vector trong cơ sở dữ liệu để tìm các đoạn văn bản liên quan nhất.
Kết quả trả về từ **Cơ sở dữ liệu Vector** sẽ chứa N đoạn văn bản liên quan nhất với câu truy vấn. Các đoạn này sẽ được **Chatbot** sử dụng để tạo ra phản hồi.
Trong ví dụ này, chúng ta sẽ viết một triển khai RAG đơn giản bằng **Python**.
Để chạy các mô hình, chúng ta sẽ sử dụng **ollama**, một công cụ dòng lệnh cho phép bạn chạy các mô hình từ Hugging Face. Với ollama, bạn **không cần** phải có quyền truy cập vào máy chủ hay dịch vụ đám mây để chạy các mô hình. Bạn có thể chạy các mô hình trực tiếp trên máy tính của mình.
Về các mô hình, hãy sử dụng những mô hình sau:
Còn về tập dữ liệu, chúng ta sẽ sử dụng **một danh sách các sự thật đơn giản về mèo**. Mỗi sự thật sẽ được coi là một đoạn văn bản trong giai đoạn lập chỉ mục.
Đầu tiên, hãy bắt đầu bằng cách cài đặt ollama từ trang web của dự án: **ollama.com**
Sau khi cài đặt xong, mở terminal và chạy lệnh sau để tải xuống các mô hình cần thiết:
Nếu bạn thấy kết quả đầu ra như sau, điều đó có nghĩa là các mô hình đã được tải xuống thành công:
Trước khi tiếp tục, để sử dụng **ollama** trong Python, hãy cài đặt thêm gói **ollama**:
Tiếp theo, tạo một script Python và tải tập dữ liệu vào bộ nhớ. Tập dữ liệu chứa danh sách các sự thật về mèo sẽ được dùng làm các đoạn văn bản trong giai đoạn lập chỉ mục.
Bạn có thể tải tập dữ liệu mẫu **tại đây**. Dưới đây là ví dụ code để tải tập dữ liệu:
Bây giờ, hãy triển khai cơ sở dữ liệu vector.
Chúng ta sẽ sử dụng mô hình embedding từ **ollama** để chuyển đổi mỗi đoạn văn bản thành một vector embedding, sau đó lưu trữ đoạn văn bản và vector tương ứng của nó vào một danh sách.
Dưới đây là ví dụ hàm tính vector embedding cho một văn bản cho trước:
Trong ví dụ này, để đơn giản, chúng ta sẽ coi mỗi dòng trong tập dữ liệu là một đoạn văn bản.
Tiếp theo, hãy triển khai hàm truy xuất nhận vào một câu truy vấn và trả về N đoạn văn bản liên quan nhất dựa trên độ tương đồng cosine. Chúng ta có thể hình dung rằng độ tương đồng cosine giữa hai vector càng cao thì chúng càng "gần nhau" trong không gian vector. Điều này có nghĩa là chúng càng giống nhau về mặt ngữ nghĩa.
Dưới đây là ví dụ hàm tính độ tương đồng cosine giữa hai vector:
Bây giờ, hãy triển khai hàm truy xuất:
Trong giai đoạn này, chatbot sẽ tạo ra phản hồi dựa trên kiến thức được truy xuất từ bước trên. Điều này được thực hiện đơn giản bằng cách thêm các đoạn văn bản vào prompt sẽ được dùng làm đầu vào cho chatbot.
Ví dụ, một prompt có thể được xây dựng như sau:
Sau đó chúng ta sử dụng **ollama** để tạo ra phản hồi. Trong ví dụ này, chúng ta sẽ dùng **instruction_prompt** làm system message:
Bạn có thể tìm thấy code hoàn chỉnh **trong file này**. Để chạy code, hãy lưu nó vào file tên **demo.py** và chạy lệnh sau:
Bây giờ bạn có thể đặt câu hỏi cho chatbot, và nó sẽ tạo ra các phản hồi dựa trên kiến thức được truy xuất từ tập dữ liệu.
Cho đến nay, chúng ta đã triển khai một hệ thống RAG đơn giản sử dụng tập dữ liệu nhỏ. Tuy nhiên, vẫn còn nhiều hạn chế:
Trong thực tế, có nhiều cách để triển khai hệ thống RAG. Dưới đây là một số loại hệ thống RAG phổ biến:
Để tìm hiểu về các loại RAG khác, bạn có thể tham khảo **bài viết này của R**
Rajeev Sharma.
RAG đại diện cho một bước tiến đáng kể trong việc làm cho các mô hình ngôn ngữ trở nên hiểu biết hơn và chính xác hơn. Bằng cách triển khai một hệ thống RAG đơn giản từ đầu, chúng ta đã khám phá các khái niệm cơ bản về embedding, truy xuất và sinh văn bản. Mặc dù cách triển khai của chúng ta còn ở mức cơ bản, nhưng nó minh họa được các nguyên tắc cốt lõi vận hành các hệ thống RAG phức tạp hơn được sử dụng trong môi trường sản xuất.
Khả năng mở rộng và cải thiện các hệ thống RAG là rất lớn, từ việc triển khai các cơ sở dữ liệu vector hiệu quả hơn đến khám phá các kiến trúc nâng cao như Graph RAG và Hybrid RAG. Khi lĩnh vực này tiếp tục phát triển, RAG vẫn là một kỹ thuật quan trọng để tăng cường các hệ thống AI với kiến thức bên ngoài trong khi vẫn duy trì khả năng sinh văn bản của chúng.
Cảm ơn vì đã tạo ra bài viết này, nó thực sự dễ theo dõi và thân thiện với người mới bắt đầu.
Trong đoạn code này, input= chunk nên được đổi thành input= query
Tôi không nghĩ vậy, vì các chunk đã có embedding trong cơ sở dữ liệu vector rồi. Phần này thực hiện embed query để có thể tính toán độ tương đồng với các mục trong cơ sở dữ liệu.
Cảm ơn rất nhiều vì đã đăng tải bài viết này! Là một sinh viên AI, tôi rất thích sự kết hợp giữa kiến thức lý thuyết và triển khai thực tế — thật sự rất thân thiện với người mới bắt đầu 😁
Bạn có nghĩ RAG có tương lai tốt không, tôi nghĩ nó giống như một bong bóng -- theo quan điểm lý thuyết
Gần đây tôi đã đọc bài đăng blog sâu sắc của bạn về Retrieval-Augmented Generation (RAG) trên Hugging Face, và tôi thấy nó rất liên quan đến hướng nghiên cứu hiện tại của mình.
Tôi đang thực hiện một dự án liên quan đến việc tự động sinh code từ các sơ đồ lớp UML, và tôi đang khám phá việc tích hợp các Mô hình Ngôn ngữ Lớn (LLM) vào quy trình này. Cụ thể, tôi đang xem xét việc áp dụng kỹ thuật RAG hoặc Low-Rank Adaptation (LoRA) để nâng cao chất lượng sinh code và khả năng thích ứng.
Bạn có sẵn lòng xem qua chủ đề nghiên cứu của tôi và đưa ra ý kiến về tính liên quan và khả thi của nó trong bối cảnh RAG hoặc LoRA không? Tôi rất mong


