RAG là gì? Tự làm chatbot hỏi đáp tài liệu bằng Python (2026)

AI

Trong bài này
  1. RAG là gì?
  2. Cần chuẩn bị
  3. Bước 1: Cài thư viện
  4. Bước 2: Chuẩn bị tài liệu và thử hỏi khi chưa có RAG
  5. Bước 3: Chọn model embedding hỗ trợ tiếng Việt
  6. Bước 4: Cắt tài liệu và lưu vào ChromaDB
  7. Bước 5: Tìm đoạn liên quan nhất (chữ R)
  8. Bước 6: Ghép tài liệu vào prompt và hỏi lại (chữ A và G)
  9. Lỗi thường gặp
  10. RuntimeError: operator torchvision::nms does not exist
  11. Dùng embedding mặc định của ChromaDB cho tiếng Việt
  12. Tìm đúng đoạn nhưng model nhỏ vẫn trả lời sai
  13. Hỏi điều tài liệu không có
  14. Câu hỏi thường gặp
  15. RAG khác fine-tune như thế nào?
  16. Có cần GPU hay API key không?
  17. Dùng tài liệu PDF hoặc Word được không?
  18. Vì sao phải thêm "query: " và "passage: "?
  19. Học tiếp gì sau bài này?
  20. Nguồn tham khảo

Các bạn hỏi ChatGPT hay một model AI về quy định của môn học mình đang học, nó vẫn trả lời rất tự tin, nhưng thường là đoán. Lý do là model chưa bao giờ đọc tài liệu của lớp các bạn. RAG là cách giải quyết chuyện này: trước khi trả lời, chương trình tìm đúng đoạn tài liệu liên quan rồi đưa cho model đọc. Trong bài này, mình giải thích RAG là gì, rồi cùng các bạn tự làm một chatbot nhỏ bằng Python để hỏi đáp một sổ tay môn học tiếng Việt.

Mọi lệnh và kết quả trong bài mình chạy thật ngày 12/11/2026 trên CPU 4 nhân, không GPU, không tài khoản, không API key. Phiên bản dùng: Python 3.12, sentence-transformers 6.3.0 (phát hành 11/11/2026), chromadb 1.5.9, transformers 5.22.2 và PyTorch 2.15 bản CPU. Làm theo hết bài mất khoảng 30–45 phút, phần lớn là thời gian tải thư viện và model.

Video demo trên kênh Huy Nguyen - Android A - Z.

RAG là gì?

RAG viết tắt của Retrieval-Augmented Generation, tạm dịch là "sinh câu trả lời có bổ sung phần tìm kiếm". Tên gọi đã nói đủ ba bước:

  • R – Retrieval (tìm kiếm): từ câu hỏi, tìm ra vài đoạn tài liệu liên quan nhất.
  • A – Augmented (bổ sung): ghép các đoạn đó vào prompt, kèm câu hỏi.
  • G – Generation (sinh): model ngôn ngữ đọc prompt và viết câu trả lời.

Để tìm "đoạn liên quan" theo nghĩa chứ không chỉ theo từ khóa, người ta dùng embedding: một model nhỏ biến mỗi câu thành một dãy số (vector). Hai câu có nghĩa gần nhau thì hai vector cũng gần nhau. Các vector được lưu trong một vector database, ở bài này là ChromaDB. Hình dưới là toàn bộ luồng mình sẽ làm.

Sơ đồ RAG: tài liệu được cắt đoạn, tạo embedding, lưu ChromaDB; câu hỏi đi qua tìm kiếm, ghép prompt và model Qwen sinh câu trả lời
Sơ đồ RAG dùng trong bài: nạp tài liệu một lần, sau đó mỗi câu hỏi đi qua ba bước R, A, G.

Cần chuẩn bị

  • Máy Windows, macOS hoặc Linux, RAM từ 8 GB trở lên. Không cần GPU.
  • Python 3.11 trở lên (transformers 5.x yêu cầu Python >= 3.11).
  • Khoảng 5 GB ổ cứng trống. Môi trường ảo của mình sau khi cài nặng 1,8 GB, phần còn lại là model.
  • Biết chạy lệnh trong terminal và đọc code Python cơ bản. Nếu chưa quen Hugging Face, các bạn đọc trước bài Hugging Face là gì.

Bước 1: Cài thư viện

Thư viện chính để tạo embedding là sentence-transformers. Bản 6.3.0 vừa lên PyPI ngày 11/11/2026, chủ yếu thêm tính năng huấn luyện mới, cách dùng cơ bản trong bài không đổi.

