DSPy: Berhenti Ngoprek Prompt Manual, Biarkan Compiler yang Optimasi

# DSPy: Berhenti Ngoprek Prompt Manual, Biarkan Compiler yang Optimasi Halo temen-temen, kali ini aku mau ngenalin satu library yang menurutku cara mikirnya beda banget dibanding tools LLM lain yang...

By Ruby Abdullah · · tutorial
dspyprompt-optimizationllmpythonrag

DSPy: Berhenti Ngoprek Prompt Manual, Biarkan Compiler yang Optimasi

Halo temen-temen, kali ini aku mau ngenalin satu library yang menurutku cara mikirnya beda banget dibanding tools LLM lain yang pernah kita bahas, namanya DSPy. Kalau selama ini kalian bikin aplikasi LLM dengan cara nulis prompt panjang, terus di-tweak dikit, di-test, kurang bagus, di-tweak lagi, ganti kata, tambah "you are an expert", tambah contoh, begitu terus sampai capek, nah DSPy hadir buat ngasih tau kalian bahwa ada cara yang lebih sistematis.

Ide besarnya gini: DSPy memperlakukan prompt bukan sebagai teks yang kita tulis tangan, tapi sebagai sesuatu yang bisa dikompilasi dan dioptimasi otomatis dari data. Kalian cukup mendeklarasikan "apa yang mau dikerjakan" (input apa, output apa), bukan "gimana cara ngomong ke modelnya". Sisanya, biar optimizer DSPy yang nyari prompt dan contoh few-shot terbaik berdasarkan metrik yang kalian tentuin.

Di tutorial ini kita bakal mulai dari nol: instalasi, konfigurasi model, konsep Signature dan Module, bikin pipeline RAG sederhana, terus masuk ke bagian paling seru yaitu optimizer yang bisa naikin akurasi program kalian tanpa kalian ngetik ulang satu kata prompt pun. Siapin kopi, ini agak mind-bending di awal tapi habis itu nagih.

Introduction

Sebelum ngoding, aku mau kalian paham dulu kenapa DSPy ada.

Masalah Prompt Engineering Manual

Prompt engineering manual itu punya beberapa penyakit yang jarang dibahas orang.

Pertama, dia rapuh. Prompt yang bagus banget di GPT-4o belum tentu bagus di Claude atau di model open source kayak Llama. Begitu kalian ganti model, semua tuning tangan kalian sering harus diulang dari nol.

Kedua, dia nggak terukur. Kalau kalian nambahin kalimat "think step by step", kalian tau itu ngaruh berapa persen ke akurasi? Biasanya nggak tau, cuma feeling. Kita jarang punya baseline dan metrik yang rapi.

Ketiga, dia nggak bisa di-scale ke pipeline besar. Kalau aplikasi kalian punya lima langkah LLM yang saling nyambung (rewrite query, retrieve, rerank, jawab, verifikasi), nge-tune lima prompt sekaligus secara manual itu mimpi buruk. Ganti prompt di langkah satu bisa ngerusak langkah empat.

Cara Pandang DSPy

DSPy minjem cara pandang dari PyTorch. Di PyTorch kalian nggak nulis gradien manual, kalian cuma mendefinisikan arsitektur model, terus optimizer yang ngurus bobotnya. Di DSPy, kalian mendefinisikan struktur program LLM, terus optimizer yang ngurus "bobotnya", yang dalam hal ini adalah instruksi prompt dan contoh few-shot.

Ada tiga konsep utama yang harus kalian kuasai.

Signature adalah deklarasi input dan output sebuah langkah LLM. Contohnya question -> answer atau context, question -> answer. Kalian nggak nulis promptnya, kalian nulis kontraknya.

Module adalah strategi yang dipakai buat menjalankan sebuah Signature. Contohnya dspy.Predict (langsung jawab), dspy.ChainOfThought (mikir dulu baru jawab), atau dspy.ReAct (mikir sambil manggil tool).

Optimizer (dulu namanya Teleprompter) adalah algoritma yang ngambil program kalian, dataset kecil, dan sebuah metrik, terus otomatis nyari instruksi dan contoh terbaik supaya metrik itu naik.

Jadi alurnya: deklarasi Signature, bungkus dalam Module, susun jadi program, siapin data plus metrik, lalu compile.

Instalasi

DSPy butuh Python 3.9 ke atas. Aku selalu saranin bikin virtual environment dulu biar dependensi nggak tabrakan sama proyek lain.

python -m venv venv

source venv/bin/activate # Linux / Mac

venv\Scripts\activate # Windows

pip install -U dspy

Kalau kalian mau ngikutin bagian RAG di bawah, install juga satu library embedding ringan:

