Beranda Blog Store
Pemrograman Python

Membangun API Chat Sendiri di Atas LLM Lokal dengan FastAPI dan Ollama

23 Agu 2026 Hartono 5 menit baca 12 Dilihat

Halo, Rekan ArtonLabs. Setelah Ollama berjalan, langkah berikutnya adalah membuatnya bisa dipakai aplikasi dengan aman, dan itulah isi tulisan ini.

Begitu Ollama berjalan, pertanyaan berikutnya biasanya: bagaimana aplikasi kami memakainya dengan aman? Memanggil port 11434 langsung dari browser bukan jawaban, karena Ollama tidak punya autentikasi dan tidak tahu apa-apa tentang pengguna Anda. Jawabannya adalah lapisan API kecil milik Anda sendiri. Di tulisan ini kami membangunnya dengan FastAPI: autentikasi token, riwayat percakapan, streaming jawaban, dan pembatasan pemakaian.

Daftar Isi

Arsitektur Singkat

Browser atau aplikasi klien memanggil API FastAPI Anda dengan token. FastAPI memeriksa token, mengambil riwayat percakapan dari database, menyusun pesan, lalu memanggil Ollama di 127.0.0.1:11434. Jawaban dialirkan kembali ke klien sambil disimpan. Ollama sendiri tidak pernah terpapar ke luar. Pola ini sama dengan yang kami pakai saat membungkus layanan model apa pun, lokal maupun cloud, dan keuntungannya adalah penyedia model bisa diganti tanpa mengubah klien.

Persiapan Proyek

python3 -m venv .venv && source .venv/bin/activate
pip install fastapi "uvicorn[standard]" ollama sqlmodel
ollama pull qwen3:8b

Pustaka ollama resmi membungkus API-nya; sqlmodel dipakai untuk riwayat agar contohnya singkat. Susun berkas app.py dengan bagian-bagian berikut.

Endpoint Chat Pertama

from fastapi import FastAPI, Depends, HTTPException, Header
from pydantic import BaseModel
import ollama

app = FastAPI(title="Chat API")
MODEL = "qwen3:8b"
SYSTEM = "Anda asisten yang menjawab ringkas dalam bahasa Indonesia."

class ChatIn(BaseModel):
    message: str
    conversation_id: int | None = None

@app.post("/chat")
def chat(body: ChatIn):
    res = ollama.chat(model=MODEL, messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": body.message},
    ])
    return {"answer": res["message"]["content"]}

Jalankan dengan uvicorn app:app --port 8000 dan coba:

curl -X POST http://127.0.0.1:8000/chat -H 'Content-Type: application/json' \
  -d '{"message": "Apa itu systemd?"}'

Kalau ini jalan, semua yang lain hanya soal menambah lapisan.

Streaming Jawaban

Menunggu model menyelesaikan seluruh jawaban di CPU bisa memakan puluhan detik. Streaming membuat pengguna melihat kata pertama dalam satu atau dua detik, dan itu mengubah rasa aplikasi sepenuhnya.

from fastapi.responses import StreamingResponse

@app.post("/chat/stream")
def chat_stream(body: ChatIn):
    def gen():
        stream = ollama.chat(model=MODEL, stream=True, messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": body.message},
        ])
        for chunk in stream:
            yield chunk["message"]["content"]
    return StreamingResponse(gen(), media_type="text/plain")

Di sisi browser, baca dengan fetch dan response.body.getReader(), lalu tambahkan potongan teks ke layar begitu tiba. Kalau Anda memakai Nginx di depan, matikan buffering untuk jalur ini dengan proxy_buffering off;, kalau tidak, potongan-potongan itu tertahan sampai penuh.

Autentikasi Token dan Batas Pemakaian

Contoh minimal yang tetap layak untuk produksi kecil: token statis per klien yang disimpan di variabel lingkungan, dan pembatasan jumlah permintaan per token per menit.

import os, time
from collections import defaultdict

TOKENS = set(os.environ.get("CHAT_TOKENS", "").split(","))
hits = defaultdict(list)

