“Pengen membuat aplikasi AI tapi bingung mulai dari mana.”
Ini yang paling sering saya dengar dari developer yang ingin masuk ke dunia AI. Banyak yang terjebak di fase research: baca terlalu banyak artikel, nonton terlalu banyak tutorial, tapi tidak ada yang dibangun.
Artikel ini berbeda. Kita akan benar-benar build aplikasi AI sederhana tapi fungsional, dari nol sampai deploy. Bukan teori — ini praktek.
Aplikasi yang Akan Kita Buat
AI Document Q&A — sebuah aplikasi web di mana user bisa upload dokumen (PDF atau teks) dan bertanya tentang isinya. Sederhana tapi ini adalah foundation dari banyak use case enterprise nyata.
Stack yang digunakan:
- Backend: Node.js dengan Hono (lightweight, fast)
- Frontend: HTML + vanilla JS (sesimpel mungkin agar fokus ke AI logic)
- AI: Anthropic Claude API
- Vector Database: Upstash Vector (free tier)
- Deploy: Railway atau Fly.io
Konsep yang Perlu Dipahami Dulu
Sebelum coding, dua konsep penting yang perlu kamu mengerti:
RAG (Retrieval-Augmented Generation)
Masalah dengan langsung kirim dokumen ke LLM: dokumen besar tidak muat dalam context window, dan mahal kalau harus kirim seluruh dokumen setiap pertanyaan.
Solusinya: RAG. Alurnya:
- Dokumen dipotong jadi chunk-chunk kecil
- Setiap chunk dikonversi jadi embedding (representasi numerik)
- Embedding disimpan di vector database
- Ketika ada pertanyaan, pertanyaan juga dikonversi jadi embedding
- Cari chunk yang paling relevan (nearest neighbor search)
- Kirim chunk relevan + pertanyaan ke LLM
Embeddings
Embedding adalah cara mengkonversi teks ke vektor angka di mana teks yang “maknanya mirip” akan punya vektor yang dekat satu sama lain. Model embedding yang populer: text-embedding-3-small dari OpenAI, atau embedding dari Voyage AI.
Langkah 1: Setup Project
mkdir ai-document-qa
cd ai-document-qa
npm init -y
npm install hono @anthropic-ai/sdk @upstash/vector pdf-parse dotenv
npm install -D typescript @types/node tsx
Buat file .env:
ANTHROPIC_API_KEY=sk-ant-xxx
UPSTASH_VECTOR_REST_URL=https://xxx.upstash.io
UPSTASH_VECTOR_REST_TOKEN=xxx
Langkah 2: Setup Vector Database
Daftar di upstash.com, buat Vector database baru dengan dimension 1536 (sesuai dengan model embedding yang akan kita pakai).
Langkah 3: Document Processing
Buat file src/document.ts:
import Anthropic from '@anthropic-ai/sdk';
import { Index } from '@upstash/vector';
import pdfParse from 'pdf-parse';
const anthropic = new Anthropic();
const vectorIndex = new Index();
// Potong teks jadi chunk dengan overlap
function chunkText(text: string, chunkSize = 500, overlap = 50): string[] {
const words = text.split(' ');
const chunks: string[] = [];
for (let i = 0; i < words.length; i += chunkSize - overlap) {
const chunk = words.slice(i, i + chunkSize).join(' ');
if (chunk.trim()) chunks.push(chunk);
}
return chunks;
}
// Generate embedding menggunakan Voyage AI via Anthropic
async function generateEmbedding(text: string): Promise<number[]> {
// Gunakan OpenAI embedding API atau Voyage AI
// Untuk simplicity, kita gunakan mock 1536-dimension vector
// Di production, integrasikan dengan embedding model yang proper
const response = await fetch('https://api.voyageai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': Bearer ${process.env.VOYAGE_API_KEY},
'Content-Type': 'application/json'
},
body: JSON.stringify({
input: text,
model: 'voyage-3'
})
});
const data = await response.json() as any;
return data.data[0].embedding;
}
// Process dokumen: extract teks, chunk, embed, simpan
export async function processDocument(
buffer: Buffer,
filename: string,
mimeType: string
): Promise<{ chunks: number }> {
let text = '';
if (mimeType === 'application/pdf') {
const pdfData = await pdfParse(buffer);
text = pdfData.text;
} else {
text = buffer.toString('utf-8');
}
const chunks = chunkText(text);
console.log(Processing ${chunks.length} chunks from ${filename});
// Generate embeddings dan simpan ke vector database
const vectors = await Promise.all(
chunks.map(async (chunk, i) => {
const embedding = await generateEmbedding(chunk);
return {
id: ${filename}-${i},
vector: embedding,
metadata: { text: chunk, filename, chunkIndex: i }
};
})
);
await vectorIndex.upsert(vectors);
return { chunks: chunks.length };
}
// Cari chunk yang relevan dengan pertanyaan
export async function searchRelevantChunks(
query: string,
topK = 5
): Promise<string[]> {
const queryEmbedding = await generateEmbedding(query);
const results = await vectorIndex.query({
vector: queryEmbedding,
topK,
includeMetadata: true
});
return results
.filter(r => r.metadata)
.map(r => (r.metadata as any).text);
}
Langkah 4: Chat dengan Dokumen
Buat file src/chat.ts:
import Anthropic from '@anthropic-ai/sdk';import { searchRelevantChunks } from './document';
const anthropic = new Anthropic();
export async function askDocument(question: string): Promise<string> {
// Ambil chunk yang relevan
const relevantChunks = await searchRelevantChunks(question);
if (relevantChunks.length === 0) {
return 'Saya tidak menemukan informasi yang relevan dalam dokumen yang diupload.';
}
const context = relevantChunks.join('\n\n---\n\n');
// Kirim ke Claude dengan konteks yang relevan
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
messages: [
{
role: 'user',
content:
Kamu adalah asisten yang membantu menjawab pertanyaan berdasarkan dokumen yang diberikan.Berikut adalah bagian-bagian dokumen yang relevan dengan pertanyaan:
${context}
Berdasarkan informasi di atas, jawab pertanyaan berikut:
${question}
Kalau jawabannya tidak ada dalam dokumen, katakan dengan jelas bahwa informasi tersebut tidak ada dalam dokumen.
}
]
});
return response.content[0].type === 'text'
? response.content[0].text
: 'Tidak dapat menghasilkan respons.';
}
Langkah 5: API Server
Buat file src/server.ts:
import { Hono } from 'hono';);import { serve } from '@hono/node-server';
import { processDocument } from './document';
import { askDocument } from './chat';
const app = new Hono();
// Upload dokumen
app.post('/upload', async (c) => {
try {
const formData = await c.req.formData();
const file = formData.get('file') as File;
if (!file) {
return c.json({ error: 'No file provided' }, 400);
}
const buffer = Buffer.from(await file.arrayBuffer());
const result = await processDocument(buffer, file.name, file.type);
return c.json({
success: true,
message:
Dokumen diproses: ${result.chunks} chunks});
} catch (error) {
return c.json({ error: 'Gagal memproses dokumen' }, 500);
}
});
// Tanya dokumen
app.post('/ask', async (c) => {
try {
const { question } = await c.req.json();
if (!question) {
return c.json({ error: 'Question is required' }, 400);
}
const answer = await askDocument(question);
return c.json({ answer });
} catch (error) {
return c.json({ error: 'Gagal mendapatkan jawaban' }, 500);
}
});
// Serve static files untuk frontend
app.get('/', (c) => {
return c.html(
<!DOCTYPE html>
<html>
<head>
<title>AI Document Q&A</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
textarea { width: 100%; height: 100px; margin: 10px 0; }
button { background: #0070f3; color: white; border: none; padding: 10px 20px; cursor: pointer; border-radius: 5px; }
#answer { margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 5px; }
</style>
</head>
<body>
<h1>AI Document Q&A</h1>
<h2>1. Upload Dokumen</h2>
<input type="file" id="fileInput" accept=".pdf,.txt">
<button onclick="uploadFile()">Upload</button>
<p id="uploadStatus"></p>
<h2>2. Tanya tentang Dokumen</h2>
<textarea id="question" placeholder="Tulis pertanyaan kamu..."></textarea>
<button onclick="askQuestion()">Tanya</button>
<div id="answer"></div>
<script>
async function uploadFile() {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert('Pilih file dulu');
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/upload', { method: 'POST', body: formData });
const data = await res.json();
document.getElementById('uploadStatus').textContent = data.message || data.error;
}
async function askQuestion() {
const question = document.getElementById('question').value;
if (!question) return alert('Tulis pertanyaan dulu');
document.getElementById('answer').textContent = 'Mencari jawaban...';
const res = await fetch('/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
const data = await res.json();
document.getElementById('answer').textContent = data.answer || data.error;
}
</script>
</body>
</html>
});
serve({ fetch: app.fetch, port: 3000 });
console.log('Server running at http://localhost:3000');
Langkah 6: Deploy
Deploy ke Railway (paling mudah):
# Install Railway CLI
npm install -g @railway/cli
Login dan deploy
railway login
railway init
railway up
Railway akan otomatis detect Node.js project dan deploy. Set environment variables melalui Railway dashboard.
Atau deploy ke Fly.io:
# Install Fly CLI
curl -L https://fly.io/install.sh | sh
Deploy
fly launch
fly secrets set ANTHROPIC_API_KEY=xxx
fly secrets set UPSTASH_VECTOR_REST_URL=xxx
fly secrets set UPSTASH_VECTOR_REST_TOKEN=xxx
fly deploy
Apa Selanjutnya?
Ini adalah foundation. Dari sini kamu bisa extend ke:
- Multi-document support — track dokumen per user dengan metadata filtering
- Conversation history — chat yang ingat context percakapan sebelumnya
- Streaming responses — jawaban muncul per token, bukan nunggu selesai semua
- Authentication — user management dengan Supabase Auth
- Document management — list, delete, organize dokumen yang sudah diupload
Kesimpulan
Membangun aplikasi AI tidak harus menunggu sampai kamu “siap” atau sampai kamu mengerti semuanya. Foundation dari semua aplikasi AI berbasis dokumen adalah sama: chunk, embed, retrieve, generate.
Mulai dari yang sederhana, deploy ke production (walaupun belum sempurna), lalu iterate berdasarkan feedback nyata.
Punya ide aplikasi AI yang mau dibangun tapi bingung bagaimana arsitekturnya? Atau butuh bantuan dari proof of concept sampai production-ready? Yuk ngobrol — hubungi mafadev dan kita wujudkan bareng.

Leave a Reply