---
title: "Embedding là gì? Chạy thử và chọn embedding model cho RAG tiếng Việt"
description: "Embedding là gì và vì sao quyết định RAG tìm đúng hay sai? Tạo vector embedding bằng Python, đo cosine similarity, so sánh và chọn embedding model tiếng Việt."
canonical: "https://200lab.io/blog/embedding-la-gi"
published: "2026-11-03T06:18:53Z"
updated: "2026-11-03T06:28:24Z"
authors: ["Việt Trần"]
tags: ["AI"]
reading_minutes: 14
access: "public"
---

Giả sử khách hỏi chatbot shop thời trang: "Mua nhầm size thì gửi lại được không?". Tài liệu của shop chỉ ghi "đổi trả", nên tìm theo từ khóa sẽ trả về rỗng. Vector embedding giúp tìm theo ý nghĩa và là phần lõi của RAG. Bài này mình chạy thử code thật và so sánh vài model, không giải thích lại RAG từ đầu.

## Embedding là gì?

Embedding là cách biến một đoạn text thành một vector, tức là danh sách các số thực mà mỗi số đại diện cho một chiều. Cách làm này cũng áp dụng được cho hình ảnh hoặc đoạn code.

Các embedding model được huấn luyện để những đoạn dữ liệu có ý nghĩa tương đồng tạo ra các vector nằm gần nhau. Khoảng cách giữa hai vector càng nhỏ thì hai nội dung càng liên quan, còn khoảng cách lớn chứng tỏ chúng ít liên quan.

## Embedding dùng để làm gì

