Giới Thiệu
Trong thế giới ứng dụng AI, khả năng trò chuyện với dữ liệu của chính bạn là một bước đột phá. Hãy tưởng tượng một trợ lý không chỉ dựa vào kiến thức đã được huấn luyện mà còn có thể học ngay từ các tài liệu bạn cung cấp. Đó chính là sức mạnh của Retrieval Augmented Generation (RAG), và hôm nay chúng ta sẽ xây dựng một ứng dụng web hoàn chỉnh mang lại khái niệm này vào cuộc sống.
Hướng dẫn này sẽ hướng dẫn bạn phát triển "Chat RAG", một ứng dụng full-stack với frontend sử dụng React/Typescript và backend sử dụng Python/FastAPI. Chúng ta sẽ đề cập đến mọi thứ từ thiết lập kiến trúc đến triển khai sản phẩm cuối cùng. Mã nguồn sẽ được cung cấp trên GitHub.
Repo: Chat-RAG-Assistant
Nội Dung Chúng Ta Xây Dựng
Ứng dụng của chúng ta sẽ cho phép người dùng:
- Tải lên các tài liệu của họ (PDF, DOCX, TXT, v.v.).
- Đặt câu hỏi trong giao diện trò chuyện theo thời gian thực.
- Nhận câu trả lời được tạo ra bởi một LLM, dựa trên ngữ cảnh của các tài liệu đã tải lên.
- Thấy được những phần nào của tài liệu đã được sử dụng để tạo ra câu trả lời.
Kết Quả Cuối Cùng
Hãy bắt đầu nào!
Sơ Đồ Kiến Trúc
Kiến trúc của chúng ta được thiết kế để có thể mở rộng, bảo trì và mô-đun.
Điều Kiện Tiên Quyết
Trước khi viết bất kỳ dòng mã nào, hãy đảm bảo rằng bạn đã cài đặt những thứ sau:
- NodeJS (v18 trở lên)
- Python (v3.12 trở lên)
- PostgreSQL (v13 trở lên)
- Docker (tùy chọn, nhưng rất được khuyến nghị để dễ dàng thiết lập)
Cấu Trúc Dự Án
Chúng ta sẽ tổ chức monorepo của mình một cách hợp lý:
Chat_RAG/
├── backend/ # Ứng dụng backend FastAPI
├── frontend/ # Ứng dụng frontend React
├── docs/ # Tài liệu, bao gồm hướng dẫn triển khai
├── scripts/ # Các script trợ giúp cho thiết lập và thực thi
└── docker-compose.yml # Định nghĩa cấu hình đa container
Quan Trọng: Tạo Cơ Sở Dữ Liệu
Trước khi bắt đầu backend, hãy đảm bảo rằng cơ sở dữ liệu mục tiêu (ví dụ: "chatrag_db" cho PostgreSQL) đã tồn tại. Backend sẽ tự động tạo tất cả các bảng cần thiết khi khởi động nếu cơ sở dữ liệu tồn tại và người dùng đã cấu hình có quyền truy cập. Tuy nhiên, nó sẽ không tự tạo cơ sở dữ liệu.
Để tạo cơ sở dữ liệu thủ công, hãy sử dụng công cụ quản lý cơ sở dữ liệu của bạn hoặc chạy:
CREATE DATABASE chatrag_db;
Sau khi cơ sở dữ liệu được tạo, bạn có thể khởi động backend và nó sẽ tự động xử lý việc tạo bảng.
Thiết Lập Máy Chủ FastAPI
FastAPI cung cấp cho chúng ta một framework vững chắc để xây dựng API của mình. Điểm vào là "backend/app/main.py".
Chúng ta cấu trúc ứng dụng thành các thư mục khác nhau:
Dịch Vụ RAG: Từ Tài Liệu đến Câu Trả Lời
Cốt lõi của ứng dụng của chúng ta là pipeline RAG, mà chúng ta sẽ xây dựng trong thư mục "backend/app/services/". Đây là cách hoạt động:
Tiếp Nhận Tài Liệu (document_processor.py)
Khi người dùng tải lên một tệp, chúng ta không thể chỉ đơn giản cho toàn bộ vào LLM. Chúng ta cần phải chia nhỏ nó.
- Tải: Sử dụng các bộ tải tài liệu của LangChain (PyPDFLoader, Docx2txtLoader, v.v.) để đọc nội dung.
- Chia: Văn bản được chia thành các "khối" nhỏ hơn, dễ quản lý hơn bằng RecursiveCharacterTextSplitter. Điều này đảm bảo rằng chúng ta có thể tìm thấy những thông tin rất cụ thể.
python
class DocumentProcessor:
def __init__(self):
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", " ", ""],
length_function=len,
)
async def save_uploaded_file(self, file_content: bytes, filename: str) -> str:
unique_filename = f"{uuid.uuid4()}{Path(filename).suffix}"
file_path = self.upload_dir / unique_filename
async with aiofiles.open(file_path, 'wb') as f:
await f.write(file_content)
return str(file_path)
Nhúng và Lưu Trữ Vectơ (vector_store.py)
Tiếp theo, chúng ta cần chuyển các khối văn bản này thành một định dạng mà máy có thể hiểu và so sánh: vectơ số, hay "embeddings".
- Nhúng: Chúng ta sử dụng mô hình nhúng, như OpenAI's "text-embedding-ada-002", để tạo vectơ cho mỗi khối văn bản.
- Lưu trữ: Những vectơ này, cùng với các khối văn bản tương ứng của chúng, được lưu trữ trong ChromaDB.
python
class VectorStoreService:
def __init__(self):
self.embeddings = OpenAIEmbeddings(api_key=settings.openai_api_key,
model=settings.embedding_model)
self.vector_store = Chroma(
persist_directory=settings.chroma_persist_directory,
embedding_function=self.embeddings,
collection_name="documents",
)
async def add_documents(self, documents: List[Document], document_id: int, document_filename: str) -> str:
for i, doc in enumerate(documents):
doc.metadata.update({
"document_id": document_id,
"document_filename": document_filename,
"chunk_index": i,
"chunk_id": f"{document_id}_{i}",
})
ids = [f"{document_id}_{i}" for i in range(len(documents))]
self.vector_store.add_documents(documents, ids=ids)
self.vector_store.persist()
Vòng Lặp Truy Xuất và Tạo Câu Trả Lời (rag_service.py)
Đây là nơi chúng ta trả lời câu hỏi của người dùng.
- Nhận Câu Hỏi: Người dùng gửi một tin nhắn qua trò chuyện.
- Truy Xuất Ngữ Cảnh: Chúng ta lấy câu hỏi của người dùng, tạo một vectơ cho nó và sử dụng điều đó để tìm kiếm trong ChromaDB. Cơ sở dữ liệu trả về các khối văn bản liên quan nhất từ các tài liệu. Đây chính là "Truy Xuất" trong RAG.
- Tăng Cường Lời Nhắc: Chúng ta xây dựng một lời nhắc chi tiết cho LLM. Lời nhắc này bao gồm câu hỏi gốc của người dùng và các khối văn bản đã được truy xuất như ngữ cảnh.
- Tạo Câu Trả Lời: Lời nhắc hoàn chỉnh này được gửi tới LLM (ví dụ: "gpt-5-nano-2025-08-07"). Mô hình tạo ra một câu trả lời dựa trên ngữ cảnh đã cung cấp. Đây là "Tạo Dựng" trong RAG.
python
class RAGService:
async def process_document_pipeline(self, document_model: DocumentModel, db: Session) -> Dict[str, Any]:
document_model.processing_status = ProcessingStatus.PROCESSING
db.commit()
processing_result = await self.document_processor.process_document(document_model.file_path, document_model)
vector_store_id = await self.vector_store.add_documents(
documents=processing_result['chunks'],
document_id=document_model.id,
document_filename=document_model.original_filename,
)
return {"vector_store_id": vector_store_id, **processing_result}
Trò Chuyện Thời Gian Thực với WebSockets
Để tạo ra một trải nghiệm mượt mà như ChatGPT, chúng ta sử dụng WebSockets. Điểm cuối trong "api/routes/chat.py" xử lý kết nối. Khi người dùng gửi một tin nhắn, nó được chuyển tới rag_service, và phản hồi được tạo sẽ được phát lại từng token một qua cùng một kết nối WebSocket. Điều này cung cấp phản hồi tức thì cho người dùng.
Chúng ta sẽ sử dụng React, TypeScript và Vite để xây dựng một giao diện người dùng sạch sẽ và phản hồi. Tôi sẽ không đi sâu vào phần Frontend; bạn có thể kiểm tra Repo trên GitHub và xem mã nguồn nếu bạn quan tâm.
Docker Hóa cho Tính Tương Thích và Triển Khai
Để đảm bảo ứng dụng của chúng ta chạy giống nhau ở mọi nơi, chúng ta sử dụng Docker. Tệp "docker-compose.yml" của chúng ta định nghĩa hai dịch vụ chính: "backend" và "frontend".
- Backend: Xây dựng hình ảnh Docker từ "backend/Dockerfile", mở cổng và thiết lập các biến môi trường cần thiết cho URL cơ sở dữ liệu, khóa API, v.v.
- Dịch vụ Frontend: Xây dựng từ "frontend/Dockerfile" và khởi động máy chủ phát triển Vite.
- Volumes: Chúng ta định nghĩa một volume được đặt tên "chroma_db" để duy trì cơ sở dữ liệu vectơ trên đĩa. Điều này đảm bảo rằng các nhúng tài liệu của chúng ta không bị mất mỗi khi chúng ta khởi động lại các container.
Với Docker, việc khởi động toàn bộ ứng dụng chỉ đơn giản là chạy một lệnh: bash docker-compose up --build
Kết Luận và Hướng Đi Tiếp Theo
Chúc mừng! Bạn đã có một bản thiết kế để xây dựng một ứng dụng trò chuyện RAG phức tạp, full-stack. Chúng ta đã thiết kế một hệ thống mô-đun với sự phân tách rõ ràng giữa việc xử lý dữ liệu ở backend và quản lý trạng thái ở frontend. Dự án này là một điểm khởi đầu tuyệt vời. Dưới đây là một số ý tưởng để nâng cao nó:
- Xác Thực Người Dùng: Thực hiện một hệ thống đăng nhập để cung cấp lưu trữ tài liệu riêng tư cho mỗi người dùng.
- RAG Nâng Cao: Thử nghiệm với các chiến lược truy xuất nâng cao hơn như xếp hạng lại hoặc biến đổi truy vấn để cải thiện độ chính xác.
- Độ Linh Hoạt của Mô Hình: Thêm một yếu tố UI cho phép người dùng chuyển đổi giữa các LLM khác nhau hoặc điều chỉnh các tham số như nhiệt độ.
Chúc bạn lập trình vui vẻ!