pip install sentence-transformers numpy

DSPy versi modern pakai LiteLLM di belakang layar, jadi hampir semua provider model bisa dipakai dengan format string yang sama: provider/nama-model. Ini enak banget karena ganti provider cuma ganti satu baris.

Konfigurasi Model

Langkah pertama di setiap program DSPy adalah bilang ke DSPy model apa yang mau dipakai.

import os

import dspy

Contoh pakai OpenAI

os.environ["OPENAIAPIKEY"] = "sk-..."

lm = dspy.LM("openai/gpt-4o-mini", maxtokens=1000, temperature=0.0)

dspy.configure(lm=lm)

Kalau kalian lebih suka model lokal pakai Ollama, tinggal ganti string modelnya:

lm = dspy.LM("ollamachat/llama3.1", apibase="http://localhost:11434", apikey="")

dspy.configure(lm=lm)

Perhatikan aku set temperature=0.0. Waktu lagi ngukur kualitas program dan ngejalanin optimizer, kita mau hasilnya sekonsisten mungkin biar perbandingan antar-kandidat prompt itu adil.

Signature: Deklarasi, Bukan Prompt

Cara paling cepat bikin Signature adalah pakai string pendek.

import dspy

qa = dspy.Predict("question -> answer")

hasil = qa(question="Apa ibu kota Indonesia dan kenapa mau dipindah?")

print(hasil.answer)

Cuma satu baris, dan DSPy otomatis nyusun prompt di belakang layar lengkap sama format output yang bisa di-parse. Kalian bisa intip prompt yang beneran dikirim dengan:

dspy.inspecthistory(n=1)

Aku saranin kalian jalanin perintah itu minimal sekali biar paham DSPy nggak sihir, dia cuma generate prompt terstruktur buat kalian.

Untuk kasus nyata, biasanya kita butuh Signature yang lebih deskriptif. Pakai class:

import dspy

class KlasifikasiTiket(dspy.Signature):

"""Klasifikasikan tiket support pelanggan ke kategori yang tepat."""

tiket: str = dspy.InputField(desc="isi pesan pelanggan")

kategori: str = dspy.OutputField(desc="salah satu dari: billing, teknis, akun, lainnya")

urgensi: str = dspy.OutputField(desc="rendah, sedang, atau tinggi")

alasan: str = dspy.OutputField(desc="satu kalimat penjelasan singkat")

classifier = dspy.Predict(KlasifikasiTiket)

out = classifier(tiket="Saya sudah bayar 3 hari lalu tapi akun masih terkunci dan besok deadline klien saya!")

print(out.kategori) # billing

print(out.urgensi) # tinggi

print(out.alasan)

Yang perlu digarisbawahi: docstring class itu jadi instruksi utama, dan desc di tiap field jadi petunjuk tambahan. Semua ini nanti bisa ditimpa dan diperbaiki otomatis sama optimizer. Jadi kalian nggak perlu bikin sempurna dari awal, cukup jelas dan jujur.

Tipe Output Terstruktur

DSPy paham type hints Python, termasuk Pydantic. Ini bikin output kalian rapi tanpa harus parsing JSON manual.

from typing import Literal

import dspy

class Sentimen(dspy.Signature):

"""Analisis sentimen review produk berbahasa Indonesia."""

review: str = dspy.InputField()

label: Literal["positif", "negatif", "netral"] = dspy.OutputField()

skor: float = dspy.OutputField(desc="keyakinan 0.0 sampai 1.0")

analis = dspy.Predict(Sentimen)

r = analis(review="Barangnya oke sih, tapi pengirimannya lama banget sampai seminggu.")

print(r.label, r.skor)

Module: Strategi Berpikir

Signature bilang APA, Module bilang GIMANA. Ini beberapa module yang paling sering kepakai.

dspy.Predict adalah yang paling dasar, langsung minta jawaban. dspy.ChainOfThought nyuruh model nulis penalaran dulu sebelum ngeluarin output final. Cukup ganti satu kata:
cot = dspy.ChainOfThought("soalmatematika -> jawaban")

hasil = cot(soalmatematika="Sebuah toko diskon 20 persen lalu diskon lagi 10 persen dari harga setelah diskon. Total diskon efektifnya berapa persen?")

print(hasil.reasoning) # penalaran langkah demi langkah

print(hasil.jawaban) # 28 persen

dspy.ReAct dipakai kalau model perlu manggil tool. Kalian cukup kasih fungsi Python biasa, DSPy yang ngurus protokolnya.
import ast

import operator

OPS = {ast.Add: operator.add, ast.Sub: operator.sub,

ast.Mult: operator.mul, ast.Div: operator.truediv}

