Dokumentasi Teknis: Cara Kerja Sistem Academic RAG
Dokumen ini menjelaskan lima hal utama:
- Bagaimana model RAG bekerja secara internal (pipeline di dalam modul
rag-model). - Workflow Admin Dashboard & pembuatan Knowledge Base (proses upload, chunking, dan indexing dokumen).
- Bagaimana API (
rag-api) mengakses model tersebut dan tahap mana saja yang dipanggil untuk menghasilkan jawaban. - Contoh JSON Payload komunikasi antara Web (Laravel) dan API (FastAPI).
- Siklus Hidup Pertanyaan dari sudut pandang internal model secara end-to-end.
Daftar Isi
- Bagian 1: Cara Kerja Internal Model
- Bagian 1.5: Workflow Admin Dashboard & Pembuatan Knowledge Base
- Bagian 2: Bagaimana API Mengakses Model
- Ringkasan Komponen Teknis
- Bagian 3: Contoh JSON Payload Komunikasi Web ↔ API
- Bagian 4: Siklus Hidup Pertanyaan
Bagian 1: Cara Kerja Internal Model (rag-model)
Model RAG bukan satu model tunggal, melainkan sebuah pipeline (rangkaian proses) yang terdiri dari beberapa komponen. Setiap komponen memiliki tugas spesifik dan ditulis sebagai kelas Python terpisah.
1.1 Komponen-Komponen Model
| Komponen | Kelas Python | Library Utama | Tugas |
|---|---|---|---|
| Embedding | VectorStore |
chromadb, sentence-transformers |
Mengubah teks menjadi vektor angka |
| Vector Search | VectorStore |
chromadb |
Mencari dokumen berdasarkan kemiripan makna |
| Keyword Search | BM25Index |
rank-bm25 |
Mencari dokumen berdasarkan kata kunci |
| Fusion | UnifiedIndexManager |
Custom (Python) | Menggabungkan hasil dua pencarian |
| Reranker | CrossEncoderReranker |
sentence-transformers (CrossEncoder) |
Menilai ulang relevansi dokumen |
| Context Builder | ContextBuilder |
Custom (Python) | Menyusun konteks untuk prompt |
| LLM Generator | LLMGenerator |
google-genai / requests |
Menghasilkan jawaban teks |
| Pipeline Utama | AcademicRAG |
Custom (Python) | Mengorkestrasi semua komponen di atas |
1.2 Tahap Hybrid Retrieval
Ini adalah tahap pencarian dokumen yang relevan. Disebut "hybrid" karena menggabungkan dua metode pencarian yang berbeda secara fundamental:
a) Dense Retrieval (Pencarian Semantik / Vector Search)
- Pertanyaan pengguna diubah menjadi vektor numerik menggunakan model embedding IndoBERT (
indobenchmark/indobert-base-p2). - Vektor tersebut dibandingkan dengan vektor seluruh dokumen yang tersimpan di ChromaDB menggunakan metrik Cosine Similarity.
- Kelas yang bertanggung jawab:
VectorStore(dirag_model/indexing/vector_store.py). - Keunggulan: Bisa menemukan dokumen yang maknanya mirip meskipun kata-katanya berbeda. Contoh: pertanyaan "persyaratan KP" bisa menemukan dokumen yang berisi "ketentuan kerja praktek".
b) Sparse Retrieval (Pencarian Kata Kunci / BM25)
- Pertanyaan dicocokkan secara statistik menggunakan algoritma BM25 (Best Matching 25) dengan parameter
k1=1.5danb=0.75. - Kelas yang bertanggung jawab:
BM25Index(dirag_model/indexing/bm25_index.py). - Keunggulan: Sangat akurat untuk menemukan dokumen yang mengandung kata kunci yang sama persis dengan pertanyaan.
c) Reciprocal Rank Fusion (RRF)
- Hasil dari kedua pencarian di atas digabungkan menggunakan teknik Reciprocal Rank Fusion dengan rumus:
skor = 1 / (k + rank)di manak=60. - Kelas yang bertanggung jawab:
UnifiedIndexManager.search_unified()(dirag_model/indexing/unified_index_manager.py). - Bobot default: Vector Search 60%, BM25 40%.
- Hasilnya adalah satu daftar dokumen yang diurutkan berdasarkan skor gabungan, memastikan keseimbangan antara kecocokan makna dan kata kunci.
Kode inti Hybrid Search & RRF (unified_index_manager.py):
def search_unified(self, query, k=10, vector_weight=0.6, bm25_weight=0.4, strategy="rrf"):
# Dense Retrieval: cari berdasarkan kemiripan vektor
vector_results = self.vector_store.similarity_search(query, k=k)
# Sparse Retrieval: cari berdasarkan kata kunci BM25
bm25_results = self.bm25_index.search(query, k=k)
# Gabungkan kedua hasil dengan Reciprocal Rank Fusion
unified_results = self._reciprocal_rank_fusion(vector_results, bm25_results)
return {"results": unified_results[:k]}
def _reciprocal_rank_fusion(self, vector_results, bm25_results, k=60):
doc_scores = {}
# Hitung skor RRF untuk setiap dokumen dari kedua sumber
for rank, doc in enumerate(vector_results):
doc_id = self._get_doc_id(doc)
doc_scores[doc_id] = doc_scores.get(doc_id, 0) + 1.0 / (k + rank + 1)
for rank, doc in enumerate(bm25_results):
doc_id = self._get_doc_id(doc)
doc_scores[doc_id] = doc_scores.get(doc_id, 0) + 1.0 / (k + rank + 1)
# Urutkan berdasarkan skor RRF tertinggi
sorted_docs = sorted(doc_scores.items(), key=lambda x: x[1], reverse=True)
return [doc_data[doc_id] for doc_id, _ in sorted_docs]
1.3 Tahap Cross-Encoder Reranking
Setelah Hybrid Retrieval menghasilkan kandidat dokumen, tahap ini menilai ulang relevansi setiap dokumen secara lebih mendalam:
- Model yang digunakan:
cross-encoder/ms-marco-MiniLM-L-6-v2(via librarysentence-transformers). - Cara kerja: Cross-Encoder menerima pasangan (pertanyaan, dokumen) sebagai input dan menghasilkan satu skor relevansi langsung. Berbeda dengan embedding biasa yang menghitung kemiripan secara terpisah, Cross-Encoder "membaca" pertanyaan dan dokumen bersamaan sehingga pemahamannya lebih akurat.
- Kelas yang bertanggung jawab:
CrossEncoderReranker(dirag_model/models/reranker.py). - Dari sekitar 20 kandidat dokumen (konfigurasi
rerank_k=20), hanya Top-5 (max_results=5) yang lolos ke tahap berikutnya.
Kode inti Reranking (reranker.py):
from sentence_transformers import CrossEncoder
class CrossEncoderReranker:
def __init__(self, config):
self.model = CrossEncoder(config.model_name, max_length=config.max_length)
def rerank(self, query, documents, top_k=5):
# Buat pasangan (pertanyaan, dokumen) untuk setiap kandidat
query_doc_pairs = [(query, doc.get('text', '')) for doc in documents]
# Cross-Encoder menilai relevansi setiap pasangan sekaligus
scores = self.model.predict(query_doc_pairs)
# Update skor dan urutkan berdasarkan skor cross-encoder
for doc, score in zip(documents, scores):
doc['cross_encoder_score'] = float(score)
doc['score'] = float(score) # ganti skor utama
reranked = sorted(documents, key=lambda x: x['cross_encoder_score'], reverse=True)
return reranked[:top_k] # Ambil Top-K saja
1.4 Tahap Context Building
Dokumen-dokumen terbaik dari reranking disusun menjadi satu blok teks konteks yang siap dibaca oleh LLM:
- Kelas yang bertanggung jawab:
ContextBuilder(dirag_model/models/context_builder.py). - Maksimum panjang konteks: 3000 karakter (dipotong jika melebihi).
- Setiap potongan dokumen diberi label nama file asli, nomor halaman (jika ada), dan skor relevansi.
- Output: satu string teks yang berisi informasi dari beberapa dokumen, dipisahkan oleh garis pembatas (
---).
1.5 Tahap LLM Generation
Tahap terakhir: menghasilkan jawaban berbahasa Indonesia dari konteks yang sudah disiapkan.
- Kelas yang bertanggung jawab:
LLMGenerator(dirag_model/models/llm_generator.py).
Jalur Gemini (Cloud):
- Library:
google-genai(SDK resmi Google). - Fungsi yang dipanggil:
client.models.generate_content(). - Model default:
gemini-2.5-flash. - Autentikasi: menggunakan
GEMINI_API_KEYdari environment variable.
Jalur Ollama (Local):
- Library:
requests(HTTP library standar Python). - Endpoint:
POST http://localhost:11434/v1/generate. - Model default:
llama3.2:latest. - Tidak memerlukan internet.
Prompt Template yang digunakan:
Kamu adalah asisten akademik. Jawab pertanyaan berikut secara LANGSUNG dan JELAS.
ATURAN PENTING:
- JANGAN gunakan kata pembuka seperti "Berdasarkan konteks..."
- Langsung jawab poinnya
- Gunakan numbered list jika ada beberapa poin
Informasi yang tersedia:
[Konteks dari dokumen]
Pertanyaan: [Pertanyaan user]
Jawaban:
Kode inti LLM Generation (llm_generator.py):
from google import genai
from tenacity import retry, stop_after_attempt, wait_exponential
class LLMGenerator:
def __init__(self, config):
if config.model_type == "gemini":
self.client = genai.Client(api_key=config.get_api_key())
elif config.model_type == "ollama":
self.endpoint = "http://localhost:11434/v1/generate"
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=2, max=10))
def generate(self, prompt, context=None):
full_prompt = self._build_prompt_with_context(prompt, context)
if self.config.model_type == "gemini":
response = self.client.models.generate_content(
model="gemini-2.5-flash",
contents=full_prompt,
config=genai.types.GenerateContentConfig(
temperature=0.3, max_output_tokens=2048
)
)
return {"answer": response.text, "success": True}
elif self.config.model_type == "ollama":
response = requests.post(self.endpoint, json={
"model": "llama3.2:latest",
"prompt": full_prompt,
"stream": False
})
return {"answer": response.json()["response"], "success": True}
Mekanisme Keamanan:
- Retry otomatis: Jika LLM gagal merespons (misal server sibuk), sistem mencoba ulang hingga 3 kali dengan jeda eksponensial (2, 4, 8 detik) menggunakan library
tenacity. - Penerjemahan error: Error teknis seperti
429 Rate Limitotomatis diterjemahkan menjadi pesan ramah: "Maaf, server AI sedang sibuk."
Bagian 1.5: Workflow Admin Dashboard & Pembuatan Knowledge Base
Sebelum model RAG dapat menjawab pertanyaan, perlu ada knowledge base (basis pengetahuan) yang berisi potongan-potongan dokumen yang sudah diindeks. Proses ini dilakukan melalui Dashboard Admin pada aplikasi web (Laravel).
1.5.1 Alur Pembuatan Knowledge Base
Berikut tahapan lengkap dari upload dokumen hingga knowledge base siap digunakan:
Admin (Browser)
│
├─ 1. Upload Dokumen PDF
│ └─ POST /admin/upload/store
│ ├─ Validasi file (hanya PDF, maks 50MB)
│ ├─ Simpan file ke storage server (disk: documents)
│ ├─ Catat metadata ke database Laravel (tabel documents)
│ └─ Trigger POST {RAG_API}/documents/refresh ← sync metadata
│
├─ 2. Proses Dokumen (Chunking)
│ └─ Admin klik tombol "Proses" per dokumen / "Proses Semua"
│ │
│ └─ POST {RAG_API}/chunking/process
│ │
│ ├─ a. Ekstraksi Teks
│ │ └─ PDF → pdfplumber/pypdf → plain text
│ │
│ ├─ b. Chunking (Pemecahan Teks)
│ │ ├─ Default chunk_size: 1000 karakter
│ │ ├─ Default chunk_overlap: 200 karakter
│ │ ├─ Pemotongan pada batas kalimat (titik/newline)
│ │ └─ Setiap chunk diberi ID unik & metadata
│ │
│ └─ c. Auto-Indexing (jika auto_index=true)
│ ├─ Simpan chunks ke chunks.json
│ ├─ Bangun Vector Store (ChromaDB + IndoBERT)
│ ├─ Bangun BM25 Index
│ └─ Knowledge Base siap digunakan ✓
│
└─ 3. Monitoring & Maintenance
├─ Halaman Knowledge Base: melihat statistik (jumlah chunks, status index)
├─ Reindex: membangun ulang seluruh indeks dari chunks.json
├─ Clear KB: menghapus seluruh knowledge base dan dokumen
└─ Sync: membersihkan record orphan dari database
1.5.2 Detail Proses Chunking
Proses chunking adalah tahap Data Engineering yang krusial. Dokumen PDF utuh tidak bisa langsung dijadikan input untuk model—harus dipecah menjadi potongan-potongan kecil (chunks) yang sesuai kapasitas model.
Cara kerja ChunkingService:
- Ekstraksi teks: File PDF dibaca menggunakan library
pdfplumber(prioritas utama) ataupypdf(fallback). Teks dari setiap halaman digabungkan menjadi satu string utuh. - Pemecahan dengan overlap: Teks dipecah menjadi potongan sepanjang
chunk_sizekarakter. Setiap potongan saling tumpang tindih sebesarchunk_overlapkarakter agar konteks antar-potongan tidak hilang. - Pemotongan cerdas: Sistem mencoba memotong di batas kalimat (tanda titik
.atau newline\n) agar potongan tidak terpotong di tengah kalimat. - Pemberian metadata: Setiap chunk diberi:
idunik (format:{doc_id}_chunk_{index})document_id(ID dokumen induk)original_filename(nama file asli untuk referensi)start_chardanend_char(posisi dalam dokumen asli)
Kode inti Chunking (chunking_service.py):
def _create_chunks(self, doc_id, text, chunk_size, chunk_overlap, original_filename):
chunks = []
start = 0
chunk_index = 0
while start < len(text):
end = start + chunk_size
chunk_text = text[start:end]
# Potong di batas kalimat agar konteks tidak terpecah
if end < len(text):
last_period = chunk_text.rfind('.')
last_newline = chunk_text.rfind('\n')
break_point = max(last_period, last_newline)
if break_point > chunk_size * 0.5:
chunk_text = chunk_text[:break_point + 1]
end = start + break_point + 1
if chunk_text.strip():
chunks.append({
"id": f"{doc_id}_chunk_{chunk_index}",
"document_id": doc_id,
"content": chunk_text.strip(),
"metadata": {
"original_filename": original_filename,
"start_char": start, "end_char": end
}
})
chunk_index += 1
start = end - chunk_overlap # overlap untuk menjaga konteks
return chunks
1.5.3 Auto-Indexing Setelah Chunking
Setelah chunking selesai, jika opsi auto_index aktif (default), sistem secara otomatis:
- Menyimpan chunks ke file
chunks.jsondi folderprocessed/(dengan mekanisme file locking agar tidak korupsi saat akses bersamaan). - Menggabungkan chunks baru dengan chunks yang sudah ada (menghapus chunks lama dari dokumen yang sama terlebih dahulu).
- Menjalankan reindex di background thread agar tidak memblokir API:
- Membangun ulang Vector Store (ChromaDB) — setiap chunk di-embed menggunakan IndoBERT.
- Membangun ulang BM25 Index — setiap chunk diindeks untuk pencarian kata kunci.
- Jika ada request reindex lain yang masuk saat proses berjalan, sistem menandai sebagai pending dan menjalankan reindex ulang setelah proses saat ini selesai.
Bagian 2: Bagaimana API Mengakses Model
Sekarang kita sudah tahu cara kerja internal model. Pertanyaan berikutnya: bagaimana rag-api (FastAPI) memanggil tahap-tahap di atas?
2.1 Titik Masuk: RAGService
File: rag-api/v1/services/rag_service.py
Kelas RAGService adalah jembatan antara endpoint HTTP dan model. Saat API server (FastAPI) pertama kali dijalankan:
RAGServicemengimpor kelasAcademicRAGdari modulrag-model.- Membuat dua instance
AcademicRAG:_baseline_rag: pipeline baseline (hanya vector search, tanpa BM25 dan reranking)._advanced_rag: pipeline advanced (hybrid search + reranking).
- Kedua instance ini tetap hidup di memori selama API berjalan (singleton pattern).
Kode inisialisasi RAGService (rag_service.py):
from rag_model import AcademicRAG
from rag_model.core.config import RAGConfig, RetrievalConfig
class RAGService:
def __init__(self):
# Pipeline baseline: hanya vector search
baseline_config = RAGConfig(retrieval=RetrievalConfig(pipeline_type="baseline"))
self._baseline_rag = AcademicRAG(config=baseline_config, response_format="api")
# Pipeline advanced: hybrid search + reranking
advanced_config = RAGConfig(retrieval=RetrievalConfig(pipeline_type="advanced"))
self._advanced_rag = AcademicRAG(config=advanced_config, response_format="api")
def query(self, question, pipeline_type="advanced", max_results=5):
rag = self._advanced_rag if pipeline_type == "advanced" else self._baseline_rag
result = rag.query(question)
return {
"query_id": f"q_{uuid.uuid4().hex[:8]}",
"answer": result.get("answer", ""),
"confidence": result.get("confidence", 0.0),
"sources": result.get("sources", [])[:max_results]
}
# Singleton: satu instance untuk seluruh API
rag_service = RAGService()
2.2 Endpoint FastAPI
File: rag-api/v1/routes/query.py
Endpoint HTTP yang menerima request dari web dan memanggil RAGService:
from fastapi import APIRouter, HTTPException
from ..services.rag_service import rag_service
router = APIRouter(prefix="/v1", tags=["Query"])
@router.post("/query", response_model=QueryResponse)
async def query(request: QueryRequest):
try:
result = rag_service.query(
question=request.question,
pipeline_type=request.pipeline_type.value, # "baseline" atau "advanced"
max_results=request.max_results
)
return QueryResponse(
status="success",
answer=result["answer"],
confidence=result["confidence"],
sources=result["sources"],
pipeline_used=request.pipeline_type.value,
response_time=time.time() - start_time
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
2.3 Sisi Web (Laravel) Mengirim Pertanyaan
File: rag-web/app/Http/Controllers/ChatController.php
Berikut cara web Laravel mengirim pertanyaan ke API dan memproses jawabannya:
// Kirim pertanyaan ke RAG API via HTTP POST
$ragApiUrl = env('RAG_API_URL', 'http://127.0.0.1:5001/v1');
$payload = [
'question' => $request->message,
'pipeline_type' => 'advanced',
'max_results' => 5
];
$response = Http::timeout(180)
->withHeaders(['Content-Type' => 'application/json'])
->post("{$ragApiUrl}/query", $payload);
if ($response->successful()) {
$data = $response->json();
$botText = $data['answer']; // Jawaban dari model
$sources = $data['sources']; // Referensi dokumen
$confidence = $data['confidence']; // Skor kepercayaan
}
2.4 Alur Internal Pipeline AcademicRAG.query()
Ketika RAGService memanggil rag.query(question), inilah tahapan internal yang terjadi di dalam kelas AcademicRAG:
Kode orkestrasi pipeline (pipeline.py):
class AcademicRAG:
def query(self, question, pipeline_type=None, max_results=None):
# Step 1: Document Retrieval (Hybrid Search)
retrieved_docs = self._retrieve_documents(question, pipeline_type, max_results)
# Step 2: Context Building
context_data = self._context_builder.build_context(retrieved_docs, question)
# Step 3: Answer Generation (LLM)
answer = self._generate_answer(question, context_data["context"])
# Step 4: Format & Return
return self._format_response(result)
def _retrieve_documents(self, question, pipeline_type, max_results):
if pipeline_type == "advanced":
# Hybrid Search: Vector + BM25 + RRF
search_results = self._unified_index_manager.search_unified(
query=question, k=max_results, strategy="rrf"
)
docs = search_results["results"]
# Reranking: ambil lebih banyak kandidat, lalu seleksi ulang
if self._reranker:
rerank_results = self._unified_index_manager.search_unified(
query=question, k=20 # rerank_k=20 kandidat
)
docs = self._reranker.rerank(question, rerank_results["results"], top_k=max_results)
else:
# Baseline: Vector Search saja
docs = self._baseline_retriever.retrieve(query=question, k=max_results)
return docs
2.5 Diagram Urutan Pemanggilan
Berikut urutan pemanggilan komponen secara visual:
RAGService.query()
│
├─ 1. Memilih pipeline (baseline atau advanced)
│
└─ AcademicRAG.query() ← Masuk ke dalam model
│
├─ 2. _retrieve_documents()
│ │
│ ├─ [Baseline] → BaselineRetriever.retrieve()
│ │ └─ VectorStore.similarity_search() ← ChromaDB
│ │
│ └─ [Advanced] → UnifiedIndexManager.search_unified()
│ ├─ VectorStore.similarity_search() ← Dense Retrieval
│ ├─ BM25Index.search() ← Sparse Retrieval
│ ├─ _reciprocal_rank_fusion() ← Fusion (RRF)
│ └─ CrossEncoderReranker.rerank() ← Reranking
│
├─ 3. ContextBuilder.build_context() ← Susun konteks
│
├─ 4. _generate_answer()
│ └─ LLMGenerator.generate()
│ ├─ [Gemini] → client.models.generate_content() ← Google SDK
│ └─ [Ollama] → requests.post(endpoint) ← HTTP POST
│
└─ 5. _format_response() ← Bungkus hasil
├─ Hitung confidence score
├─ Format sources (dokumen referensi)
└─ Return JSON ke RAGService → ke FastAPI → ke Web
2.6 Format Data yang Dikembalikan ke Web
Setelah seluruh pipeline selesai, API mengembalikan JSON dengan struktur berikut:
{
"query_id": "q_a1b2c3d4",
"answer": "Syarat mengikuti Kerja Praktek adalah ...",
"confidence": 0.85,
"sources": [
{
"id": "chunk_001",
"content": "Potongan teks dokumen...",
"score": 0.92,
"metadata": { "filename": "panduan_kp.pdf", "page": 3 }
}
],
"metadata": {
"pipeline_type": "advanced",
"retrieval_time": 0.45,
"generation_time": 1.2
}
}
Web (Laravel) menerima JSON ini dan menampilkan answer sebagai jawaban utama, sources sebagai daftar referensi, dan confidence sebagai indikator kepercayaan.
Ringkasan Komponen Teknis
| Tahapan | Kelas / File | Library | Input → Output |
|---|---|---|---|
| Dense Retrieval | VectorStore |
chromadb + IndoBERT |
Query → Dokumen mirip secara makna |
| Sparse Retrieval | BM25Index |
rank-bm25 |
Query → Dokumen mirip secara kata kunci |
| Fusion | UnifiedIndexManager |
Custom RRF | 2 daftar dokumen → 1 daftar gabungan |
| Reranking | CrossEncoderReranker |
sentence-transformers |
Kandidat dokumen → Dokumen tervalidasi |
| Context Building | ContextBuilder |
Custom | Dokumen terbaik → Blok teks konteks |
| LLM Generation | LLMGenerator |
google-genai / requests |
Prompt + Konteks → Teks jawaban |
| Orchestration | AcademicRAG |
Custom | Menjalankan semua tahap di atas secara berurutan |
| API Bridge | RAGService |
FastAPI |
HTTP Request → Panggil model → HTTP Response |
Bagian 3: Contoh JSON Payload Komunikasi Web ↔ API
Berikut adalah contoh payload JSON aktual yang dikirim dan diterima antara aplikasi web (Laravel) dan model API (FastAPI). Semua komunikasi menggunakan protokol HTTP REST dengan format JSON.
3.1 Query (User Bertanya)
Request — Web → API (POST {RAG_API_URL}/query):
{
"question": "Apa saja syarat mengikuti Kerja Praktek?",
"pipeline_type": "advanced",
"max_results": 5
}
Response — API → Web:
{
"status": "success",
"query_id": "q_a1b2c3d4",
"answer": "Syarat mengikuti Kerja Praktek adalah ...",
"confidence": 0.85,
"sources": [
{
"id": "doc_panduan_kp_chunk_3",
"content": "Potongan teks dokumen yang relevan...",
"score": 0.92,
"metadata": {
"original_filename": "panduan_kp.pdf",
"source": "panduan_kp.pdf",
"start_char": 1200,
"end_char": 2150,
"chunk_size": 950
}
}
],
"pipeline_used": "advanced",
"response_time": 1.65
}
Field pipeline_type menentukan pipeline yang digunakan:
"baseline"→ hanya Vector Search (tanpa BM25 dan Reranking)."advanced"→ Hybrid Search (Vector + BM25 + RRF) + Cross-Encoder Reranking.
3.2 Chunking (Proses Dokumen)
Request — Web → API (POST {RAG_API_URL}/chunking/process):
{
"document_ids": ["doc_6635a1_1713900000"],
"config": {
"chunk_size": 1000,
"chunk_overlap": 200
},
"auto_index": true
}
Response — API → Web:
{
"status": "success",
"job_id": "job_a1b2c3d4e5f6",
"message": "Processing started",
"documents_queued": 1
}
Proses chunking berjalan secara asinkron di background thread. Web dapat memantau progress melalui endpoint berikut.
3.3 Cek Status Chunking Job
Request — Web → API (GET {RAG_API_URL}/chunking/status/{job_id}):
Response — API → Web:
{
"status": "success",
"job_id": "job_a1b2c3d4e5f6",
"job_status": "completed",
"progress": 1.0,
"documents_processed": 1,
"documents_total": 1,
"error_message": null
}
Nilai job_status yang mungkin: "pending", "running", "completed", "failed".
3.4 Statistik Knowledge Base
Request — Web → API (GET {RAG_API_URL}/kb/stats):
Response — API → Web:
{
"status": "success",
"total_documents": 4,
"total_chunks": 127,
"indexed_chunks": 127,
"vector_store_status": "healthy",
"bm25_index_status": "healthy",
"last_indexed": "2026-04-22T10:30:00",
"storage_size_mb": 15.4
}
Status "healthy" menandakan indeks siap digunakan. Status "indexing" berarti proses pembangunan indeks sedang berjalan.
3.5 Reindex Knowledge Base
Request — Web → API (POST {RAG_API_URL}/kb/reindex):
{
"rebuild_vectors": true,
"rebuild_bm25": true,
"document_ids": null
}
document_ids: null berarti reindex semua dokumen. Jika diisi dengan array ID spesifik, hanya dokumen tersebut yang di-reindex.
Response — API → Web:
{
"status": "success",
"job_id": "reindex_a1b2c3d4",
"message": "Reindexing started"
}
3.6 Proses Semua Dokumen
Request — Web → API (POST {RAG_API_URL}/chunking/process-all):
{
"chunk_size": 1000,
"chunk_overlap": 200,
"auto_index": true
}
Endpoint ini secara otomatis mencari dokumen yang belum diproses (status "uploaded" atau "error") dan memprosesnya.
Response — API → Web:
{
"status": "success",
"job_id": "job_f6e5d4c3b2a1",
"message": "Processing started",
"documents_queued": 3
}
3.7 Ringkasan Endpoint Komunikasi
| Fitur | Method | Endpoint | Pengirim → Penerima |
|---|---|---|---|
| Tanya Jawab | POST |
/v1/query |
Web (ChatController) → API |
| Refresh Metadata | POST |
/v1/documents/refresh |
Web (setelah upload) → API |
| Proses Dokumen | POST |
/v1/chunking/process |
Web (AdminUpload) → API |
| Proses Semua | POST |
/v1/chunking/process-all |
Web (AdminUpload) → API |
| Status Job | GET |
/v1/chunking/status/{id} |
Web (polling) → API |
| Daftar Dokumen | GET |
/v1/documents |
Web (Admin) → API |
| Hapus Dokumen | DELETE |
/v1/documents/{id} |
Web (Admin) → API |
| Statistik KB | GET |
/v1/kb/stats |
Web (Admin) → API |
| Reindex KB | POST |
/v1/kb/reindex |
Web (Admin) → API |
| Clear KB | DELETE |
/v1/kb/clear |
Web (Admin) → API |
| Active Jobs | GET |
/v1/kb/jobs/active |
Web (polling) → API |
Semua endpoint menggunakan base URL yang diatur melalui environment variable RAG_API_URL (default: http://localhost:5001/v1). Komunikasi bersifat sinkron untuk query (menunggu jawaban) dan asinkron untuk chunking/indexing (mengembalikan job ID untuk polling).
Bagian 4: Siklus Hidup Pertanyaan
Bagian ini secara khusus menjawab detail alur data, bentuk format yang diterima dan dikeluarkan, serta proses internal pada model dari awal masuknya pertanyaan hingga keluarnya jawaban.
4.1 Input: Apa yang Diterima oleh Model?
Ketika pengguna mengirimkan pesan dari halaman web, web Laravel mengirimkannya melintasi jaringan internet ke API model (/v1/query) dalam format teks JSON Object (format standar pertukaran data web).
Namun, begitu data JSON tersebut tiba dan dibaca oleh kode Python (di FastAPI), data tersebut otomatis diubah menjadi tipe data Dictionary Python (tepatnya menjadi objek Pydantic Model). Jadi, ketika masuk ke tahap pemrosesan internal model, model berinteraksi dengan Dictionary/Object Python, bukan teks JSON lagi.
Dari sudut pandang model, objek (dictionary) yang masuk ini berisi:
question(String): Teks pertanyaan asli dari pengguna.pipeline_type(String): Mode kecerdasan yang diminta (biasanya bernilai"advanced"untuk menggunakan sistem hibrida penuh, atau"baseline"untuk pencarian sederhana).max_results(Integer): Jumlah maksimum referensi dokumen yang ingin ditarik (misalnya5).
4.2 Pemrosesan: Mencari di Database & Dimensi Vektor
Setelah pertanyaan diterima, model tidak langsung menjawabnya. Pertanyaan tersebut harus melalui proses pencarian database dengan langkah berikut:
- Embedding (Mengubah Teks menjadi Angka): Teks bahasa manusia tidak dipahami oleh database. Model menggunakan model AI encoder khusus berbahasa Indonesia yaitu IndoBERT (
indobenchmark/indobert-base-p2). IndoBERT menerjemahkan teks pertanyaan tersebut menjadi vektor berdimensi 768 (768-dimensional vector).- Apa maksudnya? Secara struktur data di dalam kode Python, ini hanyalah sebuah Array/List mendatar (List 1 dimensi). Namun, karena List 1 dimensi ini berisi tepat 768 angka desimal, maka dalam istilah Machine Learning/Matematika ini disebut sebagai "vektor 768 dimensi" (memiliki 768 sumbu pengukur makna). Array berisi 768 angka inilah yang menjadi representasi matematis dari pertanyaan pengguna.
- Hybrid Retrieval (Pencarian Ganda):
- Pencarian Vektor (Makna): Array 768 angka tadi dilemparkan ke ChromaDB. Database ini menghitung Cosine Similarity (jarak sudut) antara vektor pertanyaan dengan seluruh vektor potongan dokumen yang pernah di-upload admin. Hasilnya adalah dokumen yang maknanya paling mirip.
- Pencarian Kata Kunci (BM25): Secara bersamaan, pertanyaan juga dicari kemiripan kata spesifiknya menggunakan Term Frequency (BM25).
- Penggabungan Hasil (Fusion): Kedua hasil pencarian digabungkan menggunakan RRF. Output dari tahap ini berupa sebuah List of Dictionaries (Array yang berisi Dictionary) berisi Top-20 kandidat potongan dokumen beserta teks isi, asal file, dan skor sementaranya.
4.3 Output: Format Jawaban yang Sudah Siap
Setelah model LLM utama (seperti Gemini atau Ollama) meracik jawaban, hasil racikan ini dikembalikan dari API model ke Web (Laravel) dalam bentuk JSON Object final.
Isi dan format dari output ini meliputi:
status(String): Menandakan proses berhasil ("success").answer(String): Teks panjang berupa narasi jawaban bahasa Indonesia hasil racikan LLM.confidence(Float): Angka desimal skor kepercayaan diri sistem (misal0.85).sources(Array of Objects): Daftar lampiran referensi file yang digunakan untuk menyusunanswer(dibahas di poin 4.4).query_id(String): Kode unik transaksi untuk pertanyaan tersebut (misal"q_a1b2...").pipeline_used(String): Menyatakan bahwa proses memakai pipeline"advanced".response_time(Float): Waktu pemrosesan keseluruhan dalam hitungan detik.
4.4 File Source (Referensi): Dari Mana Asalnya dan Apa Arti Skornya?
Saat user melihat ada lampiran file (contoh: Panduan_KP.pdf) di bawah jawaban bot, berikut fakta dari POV model:
- Dari Mana Asalnya: File tersebut berasal dari proses persiapan (sebelum user bertanya). Admin mengupload PDF utuh, kemudian PDF tersebut di-chunking (dipecah-pecah kecil per 1000 huruf) oleh sistem menjadi banyak potongan. Potongan-potongan inilah yang ditarik oleh ChromaDB pada tahap 4.2 di atas.
- Prosesnya: Dari 20 kandidat potongan dokumen hasil pencarian awal, model menggunakan mekanisme Cross-Encoder Reranking. Reranker ini menjejalkan/memasangkan pertanyaan pengguna dengan setiap potongan teks satu per satu secara teliti, lalu menyaring dan mengeliminasi yang melenceng. Dari 20, hanya Top 5 potongan paling relevan yang diloloskan.
- Apa Itu Skornya: Skor yang terlampir pada source adalah Skor Relevansi (Cross-Encoder Score). Skor ini adalah nilai desimal yang diberikan oleh Reranker yang mengindikasikan tingkat kecocokan absolut: seberapa kuat dan presisi teks di dalam dokumen tersebut dapat menjawab pertanyaan pengguna. Semakin tinggi skornya, semakin valid dan relevan referensi dokumen tersebut.
4.5 Kesimpulan: Alur Proses End-to-End dari POV Model
Jika diringkas secara kronologis dan logis dari kacamata internal model:
- Menerima Request: Model menangkap payload JSON dari Web yang berisi pertanyaan mentah pengguna.
- Pemecahan Sandi (Embedding & BM25): Model memecah pertanyaan menjadi bahasa mesin (vektor 768 dimensi) dan frekuensi kata.
- Penggalian Memori (Hybrid Search): Model mengambil vektor tersebut dan menggeledah database (ChromaDB) untuk menemukan Top 20 potongan memori dokumen yang cocok secara makna dan kata kunci.
- Penilaian Kritis (Reranking): Model menjadi juri yang kritis (menggunakan Cross-Encoder), membaca kembali pertanyaan beserta Top 20 kandidat, membuang yang kurang relevan, dan hanya menyisakan Top 5 dokumen sumber yang paling sah beserta Skor Relevansi-nya.
- Perangkaian Konteks: Teks dari Top 5 dokumen ini dijahit menjadi satu blok "catatan kaki" besar.
- Penyusunan Kalimat (LLM Generation): Model menyodorkan pertanyaan pengguna dan "catatan kaki" tersebut ke otak utama (LLM/Gemini). Model menginstruksikan LLM: "Jawablah pertanyaan ini berdasarkan panduan yang ada di catatan kaki ini dengan bahasa yang luwes".
- Penyerahan Hasil: Setelah LLM selesai mengetik jawabannya, model membungkus hasil tersebut bersama daftar kelima dokumen sumber (termasuk asal nama file dan skornya) ke dalam "paket" JSON, lalu melemparkannya kembali ke Web untuk ditampilkan di layar pengguna.
Dokumentasi arsitektur teknis Academic RAG Pipeline.