RAG chatbot đừng bắt đầu từ chatbot
Muốn triển khai RAG cho tài liệu nội bộ? Đừng vội làm UI chat. Hãy quyết định ingestion, retrieval, eval và rollout trước.
Bụi Wire“Làm cái chatbot hỏi đáp tài liệu nội bộ đi, chắc một sprint là xong.”
Câu này mình nghe nhiều đến mức nếu mỗi lần được một ly cà phê, chắc giờ tay mình run như vừa đọc changelog lúc 2 giờ sáng. Niềm tin phổ biến là: có PDF, có embedding, có vector database, có model trả lời — thế là có RAG chatbot.
Sai ở chữ chatbot.
Với team đang build hệ thống AI thật, RAG — Retrieval-Augmented Generation, tức cho model tìm tài liệu liên quan trước khi trả lời — không phải dự án giao diện chat. Nó là dự án triển khai một đường ống quyết định: tài liệu được cắt thế nào, tìm lại ra sao, model được phép nói gì, đo chất lượng bằng gì, và rollout cho ai trước.
Nếu bạn đổi cách nghĩ sau bài này, mình mong là đổi ở điểm này: đừng chọn stack RAG vì tutorial chạy được; hãy chọn stack vì bạn biết nó sẽ hỏng ở đâu và đo được khi nào nó tốt hơn.

