# Bikin Chatbot Sendiri yang Bisa Jawab Pertanyaan dari Dokumen kamu

> Source: <https://dev.to/ardisaurus/bikin-chatbot-sendiri-yang-bisa-jawab-pertanyaan-dari-dokumen-kamu-5464>
> Published: 2026-05-21 09:50:26+00:00

Di tulisan ini kita akan membahas soal sebuah proyek kecil bernama **Simple RAG Chatbot**. Tujuannya sederhana: bikin chatbot yang bisa menjawab pertanyaan berdasarkan dokumen yang **kamu** punya — bukan ngarang dari pengetahuan model. Cocok buat kamu yang baru pertama kali denger istilah RAG dan pengen tahu cara kerjanya tanpa pusing.

Repository proyek aplikasi : [https://github.com/ardisaurus/simple-rag-chatbot](https://github.com/ardisaurus/simple-rag-chatbot)

## Masalah yang Ingin Diselesaikan

Coba bayangin kamu punya banyak banget dokumentasi produk: file Markdown, catatan teknis, panduan instalasi, FAQ. Setiap kali ada orang baru gabung, mereka selalu nanya hal yang sebenernya udah ada di dokumen — tapi siapa juga yang sempat baca semuanya?

Solusi instan yang mungkin terlintas: "kasih aja ke ChatGPT terus suruh jawab." Tapi ada dua masalah:

- Model AI seperti ChatGPT
**tidak tahu** isi dokumen pribadimu. - Kalau kamu paste seluruh dokumen ke chat, biayanya mahal dan sering kepotong limit token.

Di sinilah teknik bernama **RAG** masuk.

## Apa Itu RAG?

RAG kepanjangannya **Retrieval-Augmented Generation**. Kedengeran ribet, padahal idenya simpel banget:

-
**Retrieval**(pengambilan): cari potongan dokumen yang paling relevan dengan pertanyaan user. -
**Augmented**(diperkaya): tempelin potongan itu ke prompt yang dikirim ke LLM (model bahasa besar). -
**Generation**(pembuatan jawaban): LLM jawab berdasarkan potongan yang barusan dia terima.

Jadi LLM-nya gak perlu hapal seluruh dokumenmu. Dia cuma perlu baca beberapa paragraf yang relevan untuk setiap pertanyaan. Hemat token, hemat biaya, dan jawabannya jadi lebih akurat karena ada bukti sumbernya.

## Cara Komputer Cari Potongan yang Relevan: Embeddings

Pertanyaannya: gimana komputer tau potongan dokumen mana yang relevan sama pertanyaan user?

Jawabannya pakai konsep **embeddings**. Embedding itu cara mengubah teks menjadi sebuah deretan angka (vektor) yang merepresentasikan **makna** teks tersebut. Dua kalimat yang artinya mirip akan menghasilkan vektor yang mirip juga, walaupun kata-katanya berbeda.

Contoh:

- "How do I install the product?" →
`[0.12, -0.34, 0.88, ...]`

- "Cara memasang aplikasinya gimana?" →
`[0.11, -0.33, 0.86, ...]`

Walaupun bahasanya beda, vektornya berdekatan karena maknanya mirip.

Lalu kita simpan semua vektor dokumen di sebuah **vector database**. Saat user nanya, kita ubah pertanyaannya jadi vektor, terus cari vektor dokumen yang paling dekat. Itulah potongan yang paling relevan.

## Tools yang Dipakai di Proyek Ini

Aku sengaja pilih tools yang gratis atau super murah supaya kamu bisa self-hosting di laptop sendiri:

-
**Next.js (App Router)**— framework React untuk bikin web app. -
**TypeScript**— biar kode lebih aman dan mudah di-maintain. -
**LangChain.js**— library yang mempermudah semua langkah RAG: split dokumen, bikin embedding, retrieval, sampai panggil LLM. -
**ChromaDB**— vector database yang bisa jalan lokal pakai Docker. -
**OpenRouter**— gateway buat berbagai LLM (GPT, Claude, Llama, dll). Bayar per token, dan model default-nya cuma beberapa rupiah per pertanyaan. -
**@xenova/transformers**— library untuk bikin embedding** langsung di laptopmu**, tanpa API. Jadi gak perlu langganan OpenAI cuma buat embedding. -
**Tailwind CSS**— supaya tampilannya rapi tanpa nulis CSS panjang.

## Format Dokumen yang Didukung & Batasannya

Sebelum kita masuk ke alur teknisnya, penting kamu tahu dulu **dokumen apa aja yang bisa diproses** sama proyek ini, dan apa aja **batasan** dari implementasi sekarang. Jangan sampai kamu masukin file PDF terus heran kenapa chatbot-nya jawab "saya tidak tahu" — ternyata file-nya emang gak pernah ke-index.

### Format yang Didukung Saat Ini

Hanya tiga ekstensi yang dibaca oleh script ingest:

-
`.md`

— Markdown standar. -
`.markdown`

— sama aja, beda ekstensi. -
`.txt`

— teks polos.

Daftar ini didefinisikan di `lib/ingest.ts`

sebagai `SUPPORTED_EXTENSIONS`

. File dengan ekstensi lain akan **di-skip diam-diam** (tanpa warning) saat script jalan menjelajahi folder. Subfolder ikut dibaca rekursif, jadi kamu bebas mengelompokkan dokumen ke dalam beberapa folder.

### Format yang Belum Didukung

Berikut yang **belum** bisa langsung kamu masukin tanpa modifikasi kode:

-
**PDF**— perlu library tambahan kayak`pdf-parse`

. Cara nambahinnya udah dijelasin di file`data/docs/wiki/08-extending.md`

. -
**DOCX / Office documents**— perlu parser tersendiri (misal`mammoth`

untuk DOCX). -
**HTML**— bisa diproses, tapi butuh parser untuk strip tag HTML supaya yang ke-embed cuma teksnya. -
**MDX**— walaupun mirip Markdown, ekstensinya`.mdx`

jadi gak akan ke-pickup. Kalau kamu yakin gak ada JSX di dalamnya, gampang aja: tambahin`.mdx`

ke`SUPPORTED_EXTENSIONS`

. -
**Gambar / scan / screenshot**— proyek ini gak punya OCR. Kalau dokumenmu berbentuk gambar, perlu pipeline tambahan (misal Tesseract atau API OCR) untuk ngubah jadi teks dulu. -
**CSV / spreadsheet / JSON terstruktur**— bisa di-index sebagai teks polos, tapi hasilnya kurang optimal karena splitter-nya gak paham struktur tabel.

## Cara Kerja Aplikasinya, Step by Step

### 1. Ingestion: Masukin Dokumen ke Database

Pertama, kita siapin semua dokumen di folder `data/docs/`

. Bisa Markdown atau TXT. Lalu jalanin:

```
npm run ingest
```

Yang terjadi di balik layar:

- Script jalanin
**walk** di folder`data/docs/`

, ngambil semua file yang didukung. - Setiap file dipotong-potong jadi chunk kecil (sekitar 1000 karakter, dengan overlap 150 karakter biar konteksnya gak putus).
- Setiap chunk diubah jadi vektor pakai model embedding lokal.
- Vektor + chunk + metadata (nama file, nomor chunk) disimpan ke Chroma.

Sekarang Chroma punya "ingatan" tentang dokumenmu, siap dipakai untuk pencarian.

### 2. Pertanyaan dari User

User buka [http://localhost:3000](http://localhost:3000), ketik pertanyaan, terus pencet enter. Pertanyaan itu dikirim ke `/api/chat`

.

### 3. Retrieval: Cari Chunk yang Relevan

Backend ngambil pertanyaan, ubah jadi vektor pakai model embedding yang sama, lalu tanya ke Chroma: "kasih aku 5 chunk yang vektornya paling dekat dengan ini."

Chroma balikin 5 chunk beserta metadata-nya.

### 4. Generation: Tanya ke LLM

5 chunk tadi dirakit jadi sebuah prompt seperti ini (disederhanakan):

```
Kamu adalah asisten dokumentasi. Jawab HANYA berdasarkan konteks
berikut. Kalau jawabannya tidak ada di konteks, bilang kamu tidak tahu.

Konteks:
[1] (instalasi.md #0) Untuk memasang aplikasi, unduh installer ...
[2] (instalasi.md #1) Setelah terpasang, jalankan dari menu ...
[3] ...

Pertanyaan: Bagaimana cara memasang aplikasinya?
```

Prompt itu dikirim ke OpenRouter, dan model jawab berdasarkan konteks yang udah dikasih. Karena ada instruksi tegas "jawab hanya dari konteks", model gak akan ngarang.

### 5. Streaming: Jawaban Muncul Sedikit-Sedikit

Daripada nunggu jawaban full baru ditampilkan, jawabannya **distreaming** token per token ke browser. User langsung lihat huruf demi huruf mengalir, mirip pengalaman ChatGPT. Lebih enak dilihat dan kerasa cepat.

### 6. Sumber Ditampilkan

Di bawah setiap jawaban ada tombol "Sources" yang bisa diklik. Isinya daftar chunk yang dipakai untuk menjawab, lengkap dengan nama file dan nomor chunk-nya. Jadi user bisa verifikasi jawaban si bot.

## Kenapa Pilihan Teknisnya Begini?

### Kenapa embedding-nya lokal, bukan pakai OpenAI?

Karena bisa **gratis**. Model `Xenova/all-MiniLM-L6-v2`

cukup pintar buat dokumentasi pendek, ukurannya kecil (~30 MB), dan jalan langsung di Node.js tanpa internet. Cocok buat self-hosting.

### Kenapa LLM-nya pakai OpenRouter?

OpenRouter ngasih akses ke banyak model dengan satu API key. Kamu bisa ganti dari `gpt-4o-mini`

ke `claude-3.5-sonnet`

ke `llama-3.1`

cuma dengan ubah satu env var. Bagus buat eksperimen.

### Kenapa Chroma, bukan Pinecone atau Weaviate?

Chroma gratis, jalan lokal, dan setup-nya cuma satu perintah Docker. Untuk skala kecil-menengah (puluhan ribu chunk) udah lebih dari cukup.

### Kenapa wipe-and-replace saat ingest, bukan update incremental?

Biar simpel. Kalau ada file yang dihapus atau di-rename, gak akan ada chunk orphan yang tertinggal. Untuk dokumentasi yang gak terlalu besar, re-ingest cepat banget — hitungan detik.

## Cara Coba di Laptopmu

Singkat aja, langkah-langkahnya:

```
# 1. Install dependency
npm install

# 2. Setup env (isi OPENROUTER_API_KEY dari https://openrouter.ai/keys)
cp .env.example .env

# 3. Jalanin Chroma di Docker
npm run chroma

# 4. Index dokumentasi sample yang udah disediain
npm run ingest

# 5. Jalanin aplikasinya
npm run dev
```

Buka [http://localhost:3000](http://localhost:3000), tanya apa aja soal dokumentasi yang udah di-index. Misalnya: "What is the default chunk size?"

### Batasan Lain yang Perlu Dipahami

Selain soal format file, ada beberapa batasan implementasi yang penting kamu tahu:

-
**Encoding harus UTF-8.** Kalau file`.txt`

kamu disimpan dalam encoding lain (misal Windows-1252), karakter non-ASCII (è, ñ, é, dll) bisa berantakan saat dibaca. -
**Splitter berbasis karakter, bukan struktur.** Splitter default-nya`RecursiveCharacterTextSplitter`

, yang motong teks berdasarkan jumlah karakter. Artinya tabel Markdown atau code block panjang bisa kepotong di tengah. Untuk Markdown yang lebih rapi splitnya, kamu bisa ganti ke`MarkdownTextSplitter`

yang lebih sadar struktur heading. -
**Embedding model default-nya English-leaning.**`Xenova/all-MiniLM-L6-v2`

dilatih dominan pada teks bahasa Inggris. Untuk dokumentasi Bahasa Indonesia, hasilnya masih lumayan tapi gak seoptimal model multilingual. Kalau kualitas pencarian dirasa kurang, ganti ke model multilingual seperti`Xenova/multilingual-e5-small`

(jangan lupa re-ingest karena dimensi vektornya beda). -
**Ingest itu wipe-and-replace, bukan incremental.** Setiap kali`npm run ingest`

dijalankan, seluruh collection di Chroma dihapus dan dibangun ulang dari nol. Konsekuensinya: gak bisa update satu file aja. Tapi enaknya: gak akan ada chunk basi tertinggal. -
**Gak ada akses kontrol per-dokumen.** Semua orang yang bisa akses chatbot akan dapet jawaban dari semua dokumen yang ke-index. Kalau ada dokumen sensitif, pisahin di collection berbeda atau jangan di-index sama sekali. -
**Chunk besar bisa boros token.** Default`CHUNK_SIZE = 1000`

karakter ×`RETRIEVAL_K = 5`

chunk = ~5000 karakter konteks per pertanyaan. Kalau modelmu mahal atau context window-nya kecil, turunin salah satu angka itu.

Intinya: proyek ini sengaja dibuat **simpel dulu**. Format yang lebih kompleks dan fitur incremental update memang sengaja gak dimasukin supaya kode-nya tetep gampang dibaca. Kalau butuh, semuanya bisa kamu tambahin sendiri dengan modifikasi kecil di `lib/ingest.ts`

.

## Penutup

RAG itu konsep yang awalnya kelihatan ribet, padahal intinya cuma: **cari potongan yang relevan, tempelin ke prompt, biarkan LLM jawab berdasarkan itu**.

Proyek ini sengaja dibuat **minimal** dan **mudah dibaca**. Total kode inti cuma di folder `lib/`

dan `app/`

— tiap file punya tanggung jawab yang jelas. Kalau kamu pengen belajar gimana sistem RAG bekerja dari nol, baca aja file-filenya satu per satu, dimulai dari `lib/ingest.ts`

, lalu `lib/rag.ts`

.

Selamat ngoprek, dan semoga proyek kecil ini bisa jadi titik awal kamu bikin asisten dokumentasi sendiri!

Repository proyek aplikasi : [https://github.com/ardisaurus/simple-rag-chatbot](https://github.com/ardisaurus/simple-rag-chatbot)
