Các Mô hình Ngôn ngữ Lớn (LLM) như GPT-4 và Claude cực kỳ mạnh mẽ, nhưng chúng có một hạn chế cơ bản: kiến thức của chúng bị đóng băng tại thời điểm huấn luyện. Chúng không thể truy cập tài liệu nội bộ, cơ sở dữ liệu hay thông tin thời gian thực của bạn. Sinh tăng cường bằng truy xuất (RAG) giải quyết chính xác vấn đề này bằng cách kết hợp sức mạnh sinh của LLM với khả năng truy xuất thông tin từ các nguồn bên ngoài.
Vấn đề: Hạn chế của LLM
Trước khi nói về RAG, điều quan trọng là phải hiểu tại sao chúng ta cần nó.
- Kiến thức tĩnh: Một LLM chỉ biết những gì nó đã thấy trong quá trình huấn luyện. Nếu bạn hỏi về một sự kiện xảy ra sau thời điểm cắt, nó không thể trả lời.
- Ảo giác: Khi một LLM không biết câu trả lời, nó có xu hướng bịa ra, tạo ra thông tin có vẻ hợp lý nhưng hoàn toàn sai.
- Không truy cập được dữ liệu riêng tư: Một LLM thông thường không có quyền truy cập vào tài liệu nội bộ, ticket hay mã nguồn của công ty bạn.
RAG giải quyết cả ba vấn đề này bằng cách cung cấp cho mô hình ngữ cảnh liên quan được truy xuất từ các nguồn bên ngoài tại thời điểm truy vấn.
RAG là gì?
Sinh tăng cường bằng truy xuất là một kiến trúc làm giàu prompt gửi đến LLM bằng thông tin được truy xuất từ cơ sở tri thức bên ngoài. Thay vì chỉ dựa vào kiến thức tham số của mô hình, RAG tìm kiếm thông tin liên quan trước rồi chèn vào prompt, cho phép mô hình tạo ra các phản hồi chính xác, có căn cứ.
RAG hoạt động chi tiết như thế nào
Kiến trúc RAG bao gồm hai giai đoạn chính: Lập chỉ mục (ngoại tuyến) và Truy xuất + Sinh (trực tuyến).
Giai đoạn 1: Lập chỉ mục (Thu thập tài liệu)
Giai đoạn lập chỉ mục chuẩn bị tài liệu của bạn cho tìm kiếm ngữ nghĩa. Nó gồm bốn bước.
1. Tải tài liệu
Tài liệu có thể đến từ bất kỳ nguồn nào: tệp PDF, trang web, cơ sở dữ liệu, tệp Markdown, API. Document Loader đọc các tài liệu này và chuyển đổi chúng thành văn bản có cấu trúc.
2. Phân tách văn bản (Chunking)
LLM có cửa sổ ngữ cảnh giới hạn và tài liệu có thể rất dài. Text Splitter chia tài liệu thành các đoạn nhỏ hơn gọi là chunks. Chất lượng chunking rất quan trọng: chunks quá nhỏ mất ngữ cảnh, trong khi chunks quá lớn làm loãng mức độ liên quan.
Các chiến lược phổ biến nhất là:
- Phân tách ký tự đệ quy: Phân tách văn bản đệ quy sử dụng các dấu phân cách như
\n\n,\n,., tôn trọng cấu trúc tài liệu. - Phân tách ngữ nghĩa: Sử dụng embeddings để tìm các điểm ngắt tự nhiên trong văn bản.
- Chunk chồng lấp: Bao gồm sự chồng lấp giữa các chunks liên tiếp để bảo toàn ngữ cảnh tại ranh giới.
3. Embedding
Mỗi chunk được chuyển đổi thành một vector số (embedding) thông qua mô hình embedding (như text-embedding-3-small của OpenAI). Các vector này nắm bắt ý nghĩa ngữ nghĩa của văn bản: các câu có nghĩa tương tự sẽ có các vector gần nhau trong không gian đa chiều.
4. Vector Store
Các vector được lưu trong Vector Store (hoặc cơ sở dữ liệu vector), như ChromaDB, Pinecone, Weaviate hoặc FAISS. Cơ sở dữ liệu này được tối ưu hóa cho tìm kiếm tương tự: với một truy vấn, nó tìm các vector tương tự nhất (và do đó là các chunks văn bản liên quan nhất).
Giai đoạn 2: Truy xuất + Sinh
Khi người dùng đặt câu hỏi:
- Câu hỏi được chuyển đổi thành embedding sử dụng cùng mô hình embedding.
- Vector Store tìm các chunks tương tự nhất qua tìm kiếm tương tự (thường là cosine similarity hoặc khoảng cách Euclidean).
- Các chunks được truy xuất được chèn vào prompt làm ngữ cảnh.
- LLM tạo phản hồi dựa trên ngữ cảnh được cung cấp.
Xây dựng Pipeline RAG với LangChain
LangChain là framework Python (và JavaScript) phổ biến nhất để xây dựng ứng dụng dựa trên LLM. Nó cung cấp các abstraction cấp cao cho mọi thành phần của pipeline RAG.
Cài đặt
pip install langchain langchain-openai langchain-community chromadb
Bước 1: Tải tài liệu
LangChain cung cấp hàng chục Document Loader cho các nguồn dữ liệu khác nhau.
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # Load a PDF pdf_loader = PyPDFLoader("docs/manual.pdf") pdf_docs = pdf_loader.load() # Load a web page web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # Load all .md files from a directory dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
Bước 2: Phân tách tài liệu thành Chunks
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"Original documents: {len(all_docs)}, Chunks: {len(chunks)}")
Tham số chunk_overlap rất quan trọng: nó tạo sự chồng lấp giữa các chunks liên tiếp để ngữ cảnh không bị mất tại ranh giới.
Bước 3: Tạo Embeddings và Vector Store
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
Bước 4: Tạo Retriever
Retriever là thành phần mà, với một truy vấn, lấy các chunks liên quan nhất từ vector store.
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("How does authentication work?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
Bước 5: Xây dựng RAG Chain
Bây giờ hãy kết hợp tất cả với một LLM và một template prompt.
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" Answer the question based only on the provided context. If the context does not contain enough information, say you don't know. Context: {context} Question: {question} Answer: """) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) response = rag_chain.invoke("How does authentication work in the system?") print(response)
Kỹ thuật RAG nâng cao
Pipeline cơ bản hoạt động tốt, nhưng có một số kỹ thuật để cải thiện đáng kể chất lượng phản hồi.
Truy xuất đa truy vấn
Đôi khi truy vấn của người dùng mơ hồ hoặc không phù hợp với ngôn ngữ sử dụng trong tài liệu. Multi-Query Retriever tự động tạo các biến thể của câu hỏi gốc để nắm bắt nhiều góc nhìn.
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("What are the security best practices?")
Nén ngữ cảnh
Không phải tất cả nội dung trong một chunk đều liên quan đến truy vấn. Contextual Compression Retriever sử dụng LLM để trích xuất chỉ các phần liên quan từ mỗi chunk được truy xuất.
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
Tìm kiếm kết hợp
Tìm kiếm ngữ nghĩa thuần túy không phải lúc nào cũng tối ưu. Tìm kiếm kết hợp kết hợp tìm kiếm ngữ nghĩa (embeddings) với tìm kiếm từ vựng (BM25, khớp từ khóa) để đạt kết quả tốt hơn.
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
RAG hội thoại (có bộ nhớ)
Để xây dựng chatbot RAG nhớ ngữ cảnh hội thoại, bạn cần thêm bộ nhớ để tái cấu trúc câu hỏi của người dùng dựa trên lịch sử hội thoại.
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "Given the chat history and the user's latest question, " "reformulate the question so it is understandable without the history."), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
Các phương pháp tốt nhất
- Chọn kích thước chunk phù hợp: Thử nghiệm với các kích thước khác nhau (500-1500 token). Chunks nhỏ hơn cho câu trả lời chính xác, lớn hơn cho ngữ cảnh rộng hơn.
- Sử dụng metadata tài liệu: Thêm nguồn, ngày và danh mục làm metadata cho chunks. Điều này cho phép lọc kết quả trong quá trình truy xuất.
- Đánh giá chất lượng: Sử dụng các framework như RAGAS để đo các chỉ số như faithfulness, relevancy và context precision.
- Quản lý cập nhật tài liệu: Triển khai pipeline tái thu thập để giữ vector store đồng bộ với nguồn dữ liệu của bạn.
- Thêm re-ranker: Sau khi truy xuất ban đầu, sử dụng mô hình re-ranking (như Cohere Rerank) để sắp xếp lại kết quả dựa trên mức độ liên quan thực tế.
Kết luận
RAG đã trở thành kiến trúc tiêu chuẩn để xây dựng ứng dụng AI cần truy cập kiến thức cụ thể, cập nhật. LangChain đơn giản hóa đáng kể việc triển khai, cung cấp các abstraction cho mọi thành phần của pipeline.
Các bước tiếp theo:
- Thử nghiệm cục bộ: Bắt đầu với ChromaDB và một vài tài liệu để làm quen với pipeline.
- Khám phá LangSmith: Sử dụng LangSmith để giám sát và gỡ lỗi chain của bạn trong môi trường production.
- Thử các mô hình embedding khác nhau: So sánh các mô hình như
text-embedding-3-small,text-embedding-3-largevà các mô hình mã nguồn mở từ Sentence Transformers. - Xem tài liệu: Tài liệu LangChain là nguồn tài nguyên tuyệt vời và được cập nhật liên tục.