Theo [hướng dẫn embeddings của OpenAI](https://platform.openai.com/docs/guides/embeddings), kỹ thuật này thường dùng cho các bài toán:

- Tìm kiếm và xếp hạng kết quả theo độ liên quan.
- Gom nhóm các đoạn dữ liệu tương đồng, hay gọi là clustering.
- Gợi ý nội dung hoặc sản phẩm liên quan.
- Phát hiện điểm dữ liệu bất thường và phân loại văn bản theo nhãn gần nhất.

Nhờ biểu diễn ý nghĩa bằng số, app của bạn vẫn tìm ra kết quả đúng dù câu hỏi và tài liệu gốc không chung từ khóa nào.

## Vector embedding trong RAG hoạt động thế nào

Nếu chưa nắm hai pha xử lý cơ bản của hệ thống này, bạn có thể đọc lại bài [RAG là gì](https://200lab.io/blog/rag-la-gi).

Lúc nạp dữ liệu, hệ thống cắt tài liệu gốc thành từng đoạn nhỏ gọi là chunk. Mỗi đoạn đi qua embedding model để biến thành vector rồi lưu vào cơ sở dữ liệu chuyên dụng; phần lưu trữ này đã có bài [vector database là gì](https://200lab.io/blog/vector-database-la-gi).

```text
Lúc nạp dữ liệu (làm một lần, chạy lại khi tài liệu đổi):
  tài liệu -> cắt thành đoạn (chunk) -> embedding model -> vector -> lưu vào vector database

Lúc người dùng hỏi:
  câu hỏi -> cùng embedding model -> vector câu hỏi
          -> vector database tìm top-k đoạn gần nhất
          -> ghép các đoạn đó vào prompt -> LLM viết câu trả lời
```

Khi người dùng hỏi, câu hỏi đi qua đúng embedding model lúc nãy để lấy vector. Hệ thống tìm top-k đoạn tài liệu gần nhất rồi ghép vào prompt gửi cho LLM trả lời, nên chất lượng embedding quyết định trực tiếp chuyện RAG tìm đúng hay sai thông tin.

![Sơ đồ RAG gồm hai nhánh nạp dữ liệu và trả lời câu hỏi cùng đi qua một embedding model.](https://assets.200lab.io/sha256/db/dbfdd32f248f869dd65bee003462e92ab03707160d4fb6727fc833222f850dd6/generations/eb01bdd2c3132c22460e3e62ba2f4bd25ee137199d4f15d72076bc622a20b0ab)

## Tạo embedding bằng Python với text-embedding-3-small

Mình chạy thử bằng Python trên máy Linux với thư viện `openai` bản **3.42.1**, `numpy` bản **2.5.4** và `sentence-transformers` bản **6.2.0**.

Đầu tiên, mình tạo file `docs.py` chứa **6 câu** FAQ của một shop thời trang về đổi trả trong **7 ngày**, giao nội thành Hà Nội **24 giờ**, thanh toán bằng thẻ ATM, ví MoMo hoặc tiền mặt, giặt áo cotton dưới **40 độ**, giảm **10%** cho khách thân thiết và mở cửa từ **9 giờ** sáng đến **9 giờ** tối. Câu hỏi thử là `"Mua nhầm size thì gửi lại được không?"`, không trùng chữ nào với câu quy định đổi trả.

```python
# docs.py
DOCS = [
    "Shop hỗ trợ đổi trả trong 7 ngày nếu sản phẩm còn nguyên tem.",
    "Đơn hàng nội thành Hà Nội được giao trong 24 giờ.",
    "Bạn có thể thanh toán bằng thẻ ATM, ví MoMo hoặc tiền mặt khi nhận hàng.",
    "Áo thun cotton nên giặt ở nhiệt độ dưới 40 độ để không bị co.",
    "Khách hàng thân thiết được giảm 10% cho mọi đơn từ lần mua thứ ba.",
    "Shop mở cửa từ 9 giờ sáng đến 9 giờ tối, kể cả cuối tuần.",
]
QUERY = "Mua nhầm size thì gửi lại được không?"
```

File `embed_openai.py` gom cả **6 câu** FAQ và câu hỏi gửi chung trong một request tới `text-embedding-3-small`, rồi tính tích vô hướng để xếp hạng. Mình gọi model qua cổng API tương thích chuẩn OpenAI bằng biến môi trường `OPENAI_BASE_URL`, nên code giữ nguyên như khi gọi thẳng server của hãng.

```python
# embed_openai.py
import numpy as np
from openai import OpenAI
from docs import DOCS, QUERY

client = OpenAI()  # reads OPENAI_API_KEY from the environment

resp = client.embeddings.create(
    model="text-embedding-3-small",
    input=DOCS + [QUERY],
)
vectors = np.array([item.embedding for item in resp.data])
doc_vecs, query_vec = vectors[:-1], vectors[-1]

print("dimensions:", vectors.shape[1])
print("first 5 values:", np.round(query_vec[:5], 4).tolist())
print("vector length:", round(float(np.linalg.norm(query_vec)), 4))

# vectors are normalized to length 1, so the dot product equals cosine similarity
scores = doc_vecs @ query_vec
for i in np.argsort(-scores)[:3]:
    print(f"{scores[i]:.3f}  {DOCS[i]}")
```

Cài thư viện, đặt API key rồi chạy script:

```bash
pip install openai numpy
export OPENAI_API_KEY="sk-..."   # your key
python embed_openai.py
```

```text
dimensions: 1536
first 5 values: [0.0201, 0.0198, -0.0126, -0.0323, 0.0309]
vector length: 1.0001
0.432  Bạn có thể thanh toán bằng thẻ ATM, ví MoMo hoặc tiền mặt khi nhận hàng.
0.413  Shop hỗ trợ đổi trả trong 7 ngày nếu sản phẩm còn nguyên tem.
0.406  Khách hàng thân thiết được giảm 10% cho mọi đơn từ lần mua thứ ba.
```

Kết quả trả về vector **1536 chiều**, **5 số** đầu là `[0.0201, 0.0198, -0.0126, -0.0323, 0.0309]` và độ dài vector đạt `1.0001`. Trong top 3, câu thanh toán đứng đầu với điểm `0.432`, câu đổi trả đứng thứ hai với `0.413` và câu giảm giá đạt `0.406`. Với câu hỏi này, `text-embedding-3-small` lại xếp câu thanh toán lên trên câu trả lời đúng với điểm số rất sát nhau.

## Cosine similarity: đo độ gần giữa hai embedding

Để đo độ gần giữa hai vector nhiều chiều, các hệ thống tìm kiếm thường dùng chỉ số cosine similarity. Chỉ số này đo góc giữa hai vector để biết chúng có cùng hướng ý nghĩa hay không.

Vector trả về từ OpenAI đều được chuẩn hóa về độ dài bằng **1**. Nhờ vậy, phép tính tích vô hướng (dot product) bằng toán tử `@` trong Python cho kết quả bằng đúng cosine similarity. Khi vector đã chuẩn hóa độ dài, bạn xếp hạng theo cosine similarity hay khoảng cách Euclid đều ra cùng một thứ tự.

## Rút gọn số chiều embedding với tham số dimensions

Mặc định vector dài **1536 chiều** với `text-embedding-3-small` và **3072 chiều** với `text-embedding-3-large`. Tham số `dimensions` của OpenAI giúp rút ngắn vector đầu ra mà vẫn giữ ý nghĩa cốt lõi; `text-embedding-3-large` rút xuống **256 chiều** vẫn đạt điểm tốt hơn model cũ `text-embedding-ada-002` **1536 chiều** trên bộ đo MTEB. Mình truyền `dimensions=512` và chạy lại thì thu được vector **512 chiều**, độ dài `0.9999`, kết quả gần nhất vẫn là câu thanh toán với điểm `0.485`.

```python
# embed_dims.py: same as embed_openai.py, only adds the dimensions parameter
resp = client.embeddings.create(
    model="text-embedding-3-small",
    input=DOCS + [QUERY],
    dimensions=512,
)
```

```text
dimensions: 512
vector length: 0.9999
top match: 0.485  Bạn có thể thanh toán bằng thẻ ATM, ví MoMo hoặc tiền mặt khi nhận hàng.
```

Vector càng dài càng tốn RAM, ổ cứng và chi phí tính toán mỗi lần query. Một vector **1536 chiều** dạng `float32` chiếm `1536 x 4 = 6144 byte` (khoảng **6 KB**), nên **1 triệu** đoạn tài liệu tốn khoảng **6,1 GB** riêng phần vector. Nếu dùng vector **384 chiều**, mỗi đoạn chỉ tốn **1536 byte**, tức khoảng **1,5 GB** cho **1 triệu** đoạn.

## Chạy embedding model mã nguồn mở trên máy cá nhân

Muốn tự chạy model trên server riêng, bạn có thể thử `intfloat/multilingual-e5-small` với thông số trên [model card của multilingual-e5-small](https://huggingface.co/intfloat/multilingual-e5-small). Model có **12 layer**, khoảng **0,1 tỷ** tham số, xuất vector **384 chiều** và hỗ trợ **100 ngôn ngữ** (ngôn ngữ ít dữ liệu huấn luyện có thể kém hơn). Text dài quá **512 token** sẽ bị cắt đuôi, và bạn cần gắn tiền tố `"query: "` cho câu hỏi cùng `"passage: "` cho tài liệu để không giảm chất lượng.

```bash
pip install sentence-transformers
python embed_local.py
```

```python
# embed_local.py
from sentence_transformers import SentenceTransformer
from docs import DOCS, QUERY

model = SentenceTransformer("intfloat/multilingual-e5-small")

# E5 models expect the "query: " and "passage: " prefixes
doc_vecs = model.encode([f"passage: {d}" for d in DOCS], normalize_embeddings=True)
query_vec = model.encode(f"query: {QUERY}", normalize_embeddings=True)

print("dimensions:", doc_vecs.shape[1])
scores = doc_vecs @ query_vec
for i in scores.argsort()[::-1][:3]:
    print(f"{scores[i]:.3f}  {DOCS[i]}")
```

File `embed_local.py` dùng `sentence-transformers` chạy model này trên CPU, tự tải model từ Hugging Face ở lần đầu mà không cần API key và dữ liệu không rời khỏi máy.

```text
dimensions: 384
0.854  Shop hỗ trợ đổi trả trong 7 ngày nếu sản phẩm còn nguyên tem.
0.845  Khách hàng thân thiết được giảm 10% cho mọi đơn từ lần mua thứ ba.
0.828  Bạn có thể thanh toán bằng thẻ ATM, ví MoMo hoặc tiền mặt khi nhận hàng.
```

Output cho ra vector **384 chiều**, xếp câu đổi trả lên đầu với điểm `0.854`, kế đến là câu giảm giá `0.845` và câu thanh toán `0.828`. Điểm cosine của dòng e5 thường dồn vào khoảng **0,7** đến **1,0**, nhưng bạn chỉ cần quan tâm thứ tự xếp hạng chứ giá trị điểm lớn hay nhỏ không quan trọng.

## Cách chọn embedding model cho RAG tiếng Việt

Để kiểm tra thêm, mình tự soạn **5 câu** hỏi xoay quanh chuyện đổi size, thời gian giao hàng Hà Nội, trả tiền khi nhận, cách giặt áo và giờ mở cửa thứ bảy. Mỗi câu ứng với một đáp án trong **6 câu** FAQ, rồi mình đếm số lần model xếp đúng câu trả lời lên top 1.

```python
# 5 test questions, each with the index of the correct answer in DOCS
TESTS = [
    ("Mua nhầm size thì gửi lại được không?", 0),
    ("Bao lâu thì nhận được hàng ở Hà Nội?", 1),
    ("Có trả tiền khi ship tới được không?", 2),
    ("Giặt áo thế nào cho khỏi bị rút?", 3),
    ("Thứ bảy shop có mở cửa không?", 5),
]
```

```text
text-embedding-3-small   dims  1536  top-1 correct: 4/5
text-embedding-3-large   dims  3072  top-1 correct: 5/5
multilingual-e5-small    dims   384  top-1 correct: 5/5
```

Kết quả cho thấy `text-embedding-3-small` đúng **4/5**, còn `text-embedding-3-large` và `multilingual-e5-small` cùng đạt **5/5**. Bộ thử chỉ có **5 câu** tự nghĩ ra nên chưa đủ kết luận model nào tốt hơn cho mọi bài toán; nó chỉ nhắc mình nên tự kiểm tra trên dữ liệu thật của dự án.

Để biết `text-embedding-3-small` sai câu nào, bạn chọn câu hỏi ở bảng bên dưới rồi so điểm với `multilingual-e5-small` nhé.

| Model | Số chiều mặc định | Input tối đa | Chạy ở đâu | Chi phí ước tính (khoảng 800 token/trang) | Top-1 trên 5 câu thử |
| --- | --- | --- | --- | --- | --- |
| `text-embedding-3-small` | **1536** | **8192 token** | Gọi API | Khoảng **62.500 trang/USD** | **4/5** |
| `text-embedding-3-large` | **3072** | **8192 token** | Gọi API | Khoảng **9.615 trang/USD** | **5/5** |
| `multilingual-e5-small` | **384** | **512 token** | Máy của bạn | Không tốn tiền API, tốn tài nguyên máy | **5/5** |

Khi chọn embedding model cho sản phẩm thật, bạn nên cân nhắc:

- Độ hiểu tiếng Việt của model trên đúng từ vựng ngành hàng của bạn.
- Số chiều vector vì nó quyết định dung lượng lưu trữ và tốc độ truy vấn khi dữ liệu lớn.
- Giới hạn độ dài đầu vào quyết định cách cắt chunk vì `multilingual-e5-small` cắt ở **512 token** còn model OpenAI nhận tới **8192 token**; xem lại khái niệm token ở bài [các thuật ngữ LLM quan trọng](https://200lab.io/blog/cac-thuat-ngu-llm-quan-trong).
- Chi phí tính theo token gọi API so với tiền thuê server tự chạy, kèm yêu cầu bảo mật có cho gửi dữ liệu nội bộ ra ngoài không.
- Tra cứu trước trên [bảng xếp hạng MTEB](https://huggingface.co/spaces/mteb/leaderboard) để lọc vài ứng viên rồi chạy thử với bộ câu hỏi thật.

Theo mình, chỗ thú vị nhất ở lần chạy thử này là một model nhỏ **384 chiều** chạy trên CPU lại xếp đúng câu đổi trả lên đầu. Kết quả lệch của `text-embedding-3-small` ở câu đầu tiên cho mình thấy chọn model chỉ dựa vào tên hãng hay số chiều là chưa đủ.

## Lỗi hay gặp khi dùng embedding: trộn hai model

Một chỗ dễ sai khi làm app RAG là dùng hai model khác nhau lúc lưu dữ liệu và lúc truy vấn. Trong `mismatch.py`, mình lấy **6 vector** tài liệu từ `text-embedding-3-small` kích thước `(6, 1536)` nhân với vector câu hỏi từ `multilingual-e5-small` kích thước `(384,)` và bị lỗi `ValueError: size 384 is different from 1536`.

```python
# mismatch.py
import numpy as np
from openai import OpenAI
from sentence_transformers import SentenceTransformer
from docs import DOCS, QUERY

client = OpenAI()
resp = client.embeddings.create(model="text-embedding-3-small", input=DOCS)
doc_vecs = np.array([item.embedding for item in resp.data])  # stored with the old model

e5 = SentenceTransformer("intfloat/multilingual-e5-small")
query_vec = e5.encode(f"query: {QUERY}", normalize_embeddings=True)  # query from the new model

print(doc_vecs.shape, query_vec.shape)
scores = doc_vecs @ query_vec
```

```text
(6, 1536) (384,)
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 384 is different from 1536)
```

Tài liệu gốc và câu hỏi bắt buộc phải đi qua cùng một embedding model; hễ đổi model là phải chạy embed lại toàn bộ kho tài liệu. Nếu hai model khác nhau tình cờ có cùng số chiều thì càng khó nhận ra, vì code Python sẽ chạy không báo lỗi nhưng điểm số tính ra không còn ý nghĩa, làm bạn khó biết vì sao RAG trả lời sai.

## Chọn embedding model cho production: đo trên dữ liệu của bạn

Demo với **6 câu** FAQ thì chạy là thấy kết quả, nhưng đưa embedding lên production thì anh em cần cố định cách đo và quản lý vector ngay từ đầu. Trước khi chốt model, bạn chỉ cần làm đủ các việc sau trên chính tài liệu của sản phẩm:

- **Tự lập bộ test từ câu hỏi thật:** gom vài chục câu khách hay hỏi, gắn sẵn đoạn tài liệu đáp án rồi chạy script đếm tỷ lệ top-1 như mình làm ở trên.
- **Soi trực tiếp các câu tìm sai:** mở từng câu model xếp hạng sai để biết do model kém tiếng Việt, quên gắn tiền tố `"query: "` và `"passage: "` hay do chunk bị cắt cụt ở mốc **512 token**.
- **Cân số chiều với dung lượng lưu trữ:** thử hạ tham số `dimensions` hoặc dùng model **384 chiều** vì **1 triệu** đoạn ở **1536 chiều** chiếm khoảng **6,1 GB**, còn **384 chiều** chỉ mất khoảng **1,5 GB**.
- **Ghi metadata đi kèm vector:** lưu cứng tên model và số chiều cạnh dữ liệu để lúc đổi model thì chạy embed lại toàn bộ kho tài liệu.

> ⚠️ **Chỗ dễ sai nhất:** đổi embedding model ở API tìm kiếm mà quên re-index kho vector, lại đụng đúng hai model có cùng số chiều. Phép nhân `@` vẫn chạy bình thường và không báo lỗi `ValueError`, chỉ có điểm cosine trả về không còn ý nghĩa và LLM có thể trả lời dựa trên đoạn tài liệu sai, mà bạn không thấy dòng log lỗi nào.

> Embedding model nào cũng có bảng điểm benchmark đẹp. Số liệu đáng tin nhất vẫn là tỷ lệ top-1 trên câu hỏi thật của chính sản phẩm bạn.

Nếu bạn muốn tự tính tay tích vô hướng, độ dài vector và hiểu cách cosine xếp hạng tài liệu thì tham khảo khóa học miễn phí bên dưới tại 200Lab nhé.

![Toán học cơ bản cho AI Engineer - xác suất, thống kê, vector và ma trận](https://assets.200lab.io/sha256/ec/ecc5d90962f1cfc0308df5cf615ee806ce1f1006e98af0f475612f6844043110/generations/377990af33e9eca79c485a8311d0e1190637f2ec3cc1be1bfdd49d97b4117e31)

- [Toán học cơ bản cho AI Engineer - xác suất, thống kê, vector và ma trận](https://200lab.io/courses/math-foundations-for-ai-engineers.md)

Chúc các bạn tìm được vector cùng chiều! 😆

## Đọc thêm

- [Cách nhờ Grok Bot trả kết quả kiểm tra được trước khi giao việc](https://200lab.io/blog/grok-bot-reviewable-results.md): Nhờ Grok Bot trả kết quả kiểm tra được bằng cách đặt trước bảng có link nguồn, giờ xem và tách 5 phần để biết đâu là số liệu thật, đâu là điều bot đoán.
- [Hướng dẫn chuyển việc giữa hai Grok Bot cho công ty một người](https://200lab.io/blog/grok-bot-handoff-between-bots.md): Hướng dẫn cho nhiều Grok Bot phối hợp làm việc: chuyển việc giữa các bot, group chat nhiều bot, phân vai trò cho trợ lý AI của người bán hàng một mình.
- [Cách tiết kiệm token cho Grok Bot: mình thử lọc tin bằng Jev](https://200lab.io/blog/jev-grok-bot-decisions.md): Tiết kiệm token cho Grok Bot bằng cách giao bước chọn lọc mỗi sáng cho Jev. Sau 4 chỗ sửa script, một lần lọc 3.679 tin giảm từ 193 xuống 6 lượt gọi API.
- [Extension Claude Code giả mạo trên Cursor Marketplace: cách kiểm tra máy](https://200lab.io/blog/claude-code-extension-cursor-ssh-backdoor.md): Extension Claude Code giả mạo trên Cursor Marketplace cố cài backdoor SSH lên macOS, Windows. Gỡ extension thôi chưa đủ, phải kiểm tra key lạ trong máy.
- [Vẽ sơ đồ system design bằng AI với template Diagram Director](https://200lab.io/blog/grok-bot-diagram-director.md): Vẽ sơ đồ system design bằng AI có chuyển động để thấy luồng đọc, ghi và chỗ nghẽn khi có sự cố. Xem template Grok Bot Diagram Director dựng sơ đồ 500K RPS.
- [Mọi bài viết chủ đề AI](https://200lab.io/blog/tags/ai.md)

Toàn bộ nội dung: https://200lab.io/llms.txt
