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ênMã 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, MilvusElasticsearch/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.

Điểm yếu ở dòng thứ hai, đo thật Trên PostgreSQL 16.15, tám đoạn văn ngắn lập chỉ mục bằng 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):

RRF(d) = Σdanh sách 1 / (k + rank(d))   với k thường = 60
              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.

Tầng 1Hybrid lấy 30–50 ứng viên
→
Tầng 2Cross-encoder chấm lại từng cặp
→
Kết quảGiữ 3–8 đoạn tốt nhất cho prompt

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

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ậtVí 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-querySinh 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
HyDECho 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
Mỗi bước LLM thêm độ trễ và chi phí Bắt đầu đơn giản (hybrid + rerank), đo, rồi mới thêm biến đổi câu hỏi cho những loại câu hỏi đang thất bại.

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;
Rò rỉ qua câu trả lời Nếu một đoạn mật đã vào prompt, không có cách chắc chắn nào để ngăn mô hình nhắc tới nó. Đoạn người dùng không được xem thì không bao giờ được truy xuất.

Tự kiểm tra

Vì sao RRF dùng thứ hạng thay vì cộng thẳng điểm cosine với điểm BM25? Hai điểm khác thang đo và phân phối; cộng thẳng khiến một bên lấn át. Thứ hạng thì so sánh được giữa các hệ.
Tại sao không dùng cross-encoder cho toàn bộ kho ngay từ đầu? Cross-encoder phải chạy mô hình cho từng cặp (câu hỏi, đoạn) — với hàng triệu đoạn là quá chậm. Nên chỉ dùng để xếp lại vài chục ứng viên.
Không đoạn nào vượt ngưỡng liên quan. Hệ thống nên làm gì? Trả lời rằng không tìm thấy thông tin trong tài liệu (có thể gợi ý cách hỏi khác hoặc người liên hệ), thay vì gọi LLM với ngữ cảnh rỗng hay nhiễu.