Trang PyPI của sentence-transformers 6.3.0 với lệnh pip install và ngày phát hành
Bước 1: trang PyPI của sentence-transformers 6.3.0, bản mới nhất lúc viết bài.

Tạo môi trường ảo, cài PyTorch bản CPU cùng lúc với torchvision từ trang tải của PyTorch, rồi mới cài các thư viện còn lại. Trên Windows thì lệnh kích hoạt là .venv\Scripts\activate.

python -m venv .venv
source .venv/bin/activate
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
pip install sentence-transformers chromadb transformers accelerate pillow
Terminal cài thư viện và kiểm tra phiên bản sentence-transformers 6.3.0, chromadb 1.5.9, transformers 5.22.2, torch 2.15.0+cpu
Bước 1: cài xong, script kiem_tra.py in ra phiên bản các thư viện.

Bước 2: Chuẩn bị tài liệu và thử hỏi khi chưa có RAG

Mình viết một file tai_lieu/so-tay-mon-android.md gồm 7 mục: thông tin chung, cách tính điểm, hạn nộp assignment, điểm danh, quy định dùng AI, phần mềm và phúc khảo. Đây là tài liệu mẫu, các con số do mình đặt ra để thử nghiệm, không phải quy chế của trường nào. Các bạn có thể thay bằng đề cương môn của mình.

Nội dung file sổ tay môn Android mẫu, khoanh đỏ câu bài nộp muộn bị trừ 1 điểm cho mỗi ngày trễ
Bước 2: tài liệu mẫu, mỗi mục bắt đầu bằng tiêu đề ##.

Trước khi làm RAG, mình hỏi thẳng model Qwen3.5-0.8B: "nộp assignment muộn 2 ngày thì bị trừ bao nhiêu điểm?". Model trả lời "Điểm bị trừ là 0." Theo tài liệu thì đúng phải là 2 điểm. Model không biết tài liệu này nên đoán, và đoán sai.

Terminal: model Qwen trả lời sai Điểm bị trừ là 0 khi chưa có tài liệu
Bước 2: không có tài liệu, model trả lời sai.

Bước 3: Chọn model embedding hỗ trợ tiếng Việt

Mình chọn intfloat/multilingual-e5-small trên Hugging Face: khoảng 0,1 tỉ tham số nên chạy nhanh trên CPU, hỗ trợ 94 ngôn ngữ (danh sách có vi), giấy phép MIT. Model card ghi rõ một điểm dễ quên: câu hỏi phải bắt đầu bằng query: và đoạn tài liệu phải bắt đầu bằng passage: .

Model card multilingual-e5-small trên Hugging Face, khoanh đỏ 94 languages, License mit và 0.1B params
Bước 3: model card của multilingual-e5-small.

Thử nhanh với một câu hỏi và ba câu ngắn:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("intfloat/multilingual-e5-small")
vec_hoi = model.encode("query: nộp bài trễ hạn thì sao?", normalize_embeddings=True)
vec_doan = model.encode([
    "passage: Bài nộp muộn bị trừ 1 điểm cho mỗi ngày trễ.",
    "passage: Sinh viên vắng từ 4 buổi trở lên sẽ bị cấm thi.",
    "passage: Máy tính nên có tối thiểu 8 GB RAM.",
], normalize_embeddings=True)
print(model.similarity(vec_hoi, vec_doan))

Mỗi câu thành một vector 384 số. Câu "nộp muộn bị trừ 1 điểm" giống câu hỏi nhất (0,886), dù câu hỏi dùng chữ "trễ hạn" chứ không phải "muộn". Đó là tìm theo nghĩa. Các bạn để ý điểm của e5 thường dồn vào khoảng 0,8–0,9, nên hãy so thứ tự chứ đừng nhìn con số tuyệt đối.

Terminal: vector 384 số và độ giống cosine, câu nộp muộn đứng đầu với 0.886
Bước 3: câu liên quan nhất có độ giống cao nhất.

Bước 4: Cắt tài liệu và lưu vào ChromaDB

Tài liệu được cắt theo tiêu đề ##, mỗi mục là một đoạn. Mình tự tính embedding rồi đưa vào ChromaDB bằng PersistentClient, để dữ liệu nằm trong thư mục chroma_db và lần sau không phải tính lại.

import chromadb
from sentence_transformers import SentenceTransformer

