BAML (BoundaryML): Nulis Prompt LLM Jadi Fungsi Ber-Type yang Rapi
Temen-temen, kalau kamu udah pernah ngoding aplikasi yang manggil LLM, pasti kamu tau rasanya. Kode kamu penuh sama string prompt yang panjang, di-format pakai f-string yang berantakan, terus outputnya kamu parse pakai json.loads() sambil berdoa semoga si model ngeluarin JSON yang valid. Kalau modelnya lagi bandel dan naruh teks tambahan sebelum JSON, aplikasi kamu langsung error. Belum lagi kalau kamu mau ganti dari OpenAI ke Anthropic atau ke model lokal, kamu harus obrak-abrik kode di banyak tempat.
Nah, di tutorial ini aku mau ngenalin kamu ke satu tool yang menurutku ngubah cara aku nulis kode LLM: namanya BAML, dibikin sama tim BoundaryML. BAML itu singkatan dari Basically A Made-up Language, dan bener, dia emang bahasa pemrograman kecil yang khusus dibikin buat satu tujuan: nulis prompt LLM sebagai fungsi yang punya tipe input dan output yang jelas. Jadi bukan cuma string sembarangan, tapi fungsi beneran yang punya kontrak.
Aku bakal ajak kamu dari nol: instalasi, nulis fungsi .baml pertama, generate client-nya, manggil dari Python, testing, streaming, sampai gonta-ganti provider model. Santai aja, kita jalan pelan-pelan. Siap? Yuk.
Introduction: Kenapa BAML Itu Penting
Sebelum masuk ke koding, aku mau kamu paham dulu masalah yang mau diselesaikan sama BAML. Ini penting biar kamu ngerti kenapa tool ini worth buat dipelajari.
Bayangin kamu mau bikin fitur yang nge-ekstrak informasi dari sebuah resume. Kamu mau ambil nama, email, sama daftar skill. Cara "biasa" yang sering kita lakuin kira-kira begini:
import openai
import json
prompt = f"""
Ekstrak informasi dari resume berikut dan kembalikan dalam format JSON
dengan field name, email, dan skills.
Resume:
{resumetext}
"""
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
Berdoa semoga ini JSON yang valid
data = json.loads(response.choices[0].message.content)
Kelihatannya oke ya. Tapi ada banyak masalah tersembunyi di sini. Pertama, output-nya nggak dijamin JSON valid. Model kadang ngeluarin ``` `json `` di depan, atau nambahin kalimat "Berikut adalah hasilnya:" yang bikin json.loads() gagal. Kedua, kamu nggak tau struktur data yang bakal keluar. Apakah skills itu list of string atau list of object? IDE kamu nggak bisa bantu auto-complete karena data itu cuma dict biasa. Ketiga, prompt dan logika parsing kecampur sama kode aplikasi, jadi susah di-maintain dan di-test.
BAML datang buat nyelesein semua ini. Ide dasarnya sederhana: kamu deklarasiin fungsi LLM di file .baml khusus, lengkap sama tipe input dan output-nya. Terus BAML CLI generate kode client (Python atau TypeScript) yang type-safe. Waktu kamu manggil fungsi itu, BAML yang urus prompt-nya, urus parsing outputnya jadi objek Python yang bener, dan bahkan urus retry kalau gagal.
Kelebihan utama BAML yang bikin aku jatuh cinta:
- Type-safe beneran: output di-parse jadi objek Python dengan tipe yang jelas, jadi IDE kamu bisa auto-complete.
- Parser yang pintar: BAML punya parser namanya SAP (Schema-Aligned Parsing) yang bisa nangani output model yang nggak 100 persen JSON valid. Ada teks tambahan? Tetep bisa di-parse.
- Streaming otomatis: kamu bisa stream output parsial yang tetep ter-struktur.
- Gampang ganti provider: mau OpenAI, Anthropic, Gemini, atau model lokal via Ollama, cukup ganti konfigurasi client, kode Python-nya nggak berubah.
- VSCode playground: ada extension yang bikin kamu bisa test prompt langsung di editor tanpa jalanin kode Python sama sekali.
Oke, teorinya udah cukup. Sekarang kita praktek.
Instalasi
Buat mulai pakai BAML, kamu butuh dua hal: package Python baml-py dan BAML CLI. Sebenernya waktu kamu install baml-py, CLI-nya ikut kebawa. Jadi gampang.
Aku saranin kamu bikin virtual environment dulu biar rapi. Temen-temen, ini kebiasaan yang bagus banget buat setiap project Python.
# Bikin folder project
mkdir baml-demo
cd baml-demo
Bikin virtual environment
python3 -m venv venv
source venv/bin/activate # di Windows: venv\Scripts\activate
Install baml-py
pip install baml-py
Setelah keinstall, kamu bisa cek CLI-nya jalan atau nggak:
baml-cli --version
Kalau keluar nomor versi, berarti aman. Sekarang kita inisialisasi project BAML. Perintah baml-cli init bakal bikin struktur folder standar buat BAML:
baml-cli init
Perintah ini bikin folder bamlsrc/ di project kamu. Di dalamnya udah ada beberapa file contoh:
- clients.baml
: tempat kamu definisiin client LLM (provider mana, model apa, API key-nya dari mana). - generators.baml
: konfigurasi buat generate kode client (bahasa target, versi, output path). - resume.baml
(atau file contoh serupa): contoh fungsi biar kamu punya gambaran.
Struktur folder-nya kira-kira begini:
baml-demo/
bamlsrc/
clients.baml
generators.baml
resume.baml
venv/
Yang perlu kamu paham: semua kode BAML kamu tinggal di bamlsrc/. Nanti dari sini BAML generate kode Python ke folder terpisah (biasanya bamlclient/) yang JANGAN pernah kamu edit manual, karena bakal ke-overwrite tiap kali generate ulang.
Satu hal lagi soal API key. BAML baca API key dari environment variable. Jadi sebelum jalan, set dulu:
export OPENAIAPIKEY="sk-..."
atau kalau pakai Anthropic:
export ANTHROPIC
APIKEY="sk-ant-..."
Aku biasanya taruh ini di file .env dan load pakai python-dotenv biar nggak ribet ngetik ulang tiap buka terminal baru.
Basic Usage: Nulis Fungsi BAML Pertama
Sekarang bagian yang seru. Kita bakal nulis fungsi BAML pertama buat ekstrak informasi dari resume, kasus yang tadi aku ceritain di awal.
Langkah 1: Definisiin Client
Buka bamlsrc/clients.baml. Di sini kita definisiin "client" yang menghubungkan BAML ke provider LLM. Isinya kira-kira gini:
client GPT4 {
provider openai
options {
model "gpt-4o"
apikey env.OPENAIAPIKEY
}
}
Perhatiin sintaksnya. client artinya kita bikin client bernama GPT4. provider openai nunjukin kita pakai OpenAI. Di dalam options, kita set model sama apikey. Yang keren, env.OPENAIAPIKEY itu cara BAML baca environment variable, jadi API key kamu nggak pernah hard-coded di file.
Langkah 2: Definisiin Tipe Output (Class)
Nah ini inti dari BAML. Kita definisiin struktur data output pakai keyword class. Buka file baru, misalnya bamlsrc/resume.baml, dan tulis:
class Resume {
name string
email string
skills string[]
}
Gampang dibaca kan? Resume punya tiga field: name bertipe string, email bertipe string, dan skills bertipe array of string (ditandai string[]). Ini yang bakal jadi output terstruktur kita. Kalau kamu pernah nulis type di TypeScript atau dataclass di Python, rasanya mirip.
Langkah 3: Definisiin Fungsi LLM
Sekarang kita bikin fungsinya. Masih di file yang sama:
function ExtractResume(resumetext: string) -> Resume {
client GPT4
prompt #"
Ekstrak informasi dari resume berikut.
{{ ctx.outputformat }}
Resume:
---
{{ resumetext }}
---
"#
}
Mari kita bedah bagian per bagian, temen-temen:
- function ExtractResume(resumetext: string) -> Resume
mendeklarasikan fungsi bernamaExtractResumeyang nerima inputresumetextbertipe string, dan mengembalikan objekResume. Ini kontraknya jelas banget. - client GPT4
nunjukin fungsi ini pakai clientGPT4yang tadi kita bikin. - prompt #"..."#
itu prompt-nya. Tanda#"..."#adalah string multi-baris di BAML. - {{ resumetext }}
adalah template variable. BAML pakai sintaks mirip Jinja, jadi input fungsi bisa disisipin ke prompt. - {{ ctx.outputformat }}
ini bagian magic-nya. Ini otomatis di-replace sama instruksi format output yang di-generate BAML berdasarkan classResume. Jadi kamu nggak perlu nulis manual "kembalikan JSON dengan field name, email, skills". BAML yang urus.
Langkah 4: Generate Client
Setelah fungsi didefinisikan, kita generate kode Python-nya:
baml-cli generate
Perintah ini baca semua file di bamlsrc/ dan generate package Python di folder bamlclient/. Di dalamnya ada kode yang type-safe buat manggil fungsi ExtractResume dari Python. Ingat ya, folder ini jangan diedit manual.
Langkah 5: Panggil dari Python
Sekarang kita pakai fungsinya dari Python. Bikin file main.py:
from bamlclient import b
resume
text = """
Budi Santoso
budi.santoso@email.com
Pengalaman sebagai Backend Engineer.
Skill: Python, FastAPI, PostgreSQL, Docker
"""
resume = b.ExtractResume(resumetext=resumetext)
print(type(resume)) # client.types.Resume'>
print(resume.name) # Budi Santoso
print(resume.email) # budi.santoso@email.com
print(resume.skills) # ['Python', 'FastAPI', 'PostgreSQL', 'Docker']
Jalanin:
python main.py
Perhatiin betapa bersihnya kode ini. Nggak ada json.loads(), nggak ada f-string prompt yang berantakan, nggak ada parsing manual. Objek resume yang kamu dapet itu objek Python beneran dengan atribut yang type-safe. IDE kamu bakal auto-complete resume.name, resume.email, sama resume.skills. Kalau kamu salah ketik resume.emial, editor langsung protes. Ini game changer buat produktivitas.
Advanced Usage
Sekarang kita naik level. Aku bakal tunjukin beberapa fitur BAML yang bikin dia beda dari sekadar wrapper prompt biasa: testing, streaming, tipe yang lebih kompleks, retry, sama ganti provider.
Testing Langsung di BAML
Salah satu fitur favoritku: kamu bisa nulis test case langsung di file .baml dan jalanin lewat VSCode playground tanpa nulis kode Python sama sekali. Tambahin ini di resume.baml:
test ResumeTest1 {
functions [ExtractResume]
args {
resumetext #"
Siti Aminah
siti.aminah@email.com
Skill: React, TypeScript, Tailwind CSS
"#
}
}
Blok test mendeklarasikan test bernama ResumeTest1, nyebutin fungsi mana yang dites (ExtractResume), dan ngasih argumen input. Kalau kamu install extension BAML di VSCode, bakal muncul tombol "Run" kecil di atas blok test ini. Klik, dan kamu langsung liat hasilnya di playground: prompt yang dikirim ke model, respons mentah dari model, sama hasil akhir yang udah di-parse jadi objek Resume. Ini bikin iterasi prompt jadi cepet banget karena kamu nggak perlu jalanin ulang aplikasi Python tiap kali ganti kata di prompt.
Tipe Output yang Lebih Kompleks
Resume beneran tentu lebih kaya dari sekadar tiga field. Kita bisa bikin tipe yang nested dan pakai enum. BAML dukung ini dengan elegan:
enum SkillLevel {
Beginner
Intermediate
Expert
}
class Skill {
name string
level SkillLevel
}
class WorkExperience {
company string
role string
years int
}
class DetailedResume {
name string
email string?
skills Skill[]
experiences WorkExperience[]
}
Beberapa hal baru di sini yang penting kamu tau:
- enum SkillLevel
mendefinisikan pilihan tetap. Model dipaksa milih salah satu dari tiga nilai ini, jadi outputnya konsisten. - class Skill
sekarang objek dengannamedanlevelbertipe enum. - email string?
tanda tanya artinya field ini opsional (boleh null). Ini berguna kalau datanya kadang nggak ada. - skills Skill[]
sekarang array of object, bukan cuma array of string.
Fungsi buat tipe ini kira-kira:
function ExtractDetailedResume(resumetext: string) -> DetailedResume {
client GPT4
prompt #"
Analisis resume berikut secara detail. Tentukan level tiap skill.
{{ ctx.output
format }}
Resume:
---
{{ resumetext }}
---
"#
}
Dan di Python, kamu dapet objek nested yang rapi:
from bamlclient import b
result = b.ExtractDetailedResume(resumetext=resumetext)
for skill in result.skills:
print(f"{skill.name}: {skill.level}") # Python: Expert
for exp in result.experiences:
print(f"{exp.role} di {exp.company} ({exp.years} tahun)")
BAML otomatis nge-generate class Python buat Skill, WorkExperience, SkillLevel, sama DetailedResume. Semuanya type-safe. Aku bener-bener suka gimana kompleksitas parsing disembunyiin dari kita.
Streaming Output
Kalau kamu bikin aplikasi chat atau butuh nampilin hasil secepet mungkin ke user, streaming itu wajib. BAML bikin streaming output terstruktur jadi sangat gampang. Setiap fungsi otomatis punya versi streaming lewat b.stream:
from bamlclient import b
stream = b.stream.ExtractDetailedResume(resume
text=resumetext)
for partial in stream:
# partial adalah objek DetailedResume yang belum lengkap
# field yang belum selesai bakal bernilai None
print(partial.name)
Ambil hasil final yang lengkap
final = stream.get
finalresponse()
print(final.skills)
Yang keren dari streaming BAML, dia bukan cuma stream token mentah. Dia stream objek parsial yang udah ter-struktur. Jadi selama model masih ngetik, kamu udah bisa akses partial.name begitu field itu selesai, sementara field lain masih None. Ini cocok banget buat UI yang mau nampilin data begitu tersedia tanpa nunggu semua selesai.
Retry dan Fallback
Di dunia nyata, panggilan API kadang gagal: timeout, rate limit, atau server error. BAML punya mekanisme retry dan fallback bawaan yang kamu konfigurasi di level client. Tambahin di clients.baml:
retrypolicy Exponential {
maxretries 3
strategy {
type exponentialbackoff
delayms 200
multiplier 2
}
}
client GPT4 {
provider openai
retrypolicy Exponential
options {
model "gpt-4o"
apikey env.OPENAIAPIKEY
}
}
Dengan retrypolicy Exponential, kalau panggilan gagal, BAML otomatis coba lagi sampai 3 kali dengan jeda yang makin lama (200ms, 400ms, 800ms). Kamu nggak perlu nulis logika try/except dan loop retry sendiri di Python. Semua ini deklaratif di konfigurasi.
Kamu juga bisa bikin fallback ke client lain kalau satu provider down:
client Resilient {
provider fallback
options {
strategy [GPT4, Claude]
}
}
Client Resilient bakal coba GPT4 dulu, dan kalau gagal total, pindah ke Claude. Buat aplikasi production, ini nyawa banget.
Ganti Provider Model
Ini salah satu keunggulan terbesar BAML. Misalnya kamu mau pindah dari OpenAI ke Anthropic. Yang perlu kamu ubah cuma definisi client di clients.baml, kode Python kamu NGGAK berubah sama sekali.
Tambahin client Anthropic:
client Claude {
provider anthropic
options {
model "claude-3-5-sonnet-20241022"
apikey env.ANTHROPICAPIKEY
}
}
Terus di fungsi, tinggal ganti client GPT4 jadi client Claude:
function ExtractResume(resumetext: string) -> Resume {
client Claude
prompt #"
Ekstrak informasi dari resume berikut.
{{ ctx.outputformat }}
Resume: {{ resumetext }}
"#
}
Generate ulang (baml-cli generate), dan kode Python kamu jalan seperti biasa, cuma sekarang pakai Claude. Kalau kamu mau pakai model lokal via Ollama, tinggal bikin client dengan provider yang nunjuk ke endpoint Ollama:
client LocalLlama {
provider openai-generic
options {
baseurl "http://localhost:11434/v1"
model "llama3.1"
}
}
Ollama expose API yang kompatibel dengan format OpenAI, jadi kita pakai provider openai-generic dan arahkan baseurl ke server Ollama lokal. Bayangin betapa gampangnya A/B testing antar model. Ganti satu baris, generate ulang, selesai.
Best Practices
Setelah beberapa waktu pakai BAML di project beneran, ini beberapa saran dari aku biar pengalaman kamu makin mulus.
Pertama, JANGAN pernah edit folder bamlclient/ secara manual. Folder itu sepenuhnya di-generate. Setiap kali kamu jalanin baml-cli generate, isinya ditimpa. Kalau kamu edit manual, perubahan kamu hilang. Anggap folder itu kayak folder build, jangan disentuh.
Kedua, masukin bamlclient/ ke version control atau nggak, itu tergantung tim. Sebagian tim commit folder ini biar CI nggak perlu generate ulang. Sebagian lagi masukin ke .gitignore dan generate saat build. Dua-duanya valid. Yang penting konsisten. Kalau kamu commit, pastiin selalu generate ulang setelah ubah file .baml.
Ketiga, manfaatin {{ ctx.outputformat }} sebaik mungkin. Godaan buat nulis instruksi format manual di prompt itu ada, tapi tahan. Biarin BAML yang generate instruksi format dari class kamu. Kalau kamu ubah class, instruksi otomatis ikut update. Kalau kamu tulis manual, kamu bakal lupa update-nya dan bug muncul.
Keempat, tulis test case di file .baml sejak awal. Prompt itu rapuh. Perubahan kecil di kata-kata bisa ngubah perilaku model. Dengan punya test case, kamu bisa cepet ngecek apakah prompt masih ngeluarin output yang bener setelah kamu ubah sesuatu. Ini kayak unit test buat prompt.
Kelima, pakai tipe yang spesifik. Daripada bikin field data string yang isinya JSON teks, mending bikin class beneran. Semakin spesifik tipe output kamu, semakin bagus SAP parser BAML nangani output model yang berantakan, dan semakin sedikit bug yang muncul. Enum juga sangat membantu buat maksa model milih dari pilihan tetap.
Keenam, kelola API key lewat environment variable, selalu. Jangan pernah hard-code API key di file .baml. Pakai env.NAMAVARIABLE. Buat development, kombinasikan dengan file .env dan python-dotenv.
Ketujuh, pisahkan file .baml berdasarkan domain. Kalau project kamu gede, jangan tumpuk semua fungsi di satu file. Bikin resume.baml, email.baml, classification.baml, dan seterusnya. BAML baca semua file di bamlsrc/ jadi kamu bebas nyusun.
Kedelapan, install extension VSCode BAML. Ini bukan opsional menurutku. Playground-nya bikin iterasi prompt jadi super cepet. Kamu bisa liat prompt yang di-render, respons mentah, sama hasil parsing, semuanya real-time tanpa jalanin aplikasi. Syntax highlighting dan auto-complete-nya juga bantu banget waktu nulis .baml.
Contoh Kasus Lengkap: Klasifikasi Sentimen
Biar makin nempel, ini satu contoh utuh lagi dengan use case beda: klasifikasi sentimen dari review produk. Ini pola yang sering banget dipakai di production.
Di
bamlsrc/sentiment.baml:
enum Sentiment {
Positive
Neutral
Negative
}
class ReviewAnalysis {
sentiment Sentiment
confidence float
keywords string[]
summary string
}
function AnalyzeReview(review: string) -> ReviewAnalysis {
client GPT4
prompt #"
Analisis review produk berikut. Tentukan sentimen,
tingkat keyakinan (0 sampai 1), kata kunci penting,
dan ringkasan singkat.
{{ ctx.outputformat }}
Review:
---
{{ review }}
---
"#
}
test PositiveReview {
functions [AnalyzeReview]
args {
review "Produk ini luar biasa! Kualitasnya bagus dan pengirimannya cepat."
}
}
Dan di Python:
from bamlclient import b
review = "Barangnya oke tapi pengirimannya lama banget, agak kecewa."
analysis = b.AnalyzeReview(review=review)
print(f"Sentimen: {analysis.sentiment}") # Sentiment.Negative
print(f"Keyakinan: {analysis.confidence}") # 0.75
print(f"Kata kunci: {analysis.keywords}") # ['pengiriman lama', 'kecewa']
print(f"Ringkasan: {analysis.summary}")
Liat kan, polanya selalu sama: definisiin tipe output, tulis fungsi dengan prompt, generate, panggil dari Python. Begitu kamu paham pola ini, kamu bisa bikin fitur LLM apa aja dengan cepat dan rapi. Konsistensi pola ini yang bikin BAML enak dipakai buat tim, karena semua orang nulis kode LLM dengan struktur yang sama.
Conclusion
Oke temen-temen, kita udah jalan cukup jauh. Mari aku rangkum apa yang udah kita pelajari. BAML itu domain-specific language buat nulis prompt LLM sebagai fungsi ber-type. Alih-alih string prompt yang berantakan dan parsing JSON yang rapuh, kamu dapet fungsi dengan kontrak input-output yang jelas, output yang otomatis di-parse jadi objek Python type-safe, dan tooling yang bikin hidup lebih gampang.
Kita udah bahas cara install
baml-py sama CLI-nya, inisialisasi project pakai baml-cli init, nulis class output dan fungsi di file .baml, generate client pakai baml-cli generate`, manggil dari Python dengan cara yang bersih, nulis test langsung di BAML, streaming output terstruktur, retry dan fallback, sampai gonta-ganti provider model cuma dengan ubah konfigurasi client.
Menurutku, kekuatan terbesar BAML itu di dua hal. Pertama, dia misahin definisi prompt LLM dari kode aplikasi kamu, jadi lebih rapi dan gampang di-maintain. Kedua, dia bikin output LLM jadi predictable lewat parser SAP dan sistem type-nya, jadi kamu nggak lagi berdoa semoga JSON-nya valid. Buat siapa aja yang serius bikin aplikasi berbasis LLM, apalagi yang mau naik ke production, BAML ini investasi yang worth banget buat dipelajari.
Saranku, mulai aja dari yang kecil. Ambil satu fungsi LLM di project kamu yang sekarang pakai parsing JSON manual, terus coba pindahin ke BAML. Rasain sendiri bedanya. Aku yakin begitu kamu ngerasain type-safety dan playground-nya, kamu bakal susah balik ke cara lama.
Selamat nyoba, temen-temen. Kalau ada yang mau ditanyain soal BAML, jangan ragu buat eksplorasi dokumentasi resminya di boundaryml.com. Happy coding dan sampai ketemu di tutorial berikutnya!