def hitung(node):

if isinstance(node, ast.Constant):

return node.value

if isinstance(node, ast.BinOp):

return OPStype(node.op), hitung(node.right))

raise ValueError("ekspresi tidak didukung")

def kalkulator(ekspresi: str) -> float:

"""Hitung ekspresi matematika sederhana, contoh: '15 * 1.11'."""

return hitung(ast.parse(ekspresi, mode="eval").body)

def cariharga(namaproduk: str) -> str:

"""Cari harga produk di katalog internal."""

katalog = {"laptop": 12000000, "mouse": 250000, "monitor": 2100000}

return str(katalog.get(namaproduk.lower(), "tidak ditemukan"))

agent = dspy.ReAct("pertanyaan -> jawaban", tools=[kalkulator, cariharga])

print(agent(pertanyaan="Kalau saya beli 3 monitor, totalnya berapa setelah PPN 11 persen?").jawaban)

Perhatiin ya, docstring fungsi itu penting banget karena itu yang dibaca model buat mutusin kapan manggil tool mana.

Menyusun Program Multi-Langkah

Kekuatan asli DSPy muncul waktu kalian gabungin beberapa module jadi satu program. Caranya persis kayak bikin nn.Module di PyTorch.

import dspy

class RingkasDanKritik(dspy.Module):

def init(self):

super().init()

self.ringkas = dspy.ChainOfThought("artikel -> ringkasan")

self.kritik = dspy.ChainOfThought("artikel, ringkasan -> kritik, ringkasanfinal")

def forward(self, artikel):

r = self.ringkas(artikel=artikel)

k = self.kritik(artikel=artikel, ringkasan=r.ringkasan)

return dspy.Prediction(

ringkasanawal=r.ringkasan,

kritik=k.kritik,

ringkasan=k.ringkasanfinal,

)

program = RingkasDanKritik()

teks = "Pemerintah mengumumkan kebijakan baru terkait subsidi kendaraan listrik ..."

hasil = program(artikel=teks)

print(hasil.ringkasan)

Dua langkah di atas dua-duanya punya prompt sendiri, dan nanti optimizer bakal nge-tune keduanya sekaligus dengan memperhatikan metrik akhir. Inilah yang nggak mungkin kalian lakuin manual dengan rapi.

Studi Kasus: RAG Sederhana

Ayo bikin RAG mini biar kelihatan bentuk nyatanya. Aku sengaja pakai retriever buatan sendiri yang super sederhana supaya fokusnya tetap ke DSPy, bukan ke vector database.

import numpy as np

import dspy

from sentencetransformers import SentenceTransformer

DOKUMEN = [

"Cuti tahunan karyawan tetap adalah 12 hari kerja per tahun dan hangus jika tidak diambil dalam 18 bulan.",

"Pengajuan reimburse maksimal 30 hari setelah tanggal transaksi, lewat dari itu otomatis ditolak sistem.",

"Karyawan berhak WFH maksimal 8 hari per bulan dengan persetujuan atasan langsung.",

"Gaji dibayarkan setiap tanggal 25, jika tanggal 25 jatuh di hari libur maka dimajukan ke hari kerja sebelumnya.",

"Bonus tahunan dihitung dari performa individu 60 persen dan performa perusahaan 40 persen.",

]

encoder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

EMB = encoder.encode(DOKUMEN, normalizeembeddings=True)

def retrieve(query: str, k: int = 2):

q = encoder.encode([query], normalizeembeddings=True)[0]

skor = EMB @ q

idx = np.argsort(-skor)[:k]

return [DOKUMEN[i] for i in idx]

class RAG(dspy.Module):

def init(self, k=2):

super().init()

self.k = k

self.jawab = dspy.ChainOfThought("konteks, pertanyaan -> jawaban")

def forward(self, pertanyaan):

konteks = retrieve(pertanyaan, self.k)

out = self.jawab(konteks="\n".join(konteks), pertanyaan=pertanyaan)

return dspy.Prediction(konteks=konteks, jawaban=out.jawaban)

rag = RAG()

print(rag(pertanyaan="Kalau saya telat submit reimburse 2 bulan gimana?").jawaban)

Sampai sini kita masih pakai prompt default. Sekarang bagian yang bikin DSPy layak dipelajari.

Optimizer: Compile Program Kalian

Buat ngoptimasi, kalian butuh dua hal: data contoh dan sebuah metrik.

Datanya nggak harus banyak. Untuk optimizer few-shot, 20 sampai 50 contoh sering udah cukup ngasih lompatan besar. Yang penting jawabannya benar dan representatif.