Sơ đồ tóm tắt ý chính của bài viết.
Mục tiêu thật: biến tài liệu thành nhịp làm việc, không phải hộp chat
Một tutorial RAG với Node.js, Gemini embeddings, Groq generation và pgvector chạy trong Docker là điểm khởi đầu ổn: đủ gần production để hiểu luồng, đủ nhẹ để team thử nhanh. Nhưng nếu bạn bê nguyên tinh thần tutorial vào công ty, bạn dễ dính cái bẫy “demo trả lời được vài câu nên tưởng hệ thống đã sẵn sàng”.
Nói thẳng ra thì: RAG demo đo bằng wow; RAG production đo bằng số lần nó không làm phiền người dùng sai cách.
Trong dàn nhạc, chatbot chỉ là ca sĩ đứng trước sân khấu. Phần dễ rối nằm phía sau: người giữ nhịp ingestion, bè retrieval, nhạc trưởng orchestration — điều phối các bước gọi tool, tìm kiếm, sinh câu trả lời — và người soát bản nhạc là eval.
Với developer hoặc tech lead, mục tiêu triển khai nên viết rõ như sau:
- Người dùng hỏi được tài liệu nội bộ bằng ngôn ngữ tự nhiên.
- Câu trả lời phải trích từ đoạn tài liệu tìm được, không phóng tác từ trí nhớ model.
- Khi không đủ dữ kiện, hệ thống phải biết nói “không tìm thấy trong tài liệu”.
- Team phải đo được thay đổi sau mỗi lần chỉnh chunking, prompt, model hoặc index.
Thiếu dòng cuối, bạn không build hệ thống. Bạn đang chỉnh âm lượng theo cảm giác.
Checklist quyết định trước khi viết code
Trước khi mở VS Code, team nên chốt 6 quyết định này. Không cần họp dài, nhưng phải ghi lại.
| Quyết định | Câu hỏi cần trả lời | Gợi ý thực dụng |
|---|---|---|
| Nguồn tài liệu | Tài liệu nào được phép ingest? | Bắt đầu với 1 loại: PDF API doc, handbook, spec nội bộ |
| Chunking | Cắt tài liệu theo ký tự, heading hay semantic section? | Khởi đầu 400–600 ký tự có overlap, sau đó đo lại |
| Embedding | Dùng model nào để biến text thành vector? | Ưu tiên ổn định API và chi phí trước khi tối ưu sâu |
| Vector store | Lưu vector ở đâu? | pgvector hợp nếu team đã quen Postgres |
| Answer policy | Model được trả lời ngoài context không? | Production nên mặc định “không” |
| Eval | Biết tốt hơn bằng cách nào? | Bộ câu hỏi vàng + log retrieval + feedback người dùng |
Ở đây có một phản đề nhỏ: đừng mặc định fine-tuning là bước tiếp theo sau RAG. Fine-tuning — tinh chỉnh model bằng dữ liệu riêng — hữu ích khi bạn cần model học giọng văn, format, quy tắc nghiệp vụ lặp lại. Nhưng nếu vấn đề là “tài liệu đúng mà không ai tìm ra”, RAG thường là nhát cắt đầu tiên hợp lý hơn.
Fine-tuning có thể biến model thành tài sản chuyên biệt, nhất là trong môi trường enterprise. Nhưng nó không thay thế được retrieval khi tri thức thay đổi liên tục. API doc sửa mỗi tuần mà bạn cứ fine-tune lại thì khác gì bắt cả dàn nhạc tập lại từ đầu chỉ vì đổi một đoạn solo.
Bước 1: dựng đường ống tối thiểu trong một buổi
Mục tiêu của buổi đầu không phải làm UI đẹp. Mục tiêu là có một vòng khép kín: upload → chunk → embed → lưu → hỏi → retrieve → trả lời có context.
Một skeleton đủ dùng:
mkdir rag-docs && cd rag-docs
npm init -y
npm install express multer pdf-parse pg dotenv
npm install @google/generative-ai
Dựng Postgres với pgvector:
# docker-compose.yml
services:
db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: rag
POSTGRES_PASSWORD: rag
POSTGRES_DB: rag_docs
ports:
- "5432:5432"
docker compose up -d
Tạo bảng lưu chunk:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE document_chunks (
id BIGSERIAL PRIMARY KEY,
doc_id TEXT NOT NULL,
chunk_index INT NOT NULL,
content TEXT NOT NULL,
embedding vector(768),
metadata JSONB DEFAULT '{}'
);
Con số 768 phụ thuộc embedding model bạn dùng, nên kiểm tra dimension thật trước khi tạo schema. Đây là chi tiết nhỏ nhưng hay làm team mất cả buổi vì lỗi insert vector không khớp kích thước.
Ở bước ingestion, hãy giữ metadata tối thiểu:
source_filepage_numbernếu cóheadingnếu parse đượccreated_atcontent_hashđể tránh ingest trùng
Ví dụ cụ thể: giả sử team bạn có file payments-api.pdf. Đừng chỉ lưu chunk text. Hãy lưu thêm trang và section. Khi chatbot trả lời sai, bạn cần lần ngược xem nó lấy nhầm đoạn nào. Không có metadata, debug retrieval giống nghe một bản thu bị mất track nhạc cụ.
Bước 2: retrieval phải được xem như API, không phải helper function
Sai lầm phổ biến là viết hàm searchSimilarChunks() rồi quên mất nó là trái tim của hệ thống.
Retrieval — bước tìm đoạn tài liệu liên quan — nên có contract rõ:
async function retrieveContext({ question, topK = 4, docIds = [] }) {
// 1. embed question
// 2. query pgvector by cosine distance
// 3. return chunks with score + metadata
}
Response nên trả cả score, content, metadata, không chỉ text. Vì khi model trả lời tệ, bạn cần biết lỗi nằm ở retrieval hay generation — bước model viết câu trả lời cuối cùng.
Một prompt tối thiểu nên ép model đứng trong hàng rào context:
Bạn là trợ lý trả lời dựa trên tài liệu nội bộ.
Chỉ dùng CONTEXT bên dưới.
Nếu CONTEXT không đủ thông tin, trả lời: "Mình không tìm thấy thông tin này trong tài liệu hiện có."
CONTEXT:
{{chunks}}
QUESTION:
{{question}}
Đừng để model “giúp thêm cho đủ ý” trong bản production đầu tiên. Sự tử tế của model đôi khi là nguồn hallucination — bịa thông tin nhưng nói rất tự tin.
Bước 3: đừng ship trước khi có bộ câu hỏi vàng
RAG không có eval thì giống dàn nhạc không có nhịp phách: ai cũng chơi, nhưng không biết lệch từ đâu.
Bạn không cần hệ thống đánh giá hoành tráng ngay. Một buổi chiều đủ để tạo bộ câu hỏi vàng:
- Chọn 20–30 câu hỏi người mới hay hỏi.
- Với mỗi câu, ghi expected source: file, trang, heading hoặc đoạn đúng.
- Chạy pipeline hiện tại, lưu lại top chunks retrieved.
- Chấm thủ công 3 nhãn:
retrieval_ok: có kéo đúng đoạn không?answer_grounded: câu trả lời có bám context không?abstain_ok: khi thiếu dữ kiện, có biết từ chối không?
Nếu muốn đi xa hơn, bạn có thể mượn tư duy từ product experimentation. A/B test trả lời câu hỏi “phiên bản mới có tốt hơn trung bình không?”. Regression — mô hình hồi quy để ước lượng tác động và độ tin cậy — giúp đọc kết quả chắc hơn khi có nhiều nhóm người dùng. Uplift modeling — ước lượng ai thật sự hưởng lợi từ tính năng — nhắc bạn một điều quan trọng: không phải ai cũng cần RAG chatbot cùng lúc.
Với team Việt Nam nhỏ, mình sẽ không bắt đầu bằng mô hình thống kê phức tạp. Nhưng mình sẽ log đủ dữ liệu để sau này đo được:
- câu hỏi thuộc nhóm nào;
- tài liệu nào được retrieve;
- người dùng có bấm hữu ích không;
- câu nào bị escalated sang Slack hoặc ticket.
Bẫy triển khai: cái mới nhất thường không phải cái đúng nhất
Có ba bẫy mình thấy lặp lại.
Một: nhét cả PDF vào context window. Context window — vùng ngữ cảnh model xử lý được trong một lượt — ngày càng dài, nhưng dài không có nghĩa là rẻ, nhanh, hay dễ kiểm soát. Với tài liệu lớn, retrieval vẫn giúp giảm nhiễu và giảm chi phí mỗi request.
Hai: đổi model liên tục trước khi sửa retrieval. Nếu top chunks đã sai, model xịn hơn chỉ diễn đạt cái sai mượt hơn. Hãy xem log retrieval trước khi nâng cấp generation model.
Ba: cấu hình nằm rải trong code. Chunk size, overlap, topK, prompt version, embedding model, temperature — tất cả nên được cấu hình hóa. Tinh thần của các pipeline dùng config như Gin Config rất đáng học: code ổn định, tham số thí nghiệm nằm ngoài, mỗi run xuất được cấu hình đã dùng. Với RAG, chỉ cần một file như sau đã giúp team đỡ cãi nhau bằng trí nhớ:
{
"chunkSize": 500,
"chunkOverlap": 80,
"topK": 4,
"embeddingModel": "gemini-embedding",
"answerModel": "your-generation-model",
"promptVersion": "grounded-v1"
}
Khi kết quả thay đổi, bạn biết mình đã đổi nốt nhạc nào.
Nếu là mình, mình sẽ triển khai theo nhịp này
Nếu lead một team builder, mình sẽ không bắt đầu bằng câu “chọn model nào?”. Mình sẽ đi theo thứ tự:
- Tuần 1: dựng RAG tối thiểu với Node.js, pgvector, embedding, generation; chưa cần UI ngoài form upload và ô hỏi đáp.
- Tuần 2: thêm metadata, log retrieval, bộ câu hỏi vàng, prompt từ chối khi thiếu context.
- Tuần 3: cho 1 nhóm nhỏ dùng thật, ghi feedback, phân loại câu hỏi thất bại.
- Tuần 4: mới quyết định tối ưu: đổi chunking, thêm reranking, đổi model, hay cân nhắc fine-tuning cho format trả lời đặc thù.
Takeaway gọn: RAG chatbot đáng tin không thắng ở ô chat, mà thắng ở những quyết định âm thầm phía sau. Làm đúng nhịp, hệ thống sẽ đỡ lạc tông; làm vội, bạn chỉ có một ca sĩ tự tin hát sai lời.
---
Bụi Wire — nghiện đọc release notes lúc 2 giờ sáng
Nguồn tham khảo
- How to Build a RAG Chatbot for Your Docs with Node.js, Google Gemini, and pgvector
- Product Experimentation with Regression-Based Causal Inference: Estimating LLM Feature Impact with Python and statsmodels
- Fine-tune NVIDIA Nemotron 3 models with Amazon SageMaker AI serverless model customization | Artificial Intelligence
- Product Experimentation with Uplift Modeling: Targeting Your LLM Feature Rollout to Users Who Actually Benefit (Python Implementation)
- Building a Gin Config Controlled PyTorch Pipeline with Configurable MLP Variants, Cosine Scheduling, and Runtime Parameter Overrides - MarkTechPost