Meningkatkan parameter top_k pada vector retrieval sering dianggap solusi instan saat LLM kehilangan konteks dokumen penting. Namun, menaikkan top_k dari 5 menjadi 25 atau lebih sering kali memicu regresi performa: latensi Time to First Token (TTFT) melonjak tajam dan akurasi jawaban menurun drastis akibat context dilution.
Gejala: Lonjakan TTFT dan Fenomena Lost in the Middle
Degradasi sistem pasca ekspansi context window retrieval memunculkan dua anomali terukur pada level metrik aplikasi dan inferensi model:
- Lonjakan p99 TTFT (Time to First Token): Latensi prefill pada LLM engine berskala linear hingga kuadratik terhadap panjang input token. Menambah 20 chunk tambahan berukuran masing-masing 512 token menyuntikkan >10.000 token ekstra ke prompt. Engine upstream memerlukan waktu prefill KV cache yang jauh lebih lama sebelum menghasilkan token pertama.
- Lost in the Middle & Halusinasi: Berdasarkan riset retrieval-augmented generation, arsitektur decoder-only transformer memiliki bias atensi lebih tinggi pada awal dan akhir urutan konteks. Potongan informasi relevan yang terjepit di tengah tumpukan chunk acak (posisi 10 hingga 20) diabaikan oleh mekanisme self-attention, menyebabkan model memproduksi halusinasi berbasis prior pre-training alih-alih konteks aktual.
Analisis Root Cause
Investigasi pada backend orchestrator dan pipeline inferensi mengungkap dua akar masalah utama:
1. Context Noise Poisoning dari Bi-Encoder Single-Stage
Dense retrieval berbasis cosine similarity (vektor embedding tunggal) mengorbankan ketepatan demi kecepatan pencarian sub-linear pada jutaan record. Ketika ambang batas retrieval diperlonggar untuk menarik top-k yang lebih besar, rasio noise-to-signal melonjak. Chunk yang hanya memiliki kemiripan leksikal semu ikut terbawa dan meracuni konteks inferensi, menciptakan kontradiksi fakta dalam satu prompt.
2. Overhead Serialisasi Prompt Backend Orchestrator
Sebelum payload dikirimkan ke endpoint upstream LLM, orchestrator harus mengekstrak string, membersihkan metadata, dan merakit array pesan. Mengirim payload puluhan kilobyte membebani loop serialisasi JSON dan buffer alokasi memori pada proses worker backend, terutama di bawah beban throughput tinggi.
Arsitektur Perbaikan: Two-Stage Retrieval & Budget Pruner
Solusi teknis tidak terletak pada membesarkan context payload, melainkan memperketat filter relevansi sebelum prompt dibangun. Pipeline diubah menjadi:
- Stage 1 (Coarse Retrieval): Vector store mengambil kandidat luas (misal:
top_k=25) menggunakan bi-encoder embeddings dengan latensi rendah. - Stage 2 (Fine Re-ranking): Model Cross-Encoder (seperti
bge-reranker) menghitung interaksi token penuh antara query dan kandidat chunk, memberikan skor relevansi absolut. - Context Budget Pruning Middleware: Membuang chunk di bawah threshold skor minimum dan memotong akumulasi token agar tidak melewati batas ketat (token budget cap) sebelum serialisasi API upstream.
Implementasi Middleware Context Pruning
Kode middleware Python berikut menerapkan filter relevansi berbasis skor Cross-Encoder dan batas keras token budget untuk mencegah context noise poisoning.
from dataclasses import dataclass
from typing import Callable, List
@dataclass
class RetrievedChunk:
chunk_id: str
text: str
relevance_score: float
token_count: int
def estimate_tokens(text: str) -> int:
# ponytail: fallback tokenizer sederhana, ganti dengan tiktoken/hf tokenizer di production
return max(1, len(text) // 4)
def prune_context(
chunks: List[RetrievedChunk],
score_threshold: float,
max_token_budget: int,
tokenizer_fn: Callable[[str], int] = estimate_tokens
) -> List[RetrievedChunk]:
"""
Memangkas chunk yang tidak relevan berdasarkan threshold skor
dan membatasi total token akumulatif agar tetap dalam budget.
"""
# Sort descending berdasarkan skor reranker
sorted_chunks = sorted(chunks, key=lambda c: c.relevance_score, reverse=True)
selected_chunks: List[RetrievedChunk] = []
current_tokens = 0
for chunk in sorted_chunks:
# Filter noise: abaikan chunk di bawah threshold
if chunk.relevance_score < score_threshold:
continue
token_len = chunk.token_count if chunk.token_count > 0 else tokenizer_fn(chunk.text)
# Enforce budget: hentikan jika menambahkan chunk melebihi kuota prompt
if current_tokens + token_len > max_token_budget:
break
selected_chunks.append(chunk)
current_tokens += token_len
return selected_chunks
Runnable Assert Validation
Eksekusi script pengujian unit sederhana tanpa framework eksternal untuk memastikan mekanisme pruning berjalan sesuai kontrak logika:
def test_context_pruning():
mock_chunks = [
RetrievedChunk(chunk_id="c1", text="Doc relevan A", relevance_score=0.88, token_count=100),
RetrievedChunk(chunk_id="c2", text="Doc noise B", relevance_score=0.35, token_count=150),
RetrievedChunk(chunk_id="c3", text="Doc relevan C", relevance_score=0.79, token_count=120),
RetrievedChunk(chunk_id="c4", text="Doc relevan D", relevance_score=0.65, token_count=100),
]
# Test 1: Pemotongan chunk di bawah threshold
pruned_by_score = prune_context(mock_chunks, score_threshold=0.50, max_token_budget=1000)
assert len(pruned_by_score) == 3, f"Expected 3 chunks, got {len(pruned_by_score)}"
assert "c2" not in [c.chunk_id for c in pruned_by_score], "Noise chunk c2 lolos dari filter"
# Test 2: Pemotongan berdasarkan token budget (Budget max 200 token)
pruned_by_budget = prune_context(mock_chunks, score_threshold=0.50, max_token_budget=200)
assert len(pruned_by_budget) == 1, f"Expected 1 chunk due to budget, got {len(pruned_by_budget)}"
assert pruned_by_budget[0].chunk_id == "c1", "Chunk dengan skor tertinggi harus diprioritaskan"
# Test 3: Threshold terlalu tinggi mengosongkan konteks
empty_result = prune_context(mock_chunks, score_threshold=0.95, max_token_budget=1000)
assert len(empty_result) == 0, "Harus mengembalikan list kosong jika tidak ada chunk qualified"
print("Semua assert lolos secara valid.")
if __name__ == "__main__":
test_context_pruning()
Trade-off dan Evaluasi Operasional
Penambahan layer Cross-Encoder dan pruning middleware mengubah profil performa sistem secara signifikan:
- Compute Overhead vs Network Latency: Re-ranking Cross-Encoder menambah latensi komputasi lokal (~20–40ms pada GPU/CPU terdedikasi), namun memotong durasi TTFT upstream LLM hingga ratusan milidetik karena penurunan drastis pada beban KV cache.
- Penentuan Threshold Skor: Nilai
score_thresholdtidak boleh statis secara permanen. Kalibrasi harus dijalankan berkala terhadap dataset evaluasi (misal: Ragas atau TruLens) untuk mencegah under-retrieval pada query spesifik domain kompleks. - Pengurutan Konteks Ulang: Chunk yang lolos kurasi dapat diurutkan kembali menggunakan strategi edge placement (chunk paling relevan ditaruh di awal dan akhir teks prompt) untuk memitigasi sisa bias posisi pada model autoregresif.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!