def require_token(authorization: str = Header(default="")):
    token = authorization.removeprefix("Bearer ").strip()
    if token not in TOKENS:
        raise HTTPException(401, "token tidak dikenal")
    now = time.time()
    hits[token] = [t for t in hits[token] if now - t < 60]
    if len(hits[token]) >= 20:
        raise HTTPException(429, "terlalu banyak permintaan")
    hits[token].append(now)
    return token

@app.post("/chat", dependencies=[Depends(require_token)])
def chat(body: ChatIn): ...

Pembatas ini hidup di memori satu proses; cukup untuk satu worker Uvicorn. Kalau Anda menjalankan beberapa worker, pindahkan hitungannya ke Redis. Batasi juga panjang message (misalnya 4000 karakter) supaya satu permintaan tidak menghabiskan seluruh jendela konteks model.

Menyimpan Riwayat Percakapan

Model tidak ingat apa pun di antara permintaan. Riwayat adalah tanggung jawab API Anda: simpan setiap pesan pengguna dan jawaban model ke tabel dengan kolom conversation_id, role, content, created_at. Saat permintaan baru datang, ambil N pesan terakhir dari percakapan itu dan sertakan sebelum pesan baru. Batasi N, atau jumlah total karakternya, agar tidak melewati konteks model; untuk percakapan panjang, rangkum bagian awal dengan model itu sendiri dan simpan rangkumannya sebagai satu pesan sistem.

Satu kebiasaan yang menyelamatkan kami: simpan juga nama model dan parameter yang dipakai untuk tiap jawaban. Saat model diganti, Anda bisa membandingkan kualitas jawaban lama dan baru, bukan menebak.

Menjalankan di Produksi

Jalankan Uvicorn sebagai layanan systemd dengan pengguna biasa, di belakang Nginx dengan HTTPS. Di VinzaPanel, plugin Python Deploy membuatkan keduanya; tinggal tunjuk app:app sebagai modul ASGI dan pilih worker Uvicorn. Atur proxy_read_timeout cukup panjang (misalnya 300 detik) karena jawaban model di CPU bisa lama, dan seperti disebut di atas, matikan buffering untuk jalur streaming. Simpan token di berkas .env dengan izin ketat, bukan di kode.

Menangani Galat dan Waktu Habis

Ollama bisa sedang memuat model, kehabisan memori, atau mati. Bungkus pemanggilannya dan kembalikan galat yang jelas ke klien, bukan traceback:

import httpx

@app.post("/chat")
def chat(body: ChatIn):
    try:
        res = ollama.chat(model=MODEL, messages=[...], options={"num_ctx": 8192})
    except httpx.ConnectError:
        raise HTTPException(503, "layanan model sedang tidak tersedia")
    except ollama.ResponseError as e:
        raise HTTPException(502, f"model menolak permintaan: {e.error}")
    return {"answer": res["message"]["content"]}

Tambahkan juga batas waktu di klien Ollama (parameter timeout saat membuat ollama.Client) supaya satu permintaan yang menggantung tidak menahan worker selamanya. Parameter options adalah tempat mengatur num_ctx, temperature, dan sejenisnya per permintaan.

Mencatat Pemakaian

Setiap respons Ollama membawa hitungan token masukan dan keluaran serta durasinya (prompt_eval_count, eval_count, total_duration). Simpan angka-angka itu per token klien ke tabel sederhana. Dalam seminggu Anda tahu siapa memakai berapa, model mana yang lambat, dan apakah VPS-nya perlu dinaikkan. Tanpa catatan ini, keputusan kapasitas hanya menebak. Tambahkan satu endpoint /health yang memanggil ollama.list() dan mengembalikan 200 hanya kalau Ollama menjawab; pemantau uptime lalu memeriksa endpoint itu, sehingga Anda tahu layanan model mati sebelum pengguna mengeluh jawabannya tidak muncul.

yang aman, hemat, dan sepenuhnya di server sendiri. Lapisan ini juga tempat yang tepat untuk menambahkan RAG, yaitu menyisipkan potongan dokumen yang relevan ke dalam prompt sebelum dikirim ke model; itu bahasan artikel berikutnya.

Semoga membantu, Rekan ArtonLabs. Kalau Anda ingin kami membahas bagian tertentu lebih dalam, misalnya riwayat percakapan atau pembatasan pemakaian, tulis lewat halaman kontak.