text = open("tai_lieu/so-tay-mon-android.md", encoding="utf-8").read()
doan = []
for phan in text.split("\n## ")[1:]:
    tieu_de, _, noi_dung = phan.partition("\n")
    doan.append({"tieu_de": tieu_de.strip(), "noi_dung": noi_dung.strip()})

model = SentenceTransformer("intfloat/multilingual-e5-small")
vectors = model.encode(
    [f"passage: {d['tieu_de']}. {d['noi_dung']}" for d in doan], normalize_embeddings=True
)

client = chromadb.PersistentClient(path="chroma_db")
col = client.get_or_create_collection("so_tay_android", metadata={"hnsw:space": "cosine"})
col.upsert(
    ids=[f"doan-{i}" for i in range(len(doan))],
    embeddings=vectors.tolist(),
    documents=[d["noi_dung"] for d in doan],
    metadatas=[{"tieu_de": d["tieu_de"]} for d in doan],
)
print("Số đoạn trong collection:", col.count())
Terminal: cắt được 7 đoạn, ma trận vector 7 x 384, collection có 7 đoạn
Bước 4: 7 đoạn, mỗi đoạn một vector 384 số, đã lưu vào ChromaDB.

Bước 5: Tìm đoạn liên quan nhất (chữ R)

Khi có câu hỏi, mình tạo embedding cho câu hỏi (nhớ tiền tố query: ) rồi gọi col.query với query_embeddings. ChromaDB trả về các đoạn có khoảng cách nhỏ nhất. Khoảng cách cosine càng nhỏ thì càng giống.

vec = model.encode(f"query: {cau_hoi}", normalize_embeddings=True)
kq = col.query(query_embeddings=[vec.tolist()], n_results=3)
Terminal: tìm kiếm trả về mục Assignment và hạn nộp, Điểm danh đứng đầu cho hai câu hỏi
Bước 5: cả hai câu hỏi đều tìm đúng mục ở vị trí đầu tiên.

Bước 6: Ghép tài liệu vào prompt và hỏi lại (chữ A và G)

Bước cuối ghép các đoạn tìm được vào prompt, yêu cầu model chỉ dựa vào tài liệu, rồi gọi Qwen3.5-0.8B. Phần chính của hoi_rag.py:

prompt = (
    "Chỉ dựa vào TÀI LIỆU bên dưới để trả lời câu hỏi, ngắn gọn, bằng tiếng Việt. "
    "Nếu tài liệu không có thông tin thì trả lời: Tài liệu không nói về việc này.\n\n"
    f"TÀI LIỆU:\n{ngu_canh}\n\nCÂU HỎI: {cau_hoi}"
)
messages = [{"role": "user", "content": [{"type": "text", "text": prompt}]}]
inputs = processor.apply_chat_template(
    messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt"
).to(llm.device)
out = llm.generate(**inputs, max_new_tokens=120, do_sample=False)

Lần này cả ba câu đều đúng với tài liệu: nộp muộn 2 ngày bị trừ 2 điểm, vắng từ 4 buổi bị cấm thi, điểm thi cuối môn không dưới 4,0. Mỗi câu mất khoảng 11–13 giây trên CPU 4 nhân. Mình cho script in kèm tên mục nguồn, để người đọc tự mở tài liệu ra kiểm tra.

Terminal: hỏi với RAG, model trả lời đúng trừ 2 điểm, vắng 4 buổi bị cấm thi, điểm thi không dưới 4,0, kèm nguồn
Bước 6: có tài liệu, model trả lời đúng và có ghi nguồn.

Lỗi thường gặp

RuntimeError: operator torchvision::nms does not exist

Mình gặp lỗi này ở lần cài đầu: cài PyTorch bản CPU trước, sau đó pip install lấy torchvision từ PyPI, hai bản không khớp nhau. Lỗi hiện ra khi import sentence_transformers hoặc transformers. Cách sửa là cài lại torchvision từ đúng trang CPU của PyTorch, hoặc cài torch và torchvision trong cùng một lệnh như ở bước 1.

Terminal: lỗi RuntimeError operator torchvision::nms does not exist và lệnh cài lại torchvision bản CPU
Lỗi 1: torchvision không khớp torch bản CPU, sửa bằng cách cài lại từ index CPU.

Dùng embedding mặc định của ChromaDB cho tiếng Việt

