Dokumentasi Teknis: Cara Kerja Sistem Academic RAG

Dokumen ini menjelaskan lima hal utama:

  1. Bagaimana model RAG bekerja secara internal (pipeline di dalam modul rag-model).
  2. Workflow Admin Dashboard & pembuatan Knowledge Base (proses upload, chunking, dan indexing dokumen).
  3. Bagaimana API (rag-api) mengakses model tersebut dan tahap mana saja yang dipanggil untuk menghasilkan jawaban.
  4. Contoh JSON Payload komunikasi antara Web (Laravel) dan API (FastAPI).
  5. Siklus Hidup Pertanyaan dari sudut pandang internal model secara end-to-end.

Daftar Isi


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)

b) Sparse Retrieval (Pencarian Kata Kunci / BM25)

c) Reciprocal Rank Fusion (RRF)

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:

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:

1.5 Tahap LLM Generation

Tahap terakhir: menghasilkan jawaban berbahasa Indonesia dari konteks yang sudah disiapkan.

Jalur Gemini (Cloud):

Jalur Ollama (Local):

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:


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:

  1. Ekstraksi teks: File PDF dibaca menggunakan library pdfplumber (prioritas utama) atau pypdf (fallback). Teks dari setiap halaman digabungkan menjadi satu string utuh.
  2. Pemecahan dengan overlap: Teks dipecah menjadi potongan sepanjang chunk_size karakter. Setiap potongan saling tumpang tindih sebesar chunk_overlap karakter agar konteks antar-potongan tidak hilang.
  3. Pemotongan cerdas: Sistem mencoba memotong di batas kalimat (tanda titik . atau newline \n) agar potongan tidak terpotong di tengah kalimat.
  4. Pemberian metadata: Setiap chunk diberi:
    • id unik (format: {doc_id}_chunk_{index})
    • document_id (ID dokumen induk)
    • original_filename (nama file asli untuk referensi)
    • start_char dan end_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:

  1. Menyimpan chunks ke file chunks.json di folder processed/ (dengan mekanisme file locking agar tidak korupsi saat akses bersamaan).
  2. Menggabungkan chunks baru dengan chunks yang sudah ada (menghapus chunks lama dari dokumen yang sama terlebih dahulu).
  3. 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.
  4. 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:

  1. RAGService mengimpor kelas AcademicRAG dari modul rag-model.
  2. Membuat dua instance AcademicRAG:
    • _baseline_rag: pipeline baseline (hanya vector search, tanpa BM25 dan reranking).
    • _advanced_rag: pipeline advanced (hybrid search + reranking).
  3. 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:

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:

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:

  1. 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.
  2. 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).
  3. 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:

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:

4.5 Kesimpulan: Alur Proses End-to-End dari POV Model

Jika diringkas secara kronologis dan logis dari kacamata internal model:

  1. Menerima Request: Model menangkap payload JSON dari Web yang berisi pertanyaan mentah pengguna.
  2. Pemecahan Sandi (Embedding & BM25): Model memecah pertanyaan menjadi bahasa mesin (vektor 768 dimensi) dan frekuensi kata.
  3. 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.
  4. 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.
  5. Perangkaian Konteks: Teks dari Top 5 dokumen ini dijahit menjadi satu blok "catatan kaki" besar.
  6. 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".
  7. 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.