Vector DB 2 — Embeddings & độ tương đồng
Từ bài trước đến bài này
Ở Vector DB 1 — Tổng quan ta đã dựng bức tranh lớn: văn bản → vector → tìm hàng xóm gần nhất. Bài này mổ xẻ hai mắt xích cốt lõi nhất, cũng là hai chỗ hay bị làm ẩu nhất trong thực tế: embedding sinh ra từ đâu, chọn mô hình thế nào và đo "gần nhau" bằng phép đo gì cho đúng. Cộng thêm hai việc chuẩn bị dữ liệu quyết định chất lượng retrieval hơn cả việc chọn database: chunking và metadata.
Bối cảnh xuyên suốt: team dữ liệu NCB muốn xây trợ lý tra cứu văn bản chính sách/quy định nội bộ tiếng Việt — quy chế tín dụng, thông tư NHNN, quy trình mở tài khoản, chính sách phí. Đây là dữ liệu tiếng Việt, dài, có cấu trúc điều–khoản, và sai một ly là ra kết quả sai nghiệp vụ. Mọi quyết định kỹ thuật bên dưới đều soi vào bối cảnh này.
Nếu bạn đã đọc LLM 3 — Embeddings & tìm kiếm vector thì bài này bổ sung chiều sâu về phép đo khoảng cách và chunking mà bài kia chỉ chạm qua.
Embedding sinh ra từ đâu?
Embedding là vector số thực nhiều chiều biểu diễn ngữ nghĩa của một đoạn văn bản. Nhưng con số đó không rơi từ trên trời — nó là đầu ra của một mạng nơ-ron học sâu đã được huấn luyện để đặt các câu gần nghĩa cạnh nhau trong không gian.
Cụ thể, một mô hình embedding văn bản hiện đại thường là một Transformer encoder (họ BERT). Quy trình sinh vector:
- Tokenize: cắt văn bản thành các token con (subword). "quy chế tín dụng" → nhiều token.
- Encoder: chạy qua nhiều tầng self-attention, mỗi token nhận một vector ngữ cảnh.
- Pooling: gộp các vector token thành MỘT vector câu — thường lấy trung bình (mean pooling) hoặc lấy token
[CLS]. - (Tuỳ chọn) chuẩn hoá về độ dài 1.
Điều làm nên chất lượng không phải kiến trúc mà là cách huấn luyện: dùng contrastive learning trên hàng triệu cặp câu (câu hỏi–đoạn trả lời, câu–bản dịch, câu gần nghĩa). Mô hình được ép để cặp "dương" (liên quan) có vector gần, cặp "âm" (không liên quan) có vector xa. Đây là lý do một mô hình được finetune cho retrieval (asymmetric: query ngắn ↔ document dài) sẽ vượt xa một mô hình chỉ học ngôn ngữ chung.
Các họ mô hình phổ biến
| Nhóm | Ví dụ đại diện | Đặc điểm |
|---|---|---|
| Mã nguồn mở, tự host | sentence-transformers (SBERT), họ E5, BGE, GTE | Miễn phí, chạy trên GPU/CPU của bạn, kiểm soát dữ liệu 100% |
| Đa ngôn ngữ mở | paraphrase-multilingual, multilingual-e5, bge-m3 | Hỗ trợ tiếng Việt, huấn luyện trên 50–100+ ngôn ngữ |
| API thương mại | OpenAI text-embedding-3, Cohere embed, Voyage | Chất lượng cao, không cần hạ tầng, trả phí theo token |
Với ngân hàng, yếu tố dữ liệu không được rời khỏi hệ thống nội bộ thường đẩy lựa chọn về phía mô hình mở tự host (ví dụ multilingual-e5-large hoặc bge-m3), chạy trong mạng nội bộ. API thương mại tiện nhưng gửi văn bản chính sách ra ngoài là vướng tuân thủ.
Chọn mô hình embedding: 5 trục đánh đổi
Không có mô hình "tốt nhất" — chỉ có mô hình phù hợp. Cân nhắc 5 trục:
- Chất lượng retrieval: đo trên tập dữ liệu CỦA BẠN (tiếng Việt, văn phong pháp lý), không tin bảng xếp hạng chung. Một mô hình đứng đầu benchmark tiếng Anh có thể tệ với tiếng Việt.
- Hỗ trợ đa ngôn ngữ / tiếng Việt: bắt buộc phải là mô hình multilingual hoặc chuyên tiếng Việt. Mô hình chỉ tiếng Anh sẽ đặt "tài khoản" và "account" xa nhau một cách vô lý.
- Số chiều (dimension): 384, 768, 1024… Chiều cao → biểu diễn tinh hơn nhưng tốn lưu trữ và tính toán (xem mục sau).
- Độ dài context tối đa: mô hình chỉ nhận được N token đầu. Đa số SBERT giới hạn 512 token (~350–400 từ tiếng Việt). Vượt quá là bị cắt cụt âm thầm — phần đuôi văn bản không được embed. Đây là lý do phải chunking.
- Chi phí & vận hành: tự host (chi phí GPU, MLOps) so với API (chi phí/token, phụ thuộc mạng, rủi ro tuân thủ). Một mô hình 1024 chiều nhân với vài triệu chunk là dung lượng và RAM index đáng kể.
Nguyên tắc vàng: query và document phải dùng CÙNG một mô hình embedding. Trộn hai mô hình khác nhau thì hai vector nằm ở hai không gian khác nhau, đo khoảng cách vô nghĩa.
Chiều vector & đánh đổi
Số chiều d là "ngân sách biểu diễn". Trực giác: nhiều chiều hơn cho mô hình nhiều "trục ý nghĩa" hơn để phân biệt sắc thái. Nhưng cái giá tăng tuyến tính theo d ở nhiều chỗ:
- Lưu trữ: mỗi vector
float32chiếmd × 4byte. 5 triệu chunk × 1024 chiều × 4 byte ≈ 20 GB chỉ riêng vector thô, chưa kể index. - RAM cho index: HNSW giữ vector trong bộ nhớ → RAM tỉ lệ với
d. - Thời gian tính khoảng cách: mỗi phép cosine/dot là
dphép nhân. Query nhiều lên thìdnhân vào tất cả.
Xu hướng hiện đại là Matryoshka embedding: mô hình được huấn luyện sao cho có thể cắt bớt chiều (ví dụ 1024 → 256) mà chất lượng giảm rất ít, cho phép đánh đổi linh hoạt. Nếu mô hình không hỗ trợ, đừng tự ý cắt chiều — sẽ hỏng.
Lời khuyên thực dụng: với kho vài triệu chunk, 768–1024 chiều là điểm cân bằng tốt. Đừng chọn 3072 chiều "cho chắc" nếu chưa đo thấy nó thực sự cải thiện recall trên dữ liệu của bạn.
Chuẩn hoá L2 & quan hệ cosine ↔ dot product
Đây là điểm kỹ thuật quan trọng bậc nhất và hay bị hiểu sai.
Chuẩn hoá L2 (L2 normalize) là chia vector cho độ dài của nó, để mọi vector có độ dài (norm) bằng 1 — tức nằm trên mặt cầu đơn vị:
v_norm = v / ||v|| với ||v|| = sqrt(v1² + v2² + ... + vd²)
Quan hệ then chốt giữa hai phép đo:
- Cosine similarity đo góc giữa hai vector:
cos(a,b) = (a·b) / (||a|| · ||b||). Nó CHỈ quan tâm hướng, bỏ qua độ lớn. - Dot product (tích vô hướng)
a·bquan tâm cả hướng LẪN độ lớn.
Khi cả hai vector đã chuẩn hoá L2 (norm = 1), mẫu số ||a|| · ||b|| = 1, nên:
cosine(a, b) = dot(a, b) (khi ||a|| = ||b|| = 1)
Hệ quả thực tế cực kỳ hữu ích: nếu bạn chuẩn hoá tất cả embedding trước khi lưu, thì có thể dùng dot product (nhanh hơn, không cần chia) mà kết quả xếp hạng giống hệt cosine. Nhiều database vector và index ANN mặc định giả định vector đã chuẩn hoá vì lý do này. Đa số mô hình sentence-transformers cho phép bật normalize_embeddings=True ngay lúc encode.
Các phép đo khoảng cách/tương đồng
| Phép đo | Đo cái gì | Càng gần khi | Dùng cho |
|---|---|---|---|
| Cosine similarity | Góc giữa 2 vector | Giá trị càng gần 1 | Text/embedding ngôn ngữ — mặc định |
| Dot product | Hướng + độ lớn | Giá trị càng lớn | Khi độ lớn mang thông tin (một số mô hình) |
| Euclidean (L2) | Khoảng cách đường thẳng | Giá trị càng gần 0 | Dữ liệu số, ảnh, cụm |
| Manhattan (L1) | Tổng trị tuyệt đối chênh lệch từng chiều | Càng gần 0 | Ít dùng cho text; bền với ngoại lai |
Khi nào dùng cái nào?
- Cosine là lựa chọn mặc định cho embedding văn bản. Lý do: với text, "chủ đề/ý nghĩa" nằm ở hướng của vector, còn độ lớn thường phản ánh độ dài/tần suất từ — thứ ta KHÔNG muốn ảnh hưởng đến độ liên quan. Câu ngắn và câu dài cùng chủ đề nên được coi là gần nhau.
- Dot product: dùng khi mô hình được huấn luyện để mã hoá "độ mạnh/độ tin cậy" vào độ lớn vector (một số mô hình retrieval bất đối xứng). Nếu vector đã chuẩn hoá thì dot = cosine, không khác biệt.
- Euclidean/L2: hợp lý cho embedding ảnh, dữ liệu số nơi độ lớn thật sự có nghĩa vật lý. Lưu ý: trên vector đã chuẩn hoá L2, thứ tự xếp hạng theo Euclidean và cosine là tương đương (vì
||a−b||² = 2 − 2·cos(a,b)). - Manhattan/L1: hiếm dùng cho text; đôi khi bền hơn với chiều nhiễu.
Điều bắt buộc phải nhất quán: phép đo lúc lập index phải trùng phép đo lúc truy vấn. Index HNSW dựng theo cosine mà query bằng L2 sẽ cho kết quả sai hoàn toàn.
"Lời nguyền số chiều" (curse of dimensionality)
Ở không gian rất nhiều chiều xảy ra hiện tượng phản trực giác: khoảng cách giữa điểm gần nhất và điểm xa nhất co lại, mọi điểm trở nên "cách đều" nhau một cách tương đối. Hệ quả: khái niệm "hàng xóm gần nhất" mờ đi, và tìm kiếm chính xác (brute-force) trở nên kém hiệu quả.
May mắn là embedding thực tế không rải đều khắp không gian d chiều — chúng nằm trên một đa tạp (manifold) chiều thấp hơn nhiều, nơi ngữ nghĩa có cấu trúc. Nhờ vậy tìm kiếm ngữ nghĩa vẫn hoạt động tốt. Nhưng bài học rút ra: (1) đừng cho rằng "càng nhiều chiều càng tốt" vô hạn; (2) đây chính là lý do ta cần index ANN (approximate nearest neighbor) thay vì quét toàn bộ — chủ đề của Vector DB 3 — ANN & index.
Chuẩn bị dữ liệu (1): Chunking
Đây là nơi quyết định chất lượng retrieval nhiều nhất, hơn cả việc chọn database. Không thể embed cả một quy chế 40 trang thành 1 vector — vừa vượt context, vừa "trung bình hoá" mọi ý thành một vector nhạt nhẽo. Ta phải cắt tài liệu thành các chunk rồi embed từng chunk.
Ba trục cần chỉnh:
- Kích thước chunk: chunk quá to → nhiều ý trộn lẫn, vector nhoè, khó khớp câu hỏi cụ thể. Chunk quá nhỏ → mất ngữ cảnh, một điều bị xé làm nhiều mảnh vô nghĩa. Điểm khởi đầu hợp lý cho tiếng Việt: 300–500 token mỗi chunk (nhớ giới hạn context của mô hình).
- Overlap (chồng lấn): cho hai chunk liền kề chia sẻ một phần (ví dụ 10–20% cuối chunk trước lặp lại ở đầu chunk sau). Mục đích: một câu bị cắt ngang ranh giới chunk vẫn xuất hiện đầy đủ ở ít nhất một chunk, tránh mất thông tin ở đường cắt.
- Chiến lược cắt: đừng cắt máy móc theo số ký tự cố định. Ưu tiên cắt theo cấu trúc ngữ nghĩa:
- Theo cấu trúc văn bản: với văn bản pháp lý, cắt theo Điều / Khoản / Mục là lý tưởng — mỗi Điều là một đơn vị ý nghĩa tự nhiên.
- Theo đoạn/câu: gộp các câu đến khi đủ kích thước, không cắt giữa câu.
- Recursive splitting: thử cắt theo dấu phân đoạn lớn trước (heading), nếu vẫn quá dài mới xuống mức nhỏ hơn.
Với chính sách ngân hàng, cắt theo Điều/Khoản gần như luôn thắng cắt theo độ dài cố định, vì câu hỏi nghiệp vụ thường ánh xạ 1–1 với một điều khoản.
Chuẩn bị dữ liệu (2): Metadata
Mỗi chunk không nên chỉ có vector và text — hãy đính kèm metadata có cấu trúc:
- Nguồn: tên văn bản, số hiệu, cơ quan ban hành.
- Vị trí: số Điều/Khoản, số trang → để trích dẫn chính xác khi trả lời.
- Thời gian: ngày ban hành, ngày hiệu lực, còn hiệu lực hay đã bị thay thế.
- Phân loại: mảng nghiệp vụ (tín dụng, huy động, thẻ…), mức bảo mật.
Metadata phục vụ hai việc sống còn: lọc trước/sau khi tìm (chỉ tìm trong văn bản CÒN HIỆU LỰC, đúng mảng nghiệp vụ) và trích dẫn nguồn để cán bộ tin được câu trả lời. Một trợ lý chính sách không dẫn được "theo Điều 5 Quy chế X" thì vô dụng trong ngân hàng.
Ví dụ code Python (sentence-transformers)
Minh hoạ toàn bộ vòng: embed, chuẩn hoá, và đo tương đồng. Đây là code minh hoạ Python — không phải SQL chạy trong sandbox.
from sentence_transformers import SentenceTransformer
import numpy as np
# Mô hình đa ngôn ngữ hỗ trợ tiếng Việt (minh hoạ)
model = SentenceTransformer("intfloat/multilingual-e5-large")
# Các chunk chính sách (đã cắt theo Điều/Khoản)
docs = [
"Điều 5. Khách hàng cá nhân mở tài khoản thanh toán phải xuất trình CCCD còn hiệu lực.",
"Điều 12. Phí duy trì tài khoản được thu vào ngày làm việc cuối cùng của tháng.",
"Điều 3. Hạn mức rút tiền mặt tại ATM tối đa 20 triệu đồng mỗi ngày.",
]
query = "cần giấy tờ gì để mở tài khoản?"
# normalize_embeddings=True -> vector có norm = 1
doc_emb = model.encode(docs, normalize_embeddings=True)
q_emb = model.encode([query], normalize_embeddings=True)[0]
# Vì đã chuẩn hoá L2: dot product == cosine similarity
scores = doc_emb @ q_emb # tích vô hướng
best = int(np.argmax(scores))
print(f"Số chiều vector: {doc_emb.shape[1]}")
print(f"Chunk khớp nhất: {docs[best]}")
print(f"Điểm cosine: {scores[best]:.3f}")
Điểm cần thấy: sau normalize_embeddings=True, phép doc_emb @ q_emb (dot product) chính là cosine — nhanh và gọn. Chunk khớp nhất phải là Điều 5, dù câu hỏi không chứa từ "CCCD" hay "xuất trình" nào.
Đánh giá chất lượng embedding/retrieval
Đừng chọn mô hình bằng cảm tính. Xây một tập đánh giá nhỏ (gold set): vài chục đến vài trăm câu hỏi nghiệp vụ thật, mỗi câu gán sẵn chunk đúng phải được trả về. Rồi đo:
| Chỉ số | Ý nghĩa | Khi nào quan trọng |
|---|---|---|
| Recall@k | Tỉ lệ câu hỏi mà chunk đúng nằm trong top-k kết quả | Quan trọng nhất cho RAG: chỉ cần đúng LỌT vào ngữ cảnh |
| MRR (Mean Reciprocal Rank) | Trung bình nghịch đảo thứ hạng của kết quả đúng đầu tiên | Khi thứ hạng #1 quan trọng |
| nDCG@k | Chất lượng xếp hạng có tính đến vị trí và độ liên quan phân cấp | Khi nhiều kết quả cùng liên quan ở mức khác nhau |
| Precision@k | Tỉ lệ kết quả top-k thực sự liên quan | Khi muốn giảm nhiễu đưa vào LLM |
Với RAG, Recall@k (ví dụ Recall@5) thường là chỉ số bắc cầu quan trọng nhất: nếu chunk đúng không lọt vào top-k thì LLM chắc chắn trả lời sai, bất kể prompt hay đến đâu. Quy trình chuẩn: cố định gold set, thử 2–3 mô hình embedding + vài cấu hình chunking, chọn cái cho Recall@5 cao nhất.
Re-embed khi đổi mô hình
Một sự thật hay bị bỏ quên: vector gắn chặt với mô hình sinh ra nó. Nếu bạn đổi mô hình embedding (nâng cấp phiên bản, đổi nhà cung cấp, đổi số chiều), thì:
- Toàn bộ kho vector cũ trở nên vô dụng — chúng ở không gian khác với vector query mới.
- Phải re-embed lại 100% dữ liệu bằng mô hình mới, rồi thay index.
Đây là thao tác tốn kém (tính toán lại vài triệu chunk) nên cần lên kế hoạch: chạy re-embed vào bảng/index song song (blue-green), đánh giá lại trên gold set trước khi cắt chuyển. Đừng bao giờ trộn vector cũ và mới trong cùng một index. Vì vậy, hãy cân nhắc kỹ ngay từ đầu — đổi mô hình về sau là một cuộc di trú dữ liệu, không phải một dòng cấu hình.
Use case thực tế
Bối cảnh: NCB xây trợ lý tra cứu chính sách nội bộ. Kho gồm 1.200 văn bản (quy chế, quy trình, thông tư áp dụng) — khoảng 48.000 chunk sau khi cắt theo Điều/Khoản, trung bình ~380 token/chunk.
Quyết định kỹ thuật:
- Mô hình:
multilingual-e5-large(1024 chiều), tự host trong mạng nội bộ — dữ liệu chính sách không được ra ngoài. Bật chuẩn hoá L2 khi encode. - Phép đo: cosine (tương đương dot product vì đã chuẩn hoá) — chuẩn cho text.
- Chunking: cắt theo Điều/Khoản, overlap ~15%; mỗi chunk gắn metadata
{so_hieu, dieu, ngay_hieu_luc, con_hieu_luc, mang_nghiep_vu}. - Lưu trữ: dung lượng vector thô ≈ 48.000 × 1024 × 4 byte ≈ 196 MB — nhỏ, thoải mái dùng HNSW in-memory.
Đo lường: gold set 150 câu hỏi thật từ cán bộ chi nhánh. So 3 phương án:
| Cấu hình | Recall@5 | MRR |
|---|---|---|
| Cắt cố định 512 token, không overlap | 0.71 | 0.58 |
| Cắt theo Điều/Khoản + overlap 15% | 0.89 | 0.77 |
| Cắt theo Điều/Khoản + mô hình chỉ tiếng Anh | 0.52 | 0.40 |
Kết luận: chỉ đổi chiến lược chunking (không đổi mô hình) đã kéo Recall@5 từ 0.71 lên 0.89 — cải thiện lớn hơn nhiều so với việc loay hoay đổi database. Và mô hình chỉ tiếng Anh sụp đổ trên tiếng Việt, khẳng định tính bắt buộc của mô hình đa ngôn ngữ. Bộ lọc metadata con_hieu_luc = true loại bỏ hoàn toàn các câu trả lời dẫn văn bản đã hết hiệu lực — điều tối quan trọng về tuân thủ.
Ghi nhớ
- Embedding là đầu ra của Transformer encoder huấn luyện bằng contrastive learning; chất lượng đến từ cách huấn luyện, không phải kiến trúc.
- Chọn mô hình theo 5 trục: chất lượng (đo trên dữ liệu của bạn), đa ngôn ngữ, số chiều, độ dài context, chi phí/tự host. Ngân hàng thường phải tự host mô hình mở đa ngôn ngữ vì tuân thủ.
- Query và document phải cùng một mô hình. Đổi mô hình = phải re-embed 100% kho vector.
- Chuẩn hoá L2 đưa vector về norm = 1; khi đó cosine = dot product → dùng dot product cho nhanh.
- Cosine là mặc định cho text (đo hướng, bỏ qua độ lớn); L2/Manhattan hợp dữ liệu số. Phép đo lúc index phải trùng lúc query.
- Chunking quyết định chất lượng nhất: cắt theo cấu trúc (Điều/Khoản) + overlap thắng cắt cố định; kích thước 300–500 token cho tiếng Việt.
- Metadata cho phép lọc (còn hiệu lực, đúng nghiệp vụ) và trích dẫn nguồn — bắt buộc trong ngân hàng.
- Đánh giá bằng Recall@k (quan trọng nhất cho RAG), MRR, nDCG trên một gold set câu hỏi thật; đừng chọn mô hình bằng cảm tính.
- "Lời nguyền số chiều": nhiều chiều không phải lúc nào cũng tốt hơn; đây là lý do cần index ANN (xem Vector DB 3).
Bài viết liên quan
Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.
Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.
Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.
Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.