Nếu không truyền embedding, ChromaDB tự tải model all-MiniLM-L6-v2 (file onnx khoảng 79 MB). Mình thử 4 câu hỏi: model mặc định tìm sai 2 câu, ví dụ hỏi "nộp bài trễ" lại ra mục "Thông tin chung". Với multilingual-e5-small, 3/4 câu đúng ở vị trí đầu. Câu còn lại ("dùng ChatGPT") e5 xếp mục "Quy định dùng AI" ở vị trí thứ hai. Vì vậy các bạn nên chọn model embedding có hỗ trợ tiếng Việt và lấy 2–3 đoạn chứ đừng chỉ lấy 1.

Terminal so sánh: embedding mặc định của ChromaDB tìm sai 2 trên 4 câu, multilingual-e5-small sai 1 trên 4
Lỗi 2: embedding mặc định tìm sai 2/4 câu tiếng Việt, e5 sai 1/4.

Tìm đúng đoạn nhưng model nhỏ vẫn trả lời sai

Đây là lỗi quan trọng nhất. Mình hỏi "có được dùng ChatGPT để làm bài không?", mục "Quy định dùng AI" đã nằm trong các đoạn tìm được, nhưng Qwen3.5-0.8B vẫn trả lời "Không". Đổi câu hỏi cho sát chữ trong tài liệu ("công cụ AI") thì model vẫn nói "Không", rồi câu sau lại nhắc quy định ghi AI_LOG.md, tự mâu thuẫn. Tài liệu thật ra cho phép dùng AI nếu ghi lại câu hỏi. Model dưới 1 tỉ tham số đọc hiểu các câu có điều kiện còn kém. Muốn chạy thật thì nên dùng model lớn hơn, ví dụ chạy qua Ollama (mình chưa thử model lớn hơn trong bài này), và luôn hiển thị đoạn nguồn để người dùng kiểm tra.

Terminal: nguồn tìm được có Quy định dùng AI nhưng model trả lời sai là không được dùng AI
Lỗi 3: nguồn đúng, câu trả lời sai. RAG giảm đoán mò chứ không loại bỏ hoàn toàn.

Hỏi điều tài liệu không có

Với câu "học phí bao nhiêu", model trả lời đúng là tài liệu không nêu. Với câu "học ở phòng nào", model lại viết một câu vô nghĩa. Mình đã thử chặn bằng ngưỡng khoảng cách nhưng không được. Hai câu hỏi ngoài tài liệu có khoảng cách 0,149–0,150, còn câu "dùng ChatGPT" có trong tài liệu lại là 0,190. Với e5-small và tài liệu này, ngưỡng khoảng cách không tách được hai trường hợp. Cách an toàn hơn là in nguồn kèm câu trả lời và dặn người dùng kiểm tra lại.

Terminal: câu hỏi phòng học bị trả lời vô nghĩa, câu học phí được trả lời là tài liệu không nêu, kèm khoảng cách
Lỗi 4: câu hỏi ngoài tài liệu, khoảng cách không đủ để phân biệt.

Câu hỏi thường gặp

RAG khác fine-tune như thế nào?

Fine-tune là huấn luyện thêm để thay đổi trọng số của model, tốn thời gian và cần GPU. RAG không đụng vào model, chỉ đưa thêm tài liệu vào prompt mỗi lần hỏi. Khi tài liệu thay đổi, các bạn chỉ cần nạp lại vào ChromaDB.

Có cần GPU hay API key không?

Không. Toàn bộ bài này chạy trên CPU 4 nhân, không đăng nhập Hugging Face. Đổi lại là chậm: khoảng 11–13 giây cho mỗi câu trả lời với Qwen3.5-0.8B.

Dùng tài liệu PDF hoặc Word được không?

Được, nhưng phải chuyển sang văn bản thuần trước rồi mới cắt đoạn. Bài này mình chỉ thử với file Markdown. Với tài liệu dài, nên cắt đoạn vài trăm từ và giữ tiêu đề mục trong metadata như ở bước 4.

Vì sao phải thêm "query: " và "passage: "?

Model card của multilingual-e5 yêu cầu như vậy với bài toán tìm kiếm câu hỏi – đoạn văn, vì model được huấn luyện theo đúng định dạng đó. Model embedding khác có thể không cần, các bạn đọc model card của model mình chọn.

Học tiếp gì sau bài này?

Các bạn có thể bọc hàm tìm kiếm thành một tool cho AI agent bằng MCP server, hoặc thử model embedding và model sinh lớn hơn rồi so sánh kết quả với bộ câu hỏi trong bài.

Nguồn tham khảo

Ảnh chụp và thử nghiệm ngày 12/11/2026.