LLM 3 — Embeddings & tìm kiếm vector
Mở đầu: từ "khớp từ" đến "hiểu ý"
Khi bạn gõ vào ô tìm kiếm truyền thống cụm "cách hạ sốt cho trẻ", hệ thống sẽ đi tìm những tài liệu chứa đúng các từ đó. Nếu một bài viết rất hay lại dùng cụm "giảm thân nhiệt cho bé", nó có thể bị bỏ sót hoàn toàn — vì không có từ nào trùng khớp. Đây là giới hạn cốt lõi của tìm kiếm từ khoá (lexical search): máy so khớp ký tự, chứ không hiểu nghĩa.
Embedding và tìm kiếm vector (vector search) ra đời để giải quyết đúng vấn đề này. Ý tưởng: thay vì so khớp từ, ta biến mỗi đoạn văn bản thành một dãy số (vector) sao cho những đoạn có ý nghĩa gần nhau thì vector cũng gần nhau trong không gian. Khi đó "giảm thân nhiệt cho bé" và "hạ sốt cho trẻ" nằm sát nhau, và ta tìm được nhau dù không chung một từ nào.
Đây chính là nền tảng của RAG (Retrieval-Augmented Generation) — kỹ thuật cho phép mô hình như Claude trả lời dựa trên kho tài liệu riêng của bạn. Bài này tập trung vào lớp nền: embedding, độ đo tương đồng, cách tạo và lưu trữ vector, chunking, và index xấp xỉ (ANN). Hiểu chắc phần này, bạn sẽ xây RAG (xem LLM 4 — RAG) một cách vững vàng thay vì "ghép cho chạy".
Nếu bạn muốn ôn lại trực giác về embedding ở tầng khái niệm ML, xem thêm AI 5 — NLP & Embeddings. Bài này đi sâu vào ứng dụng kỹ thuật: lưu vào PostgreSQL và truy vấn trong sản xuất.
Embedding là gì?
Embedding là phép biểu diễn một đối tượng (ở đây là văn bản) thành một vector số thực nhiều chiều — ví dụ 1024 chiều. Mỗi chiều không có ý nghĩa "con người đọc được", nhưng toàn bộ vector mã hoá được ý nghĩa ngữ nghĩa của đoạn văn bản đó.
Tính chất then chốt: văn bản gần nhau về ý nghĩa → vector gần nhau về khoảng cách hình học. Mô hình embedding được huấn luyện để đảm bảo tính chất này. Ví dụ, vector của "con chó" gần vector "con mèo" (đều là thú cưng) hơn là gần vector "hoá đơn tiền điện".
Semantic vs lexical: khác biệt cốt lõi
| Tiêu chí | Tìm kiếm từ khoá (lexical) | Tìm kiếm ngữ nghĩa (semantic) |
|---|---|---|
| So khớp trên | Ký tự / token trùng nhau | Ý nghĩa (vector gần nhau) |
| Xử lý từ đồng nghĩa | Kém (phải khai báo thủ công) | Tốt (hiểu tự nhiên) |
| Xử lý lỗi chính tả nhẹ | Kém | Khá tốt |
| Khớp chính xác mã/số/tên riêng | Rất tốt | Có thể kém |
| Chi phí | Rẻ | Cần tạo & lưu vector |
Điểm cần nhớ ngay: semantic không phải luôn tốt hơn lexical. Với mã sản phẩm "SKU-9921" hay tên riêng chính xác, tìm từ khoá lại thắng. Vì thế phần cuối bài ta sẽ nói về hybrid search — kết hợp cả hai.
Độ đo tương đồng: đo "gần" như thế nào?
Khi đã có vector, ta cần một cách đo "hai vector gần nhau đến đâu". Ba độ đo phổ biến nhất:
1. Cosine similarity (phổ biến nhất)
Đo góc giữa hai vector, bỏ qua độ dài. Giá trị chạy từ -1 (ngược hướng) đến 1 (cùng hướng). Với embedding văn bản, cosine ~1 nghĩa là rất tương đồng.
$$\text{cosine}(A, B) = \frac{A \cdot B}{|A| , |B|}$$
Vì cosine chỉ quan tâm hướng, nó ít bị ảnh hưởng bởi việc một đoạn văn dài hơn (vector "to" hơn). Đây là lý do nó là lựa chọn mặc định cho phần lớn hệ RAG.
2. Dot product (tích vô hướng)
$A \cdot B = \sum A_i B_i$. Nếu các vector đã được chuẩn hoá (độ dài = 1) thì dot product tương đương cosine — và tính nhanh hơn vì bỏ được phép chia. Nhiều mô hình trả về vector đã chuẩn hoá sẵn, khi đó dùng dot product là hợp lý.
3. Khoảng cách L2 (Euclidean)
Khoảng cách "đường chim bay" giữa hai điểm. Càng nhỏ càng gần. Với vector đã chuẩn hoá, thứ tự xếp hạng theo L2 và theo cosine là tương đương — nên chọn cái nào thường chỉ là quy ước.
Chuẩn hoá vector (normalization): đưa mọi vector về độ dài 1. Việc này giúp cosine, dot và L2 nhất quán với nhau, và tránh việc vector dài "áp đảo". Quy tắc thực dụng: dùng cùng một độ đo cho lúc tạo index và lúc truy vấn, đừng trộn lẫn.
Tạo embedding: dùng mô hình nào?
Đây là điểm cần nói rõ vì dễ nhầm: Anthropic không cung cấp API embedding riêng. Claude là mô hình sinh văn bản (generation), không phải mô hình embedding. Trong tài liệu chính thức, Anthropic khuyến nghị dùng Voyage AI — đối tác được Anthropic giới thiệu — làm nhà cung cấp embedding.
Voyage AI (khuyến nghị)
Voyage AI cung cấp nhiều mô hình embedding chất lượng cao. Một số điểm đáng chú ý (tính đến giữa 2025):
- Họ voyage-3 là mô hình đa mục đích, với cửa sổ ngữ cảnh 32.000 token và số chiều mặc định 1024.
- Có biến thể tối ưu chi phí (
-lite) và biến thể chất lượng cao hơn (voyage-3-large,voyage-3.5). - Hỗ trợ kỹ thuật Matryoshka cho phép cắt bớt số chiều (ví dụ xuống 256) để tiết kiệm bộ nhớ mà vẫn giữ phần lớn chất lượng.
Lưu ý bối cảnh: Voyage AI đã được MongoDB mua lại (đầu 2025) và nay hoạt động dưới tên "Voyage AI by MongoDB". Đây là quan hệ đối tác/khuyến nghị, không phải Anthropic sở hữu Voyage.
Mô hình mở (self-host)
Nếu cần chạy nội bộ (dữ liệu nhạy cảm, không muốn gọi API ngoài), có các mô hình mở phổ biến:
- sentence-transformers (thư viện gốc, nhiều model).
- BGE (BAAI general embedding) — mạnh, có bản đa ngôn ngữ.
- E5 (multilingual-e5) — tốt cho tiếng Việt và đa ngôn ngữ.
Chọn model theo tiêu chí nào?
- Chất lượng retrieval: tham khảo benchmark như MTEB, nhưng nên tự đánh giá trên dữ liệu thật của bạn.
- Chi phí: API tính tiền theo token; self-host tốn hạ tầng GPU/CPU.
- Độ dài ngữ cảnh: model chịu được đoạn dài bao nhiêu token.
- Số chiều: chiều càng nhiều càng tốn bộ nhớ và index chậm hơn.
- Đa ngôn ngữ: với nội dung tiếng Việt, ưu tiên model đa ngôn ngữ.
Quy tắc vàng, nhắc lại vì cực kỳ quan trọng: phải dùng cùng một model để tạo embedding cho tài liệu (index) và cho câu truy vấn (query). Vector từ hai model khác nhau không so sánh được — đổi model là phải tạo lại (re-index) toàn bộ.
Toàn cảnh luồng dữ liệu
Sơ đồ này là "khung xương" của mọi hệ RAG: tài liệu được chia nhỏ, đưa qua model để thành vector, lưu vào pgvector; khi có câu hỏi, ta biến câu hỏi thành vector bằng cùng model, rồi tìm các chunk gần nhất.
Tạo & lưu embedding (minh hoạ)
Ví dụ dưới đây minh hoạ việc gọi Voyage AI để tạo embedding cho các chunk và lưu vào PostgreSQL có bật pgvector. Đây là code minh hoạ — cần cài extension, có API key và bảng thật mới chạy được.
# (minh hoạ) Tạo embedding bằng Voyage AI và lưu vào pgvector
import voyageai
import psycopg2
vo = voyageai.Client() # đọc VOYAGE_API_KEY từ biến môi trường
chunks = [
"Cách hạ sốt an toàn cho trẻ nhỏ tại nhà.",
"Hướng dẫn thanh toán hoá đơn tiền điện online.",
"Triệu chứng và cách xử trí khi bé bị sốt cao.",
]
# input_type="document" báo cho model biết đây là dữ liệu để index
result = vo.embed(chunks, model="voyage-3", input_type="document")
vectors = result.embeddings # list các vector 1024 chiều
conn = psycopg2.connect("dbname=kb user=app")
cur = conn.cursor()
# Lưu ý: kèm metadata (nguồn, tiêu đề) rất quan trọng cho RAG
for text, vec in zip(chunks, vectors):
cur.execute(
"INSERT INTO documents (content, embedding, source) "
"VALUES (%s, %s, %s)",
(text, vec, "so-tay-y-te.pdf"),
)
conn.commit()
Khi truy vấn, ta biến câu hỏi thành vector với input_type="query" (nhiều model của Voyage phân biệt document vs query để tối ưu chất lượng), rồi so khớp với các vector đã lưu.
Lưu & truy vấn với pgvector
pgvector là extension của PostgreSQL cho phép lưu và tìm kiếm vector ngay trong database quan hệ — không cần một "vector database" riêng. Điều này cực kỳ tiện: dữ liệu nghiệp vụ, metadata và vector nằm chung một chỗ, transaction và JOIN vẫn dùng bình thường. Chi tiết cài đặt extension xem PostgreSQL — Extensions.
Kiểu dữ liệu và bảng
-- (minh hoạ) Bật extension và tạo bảng có cột vector
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id bigserial PRIMARY KEY,
content text NOT NULL,
source text,
embedding vector(1024) -- số chiều phải khớp với model
);
Số trong vector(1024) phải bằng đúng số chiều model trả về. Dùng voyage-3 mặc định là 1024; nếu cắt Matryoshka xuống 256 thì khai báo vector(256).
Toán tử khoảng cách
pgvector cung cấp các toán tử để đo khoảng cách trực tiếp trong SQL:
| Toán tử | Ý nghĩa | Dùng khi |
|---|---|---|
<-> | Khoảng cách L2 (Euclidean) | Vector chuẩn hoá / mặc định |
<=> | Khoảng cách cosine (1 − cosine similarity) | Phổ biến nhất cho văn bản |
<#> | Dot product âm | Vector đã chuẩn hoá, cần nhanh |
Với <=>, giá trị càng nhỏ càng gần (vì nó là khoảng cách, không phải similarity). Muốn ra điểm similarity thì tính 1 - (embedding <=> query).
Truy vấn nearest neighbor (minh hoạ)
-- (minh hoạ) Tìm k=5 chunk gần nhất về ngữ nghĩa với câu hỏi
-- :query_vec là vector của câu hỏi, tạo bằng CÙNG model
SELECT
id,
content,
source,
1 - (embedding <=> :query_vec) AS similarity
FROM documents
ORDER BY embedding <=> :query_vec -- cosine: nhỏ nhất = gần nhất
LIMIT 5;
Mẫu ORDER BY embedding <=> query LIMIT k là khuôn mẫu cốt lõi của mọi truy vấn vector trong pgvector. Nhớ: ORDER BY theo khoảng cách, và LIMIT k để lấy top-k.
Chunking: vì sao và như thế nào?
Ta không thể (và không nên) embedding cả một tài liệu 50 trang thành một vector duy nhất. Vì sao?
- Giới hạn context của model: model chỉ nhận được tối đa một số token nhất định.
- Độ chính xác retrieval: một vector cho cả tài liệu dài sẽ "trung bình hoá" mọi ý, làm nhoè. Chia nhỏ giúp mỗi vector đại diện cho một ý cụ thể, khớp câu hỏi chính xác hơn.
- Đưa vào prompt: khi làm RAG, ta chỉ nhét vài chunk liên quan nhất vào prompt của Claude, không thể nhét cả tài liệu.
Chiến lược chunking
- Theo cấu trúc: cắt theo đoạn, mục, câu — tôn trọng ranh giới ngữ nghĩa tự nhiên (đừng cắt giữa câu).
- Kích thước: một khoảng hợp lý thường là ~200–800 token mỗi chunk. Nhỏ quá thì mất ngữ cảnh; lớn quá thì bị nhoè và tốn context.
- Overlap (chồng lấn): cho các chunk kề nhau chia sẻ một phần (ví dụ 10–20%) để tránh mất ý ở ranh giới. Câu quan trọng nằm đúng chỗ cắt sẽ không bị "đứt đôi".
- Metadata kèm chunk: lưu kèm nguồn tài liệu, tiêu đề mục, số trang, ngày. Metadata giúp lọc (chỉ tìm trong tài liệu năm 2025), trích dẫn nguồn cho câu trả lời, và gỡ lỗi.
Chunking là nơi nhiều hệ RAG "thắng hay thua". Không có con số vàng tuyệt đối — hãy thử vài kích thước trên dữ liệu thật và đo chất lượng retrieval.
ANN: tìm gần đúng nhưng đủ nhanh
Nếu bạn có 1.000 vector, việc so khớp câu hỏi với từng vector (brute-force, gọi là "exact nearest neighbor") vẫn nhanh. Nhưng với hàng triệu vector, quét toàn bộ mỗi truy vấn sẽ quá chậm.
ANN (Approximate Nearest Neighbor) giải quyết bằng cách xây một cấu trúc index cho phép tìm nhanh — đổi lại chấp nhận có thể bỏ sót vài kết quả (giảm recall một chút). Đây là đánh đổi recall vs tốc độ: index tốt cho ta ~99% kết quả đúng với tốc độ nhanh hơn nhiều lần.
HNSW — index được ưa dùng
pgvector hỗ trợ hai loại index chính: HNSW và IVFFlat. HNSW (Hierarchical Navigable Small World) thường cho chất lượng tốt hơn và là lựa chọn mặc định của nhiều hệ thống sản xuất.
-- (minh hoạ) Tạo HNSW index cho cột embedding, dùng khoảng cách cosine
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Chú ý vector_cosine_ops phải khớp với toán tử bạn sẽ dùng khi truy vấn (<=>). Nếu dùng L2 thì là vector_l2_ops, dot product thì vector_ip_ops.
Các tham số HNSW cần biết
m(mặc định 16): số kết nối tối đa mỗi nút trong đồ thị.mlớn → đồ thị dày hơn → tìm chính xác/nhanh hơn nhưng tốn bộ nhớ và build lâu hơn.ef_construction(mặc định 64): số ứng viên xét khi xây index. Lớn hơn → recall tốt hơn nhưng build chậm hơn.ef_search(mặc định 40): số ứng viên xét khi truy vấn. Đây là "cần gạt" quan trọng nhất lúc chạy: tăngef_search→ recall cao hơn nhưng truy vấn chậm hơn.
ef_search có thể chỉnh theo phiên mà không cần build lại index:
-- (minh hoạ) Tăng recall cho các truy vấn trong phiên hiện tại
SET hnsw.ef_search = 100;
Chiến lược thực dụng: để m và ef_construction ở mặc định lúc đầu, rồi điều chỉnh ef_search để cân giữa recall và độ trễ cho đúng nhu cầu.
Hybrid search: kết hợp vector + full-text
Như đã nói, semantic search yếu ở khớp chính xác từ khoá, tên riêng, mã số. Ngược lại, full-text search (BM25 / tsvector trong PostgreSQL) yếu ở từ đồng nghĩa và ý nghĩa. Hybrid search kết hợp cả hai để bù nhược điểm cho nhau.
Cách làm phổ biến:
- Chạy song song: một nhánh tìm vector (
<=>), một nhánh full-text (ts_ranktrêntsvector— xem PostgreSQL — Full-text search). - Hợp nhất kết quả bằng một thuật toán như RRF (Reciprocal Rank Fusion) — cộng điểm dựa trên thứ hạng ở mỗi nhánh, không cần cân chỉnh trọng số phức tạp.
- (Tuỳ chọn) rerank top kết quả bằng một mô hình reranker để sắp xếp lại theo mức liên quan.
Trong PostgreSQL, cái hay là bạn có thể làm cả hai ngay trong một database — tsvector cho full-text và pgvector cho vector — rồi hợp nhất bằng SQL. Không cần dựng thêm hệ thống riêng. Ta sẽ dùng hybrid search thực tế trong LLM 4 — RAG.
Use case thực tế: tìm kiếm ngữ nghĩa trên kho tài liệu nội bộ
Hãy hình dung một công ty có kho 5.000 tài liệu nội bộ: quy trình, chính sách, sổ tay kỹ thuật. Nhân viên muốn hỏi bằng ngôn ngữ tự nhiên: "Quy trình xin nghỉ phép dài hạn thế nào?" Với tìm kiếm từ khoá cũ, nếu tài liệu ghi "chế độ nghỉ không lương kéo dài" thì kết quả có thể trống rỗng.
Luồng xây dựng:
- Ingest: với mỗi tài liệu, chia thành chunk ~500 token, overlap ~15%, kèm metadata (tên file, phòng ban, ngày cập nhật).
- Embed: đưa từng chunk qua voyage-3 (
input_type="document"), ra vector 1024 chiều. - Store: lưu vào bảng
documentsvới cộtembedding vector(1024), tạo HNSW index vớivector_cosine_ops. - Query: câu hỏi của nhân viên được embed (
input_type="query"), rồiORDER BY embedding <=> :q LIMIT 5. - Lọc metadata: thêm
WHERE department = 'HR'để chỉ tìm trong đúng phòng ban — đây là ưu thế lớn khi vector nằm ngay trong PostgreSQL. - Hybrid (tuỳ chọn): với câu hỏi chứa mã tài liệu chính xác, trộn thêm full-text để không bỏ sót.
Kết quả: nhân viên tìm ra tài liệu đúng dù dùng từ ngữ khác hẳn, kèm trích dẫn nguồn từ metadata. Đây chính là "phần retrieval" của một hệ RAG hoàn chỉnh.
Ứng dụng ngoài RAG
Embedding không chỉ để tìm kiếm/RAG. Vì nó biến ý nghĩa thành khoảng cách hình học, nó mở ra nhiều bài toán:
- Khử trùng lặp (dedup): hai đoạn văn gần như trùng nghĩa sẽ có vector rất gần → phát hiện nội dung lặp/đạo.
- Phân cụm (clustering): gom các văn bản tương tự thành nhóm (ví dụ gom phản hồi khách hàng theo chủ đề) mà không cần nhãn trước.
- Gợi ý (recommendation): "sản phẩm/bài viết tương tự" = tìm vector gần nhất với món người dùng đang xem.
- Phân loại (classification): so vector văn bản với vector của các nhãn mẫu, hoặc dùng làm đặc trưng đầu vào cho một bộ phân loại nhẹ.
Ghi nhớ
- Embedding biến văn bản thành vector nhiều chiều: ý nghĩa gần nhau → vector gần nhau. Đây là semantic search, khác hẳn khớp từ khoá (lexical).
- Độ đo: cosine phổ biến nhất; dot product ~ cosine khi vector đã chuẩn hoá; L2 cho kết quả xếp hạng tương đương khi đã chuẩn hoá. Dùng một độ đo nhất quán cho index và query.
- Anthropic không có embedding API; khuyến nghị dùng Voyage AI (đối tác) hoặc mô hình mở (sentence-transformers, BGE, E5). Luôn dùng cùng model cho index và query.
- pgvector: kiểu
vector(n), toán tử<->(L2),<=>(cosine),<#>(dot). Khuôn mẫu truy vấn:ORDER BY embedding <=> q LIMIT k. - Chunking: chia tài liệu vì giới hạn context và độ chính xác retrieval; ~200–800 token, có overlap, kèm metadata.
- ANN/HNSW: cần khi dữ liệu lớn; đánh đổi recall vs tốc độ. Tham số:
m,ef_construction(lúc build),ef_search(lúc query — chỉnh nhanh nhất). - Hybrid search = vector + full-text (
tsvector), hợp nhất bằng RRF, để bù nhược điểm của mỗi bên. - Ngoài RAG, embedding còn dùng cho dedup, phân cụm, gợi ý và phân loại.
Bước tiếp theo: ghép tất cả lại thành một hệ hỏi-đáp hoàn chỉnh trong LLM 4 — RAG, với phần soạn prompt đưa ngữ cảnh vào Claude đã bàn ở LLM 2 — Prompting.
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ế.