Truy xuất: hybrid search, rerank và viết lại câu hỏi
Truy xuất là nơi phần lớn chất lượng RAG được quyết định. Bài này đi từ tìm kiếm vector đơn giản tới quy trình nhiều tầng mà các hệ thống thực tế đang dùng.
Tìm kiếm ngữ nghĩa và tìm kiếm từ khoá
| Dense (vector) | Sparse (BM25, từ khoá) | |
|---|---|---|
| Mạnh ở | Đồng nghĩa, diễn đạt khác, câu hỏi tự nhiên | Mã số, tên riêng, thuật ngữ hiếm, trích nguyên văn |
| Yếu ở | “SKU-48213”, “Điều 12.3”, từ viết tắt nội bộ | “làm ở nhà” ≠ “làm việc từ xa” |
| Ví dụ công cụ | pgvector, Qdrant, Milvus | Elasticsearch/OpenSearch, PostgreSQL full-text |
BM25 chấm điểm một đoạn dựa trên số lần từ trong câu hỏi xuất hiện (có bão hoà), độ hiếm của từ trên toàn kho (IDF) và độ dài đoạn. Nó là thuật toán cũ, đơn giản — và vẫn là đối thủ rất khó vượt với câu hỏi chứa từ khoá chính xác.
to_tsvector('english', …) và truy vấn qua websearch_to_tsquery — đối sánh từ vựng chấm điểm bằng ts_rank_cd, cùng họ với BM25 tuy không trùng.
Truy vấn SKU-48213 trả về đúng 1 dòng, chính là đoạn chứa mã đó. Truy vấn remote working trả về đoạn quy định làm việc từ xa.
Nhưng truy vấn work from home trả về 0 dòng — đáp án nằm ngay ở đoạn 1 mà không chung với câu hỏi một gốc từ nào.
Không tham số nào chữa được; chỉ có thêm một tầng truy xuất ngữ nghĩa mới cứu được.
Hybrid search và Reciprocal Rank Fusion
Chạy cả hai loại tìm kiếm rồi hợp nhất danh sách. Điểm của hai hệ khác thang đo (cosine 0–1, BM25 không giới hạn), nên cách hợp nhất phổ biến là dùng thứ hạng thay vì điểm — Reciprocal Rank Fusion (RRF):
vector BM25 RRF (k = 60)
đoạn "Điều 12" rank 4 rank 1 1/64 + 1/61 = 0.0320
đoạn "WFH" rank 1 rank 9 1/61 + 1/69 = 0.0309
đoạn "Phép năm" rank 2 — 1/62 = 0.0161
Đoạn xếp khá ở cả hai danh sách được đẩy lên đầu; đoạn chỉ tốt ở một bên vẫn có cơ hội. Để ý khoảng cách rất mỏng — 0,0320 so với 0,0309, chênh 3,7% — nghĩa là với k = 60, hạng nhất ở một danh sách không đè bẹp mọi thứ còn lại.
Xếp hạng lại (rerank)
Embedding mã hoá câu hỏi và đoạn văn riêng rẽ (bi-encoder) nên nhanh nhưng thô. Cross-encoder đọc câu hỏi và đoạn văn cùng lúc nên đánh giá mức liên quan chính xác hơn nhiều — nhưng chậm, không thể chạy trên cả kho.
Có thể dùng mô hình rerank mã nguồn mở hoặc API rerank của nhà cung cấp. Đổi lại bạn trả thêm độ trễ (thường vài chục tới vài trăm mili-giây), nên cần đo xem chất lượng tăng có xứng đáng không.
Chọn top-k và ngưỡng
- k quá nhỏ: dễ bỏ sót đoạn chứa đáp án — recall thấp.
- k quá lớn: prompt dài, đắt, và các đoạn nhiễu có thể kéo mô hình lạc hướng.
- Ngưỡng điểm: bỏ đoạn có độ liên quan quá thấp. Nếu không còn đoạn nào, trả lời “không tìm thấy thông tin” thay vì để mô hình tự bịa.
- Đa dạng hoá (MMR): tránh 5 đoạn gần như trùng nội dung chiếm hết chỗ.
Biến đổi câu hỏi
Câu hỏi của người dùng thường ngắn, mơ hồ hoặc phụ thuộc hội thoại trước. Một bước LLM nhỏ trước khi truy xuất có thể cải thiện đáng kể:
| Kỹ thuật | Ví dụ |
|---|---|
| Viết lại theo hội thoại | “Còn thử việc thì sao?” → “Nhân viên thử việc có được làm việc từ xa không?” |
| Multi-query | Sinh 3 cách hỏi khác nhau, truy xuất từng câu, hợp nhất bằng RRF |
| Tách câu hỏi | “So sánh chính sách phép của 2023 và 2025” → 2 truy vấn riêng |
| HyDE | Cho LLM viết một câu trả lời giả định, dùng embedding của câu trả lời đó để tìm — vì nó “trông giống” tài liệu hơn câu hỏi |
| Trích bộ lọc | “quy định HR mới nhất” → lọc department = 'HR', sắp theo updated_at |
Lọc metadata và phân quyền
Phân quyền phải xảy ra trong truy vấn truy xuất, không phải sau khi sinh câu trả lời:
-- Người dùng thuộc nhóm {'sales', 'all-staff'}
SELECT content
FROM chunks
WHERE allowed_groups && ARRAY['sales', 'all-staff'] -- có giao với nhóm của người dùng
ORDER BY embedding <=> $1
LIMIT 30;