- Published on
Day 37: Reranking Cho Production RAG
- Authors

- Name
- Trần Mạnh Thắng
- @TranManhThang96
1. Reranking giải quyết vấn đề gì?
Trong Day 36, pipeline Hybrid Search đã lấy candidate bằng BM25, dense retrieval và Reciprocal Rank Fusion. Bước đó tối ưu cho recall: cố gắng không bỏ sót chunk có khả năng liên quan. Nhưng top result của retriever không luôn là context tốt nhất cho LLM.
Các lỗi thường gặp khi chỉ dùng retrieval top-k:
- Dense retrieval hiểu ý nghĩa tổng quát nhưng có thể bỏ qua phủ định, điều kiện, version hoặc tên riêng.
- BM25 bắt đúng keyword nhưng có thể đưa lên cao đoạn chỉ nhắc từ khóa, không trả lời câu hỏi.
- RRF merge nhiều nguồn tốt cho recall nhưng thứ tự sau merge vẫn còn nhiễu.
- Chunk gần giống nhau làm top-k bị trùng, khiến context thiếu đa dạng.
- Query tiếng Việt trộn English, acronym, mã lỗi hoặc tên sản phẩm làm embedding khó xếp đúng.
Reranker nhận từng cặp:
(query, candidate_chunk) -> relevance_score
Sau đó hệ thống sắp xếp lại candidate và chỉ lấy top nhỏ hơn để đưa vào prompt.
Mental model:
Retriever: tìm rộng để không bỏ sót.
Reranker: chấm đắt hơn nhưng chính xác hơn trên tập candidate nhỏ.
Context builder: lấy vài chunk tốt nhất, giữ citation và giới hạn token.
Điểm quan trọng: reranker không cứu được chunk không nằm trong candidate pool. Nếu retrieve top 50 không có chunk đúng, rerank top 50 cũng không thể tạo ra chunk đúng. Vì vậy production pattern thường là retrieve rộng trước, sau đó rerank hẹp.
2. Bi-encoder vs cross-encoder
Bi-encoder encode query và document riêng biệt:
query -> embedding_q
doc -> embedding_d
score = similarity(embedding_q, embedding_d)
Ưu điểm là document embedding có thể tính offline và lưu trong Vector DB. Query runtime chỉ cần embed query rồi search nhanh. Nhược điểm là query và document không tương tác token-by-token khi chấm điểm, nên ranking có thể sai ở các câu hỏi cần hiểu chi tiết.
Cross-encoder encode query và document cùng lúc:
[query, doc] -> transformer -> relevance_score
Ưu điểm là model nhìn thấy tương tác giữa từng token của query và chunk, nên thường xếp hạng chính xác hơn. Nhược điểm là không thể precompute score cho mọi query-doc pair. Nếu có 100 candidate, cross-encoder phải chạy 100 cặp ở runtime.
| Cách làm | Dùng cho | Ưu điểm | Nhược điểm | Production note |
|---|---|---|---|---|
| Bi-encoder | First-stage retrieval | Nhanh, index được offline, scale tốt | Ranking chưa đủ tinh | Default cho dense retrieval |
| Cross-encoder | Second-stage reranking | Top-k precision tốt | Latency/cost tăng theo số candidate | Rerank top 20-100, không rerank toàn corpus |
| Late-interaction | Search/rerank chất lượng cao | Giữ token-level matching tốt hơn bi-encoder | Serving/index phức tạp | Phù hợp team search mạnh |
| LLM rerank | Query đặc biệt phức tạp | Linh hoạt, có thể giải thích | Đắt, chậm, khó deterministic | Dùng có chọn lọc hoặc offline analysis |
Rule thực dụng: dùng bi-encoder để retrieve, dùng cross-encoder để rerank.
3. Reranker là gì?
Reranker là một model hoặc service nhận query và danh sách candidate, trả về relevance score hoặc danh sách đã sắp xếp.
Input tốt cho reranker không chỉ là chunk.text. Nó nên chứa đủ ngữ cảnh ngắn:
titlecủa tài liệu.section_pathhoặc heading.- Nội dung chunk đã cắt vừa token budget.
source_type,document_version,effective_datenếu domain phụ thuộc version.page_start,page_end,chunk_idđể giữ citation.
Ví dụ format text đưa vào reranker:
Title: Chính sách nghỉ phép 2026
Section: HR > Leave Policy > Annual Leave
Version: 2026-01
Text: Nhân viên full-time có 12 ngày nghỉ phép năm...
Không nên đưa cả document dài vào reranker. Tài liệu dài sẽ bị truncate, tăng latency và có thể làm mất đúng phần chứa đáp án. Chunking tốt từ Day 35 vẫn rất quan trọng.
4. BGE reranker
BGE reranker là nhóm reranker open-source từ BAAI, thường dùng như cross-encoder cho search/RAG. Với dữ liệu tiếng Việt hoặc multilingual, một lựa chọn thực tế là BAAI/bge-reranker-v2-m3 vì model này hỗ trợ multilingual và có thể chạy self-host.
Ưu điểm:
- Không gửi dữ liệu ra managed API nếu self-host.
- Có thể batch inference để tối ưu throughput.
- Có thể fine-tune trên query/document domain riêng khi có dữ liệu.
- Phù hợp để học và để xây baseline nội bộ.
Nhược điểm:
- Cần CPU/GPU serving, autoscale, monitoring và model lifecycle.
- Latency tăng theo
candidate_count * chunk_length. - Cần kiểm tra license, hardware và khả năng vận hành trước production.
Ví dụ dùng sentence-transformers:
from sentence_transformers import CrossEncoder
model = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)
query = "Nhân viên full-time được nghỉ phép bao nhiêu ngày trong năm 2026?"
passages = [
"Title: Chính sách nghỉ phép 2026\nText: Nhân viên full-time có 12 ngày nghỉ phép năm.",
"Title: Quy định bảo mật\nText: Nhân viên phải đổi mật khẩu mỗi 90 ngày.",
]
scores = model.predict([(query, passage) for passage in passages])
Trong production, đoạn trên nên được bọc trong service có batching, timeout, circuit breaker, metric và model version.
5. Cohere Rerank concept
Cohere Rerank là managed rerank API. Thay vì tự host model, hệ thống gửi query và documents lên API, nhận về relevance score cùng thứ tự kết quả.
Ưu điểm:
- Ship nhanh, ít vận hành model serving.
- API đã có batching, model hosting, scaling.
- Dễ dùng để tạo baseline chất lượng trước khi quyết định self-host.
Nhược điểm:
- Có network latency và rate limit.
- Cost tăng theo số request, số document và độ dài document.
- Cần đánh giá data privacy, PII, data residency và hợp đồng xử lý dữ liệu.
- Vendor/model version thay đổi có thể làm score distribution đổi.
Ví dụ concept với Python SDK:
import os
import cohere
co = cohere.Client(token=os.environ["COHERE_API_KEY"])
response = co.v2.rerank(
model="rerank-v4.0-pro",
query="Nhân viên full-time được nghỉ phép bao nhiêu ngày?",
documents=[
"Title: Chính sách nghỉ phép\nText: Nhân viên full-time có 12 ngày nghỉ phép năm.",
"Title: Chính sách bảo mật\nText: Không chia sẻ mật khẩu cho người khác.",
],
top_n=2,
)
for item in response.results:
print(item.index, item.relevance_score)
Trong hệ thống thật, không hard-code model. Hãy đưa model name, top_n, timeout và max_tokens_per_doc vào config.
6. Two-stage retrieval
Pipeline production hợp lý sau Day 36:
user query
-> normalize query
-> BM25 top 50 + vector top 50
-> merge bằng RRF
-> filter tenant/ACL/deleted/index_version
-> dedupe theo document_id/chunk_id/text_hash
-> truncate text theo max tokens per doc
-> rerank top 50 hoặc top 100
-> lấy top 5-10
-> context builder giữ citation
-> LLM answer
Thứ tự ACL rất quan trọng. Filter quyền phải chạy trước rerank và trước khi gửi sang managed API. Nếu candidate không đúng quyền được gửi vào prompt hoặc ra API ngoài, dữ liệu đã bị leak.
Candidate sizing:
| Cấu hình | Khi phù hợp | Rủi ro |
|---|---|---|
| Retrieve top 20 -> rerank top 5 | Corpus nhỏ, SLA rất chặt | Dễ bỏ sót chunk đúng |
| Retrieve top 50 -> rerank top 5/8 | Default tốt cho nhiều RAG nội bộ | Vẫn cần đo Recall@50 |
| Retrieve top 100 -> rerank top 10 | Cần recall cao, query đa dạng, corpus nhiều nhiễu | Latency/cost tăng |
| Retrieve top 200+ -> rerank top 10 | Chỉ dùng khi đã chứng minh cần | Có thể quá đắt, p95 xấu |
Default thực dụng cho bài học: BM25 top 50, vector top 50, RRF merge, dedupe còn khoảng 50-100 candidate, rerank lấy top 5-10.
7. Latency và cost trade-off
Reranker nằm trước bước LLM generation, nên nó ảnh hưởng trực tiếp tới time-to-first-token.
Các yếu tố làm latency tăng:
- Số candidate rerank.
- Độ dài chunk sau khi format.
- Model size.
- CPU vs GPU.
- Batch size và queue time.
- Managed API network latency, rate limit và retry.
Trade-off quan trọng:
| Quyết định | Quality | Latency | Cost | Ghi chú |
|---|---|---|---|---|
| Không rerank | Thấp đến vừa | Tốt | Thấp | Chỉ ổn nếu eval chứng minh top-k đã đủ |
| Rerank top 20 | Vừa | Tốt | Vừa | Phù hợp SLA chặt |
| Rerank top 50 | Tốt | Vừa | Vừa | Default nên thử |
| Rerank top 100 | Tốt hơn nếu retrieval recall cao | Chậm hơn | Cao hơn | Cần benchmark p95 |
| Self-host BGE | Tốt nếu vận hành ổn | Phụ thuộc hardware | Predictable hơn ở scale | Cần GPU/CPU serving |
| Cohere Rerank | Tốt, ship nhanh | Có network latency | Pay-per-use | Cần review privacy |
Kỹ thuật giảm latency:
- Dedupe trước rerank.
- Cắt text theo heading/sentence boundary và giới hạn
max_charshoặcmax_tokens_per_doc. - Batch candidate pairs.
- Cache với query normalize nếu không chứa PII và dữ liệu không quá volatile.
- Timeout reranker và fallback về RRF rank.
- Chỉ rerank khi query cần độ chính xác cao, ví dụ policy/legal/support.
- Dùng model nhỏ hơn cho traffic thường, model mạnh hơn cho query có risk cao.
8. Evaluation: Recall@k và MRR
Reranking nên được đo bằng query set có ground truth, không đo bằng cảm giác.
Một eval item tối thiểu:
{
"query_id": "q001",
"query": "Nhân viên full-time có bao nhiêu ngày nghỉ phép năm 2026?",
"relevant_chunk_ids": ["hr_leave_2026:chunk_003"]
}
Các metric:
| Metric | Dùng để đo | Cách đọc |
|---|---|---|
| Recall@50 before rerank | Candidate pool có chứa chunk đúng không | Thấp thì sửa retrieval/chunking trước |
| Recall@5 after rerank | Context cuối có chứa chunk đúng không | Cao hơn nghĩa là rerank giúp top context |
| MRR@10 | Chunk đúng đầu tiên lên gần đầu không | Tốt cho câu hỏi có một đáp án chính |
| nDCG@10 | Ranking nhiều mức relevance có tốt không | Tốt khi qrels có điểm 0/1/2/3 |
| Context precision | Context đưa vào prompt ít noise không | Liên quan trực tiếp đến faithfulness |
| p50/p95/p99 latency | Có đạt SLA không | Luôn đo riêng search, rerank, LLM |
Nếu Recall@50 thấp, reranker không phải giải pháp chính. Cần xem lại chunking, query normalization, BM25, embedding model, metadata filter hoặc RRF.
9. Code Python gần production
Ví dụ dưới đây tập trung vào layer reranking. Hàm hybrid_retrieve được giả định đến từ pipeline Day 36.
from __future__ import annotations
import logging
import os
import time
from dataclasses import dataclass
from typing import Callable, Protocol, Sequence
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class AccessContext:
tenant_id: str
roles: frozenset[str]
@dataclass(frozen=True)
class CandidateChunk:
id: str
document_id: str
chunk_id: str
tenant_id: str
acl_roles: frozenset[str]
title: str
section_path: tuple[str, ...]
text: str
source_uri: str
page_start: int | None
page_end: int | None
retrieval_score: float
retrieval_rank: int
retriever: str
def text_for_reranker(self, max_chars: int = 2_500) -> str:
section = " > ".join(self.section_path)
body = self.text[:max_chars]
return (
f"Title: {self.title}\n"
f"Section: {section}\n"
f"Source: {self.source_uri}\n"
f"Text: {body}"
)
@dataclass(frozen=True)
class RankedChunk:
chunk: CandidateChunk
rerank_score: float
final_rank: int
class Reranker(Protocol):
name: str
model_version: str
def score(self, query: str, candidates: Sequence[CandidateChunk]) -> list[float]:
"""Return one relevance score per candidate, same order as input."""
class BgeCrossEncoderReranker:
name = "bge-cross-encoder"
def __init__(
self,
model_name: str = "BAAI/bge-reranker-v2-m3",
max_length: int = 512,
device: str | None = None,
) -> None:
from sentence_transformers import CrossEncoder
self.model_version = model_name
self.model = CrossEncoder(model_name, max_length=max_length, device=device)
def score(self, query: str, candidates: Sequence[CandidateChunk]) -> list[float]:
pairs = [(query, candidate.text_for_reranker()) for candidate in candidates]
scores = self.model.predict(pairs)
return [float(score) for score in scores]
class CohereReranker:
name = "cohere-rerank"
def __init__(
self,
api_key: str | None = None,
model: str = "rerank-v4.0-pro",
max_tokens_per_doc: int = 1_200,
) -> None:
import cohere
self.model_version = model
self.client = cohere.Client(token=api_key or os.environ["COHERE_API_KEY"])
self.max_tokens_per_doc = max_tokens_per_doc
def score(self, query: str, candidates: Sequence[CandidateChunk]) -> list[float]:
documents = [candidate.text_for_reranker() for candidate in candidates]
response = self.client.v2.rerank(
model=self.model_version,
query=query,
documents=documents,
top_n=len(documents),
max_tokens_per_doc=self.max_tokens_per_doc,
)
scores = [float("-inf")] * len(candidates)
for result in response.results:
scores[result.index] = float(result.relevance_score)
return scores
HybridRetrieveFn = Callable[[str, int], list[CandidateChunk]]
def has_access(candidate: CandidateChunk, access: AccessContext) -> bool:
if candidate.tenant_id != access.tenant_id:
return False
return bool(candidate.acl_roles & access.roles)
def dedupe_candidates(candidates: Sequence[CandidateChunk]) -> list[CandidateChunk]:
best_by_chunk: dict[str, CandidateChunk] = {}
for candidate in candidates:
previous = best_by_chunk.get(candidate.id)
if previous is None or candidate.retrieval_score > previous.retrieval_score:
best_by_chunk[candidate.id] = candidate
return sorted(
best_by_chunk.values(),
key=lambda item: (item.retrieval_rank, -item.retrieval_score),
)
def fallback_rank(candidates: Sequence[CandidateChunk], final_k: int) -> list[RankedChunk]:
return [
RankedChunk(chunk=candidate, rerank_score=float("nan"), final_rank=index + 1)
for index, candidate in enumerate(candidates[:final_k])
]
def retrieve_then_rerank(
*,
query: str,
access: AccessContext,
hybrid_retrieve: HybridRetrieveFn,
reranker: Reranker,
retrieve_k: int = 100,
rerank_k: int = 50,
final_k: int = 8,
rerank_timeout_ms: int = 800,
) -> list[RankedChunk]:
started = time.perf_counter()
raw_candidates = hybrid_retrieve(query, retrieve_k)
permitted = [item for item in raw_candidates if has_access(item, access)]
deduped = dedupe_candidates(permitted)
rerank_input = deduped[:rerank_k]
if not rerank_input:
return []
try:
scores = reranker.score(query, rerank_input)
except Exception:
logger.exception(
"reranker_failed",
extra={
"reranker": reranker.name,
"model_version": reranker.model_version,
"retrieve_k": retrieve_k,
"rerank_k": rerank_k,
},
)
return fallback_rank(rerank_input, final_k)
elapsed_ms = (time.perf_counter() - started) * 1000
if elapsed_ms > rerank_timeout_ms:
logger.warning(
"reranker_slow",
extra={
"elapsed_ms": round(elapsed_ms, 2),
"timeout_ms": rerank_timeout_ms,
"reranker": reranker.name,
},
)
return fallback_rank(rerank_input, final_k)
ranked_pairs = sorted(
zip(rerank_input, scores, strict=True),
key=lambda item: item[1],
reverse=True,
)
return [
RankedChunk(chunk=chunk, rerank_score=float(score), final_rank=index + 1)
for index, (chunk, score) in enumerate(ranked_pairs[:final_k])
]
Production note cho code trên:
hybrid_retrievephải filtertenant_id,deleted_at,index_versioncàng sớm càng tốt ở database/search engine.- Reranker không được nhận candidate chưa qua ACL.
- Với self-host BGE, nên expose qua service riêng để có request timeout thực sự, batching và autoscale. Skeleton trên chỉ minh họa timeout budget ở application layer.
- Với managed API, đặt timeout/retry/circuit breaker ở HTTP client hoặc SDK layer.
- Log cần có
query_id,candidate_count,reranker_model,before_rank,after_rank, latency và fallback flag.
10. Evaluation code
from collections.abc import Iterable
def recall_at_k(ranked_ids: Sequence[str], relevant_ids: set[str], k: int) -> float:
if not relevant_ids:
return 0.0
retrieved = set(ranked_ids[:k])
return len(retrieved & relevant_ids) / len(relevant_ids)
def mrr_at_k(ranked_ids: Sequence[str], relevant_ids: set[str], k: int) -> float:
for rank, chunk_id in enumerate(ranked_ids[:k], start=1):
if chunk_id in relevant_ids:
return 1.0 / rank
return 0.0
def evaluate_ranker(
run: dict[str, list[str]],
qrels: dict[str, set[str]],
recall_k: int = 5,
mrr_k: int = 10,
) -> dict[str, float]:
recall_scores = []
mrr_scores = []
for query_id, relevant_ids in qrels.items():
ranked_ids = run.get(query_id, [])
recall_scores.append(recall_at_k(ranked_ids, relevant_ids, recall_k))
mrr_scores.append(mrr_at_k(ranked_ids, relevant_ids, mrr_k))
total = max(len(qrels), 1)
return {
f"Recall@{recall_k}": sum(recall_scores) / total,
f"MRR@{mrr_k}": sum(mrr_scores) / total,
}
Cách chạy eval đúng:
- Tạo baseline
hybrid_only_run: danh sáchchunk_idsau RRF, chưa rerank. - Tạo
hybrid_rerank_run: danh sáchchunk_idsau rerank. - So sánh Recall@5, MRR@10 và latency cùng query set.
- Tách query theo tag: keyword-heavy, semantic, acronym, no-diacritic, policy version, tiếng Việt/English mix.
- Đọc ít nhất 5 query improved và 5 query regressed để biết vì sao metric đổi.
11. Production readiness
Dùng reranker trong production được không? Có, nếu thỏa các điều kiện sau:
- Có eval set đại diện và reranker cải thiện metric quan trọng, ví dụ Recall@5 hoặc MRR@10, không chỉ cải thiện demo.
- Candidate pool trước rerank có recall đủ cao, thường đo Recall@50 hoặc Recall@100.
- ACL, tenant, deleted document và index version được filter trước rerank.
- Latency p95/p99 sau khi thêm rerank vẫn nằm trong SLA.
- Có timeout, fallback về retrieval rank và monitoring lỗi.
- Có quyết định rõ managed vs self-host dựa trên privacy, cost, traffic và năng lực vận hành.
- Có versioning cho reranker model, prompt/context builder và config
retrieve_k,rerank_k,final_k. - Có regression test cho citation correctness và các query rủi ro cao.
Nếu chưa có eval set, chưa có ACL filter hoặc không đo latency percentile, chưa nên bật reranker mặc định cho production traffic. Có thể bật bằng feature flag cho internal users hoặc shadow traffic để thu số liệu.
12. Checklist cuối bài
- Giải thích được vì sao retrieval top-k chưa đủ cho RAG production.
- Phân biệt được bi-encoder và cross-encoder bằng latency, cost và quality.
- Mô tả được BGE reranker và Cohere Rerank ở mức concept lẫn trade-off.
- Thiết kế được pipeline retrieve top 50/100 -> rerank top 5/10.
- Biết đặt ACL filter trước rerank.
- Đo được Recall@k và MRR trước/sau rerank.
- Có latency p50/p95/p99 cho search, rerank và total retrieval.
- Có fallback khi reranker fail hoặc quá chậm.
- Trả lời được production readiness bằng điều kiện cụ thể, không trả lời chung chung.
Tài liệu
1. Mental model nhanh
Reranking là bước xếp hạng lại một candidate pool nhỏ hơn sau retrieval.
Hybrid retrieval = recall layer
Reranker = precision layer
Context builder = token/citation layer
LLM = answer layer
Ba câu cần hỏi trước khi thêm reranker:
- Candidate pool trước rerank có chứa chunk đúng chưa?
- Reranker có đưa chunk đúng lên top context tốt hơn không?
- Latency/cost tăng thêm có đáng với chất lượng tăng thêm không?
2. Decision matrix
| Context | Lựa chọn hợp lý | Lý do |
|---|---|---|
| Prototype hoặc eval nhanh | Cohere Rerank hoặc managed rerank API | Ít vận hành, tạo baseline nhanh |
| Dữ liệu nhạy cảm, không được gửi ra ngoài | Self-host BGE reranker | Kiểm soát privacy và network boundary |
| Traffic thấp, quality quan trọng | Cross-encoder rerank top 50/100 | Latency tăng nhưng chấp nhận được |
| Traffic cao, SLA chặt | Rerank top 20/50 hoặc conditional rerank | Giữ p95 ổn định hơn |
| Corpus nhỏ, retrieval đã rất tốt | Có thể chưa cần reranker | Tránh thêm complexity khi metric không tăng |
| Legal/compliance/support policy | Nên rerank và eval kỹ | Cần top context chính xác, citation đúng |
3. Cấu hình khởi điểm
| Tham số | Giá trị khởi điểm | Khi tăng | Khi giảm |
|---|---|---|---|
bm25_top_k | 50 | Query nhiều keyword, cần recall cao | BM25 nhiều nhiễu |
vector_top_k | 50 | Semantic query đa dạng | Latency search cao |
rrf_k | 60 | Muốn giảm ảnh hưởng rank đầu | Ít khi cần đổi sớm |
rerank_k | 50 | Recall@50 tốt và cần top precision hơn | p95/cost cao |
final_k | 5-10 | Câu trả lời cần nhiều evidence | Prompt bị noise hoặc quá dài |
max_tokens_per_doc | 512-1200 | Chunk ngắn bị thiếu context | Reranker chậm hoặc truncate sai |
rerank_timeout_ms | 500-1500 | SLA rộng, quality ưu tiên | Chat cần phản hồi nhanh |
Không copy cấu hình này vào production mà không benchmark. Nó là điểm bắt đầu để chạy eval.
4. Metrics bắt buộc
| Metric | Nơi đo | Ý nghĩa |
|---|---|---|
| Recall@50 before rerank | Candidate pool | Retriever có tìm thấy chunk đúng không |
| Recall@5 after rerank | Final context | Reranker có đưa chunk đúng vào prompt không |
| MRR@10 | Final ranking | Chunk đúng đầu tiên đứng cao không |
| nDCG@10 | Final ranking | Ranking có tôn trọng relevance nhiều mức không |
| Context precision | Context builder | Prompt ít chunk nhiễu không |
| Citation correctness | Answer audit | Câu trả lời cite đúng nguồn không |
| p50/p95/p99 rerank latency | Runtime | Có đạt SLA không |
| Fallback rate | Runtime | Reranker fail/timeout nhiều không |
| Cost per 1K queries | Finance/ops | Có scale được theo traffic không |
5. Eval report template
| Pipeline | Recall@5 | MRR@10 | nDCG@10 | p95 search ms | p95 rerank ms | p95 total ms | Cost/1K | Note |
|---|---:|---:|---:|---:|---:|---:|---:|---|
| Hybrid only | | | | | 0 | | | |
| Hybrid + BGE rerank top 50 | | | | | | | | |
| Hybrid + Cohere rerank top 50 | | | | | | | | |
| Hybrid + rerank top 100 | | | | | | | | |
Phân tích bắt buộc sau bảng:
- 5 query improved: vì sao reranker giúp?
- 5 query regressed: vì sao reranker làm tệ hơn?
- Nhóm query nào hưởng lợi nhiều nhất?
- Nhóm query nào cần sửa chunking/retrieval thay vì rerank?
- Cấu hình nào đạt quality tốt nhất trong latency budget?
6. Runbook rollout
- Chốt query eval set và qrels.
- Chạy baseline Hybrid Search từ Day 36.
- Thêm reranker phía sau RRF, phía sau ACL filter.
- Chạy offline eval với nhiều cấu hình
rerank_k: 20, 50, 100. - Benchmark latency với độ dài chunk thật.
- Chạy security test: tenant A không thể rerank chunk tenant B.
- Shadow traffic: log ranking mới nhưng chưa dùng để trả lời user.
- So sánh answer quality và citation correctness.
- Bật feature flag cho internal users hoặc một phần traffic.
- Theo dõi p95/p99, fallback rate, cost và complaint rate.
- Rollback nếu latency vượt SLA hoặc regression ở query rủi ro cao.
7. Observability
Log mỗi request nên có:
query_id,tenant_id,user_role_group.retriever_versions: BM25 config, embedding model, index version.retrieve_k,rerank_k,final_k.- Candidate ids trước rerank và sau rerank.
- Retrieval score, RRF rank, reranker score.
- Reranker provider, model version, timeout, retry count.
- Latency từng stage: normalize, BM25, vector, RRF, ACL filter, rerank, context build.
- Fallback reason nếu có.
Không log raw query hoặc raw chunk nếu chứa PII mà chưa có policy. Có thể log hash, redacted text hoặc sample theo allowlist.
8. Failure modes
| Lỗi | Dấu hiệu | Cách xử lý |
|---|---|---|
| Candidate pool thiếu chunk đúng | Recall@50 thấp | Sửa retrieval, chunking, embedding, BM25 hoặc RRF |
| Reranker làm tụt ranking | MRR giảm, query regressed nhiều | Đổi model, format input, fine-tune hoặc threshold |
| ACL leak | Candidate không đúng quyền xuất hiện trong log/API/prompt | Filter trước rerank, thêm regression test |
| Latency spike | p95/p99 tăng mạnh | Giảm rerank_k, batch tốt hơn, timeout/fallback |
| Cost spike | Cost/1K queries tăng | Conditional rerank, cache, giảm candidate/text length |
| Score khó hiểu | Threshold fail khi đổi model | Calibrate score theo eval set, không xem score là confidence |
| Citation sai | Answer cite nhầm source | Giữ stable chunk_id, source_uri, page metadata qua mọi stage |
9. Managed vs self-host checklist
Managed API phù hợp khi:
- Team cần ship nhanh.
- Data policy cho phép gửi query/chunk ra provider.
- Traffic chưa quá lớn hoặc cost dự đoán được.
- SLA của provider và rate limit phù hợp.
Self-host phù hợp khi:
- Dữ liệu nhạy cảm hoặc yêu cầu data residency chặt.
- Traffic đủ lớn để tối ưu cost bằng hạ tầng riêng.
- Team có năng lực vận hành CPU/GPU inference.
- Cần fine-tune hoặc kiểm soát model version chặt.
10. Production readiness checklist
- Có qrels và eval set đại diện.
- Có baseline Hybrid Search trước khi thêm reranker.
- Recall@50 hoặc Recall@100 trước rerank đủ cao.
- Reranker cải thiện Recall@5, MRR@10 hoặc metric mục tiêu.
- Đã đo latency p50/p95/p99 bằng dữ liệu thật.
- Đã lọc tenant, ACL, deleted, index version trước rerank.
- Có timeout và fallback về retrieval rank.
- Có feature flag và rollback path.
- Có model/config versioning.
- Có cost estimate cho traffic hiện tại và 3-6 tháng tới.
- Có policy logging cho PII.
- Có citation correctness test.
11. Quiz nhanh
- Vì sao reranker không thể sửa lỗi khi chunk đúng không nằm trong top 50 candidate?
- Khi nào nên dùng cross-encoder thay vì chỉ dùng bi-encoder?
- Vì sao phải filter ACL trước khi gọi managed rerank API?
- Nếu Recall@50 thấp nhưng MRR@10 sau rerank cao, bạn kết luận gì?
- Nếu MRR tăng nhưng p95 latency vượt SLA, bạn thử những thay đổi nào trước?
Đáp án gợi ý:
- Reranker chỉ sắp xếp candidate có sẵn, không search lại toàn corpus.
- Khi top context precision, citation quality hoặc câu hỏi có nuance quan trọng hơn latency tăng thêm.
- Vì gửi chunk không đúng quyền ra ngoài đã là data leak, dù sau đó không đưa vào prompt.
- Reranker tốt trên candidate có sẵn, nhưng retrieval vẫn bỏ sót nhiều đáp án. Cần cải thiện retrieval/chunking.
- Giảm
rerank_k, dedupe tốt hơn, truncate text, batching, cache, conditional rerank hoặc đổi model nhỏ hơn.
12. Câu trả lời production readiness
Reranker dùng được trong production, nhưng không nên bật chỉ vì "nghe có vẻ tốt hơn". Quyết định production cần dựa trên eval và SLA:
- Nếu metric tăng rõ, latency/cost nằm trong budget, privacy được xử lý và có fallback, nên dùng.
- Nếu metric không tăng hoặc candidate recall thấp, hãy sửa retrieval trước.
- Nếu data policy không cho gửi dữ liệu ra ngoài, chỉ dùng managed API khi có approval rõ, còn lại self-host.
- Nếu p95 vượt SLA, dùng conditional rerank hoặc giảm candidate thay vì hy sinh toàn bộ trải nghiệm chat.
Bài tập
Mục tiêu
Sau bài tập này bạn sẽ có một pipeline RAG retrieval gần production:
BM25 top 50 + Vector top 50
-> RRF merge
-> ACL filter
-> dedupe
-> rerank top 50
-> final top 5/10
-> evaluate Recall@k / MRR
Thời lượng đề xuất: 120-180 phút.
1. Chuẩn bị
Yêu cầu:
- Python 3.10+.
- Pipeline Day 36 đã có BM25, vector search và RRF.
- Một corpus nhỏ có
chunk_id,document_id,tenant_id,acl_roles,title,text,source_uri. - Một query set có qrels.
Cài thư viện nếu muốn chạy BGE reranker local:
pip install sentence-transformers numpy
Nếu muốn thử Cohere Rerank:
pip install cohere
export COHERE_API_KEY="..."
2. Dataset và qrels tối thiểu
Tạo file hoặc object Python tương đương:
QUERIES = [
{
"query_id": "q001",
"query": "Nhân viên full-time có bao nhiêu ngày nghỉ phép năm 2026?",
"tags": ["policy", "version", "vietnamese"],
},
{
"query_id": "q002",
"query": "VPN error 809 xử lý như thế nào?",
"tags": ["keyword-heavy", "incident"],
},
{
"query_id": "q003",
"query": "Can contractor access production database?",
"tags": ["english", "acl", "security"],
},
]
QRELS = {
"q001": {"hr_leave_2026:chunk_003"},
"q002": {"it_vpn_runbook:chunk_007"},
"q003": {"security_access_policy:chunk_011"},
}
Mở rộng lên ít nhất 30 query nếu muốn kết quả có ý nghĩa hơn. Nên có query tốt, query khó, query tiếng Việt không dấu, acronym, mã lỗi, và query trộn English/Vietnamese.
3. Step 1 - Chạy baseline Hybrid Search
Từ Day 36, viết hàm trả về danh sách chunk_id sau RRF, chưa rerank.
def run_hybrid_only(query: str, top_k: int = 10) -> list[str]:
bm25_hits = bm25_search(query, top_k=50)
vector_hits = vector_search(query, top_k=50)
merged = reciprocal_rank_fusion([bm25_hits, vector_hits])
permitted = acl_filter(merged)
deduped = dedupe(permitted)
return [hit.chunk_id for hit in deduped[:top_k]]
Yêu cầu:
- Không có chunk khác tenant.
- Không có chunk user không có quyền.
- Có log số lượng candidate sau từng bước.
4. Step 2 - Thêm BGE reranker
from sentence_transformers import CrossEncoder
class LocalBgeReranker:
def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3") -> None:
self.model = CrossEncoder(model_name, max_length=512)
def rerank(self, query: str, candidates: list[dict], top_n: int = 10) -> list[dict]:
pairs = [
(
query,
"Title: {title}\nSection: {section}\nText: {text}".format(
title=item.get("title", ""),
section=" > ".join(item.get("section_path", [])),
text=item["text"][:2500],
),
)
for item in candidates
]
scores = self.model.predict(pairs)
ranked = sorted(
zip(candidates, scores, strict=True),
key=lambda item: float(item[1]),
reverse=True,
)
return [
{
**candidate,
"rerank_score": float(score),
"final_rank": index + 1,
}
for index, (candidate, score) in enumerate(ranked[:top_n])
]
Pipeline:
def run_hybrid_plus_rerank(query: str, reranker: LocalBgeReranker) -> list[str]:
bm25_hits = bm25_search(query, top_k=50)
vector_hits = vector_search(query, top_k=50)
merged = reciprocal_rank_fusion([bm25_hits, vector_hits])
permitted = acl_filter(merged)
deduped = dedupe(permitted)
candidates = [hit.to_dict() for hit in deduped[:50]]
ranked = reranker.rerank(query, candidates, top_n=10)
return [item["chunk_id"] for item in ranked]
Nếu máy yếu, giảm rerank_k xuống 20 trước, sau đó benchmark lại top 50.
5. Step 3 - Optional Cohere Rerank
Dùng managed API để so sánh baseline nhanh:
import os
import cohere
class CohereRerankClient:
def __init__(self, model: str = "rerank-v4.0-pro") -> None:
self.client = cohere.Client(token=os.environ["COHERE_API_KEY"])
self.model = model
def rerank(self, query: str, candidates: list[dict], top_n: int = 10) -> list[dict]:
documents = [
"Title: {title}\nText: {text}".format(
title=item.get("title", ""),
text=item["text"][:2500],
)
for item in candidates
]
response = self.client.v2.rerank(
model=self.model,
query=query,
documents=documents,
top_n=top_n,
max_tokens_per_doc=1200,
)
ranked = []
for rank, result in enumerate(response.results, start=1):
item = dict(candidates[result.index])
item["rerank_score"] = float(result.relevance_score)
item["final_rank"] = rank
ranked.append(item)
return ranked
Chỉ chạy phần này nếu dữ liệu được phép gửi ra ngoài. Nếu corpus có PII hoặc dữ liệu nội bộ nhạy cảm, dùng dữ liệu giả lập hoặc bỏ qua managed API.
6. Step 4 - Evaluation
def recall_at_k(ranked_ids: list[str], relevant_ids: set[str], k: int) -> float:
if not relevant_ids:
return 0.0
return len(set(ranked_ids[:k]) & relevant_ids) / len(relevant_ids)
def mrr_at_k(ranked_ids: list[str], relevant_ids: set[str], k: int) -> float:
for rank, chunk_id in enumerate(ranked_ids[:k], start=1):
if chunk_id in relevant_ids:
return 1.0 / rank
return 0.0
def evaluate(run: dict[str, list[str]], qrels: dict[str, set[str]]) -> dict[str, float]:
recall_5 = []
mrr_10 = []
for query_id, relevant_ids in qrels.items():
ranked_ids = run.get(query_id, [])
recall_5.append(recall_at_k(ranked_ids, relevant_ids, 5))
mrr_10.append(mrr_at_k(ranked_ids, relevant_ids, 10))
total = len(qrels) or 1
return {
"Recall@5": sum(recall_5) / total,
"MRR@10": sum(mrr_10) / total,
}
Chạy:
hybrid_run = {
item["query_id"]: run_hybrid_only(item["query"], top_k=10)
for item in QUERIES
}
reranker = LocalBgeReranker()
rerank_run = {
item["query_id"]: run_hybrid_plus_rerank(item["query"], reranker)
for item in QUERIES
}
print("Hybrid only:", evaluate(hybrid_run, QRELS))
print("Hybrid + rerank:", evaluate(rerank_run, QRELS))
7. Step 5 - Benchmark latency
import statistics
import time
def benchmark(fn, queries: list[dict], repeat: int = 3) -> dict[str, float]:
latencies = []
for _ in range(repeat):
for item in queries:
started = time.perf_counter()
fn(item["query"])
latencies.append((time.perf_counter() - started) * 1000)
latencies = sorted(latencies)
p95_index = int(0.95 * (len(latencies) - 1))
return {
"avg_ms": statistics.mean(latencies),
"p50_ms": statistics.median(latencies),
"p95_ms": latencies[p95_index],
"max_ms": max(latencies),
}
Đo riêng:
- Hybrid only.
- Hybrid + BGE rerank top 20.
- Hybrid + BGE rerank top 50.
- Optional Hybrid + Cohere rerank top 50.
8. Report cần nộp
| Pipeline | Recall@5 | MRR@10 | p50 ms | p95 ms | Cost/1K query | Ghi chú |
|---|---:|---:|---:|---:|---:|---|
| Hybrid only | | | | | | |
| Hybrid + BGE top 20 | | | | | | |
| Hybrid + BGE top 50 | | | | | | |
| Hybrid + Cohere top 50 | | | | | | |
Thêm phân tích:
- 5 query improved sau rerank.
- 5 query regressed sau rerank.
- Query nào không được cải thiện vì chunk đúng không nằm trong candidate pool?
rerank_k=20hayrerank_k=50đáng dùng hơn theo SLA?- Nếu bật production, bạn dùng BGE self-host hay Cohere managed? Vì sao?
9. Quiz
- Vì sao cần retrieve top 50/100 rồi mới rerank top 5/10?
- Reranking cải thiện Recall@50 hay Recall@5? Giải thích.
- Nếu p95 tăng từ 450ms lên 1.600ms sau rerank, bạn xử lý thế nào?
- Nếu dùng Cohere Rerank cho tài liệu nội bộ, bạn phải kiểm tra những điều kiện gì?
- Khi nào không nên dùng reranker?
10. Acceptance criteria
- Có pipeline Hybrid only chạy được.
- Có pipeline Hybrid + rerank chạy được.
- ACL filter chạy trước rerank.
- Có số liệu Recall@5 và MRR@10 trước/sau rerank.
- Có benchmark p50/p95.
- Có bảng so sánh ít nhất 2 cấu hình
rerank_k. - Có phân tích improved/regressed query.
- Có câu trả lời production readiness rõ ràng.
11. Câu trả lời production readiness mẫu
Ví dụ câu trả lời tốt:
Có thể dùng reranker trong production cho nhóm query policy/support vì Hybrid + BGE top 50 tăng MRR@10 từ 0.61 lên 0.78 và Recall@5 từ 0.70 lên 0.84 trên 120 query eval. p95 retrieval tăng từ 420ms lên 780ms, vẫn dưới SLA 1.200ms. Hệ thống đã filter tenant/ACL trước rerank, có timeout 800ms và fallback về RRF rank. Giai đoạn đầu bật bằng feature flag cho 20% internal traffic, log before/after rank và theo dõi citation correctness.
Ví dụ câu trả lời chưa đạt:
Nên dùng reranker vì nó thông minh hơn vector search.
Câu trả lời chưa đạt vì không có metric, latency, security condition, fallback và rollout plan.