Dự án AI: chatbot hỏi đáp tài liệu nội bộ với RAG
Xây chatbot trả lời dựa trên tài liệu vận hành nhà máy: chia đoạn, nhúng vector, tìm kiếm ngữ nghĩa và sinh câu trả lời có trích dẫn.
Nội dung bài viết
Kỹ sư mới vào nhà máy thường mất hàng tuần để tìm thông tin trong hàng trăm trang hướng dẫn vận hành, quy trình xử lý sự cố và datasheet. Dự án này xây một chatbot hỏi đáp tài liệu nội bộ theo kiến trúc RAG (Retrieval-Augmented Generation): mô hình ngôn ngữ chỉ trả lời dựa trên đoạn tài liệu liên quan nhất mà hệ thống tìm được.
Vì sao không hỏi thẳng LLM?
- LLM không biết tài liệu nội bộ của bạn.
- Khi không biết, LLM có thể "bịa" câu trả lời nghe rất thuyết phục.
- RAG buộc câu trả lời bám vào nguồn và trích dẫn được: người dùng kiểm tra lại ngay.
Kiến trúc
- Nạp tài liệu: đọc PDF, Word, trang wiki thành văn bản.
- Chia nhỏ (chunking): cắt thành đoạn khoảng 300–800 từ, có chồng lấn.
- Nhúng (embedding): biến mỗi đoạn thành một vector số biểu diễn ý nghĩa.
- Tìm kiếm: nhúng câu hỏi, lấy các đoạn có vector gần nhất.
- Sinh câu trả lời: gửi câu hỏi + các đoạn tìm được cho LLM, yêu cầu chỉ trả lời dựa trên đó.
Bước 1–3: nạp, chia nhỏ và nhúng tài liệu
pip install pypdf sentence-transformers numpy
import numpy as np
from pypdf import PdfReader
from sentence_transformers import SentenceTransformer
def load_pdf(path):
reader = PdfReader(path)
return [(i + 1, page.extract_text() or "") for i, page in enumerate(reader.pages)]
def chunk(text, size=600, overlap=100):
words = text.split()
step = size - overlap
return [" ".join(words[i:i + size]) for i in range(0, max(len(words), 1), step)]
docs = [] # (nguồn, nội dung)
for page_no, text in load_pdf("huong-dan-van-hanh.pdf"):
for part in chunk(text):
if part.strip():
docs.append((f"huong-dan-van-hanh.pdf, trang {page_no}", part))
# Mô hình embedding đa ngôn ngữ, hỗ trợ tiếng Việt
embedder = SentenceTransformer("intfloat/multilingual-e5-small")
doc_vecs = embedder.encode([f"passage: {d[1]}" for d in docs], normalize_embeddings=True)
np.save("doc_vecs.npy", doc_vecs)
Họ mô hình E5 yêu cầu thêm tiền tố passage: cho tài liệu và query: cho câu hỏi. Mỗi mô hình embedding có quy ước riêng, hãy đọc model card trước khi dùng.
Bước 4: tìm đoạn liên quan
def search(question, k=4):
q = embedder.encode([f"query: {question}"], normalize_embeddings=True)[0]
scores = doc_vecs @ q # vector đã chuẩn hóa: tích vô hướng = cosine
top = np.argsort(-scores)[:k]
return [(docs[i][0], docs[i][1], float(scores[i])) for i in top]
Với vài nghìn đoạn, tìm kiếm bằng NumPy là đủ nhanh. Khi lên hàng triệu đoạn, chuyển sang thư viện vector chuyên dụng như FAISS hoặc một vector database.
Bước 5: sinh câu trả lời có trích dẫn
def build_prompt(question, hits):
context = "\n\n".join(f"[{i + 1}] ({src})\n{text}" for i, (src, text, _) in enumerate(hits))
return (
"Bạn là trợ lý kỹ thuật của nhà máy. Chỉ trả lời dựa trên các đoạn tài liệu dưới đây. "
"Ghi số trích dẫn [1], [2]... sau mỗi ý. Nếu tài liệu không có thông tin, "
"hãy nói rõ là không tìm thấy, không được đoán.\n\n"
f"TÀI LIỆU:\n{context}\n\nCÂU HỎI: {question}"
)
question = "Máy báo lỗi E-205 thì xử lý thế nào?"
hits = search(question)
prompt = build_prompt(question, hits)
answer = ask_llm(prompt) # gọi API LLM bạn dùng (dịch vụ đám mây hoặc mô hình chạy cục bộ)
print(answer)
ask_llm là chỗ bạn kết nối tới nhà cung cấp LLM. Với tài liệu mật của công ty, cân nhắc mô hình chạy cục bộ hoặc dịch vụ cam kết không dùng dữ liệu để huấn luyện.
Đánh giá chất lượng
- Lập bộ 30–50 câu hỏi thật kèm đáp án đúng do kỹ sư lâu năm viết.
- Đo riêng hai phần: tìm đúng đoạn (đoạn chứa đáp án có nằm trong top-k không) và trả lời đúng.
- Phần lớn lỗi RAG đến từ khâu tìm kiếm: chia đoạn quá dài/ngắn, PDF scan không trích được chữ (cần OCR), bảng biểu bị vỡ.
Hướng phát triển
- Kết hợp tìm kiếm từ khóa (BM25) với tìm kiếm vector để bắt chính xác mã lỗi như "E-205".
- Thêm giao diện web, đăng nhập và phân quyền tài liệu.
- Ghi log câu hỏi không trả lời được để bổ sung tài liệu.

