5.2 Bi-encoder vs cross-encoder: một chi tiết kiến trúc, hai thế giới
Câu hỏi cần trả lời ở mục này: vì sao cross-encoder tốt hơn hẳn, chứ không phải rằng nó tốt hơn. Nếu bạn hiểu chỗ này thì mọi thứ còn lại của Phần 5 là hệ quả.
Bi-encoder: nén trước, hỏi sau
Phần tiêu đề “Bi-encoder: nén trước, hỏi sau”Một embedding model (bi-encoder) chạy như sau:
lúc index (chạy một lần, offline): vec_doc = encode("Thanh toán thất bại: giao dịch bị hoàn trong 3–5 ngày…") ↓ [0.12, −0.44, …] ← 768 số, lưu vào index
lúc truy vấn (online): vec_query = encode("tiền đã bị trừ mà đơn không thành công") điểm = cosine(vec_query, vec_doc) ← một phép nhân vô hướngChú ý dòng thứ nhất: tài liệu được nén thành 768 số trước khi ai biết câu hỏi là gì.
Đó không phải một chi tiết triển khai, đó là một giới hạn thông tin. Đoạn văn bản kia có thể trả lời được hàng trăm câu hỏi khác nhau — “bao lâu thì được hoàn tiền”, “đơn lỗi thì sao”, “tiền đi đâu”, “có mất phí không”. Cả trăm câu ấy phải cùng chia nhau một vector 768 chiều duy nhất, cố định. Vector đó buộc phải là một bản tóm tắt trung bình, mờ đi ở mọi hướng, để không sai quá ở hướng nào.
Đây cũng là lý do bi-encoder giỏi việc “đoạn này có cùng chủ đề không” và dở việc “đoạn này có trả lời chính xác câu này không”.
Cross-encoder: không nén gì cả
Phần tiêu đề “Cross-encoder: không nén gì cả”lúc truy vấn (không có gì tính trước được): điểm = model("[CLS] tiền đã bị trừ mà đơn không thành công [SEP] Thanh toán thất bại: giao dịch bị hoàn trong 3–5 ngày… [SEP]") ↓ một số duy nhấtCâu hỏi và tài liệu được ghép thành một chuỗi và đi qua model cùng nhau. Ở mọi lớp self-attention, mọi token của câu hỏi nhìn thấy mọi token của tài liệu và ngược lại.
Cụ thể hoá cho dễ thấy. Với cặp trên, cross-encoder có thể học được các liên kết:
"trừ tiền" ⟷ "giao dịch bị hoàn" (tiền ra rồi tiền về)"đơn không thành" ⟷ "thất bại" (đồng nghĩa trong ngữ cảnh này)"mà" ⟷ quan hệ nghịch (trừ rồi NHƯNG không thành)Liên kết thứ ba là thứ bi-encoder gần như chắc chắn mất. Chữ “mà” mang toàn bộ ngữ
nghĩa nghịch của câu hỏi, và khi câu hỏi bị nén thành một vector thì nó tan vào 768 số.
Cross-encoder có thể để token mà attend trực tiếp vào thất bại.
Bảng đối chiếu
Phần tiêu đề “Bảng đối chiếu”| Bi-encoder | Cross-encoder | |
|---|---|---|
| Số lần chạy model / truy vấn | 1 (chỉ encode câu hỏi) | k (mỗi ứng viên một lần) |
| Tính trước tài liệu | được, offline | không thể |
| Query attend vào doc | không bao giờ | mọi token × mọi token |
| Chi phí khi corpus tăng ×1000 | không đổi (ANN) | ×1000 — bất khả thi |
| Chỗ dùng đúng | stage 1, quét toàn corpus | stage 2, xếp lại top-k |
| Cái nó trả về | vector (so được với nhau) | một số, không so được giữa các truy vấn (5.3) |
Dòng đầu và dòng ba giải thích tất cả: cross-encoder trả tiền bằng số lần chạy model để mua lấy quyền cho câu hỏi và tài liệu nhìn thấy nhau.
Vì sao “cứ dùng cross-encoder cho tất cả” là bất khả thi
Phần tiêu đề “Vì sao “cứ dùng cross-encoder cho tất cả” là bất khả thi”Bảng dưới là hệ quả trực tiếp của dòng “số lần chạy model / truy vấn”:
| Corpus | Bi-encoder + ANN | Cross-encoder thuần |
|---|---|---|
| 50 doc (corpus cẩm nang) | 1 lần encode | 50 lần |
| 100.000 doc | 1 lần encode + tra HNSW | 100.000 lần |
| 10.000.000 doc | 1 lần encode + tra HNSW | 10.000.000 lần |
Bi-encoder có một thứ mà cross-encoder không bao giờ có được: vì tài liệu đã thành vector độc lập với câu hỏi, bạn xây được cấu trúc dữ liệu trên các vector đó (HNSW, IVF — xem Tầng 5) và tìm gần đúng trong thời gian dưới tuyến tính. Cross-encoder không thể, vì không có “vector tài liệu” nào tồn tại trước khi câu hỏi đến. Mọi cặp phải được tính, không có tắt nào.
Đó là toàn bộ lý do kiến trúc hai giai đoạn tồn tại — không phải vì ai đó thấy nó gọn gàng, mà vì không có lựa chọn nào khác.
Ở giữa hai thế giới: late interaction
Phần tiêu đề “Ở giữa hai thế giới: late interaction”Có một điểm giữa: lưu vector cho từng token của tài liệu (tính trước được), rồi lúc truy vấn mới cho các token gặp nhau qua một phép so khớp rẻ. Đó là ColBERT — 5.6. Nó mua lại một phần khả năng tương tác của cross-encoder, và trả bằng dung lượng index.
Ba mức, một trục duy nhất — tương tác giữa query và doc bị đẩy sớm hay muộn:
sớm nhất ──────────────────────────────────────────────► muộn nhấtbi-encoder late interaction cross-encoder(1 vector/doc) (1 vector/token) (không vector nào)index nhỏ index phồng 10–100× không index đượcrẻ nhất đắt nhất, tốt nhấtĐiều này ràng buộc gì lên thiết kế của bạn
Phần tiêu đề “Điều này ràng buộc gì lên thiết kế của bạn”| Vì | Nên |
|---|---|
| Cross-encoder không tính trước được | Đặt nó sau một stage rẻ, luôn luôn. Không có kiến trúc một-giai-đoạn nào dùng cross-encoder |
| Chi phí tỷ lệ thuận với k | k là núm điều chỉnh chi phí rõ ràng nhất bạn có (5.4) |
| Chi phí cũng tỷ lệ với độ dài đoạn | Chunk dài làm reranker đắt lên và loãng đi (5.9) |
| Nó không thấy tài liệu ngoài top-k | Chất lượng cuối bị chặn trên bởi recall@k (5.4) |
→ Nên áp dụng vào AI Agent: đừng bao giờ hỏi “dùng embedding hay dùng reranker”. Chúng không thay thế nhau — chúng đứng ở hai giai đoạn khác nhau vì hai lý do kỹ thuật khác nhau. Câu hỏi đúng là “k bằng bao nhiêu”.