Vấn đề: LLM thông minh nhưng không biết gì về dữ liệu của bạn
ChatGPT hay Claude trả lời rất tốt về kiến thức chung. Nhưng thử hỏi chúng về tài liệu nội bộ công ty, database sản phẩm, hay PDF kỹ thuật của dự án đang làm — bạn sẽ nhận được câu trả lời bịa hoặc không liên quan. Đây không phải lỗi của model, mà đơn giản là chúng không có quyền truy cập vào dữ liệu riêng của bạn.
Fine-tune nghe hay đó — nhưng thực tế thì không dễ. Chi phí train một lần có thể tốn từ vài trăm đến vài nghìn đô tùy model size, cần GPU đủ mạnh, và mỗi khi tài liệu cập nhật là lại phải train lại từ đầu. Mình đã thử con đường đó và bỏ cuộc sau 2 tuần khi nhẩm tính chi phí vận hành.
RAG (Retrieval-Augmented Generation) là cách tiếp cận thực dụng hơn nhiều. Thay vì nhồi kiến thức vào model, bạn cho phép model tra cứu tài liệu liên quan trước khi trả lời — giống như cho nó một chồng tài liệu tham khảo ngay lúc cần.
Khái niệm cốt lõi bạn cần nắm trước khi code
RAG hoạt động theo 3 bước
- Indexing: Tài liệu được chia nhỏ thành các đoạn (chunk), chuyển thành vector embedding rồi lưu vào vector database.
- Retrieval: Khi user đặt câu hỏi, hệ thống tìm các đoạn tài liệu có vector gần nhất với câu hỏi đó.
- Generation: Đưa các đoạn tài liệu tìm được cùng câu hỏi vào LLM để sinh ra câu trả lời có căn cứ, không hallucinate.
Các thành phần trong LangChain
LangChain chia pipeline thành 6 building block. Hiểu rõ từng cái giúp bạn debug nhanh hơn nhiều khi gặp vấn đề:
- Document Loaders: Load tài liệu từ PDF, TXT, URL, database…
- Text Splitters: Chia văn bản dài thành các chunk nhỏ hơn
- Embeddings: Chuyển text thành vector số học
- Vector Stores: Lưu trữ và tìm kiếm vector (Chroma, FAISS, Pinecone…)
- Retrievers: Interface chuẩn để truy vấn vector store
- Chains: Kết nối các thành phần thành pipeline hoàn chỉnh
Thực hành: Xây dựng RAG từ đầu
Bước 1: Cài đặt dependencies
Tạo virtualenv và cài các package cần thiết:
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install langchain langchain-community langchain-openai
pip install chromadb
pip install pypdf # nếu cần đọc PDF
pip install python-dotenv
Tạo file .env để lưu API key:
OPENAI_API_KEY=sk-...your-key-here...
Bước 2: Chuẩn bị tài liệu và xây dựng Vector Store
Demo dưới đây dùng text thuần để dễ theo dõi. Sau khi chạy được, bạn hoàn toàn có thể thay bằng PDF loader hay web scraper — cấu trúc code không thay đổi.
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
load_dotenv()
# Load tài liệu
loader = TextLoader("docs/manual.txt", encoding="utf-8")
documents = loader.load()
# Chia nhỏ thành chunk
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # Mỗi chunk tối đa 500 ký tự
chunk_overlap=50, # Overlap 50 ký tự để không mất context
length_function=len,
)
chunks = splitter.split_documents(documents)
print(f"Tổng số chunks: {len(chunks)}")
# Tạo embeddings và lưu vào Chroma
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # Lưu xuống disk để tái sử dụng
)
print("Vector store đã được tạo xong!")
Chunk size ảnh hưởng rất nhiều đến chất lượng: 500–800 ký tự thường hoạt động tốt cho tài liệu kỹ thuật. Chunk quá nhỏ (dưới 200) thì mất context; quá lớn (trên 1500) thì retrieval kém chính xác vì vector trở nên “mờ nhạt”. Thử nghiệm với dữ liệu thực của bạn trước khi chốt con số.
Bước 3: Xây dựng RAG Chain
Phần này nối retriever với LLM — cũng là chỗ bạn kiểm soát được cách model trả lời:
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Load lại vector store từ disk (nếu đã tạo trước)
embeddings = OpenAIEmbeddings()
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings
)
# Tạo retriever — lấy 3 chunk liên quan nhất
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
)
# Tùy chỉnh prompt để model trả lời dựa trên context
prompt_template = """Bạn là trợ lý kỹ thuật. Dựa vào thông tin sau đây để trả lời câu hỏi.
Nếu không tìm thấy thông tin liên quan, hãy nói rõ là không có trong tài liệu.
Thông tin tham khảo:
{context}
Câu hỏi: {question}
Trả lời:"""
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# Tạo chain
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # "stuff" = nhét toàn bộ context vào 1 prompt
retriever=retriever,
chain_type_kwargs={"prompt": PROMPT},
return_source_documents=True # Trả về nguồn để debug
)
Bước 4: Chạy thử và xem kết quả
def ask(question: str):
result = qa_chain.invoke({"query": question})
print(f"\nCâu hỏi: {question}")
print(f"\nTrả lời: {result['result']}")
print("\n--- Nguồn tham khảo ---")
for i, doc in enumerate(result['source_documents']):
print(f"[{i+1}] {doc.page_content[:150]}...")
print(f" (từ file: {doc.metadata.get('source', 'unknown')})")
print()
# Test thử
ask("Quy trình deploy ứng dụng lên production là gì?")
ask("Cách xử lý lỗi timeout trong hệ thống?")
Bước 5: Nâng cấp — Dùng LCEL (LangChain Expression Language)
LangChain từ phiên bản 0.2 trở đi khuyến khích dùng LCEL thay cho các chain cũ. Code ngắn hơn, dễ pipe thêm bước xử lý, và hỗ trợ streaming sẵn:
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# LCEL chain — dễ đọc, dễ thêm bước xử lý
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| PROMPT
| llm
| StrOutputParser()
)
# Stream response (thích hợp cho web app)
for chunk in rag_chain.stream("Hướng dẫn cài đặt môi trường development?"):
print(chunk, end="", flush=True)
print()
Xử lý trường hợp dùng PDF
Phần lớn dự án thực tế mình làm đều có dữ liệu dạng PDF — tài liệu kỹ thuật, spec sheet, nội quy công ty. Đổi loader là xong, phần còn lại giữ nguyên:
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("docs/technical-spec.pdf")
pages = loader.load() # Mỗi trang là 1 Document
# Sau đó split và index như bình thường
chunks = splitter.split_documents(pages)
Tổng kết và hướng mở rộng
Cách tiếp cận này mình đã dùng để xây chatbot hỗ trợ kỹ thuật nội bộ — khoảng 200 tài liệu, tổng ~50MB PDF. Sau 2 tháng vận hành, tỷ lệ câu hỏi được trả lời đúng vào khoảng 85%, so với con số 0% khi không có RAG. Điểm mấu chốt không phải code, mà là chất lượng dữ liệu đầu vào: tài liệu rõ ràng, có cấu trúc thì kết quả tốt; tài liệu scan mờ hoặc lộn xộn thì pipeline chuẩn đến đâu cũng không cứu được.
Muốn đẩy accuracy lên cao hơn? Một số hướng đáng thử:
- Hybrid Search: Kết hợp vector search với BM25 (keyword search) để tăng độ chính xác
- Reranking: Dùng cross-encoder model để re-rank kết quả trước khi đưa vào LLM
- Conversation Memory: Thêm chat history để hệ thống nhớ context của cuộc hội thoại
- Alternative Vector Stores: Cần production-grade thì xem xét Pinecone, Weaviate hoặc pgvector (PostgreSQL)
- Embeddings thay thế: Không muốn dùng OpenAI? Thử
HuggingFaceEmbeddingsvới modelBAAI/bge-m3— hỗ trợ tiếng Việt khá tốt và hoàn toàn miễn phí
Bắt đầu với dataset nhỏ — 20 đến 50 tài liệu là đủ để thấy hệ thống hoạt động. Đo accuracy bằng cách tự đặt câu hỏi và so với đáp án mong muốn, rồi mới scale lên. Bỏ qua bước evaluate là sai lầm phổ biến nhất — đến lúc deploy production mới phát hiện vấn đề thì sửa rất tốn công.