import dspy

trainset = [

dspy.Example(

pertanyaan="Berapa hari cuti tahunan saya?",

jawaban="12 hari kerja per tahun, dan hangus kalau tidak diambil dalam 18 bulan.",

).withinputs("pertanyaan"),

dspy.Example(

pertanyaan="Saya belanja tanggal 1, sekarang tanggal 20, masih bisa reimburse?",

jawaban="Masih bisa, karena batasnya 30 hari setelah tanggal transaksi.",

).withinputs("pertanyaan"),

dspy.Example(

pertanyaan="Gajian tanggal 25 tapi itu hari Minggu, dibayar kapan?",

jawaban="Dimajukan ke hari kerja sebelumnya.",

).withinputs("pertanyaan"),

dspy.Example(

pertanyaan="Bonus tahunan dihitung dari apa saja?",

jawaban="60 persen performa individu dan 40 persen performa perusahaan.",

).withinputs("pertanyaan"),

dspy.Example(

pertanyaan="Maksimal WFH berapa hari sebulan?",

jawaban="8 hari per bulan dengan persetujuan atasan langsung.",

).withinputs("pertanyaan"),

]

withinputs itu penting. Dia nandain field mana yang jadi input, sisanya dianggap label.

Sekarang metriknya. Metrik itu fungsi biasa yang nerima contoh asli dan prediksi, lalu balikin skor.

def metrikjawaban(example, pred, trace=None):

"""Cek apakah jawaban prediksi memuat inti dari jawaban acuan."""

acuan = example.jawaban.lower()

prediksi = pred.jawaban.lower()

katakunci = [w for w in acuan.split() if len(w) > 4]

if not katakunci:

return 0.0

cocok = sum(1 for w in katakunci if w in prediksi)

return cocok / len(katakunci) >= 0.5

Untuk kasus produksi, kalian bisa bikin metrik yang lebih canggih, bahkan pakai LLM sebagai juri:

class NilaiJawaban(dspy.Signature):

"""Nilai apakah jawaban prediksi secara faktual setara dengan jawaban acuan."""

pertanyaan: str = dspy.InputField()

acuan: str = dspy.InputField()

prediksi: str = dspy.InputField()

setara: bool = dspy.OutputField()

juri = dspy.Predict(NilaiJawaban)

def metrikllm(example, pred, trace=None):

hasil = juri(pertanyaan=example.pertanyaan, acuan=example.jawaban, prediksi=pred.jawaban)

return bool(hasil.setara)

Terakhir, compile.

from dspy.teleprompt import BootstrapFewShot

optimizer = BootstrapFewShot(metric=metrikjawaban, maxbootstrappeddemos=4, maxlabeleddemos=4)

ragoptimized = optimizer.compile(RAG(), trainset=trainset)

print(ragoptimized(pertanyaan="Kalau saya telat submit reimburse 2 bulan gimana?").jawaban)

Apa yang barusan terjadi? DSPy menjalankan program kalian di data latih, ngumpulin jejak eksekusi yang berhasil menurut metrik, lalu memilih jejak terbaik itu jadi contoh few-shot yang otomatis diselipkan ke prompt. Kalian nggak ngetik satu contoh pun ke dalam prompt, tapi promptnya sekarang jauh lebih kaya.

Optimizer yang Lebih Kuat

BootstrapFewShot itu pintu masuk. Kalau kalian punya data lebih banyak dan budget lebih, coba MIPROv2 yang bukan cuma nyari contoh tapi juga menulis ulang instruksinya.
from dspy.teleprompt import MIPROv2

optimizer = MIPROv2(metric=metrikjawaban, auto="light")

ragmipro = optimizer.compile(RAG(), trainset=trainset, requirespermissiontorun=False)

Mode auto bisa diisi light, medium, atau heavy, yang menentukan berapa banyak kandidat prompt yang dicoba. Semakin heavy semakin mahal dan semakin lama, tapi biasanya juga semakin bagus.

Evaluasi yang Benar

Jangan pernah nilai hasil optimasi cuma dari beberapa contoh yang dicoba tangan. Pakai evaluator bawaan dan devset yang terpisah dari trainset.

from dspy.evaluate import Evaluate

devset = [

dspy.Example(pertanyaan="Cuti saya hangus kapan?",

jawaban="Kalau tidak diambil dalam 18 bulan.").withinputs("pertanyaan"),

dspy.Example(pertanyaan="Kalau atasan tidak setuju, boleh WFH?",

jawaban="Tidak, WFH butuh persetujuan atasan langsung.").withinputs("pertanyaan"),

]

evaluator = Evaluate(devset=devset, metric=metrikjawaban, numthreads=4, displayprogress=True)

