はじめに
上位 K 位のドキュメントを取得しました。しかし、順序が間違っている、または チャンクに冗長な情報が多すぎます → LLM のノイズが多く、応答が悪くなります。
例: 5 つのチャンクを取得すると、4 番目のチャンクに正しい答えが含まれます。ただし、LLM はチャンク 1 (ランクは高いが関連性は低い) に焦点を当てます。再ランキングによりチャンク 4 が上位に押し上げられ、品質が向上します。
この記事では、次の 2 つのテクニックについて説明します。
- 再ランキング — 実際の関連性に基づいて結果を並べ替えます
- コンテキスト圧縮 — 無関係な部分を圧縮/削除します
Retrieval Pipeline nâng cao:
Query → Retrieve (top-20) → Re-Rank (chọn top-5) → Compress → LLM
↑ recall cao ↑ precision cao ↑ ít noise
(lấy nhiều) (chọn đúng) (nén gọn)
1. なぜ再ランキングが必要なのでしょうか?
1.1 Bi-Encoder (埋め込み検索) の制限事項
Bi-Encoder (vector search):
Query → Encoder A → vector_q ─┐
├── cosine similarity
Doc → Encoder B → vector_d ─┘
Ưu: NHANH (pre-compute embeddings, tìm bằng ANN)
Nhược: Encode query và doc RIÊNG RẼ → bỏ lỡ cross-attention
→ ranking có thể sai thứ tự
Cross-Encoder (re-ranker):
[Query + Doc] → Encoder → relevance score (0-1)
Ưu: CHÍNH XÁC hơn (xem query+doc cùng lúc, full attention)
Nhược: CHẬM (phải chạy model cho mỗi cặp query-doc)
1.2 戦略: 多数を取得 → 再ランク付け → 少数を選択
Bi-Encoder Cross-Encoder
(fast, rough) (slow, accurate)
│ │
Top-100 docs ──────→ Top-20 ──────→ Top-5 ──────→ LLM
(recall cao) (precision cao)
2. クロスエンコーダーによる再ランキング
2.1 文変換機能を使用する
"""Cross-Encoder re-ranking với sentence-transformers"""
from sentence_transformers import CrossEncoder
# Load model cross-encoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
query = "Nghỉ phép bao nhiêu ngày?"
# Documents từ retrieval (top-20)
docs = [
"Công ty thành lập năm 2020, trụ sở tại TP.HCM.",
"Nhân viên full-time được 15 ngày phép có lương mỗi năm.",
"Quy trình tuyển dụng gồm 3 vòng phỏng vấn.",
"Nhân viên trên 5 năm được thêm 3 ngày phép.",
"Lương được trả vào ngày 5 hàng tháng.",
]
# Re-rank: tính relevance score cho mỗi cặp (query, doc)
pairs = [(query, doc) for doc in docs]
scores = reranker.predict(pairs)
# Sắp xếp theo score giảm dần
ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)
for doc, score in ranked:
print(f"[{score:.3f}] {doc}")
# Output:
# [0.987] Nhân viên full-time được 15 ngày phép có lương mỗi năm.
# [0.912] Nhân viên trên 5 năm được thêm 3 ngày phép.
# [0.023] Lương được trả vào ngày 5 hàng tháng.
# [0.008] Quy trình tuyển dụng gồm 3 vòng phỏng vấn.
# [0.003] Công ty thành lập năm 2020, trụ sở tại TP.HCM.
2.2 LangChain への統合
"""Re-ranking trong LangChain pipeline"""
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
# Base retriever (retrieve top-20)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
# Cross-encoder reranker
model = HuggingFaceCrossEncoder(model_name="cross-encoder/ms-marco-MiniLM-L-6-v2")
reranker = CrossEncoderReranker(model=model, top_n=5) # Chỉ giữ top-5
# Pipeline: retrieve 20 → rerank → top 5
reranking_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever,
)
results = reranking_retriever.invoke("Nghỉ phép bao nhiêu ngày?")
# Trả về 5 docs chính xác nhất (đã re-ranked)
💡 演習 1: 上位 5 の結果を比較します: (a) 直接検索ベクトルの上位 5、(b) ベクトル検索の上位 20 → 上位 5 を再ランク付けします。どちらがより正確ですか?
3. Cohere 再ランク — API ベース
3.1 Cohere Rerank APIの使用
"""Cohere Rerank — production-grade reranking API"""
from langchain_cohere import CohereRerank
from langchain.retrievers import ContextualCompressionRetriever
# Cohere reranker (cần API key)
reranker = CohereRerank(
model="rerank-v3.5",
top_n=5,
)
reranking_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever, # top-20
)
results = reranking_retriever.invoke("Nghỉ phép bao nhiêu ngày?")
3.2 リランカーの比較
| リランカー | 品質 | スピード | コスト | いつ使用するか |
|---|---|---|---|---|
| クロスエンコーダー (ローカル) | ⭐⭐⭐ | 遅い | 無料 | プロトタイプ、オフライン |
| Cohere 再ランク | ⭐⭐⭐⭐ | 速い | APIコスト | 制作 |
| ジナ リランカー | ⭐⭐⭐⭐ | 速い | APIコスト | 代替案 |
| FlashRank (ローカル) | ⭐⭐⭐ | 非常に速い | 無料 | エッジ、低遅延 |
4. コンテキスト圧縮
4.1 問題: チャンクが長すぎます
Retrieved chunk (500 từ):
"Công ty XYZ được thành lập năm 2010 tại Hà Nội.
Qua 15 năm phát triển, công ty đã mở rộng ra nhiều lĩnh vực.
[... 400 từ không liên quan ...]
Nhân viên full-time được 15 ngày phép/năm. ← CÂU TRẢ LỜI
[... 50 từ nữa ...]"
→ 490/500 từ là NOISE! LLM phải đọc hết → lãng phí tokens + giảm accuracy
コンテキスト圧縮は、関連する部分のみを 抽出します。
Compressed: "Nhân viên full-time được 15 ngày phép/năm."
→ 1 câu duy nhất, đúng trọng tâm!
4.2 LLM ベースの圧縮
"""Dùng LLM để nén context — chỉ giữ phần liên quan"""
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain.retrievers import ContextualCompressionRetriever
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# LLM extractor: đọc chunk + query → trích xuất phần liên quan
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=base_retriever,
)
results = compression_retriever.invoke("Nghỉ phép bao nhiêu ngày?")
# Mỗi document.page_content đã được NÉN — chỉ giữ phần liên quan
4.3 LLM フィルター — 無関係なチャンクを削除する
"""LLMChainFilter: giữ/bỏ toàn bộ chunk (không trích xuất)"""
from langchain.retrievers.document_compressors import LLMChainFilter
# Filter: giữ chunk liên quan, bỏ chunk không liên quan
filter_compressor = LLMChainFilter.from_llm(llm)
filter_retriever = ContextualCompressionRetriever(
base_compressor=filter_compressor,
base_retriever=base_retriever,
)
# Nếu retrieve 20 chunks → filter có thể giữ 3-5 chunks liên quan
results = filter_retriever.invoke("Nghỉ phép bao nhiêu ngày?")
4.4 EmbeddingsFilter — 高速、LLM 不要
"""EmbeddingsFilter: lọc bằng similarity threshold, không tốn LLM call"""
from langchain.retrievers.document_compressors import EmbeddingsFilter
from langchain_openai import OpenAIEmbeddings
embeddings_filter = EmbeddingsFilter(
embeddings=OpenAIEmbeddings(),
similarity_threshold=0.75, # Chỉ giữ chunks có similarity >= 0.75
)
filter_retriever = ContextualCompressionRetriever(
base_compressor=embeddings_filter,
base_retriever=base_retriever,
)
💡 演習 2: 3 つのコンプレッサー: LLMChainExtractor、LLMChainFilter、EmbeddingsFilter を比較します。測定: (a) 出力品質、(b) レイテンシ、(c) トークン コスト。
5. 完全なパイプライン: 取得 → 再ランク付け → 圧縮
"""Full pipeline: retrieve 20 → rerank top 5 → compress"""
from langchain.retrievers.document_compressors import DocumentCompressorPipeline
# Pipeline: rerank TRƯỚC, compress SAU
pipeline = DocumentCompressorPipeline(
transformers=[
reranker, # CrossEncoder: 20 → top-5
compressor, # LLMChainExtractor: nén mỗi chunk
]
)
full_retriever = ContextualCompressionRetriever(
base_compressor=pipeline,
base_retriever=base_retriever, # top-20
)
results = full_retriever.invoke("Nghỉ phép bao nhiêu ngày?")
# 5 chunks đã rerank + compress → feed vào LLM
Pipeline flow:
Query: "Nghỉ phép bao nhiêu ngày?"
│
├── Bi-Encoder retrieve top-20 (fast, ~50ms)
│
├── Cross-Encoder rerank → top-5 (slow, ~200ms)
│
├── LLM compress 5 chunks (slow, ~500ms)
│
└── LLM generate answer (~1000ms)
Total: ~1.8s — acceptable cho chatbot
概要
| コンセプト | 覚えておいてください |
|---|---|
| バイエンコーダ | 検索に使用される高速な離散エンコーディング |
| クロスエンコーダー | 低速、正確、クエリ + ドキュメントをエンコード |
| 再ランキング | たくさん取得→再ランキング→少し選ぶ |
| Cohere 再ランク | API 実稼働グレード、高速、正確 |
| LLMChainExtractor | チャンクから関連部分を抽出 |
| LLMChainFilter | チャンク全体を保持/削除 |
| 埋め込みフィルタ | 類似度によるフィルター、LLM は不要 |
| パイプライン | 再ランク → 圧縮 → LLM |
一般的な演習
- ✅ 2 つの小さな演習 (1、2) を完了します。
- 完全なパイプライン: 実装: 50 を取得 → 10 を再ランク付け → 圧縮 → 生成。 15 の質問でテストします。精度@5 とレイテンシを測定します。
- A/B テスト: 回答の品質を比較します (GPT-4 を使用して評価します): (a) 再ランキングなしの RAG、(b) 再ランキングありの RAG、(c) RAG 再ランキング + 圧縮。
- カスタム リランカー: ドメイン固有のデータ (ベトナム語の Q&A など) で 1 つのクロスエンコーダーをトレーニングします。事前トレーニング済みモデルと比較します。
次の記事: Graph RAG — Knowledge Graph + Vector Search — は、グラフ データベースとベクトル検索の力を組み合わせて、複雑な複数ステップの質問に答えます。