print("Sebelum optimasi:", evaluator(RAG()))

print("Sesudah optimasi:", evaluator(ragoptimized))

Angka inilah yang jadi bukti. Kalau naik, bagus. Kalau nggak naik, berarti metrik kalian yang salah atau datanya kurang representatif, dan itu informasi yang jauh lebih berguna daripada feeling.

Menyimpan dan Memuat Program

Setelah puas, simpan hasil compile-nya biar nggak perlu ngulang optimasi tiap deploy.

ragoptimized.save("raghrd.json")

ragbaru = RAG()

ragbaru.load("raghrd.json")

print(ragbaru(pertanyaan="Reimburse maksimal berapa lama?").jawaban)

File JSON itu isinya instruksi dan demo hasil optimasi, bukan bobot model. Ringan, bisa masuk git, dan gampang di-review tim.

Tips dan Best Practice

Mulai dari yang sederhana. Jangan langsung bikin program tujuh langkah. Bikin satu dspy.Predict dulu, ukur, baru tambah kompleksitas kalau memang metriknya nuntut.

Investasikan waktu paling banyak di metrik, bukan di prompt. Di DSPy, metrik itu setir kalian. Metrik yang asal bikin optimizer ngejar hal yang salah dengan sangat rajin.

Pisahkan trainset, devset, dan testset. Optimasi di trainset, pilih konfigurasi di devset, laporkan angka final di testset yang belum pernah disentuh.

Pakai model murah buat eksekusi, model pintar buat optimasi. DSPy mendukung setting model berbeda per module, jadi kalian bisa hemat biaya signifikan.

Selalu cek dspy.inspecthistory() waktu ada yang aneh. Sembilan dari sepuluh masalah kelihatan jelas begitu kalian lihat prompt aslinya.

Aktifkan cache waktu bereksperimen supaya panggilan yang sama nggak dibayar dua kali. DSPy melakukan ini secara default lewat LiteLLM, dan itu sangat menyelamatkan dompet waktu ngejalanin optimizer berulang kali.

Jangan pernah pakai eval() mentah buat tool kalkulator seperti yang sering kalian lihat di contoh internet. Pakai parser AST yang dibatasi seperti contoh di atas, karena tool kalian bakal dipanggil dengan string yang dikarang model.

Kesimpulan

DSPy mengubah cara kita bikin aplikasi LLM dari seni menyusun kalimat jadi disiplin rekayasa yang bisa diukur. Poin-poin penting yang perlu kalian bawa pulang:

Kalian mendeklarasikan Signature (input dan output), bukan menulis prompt. Ini bikin program kalian portabel antar model.

Module menentukan strategi berpikir, dari Predict yang langsung jawab, ChainOfThought yang menalar, sampai ReAct yang memanggil tool.

Program multi-langkah disusun seperti nn.Module di PyTorch, dan semua langkahnya bisa dioptimasi sekaligus.

Optimizer seperti BootstrapFewShot dan MIPROv2 mencari contoh dan instruksi terbaik secara otomatis berdasarkan metrik kalian, sesuatu yang mustahil dilakukan manual dengan konsisten.

Metrik dan data adalah investasi utama kalian. Prompt jadi artefak hasil kompilasi, bukan hasil tebak-tebakan semalam suntuk.

Coba mulai dari satu use case kecil di kantor kalian, misalnya klasifikasi tiket atau ekstraksi field dari dokumen. Kumpulin 30 contoh berlabel, tulis metrik sederhana, lalu compile. Begitu kalian lihat angka akurasi naik tanpa nyentuh prompt sama sekali, kalian bakal ngerti kenapa aku semangat banget nulis tutorial ini. Selamat mencoba.

Artikel Terkait

Tutorial LangChain: Framework Paling Populer untuk Membangun Aplikasi LLM

Tutorial LangChain: Framework Paling Populer untuk Membangun Aplikasi LLM LangChain adalah framework open-source yang di...

RAGAS: Framework Evaluasi untuk Pipeline RAG

RAGAS: Framework Evaluasi untuk Pipeline RAG Pendahuluan Retrieval-Augmented Generation (RAG) telah menjadi arsitektur s...

DSPy: Framework untuk Optimasi LLM Secara Programatik

DSPy: Framework untuk Optimasi LLM Secara Programatik Prompt engineering secara manual adalah proses yang melelahkan dan...

Tutorial Lengkap LlamaIndex: Membangun Aplikasi RAG dengan LLM

Tutorial Lengkap LlamaIndex: Membangun Aplikasi RAG dengan LLM LlamaIndex adalah framework data yang powerful untuk memb...