Outlines: Structured Generation dari LLM dengan Constrained Decoding
Salah satu tantangan terbesar saat bekerja dengan Large Language Models (LLM) adalah mendapatkan output yang terstruktur dan valid secara konsisten. Ketika Anda membutuhkan JSON yang valid, enum dari pilihan tertentu, atau format spesifik lainnya, LLM seringkali menghasilkan output yang tidak sesuai format. Outlines hadir untuk menyelesaikan masalah ini dengan pendekatan constrained decoding di level token, menjamin output selalu valid secara struktural.
Bagaimana Constrained Decoding Bekerja?
Sebelum membahas Outlines, penting untuk memahami perbedaan dua pendekatan structured generation:
Pendekatan Retry-Based (seperti Instructor)
Library seperti Instructor menggunakan pendekatan "generate-then-validate":
Masalahnya: biaya API meningkat, latensi tidak dapat diprediksi, dan tidak ada jaminan konvergensi.
Pendekatan Constrained Decoding (Outlines)
Outlines menggunakan pendekatan yang fundamentally berbeda:
Hasilnya: output dijamin valid 100% tanpa perlu retry, dengan overhead komputasi yang minimal.
Instalasi
Instalasi Outlines cukup straightforward:
pip install outlines
Untuk menggunakan dengan model transformers lokal:
pip install outlines[transformers]
Untuk integrasi dengan llama.cpp:
pip install outlines[llamacpp]
Untuk integrasi dengan vLLM:
pip install outlines[vllm]
JSON Generation dengan Pydantic Models
Fitur paling populer dari Outlines adalah kemampuan menghasilkan JSON yang valid berdasarkan Pydantic model.
Contoh Dasar
import outlines
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
Definisikan schema dengan Pydantic
class Alamat(BaseModel):
jalan: str = Field(description="Nama jalan dan nomor")
kota: str = Field(description="Nama kota")
provinsi: str = Field(description="Nama provinsi")
kodepos: str = Field(description="Kode pos 5 digit")
class Karyawan(BaseModel):
nama: str = Field(description="Nama lengkap karyawan")
usia: int = Field(ge=18, le=65, description="Usia karyawan")
email: str = Field(description="Alamat email")
departemen: str = Field(description="Departemen kerja")
gaji: float = Field(gt=0, description="Gaji bulanan")
alamat: Alamat
keahlian: List[str] = Field(description="Daftar keahlian")
Load model
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
Buat generator dengan schema JSON
generator = outlines.generate.json(model, Karyawan)
Generate data terstruktur
prompt = """Buatkan data karyawan fiktif untuk perusahaan teknologi di Jakarta.
Karyawan ini adalah seorang senior data engineer."""
result = generator(prompt)
print(type(result)) #
print(result.nama)
print(result.departemen)
print(result.modeldumpjson(indent=2))
Nested dan Complex Schemas
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
import outlines
class Prioritas(str, Enum):
TINGGI = "tinggi"
SEDANG = "sedang"
RENDAH = "rendah"
class Status(str, Enum):
TODO = "todo"
INPROGRESS = "inprogress"
REVIEW = "review"
DONE = "done"
class SubTugas(BaseModel):
judul: str
selesai: bool
class Tugas(BaseModel):
id: int
judul: str = Field(maxlength=100)
deskripsi: str
prioritas: Prioritas
status: Status
assignee: str
estimasijam: float = Field(gt=0)
subtugas: List[SubTugas]
tags: List[str]
class SprintPlan(BaseModel):
namasprint: str
durasihari: int = Field(ge=7, le=30)
tujuan: str
tugaslist: List[Tugas]
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
generator = outlines.generate.json(model, SprintPlan)
prompt = """Buatkan sprint plan untuk tim development yang sedang mengerjakan
fitur rekomendasi produk menggunakan machine learning. Sprint ini berdurasi
2 minggu dengan 3 tugas utama."""
sprint = generator(prompt)
print(sprint.modeldumpjson(indent=2))
Regex-Guided Generation
Outlines memungkinkan Anda mengontrol format output menggunakan regular expression:
import outlines
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
Generate nomor telepon Indonesia
phonepattern = r"\+62[0-9]{9,12}"
generator = outlines.generate.regex(model, phonepattern)
result = generator("Berikan nomor telepon Indonesia: ")
print(result) # contoh: +628123456789
Generate tanggal format ISO
datepattern = r"[0-9]{4}-[0-1][0-9]-[0-3][0-9]"
generator = outlines.generate.regex(model, datepattern)
result = generator("Tanggal hari ini dalam format ISO: ")
print(result) # contoh: 2026-04-30
Generate alamat email
emailpattern = r"[a-z0-9.%+-]+@[a-z0-9.-]+\.[a-z]{2,}"
generator = outlines.generate.regex(model, emailpattern)
result = generator("Email kontak customer service: ")
print(result)
Generate format khusus: kode produk
productcodepattern = r"[A-Z]{3}-[0-9]{4}-[A-Z]{2}"
generator = outlines.generate.regex(model, productcodepattern)
result = generator("Kode produk untuk laptop gaming: ")
print(result) # contoh: LAP-2026-GM
Generate IPv4 address
ippattern = r"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
generator = outlines.generate.regex(model, ippattern)
result = generator("IP address server database: ")
print(result)
Choice dan Enum Generation
Ketika Anda ingin membatasi output ke pilihan tertentu:
import outlines
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
Sentiment analysis dengan pilihan terbatas
sentiments = ["positif", "negatif", "netral"]
generator = outlines.generate.choice(model, sentiments)
reviews = [
"Produk ini sangat bagus! Kualitasnya luar biasa.",
"Pelayanannya buruk, pengiriman sangat lambat.",
"Produknya biasa saja, sesuai harga.",
]
for review in reviews:
prompt = f"Analisis sentimen review berikut: '{review}'\nSentimen: "
sentiment = generator(prompt)
print(f"Review: {review[:50]}... -> {sentiment}")
Klasifikasi kategori tiket support
categories = [
"billing",
"technicalissue",
"featurerequest",
"accountmanagement",
"generalinquiry"
]
generator = outlines.generate.choice(model, categories)
ticket = "Saya tidak bisa login ke akun saya setelah reset password"
prompt = f"Klasifikasikan tiket support berikut: '{ticket}'\nKategori: "
category = generator(prompt)
print(f"Kategori: {category}")
Deteksi bahasa
languages = ["indonesian", "english", "malay", "javanese", "sundanese"]
generator = outlines.generate.choice(model, languages)
text = "Kulo badhe tumbas sega goreng"
prompt = f"Deteksi bahasa dari teks berikut: '{text}'\nBahasa: "
language = generator(prompt)
print(f"Bahasa terdeteksi: {language}")
Type-Guided Generation
Outlines juga mendukung generation berdasarkan tipe data Python:
import outlines
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
Generate integer
intgenerator = outlines.generate.format(model, int)
result = intgenerator("Berapa jumlah penduduk Jakarta dalam jutaan? ")
print(f"Hasil (int): {result}, tipe: {type(result)}")
Generate float
floatgenerator = outlines.generate.format(model, float)
result = floatgenerator("Berapa nilai pi sampai 5 desimal? ")
print(f"Hasil (float): {result}, tipe: {type(result)}")
Generate boolean
boolgenerator = outlines.generate.format(model, bool)
result = boolgenerator("Apakah Python adalah bahasa compiled? ")
print(f"Hasil (bool): {result}, tipe: {type(result)}")
Integrasi dengan Berbagai Model
Menggunakan Transformers (Hugging Face)
import outlines
Model dari Hugging Face
model = outlines.models.transformers(
"microsoft/Phi-3-mini-4k-instruct",
device="cuda" # atau "cpu"
)
Dengan konfigurasi sampling
generator = outlines.generate.json(model, MySchema)
result = generator(
prompt,
maxtokens=1024,
temperature=0.7,
topp=0.9
)
Menggunakan llama.cpp
import outlines
Model GGUF lokal via llama.cpp
model = outlines.models.llamacpp(
"path/to/model.gguf",
nctx=4096,
ngpulayers=35
)
generator = outlines.generate.json(model, MySchema)
result = generator(prompt)
Menggunakan vLLM
import outlines
Model dengan vLLM untuk inferensi cepat
model = outlines.models.vllm(
"microsoft/Phi-3-mini-4k-instruct",
tensorparallelsize=1
)
generator = outlines.generate.json(model, MySchema)
Batch generation - sangat efisien dengan vLLM
prompts = [
"Ekstrak informasi dari: Budi, 30 tahun, Jakarta",
"Ekstrak informasi dari: Siti, 25 tahun, Surabaya",
"Ekstrak informasi dari: Ahmad, 35 tahun, Bandung",
]
results = generator(prompts)
for r in results:
print(r.modeldumpjson())
Contoh Praktis: Pipeline Ekstraksi Data dengan Output Valid
Berikut contoh lengkap membangun pipeline ekstraksi data yang menjamin output selalu valid:
import outlines
from pydantic import BaseModel, Field, validator
from typing import List, Optional
from enum import Enum
import json
Schema untuk ekstraksi informasi invoice
class MataUang(str, Enum):
IDR = "IDR"
USD = "USD"
EUR = "EUR"
SGD = "SGD"
class ItemInvoice(BaseModel):
namaitem: str = Field(description="Nama produk atau jasa")
kuantitas: int = Field(ge=1, description="Jumlah item")
hargasatuan: float = Field(gt=0, description="Harga per unit")
total: float = Field(gt=0, description="Total harga item")
class InfoPerusahaan(BaseModel):
nama: str = Field(description="Nama perusahaan")
alamat: str = Field(description="Alamat perusahaan")
npwp: Optional[str] = Field(None, description="Nomor NPWP jika tersedia")
class Invoice(BaseModel):
nomorinvoice: str = Field(description="Nomor invoice")
tanggal: str = Field(description="Tanggal invoice format YYYY-MM-DD")
penerbit: InfoPerusahaan
penerima: InfoPerusahaan
items: List[ItemInvoice]
subtotal: float = Field(gt=0)
pajakpersen: float = Field(ge=0, le=100)
totalpajak: float = Field(ge=0)
grandtotal: float = Field(gt=0)
matauang: MataUang
catatan: Optional[str] = None
Inisialisasi model dan generator
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
invoicegenerator = outlines.generate.json(model, Invoice)
Teks invoice untuk diekstrak
invoicetext = """
INVOICE #INV-2026-0042
Dari:
PT Teknologi Maju Indonesia
Jl. Sudirman No. 123, Jakarta Selatan
NPWP: 01.234.567.8-901.000
Kepada:
CV Sumber Makmur
Jl. Gatot Subroto No. 456, Bandung
Tanggal: 15 April 2026
Item:
Jasa Konsultasi IT - 40 jam @ Rp 500.000 = Rp 20.000.000
Lisensi Software Enterprise - 5 unit @ Rp 2.000.000 = Rp 10.000.000
Pelatihan Karyawan - 2 sesi @ Rp 3.000.000 = Rp 6.000.000
Subtotal: Rp 36.000.000
PPN (11%): Rp 3.960.000
Total: Rp 39.960.000
Catatan: Pembayaran paling lambat 30 hari setelah tanggal invoice.
"""
Ekstraksi data - dijamin menghasilkan output valid
prompt = f"""Ekstrak semua informasi dari invoice berikut ke dalam format terstruktur:
{invoicetext}
Data invoice:"""
invoicedata = invoicegenerator(prompt)
Output dijamin valid dan bertipe Invoice
print(f"Nomor Invoice: {invoicedata.nomorinvoice}")
print(f"Penerbit: {invoicedata.penerbit.nama}")
print(f"Penerima: {invoicedata.penerima.nama}")
print(f"Jumlah item: {len(invoicedata.items)}")
print(f"Grand Total: {invoicedata.matauang.value} {invoicedata.grandtotal:,.0f}")
print(f"\nFull JSON:")
print(invoicedata.modeldumpjson(indent=2))
Pipeline batch processing
class HasilEkstraksi(BaseModel):
nama: str
jabatan: Optional[str] = None
perusahaan: Optional[str] = None
email: Optional[str] = None
telepon: Optional[str] = None
lokasi: Optional[str] = None
persongenerator = outlines.generate.json(model, HasilEkstraksi)
Data kartu nama untuk diekstrak
kartunamalist = [
"""Dr. Andi Prasetyo, M.Sc.
Chief Technology Officer
PT Inovasi Digital Nusantara
andi.prasetyo@inovasi.co.id
+62 812-3456-7890
Jakarta, Indonesia""",
"""Sarah Chen, MBA
Regional Sales Director
GlobalTech Solutions Pte Ltd
sarah.chen@globaltech.sg
+65 9123 4567
Singapore""",
"""Bambang Widodo
Senior Data Scientist
Bank Central Digital
b.widodo@bcd.co.id
021-5551234
Surabaya, Indonesia""",
]
print("\n=== Batch Extraction Results ===")
for i, kartu in enumerate(kartunamalist):
prompt = f"Ekstrak informasi kontak dari kartu nama berikut:\n{kartu}\n\nData:"
result = persongenerator(prompt)
print(f"\n--- Kartu Nama {i+1} ---")
print(f"Nama: {result.nama}")
print(f"Jabatan: {result.jabatan}")
print(f"Perusahaan: {result.perusahaan}")
print(f"Email: {result.email}")
print(f"Telepon: {result.telepon}")
print(f"Lokasi: {result.lokasi}")
Pipeline Klasifikasi Dokumen
import outlines
from pydantic import BaseModel, Field
from typing import List
from enum import Enum
class KategoriDokumen(str, Enum):
KONTRAK = "kontrak"
INVOICE = "invoice"
PROPOSAL = "proposal"
LAPORAN = "laporan"
SURAT = "surat"
MEMO = "memo"
class TingkatUrgensi(str, Enum):
URGENT = "urgent"
NORMAL = "normal"
RENDAH = "rendah"
class KlasifikasiDokumen(BaseModel):
kategori: KategoriDokumen
urgensi: TingkatUrgensi
ringkasan: str = Field(maxlength=200)
katakunci: List[str] = Field(maxlength=5)
departementujuan: str
perlutindakan: bool
tindakandisarankan: Optional[str] = None
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
classifier = outlines.generate.json(model, KlasifikasiDokumen)
dokumen = """
MEMO INTERNAL
Perihal: Permintaan Anggaran Darurat untuk Perbaikan Server
Kepada seluruh jajaran manajemen,
Dengan ini kami informasikan bahwa server utama data center mengalami
kerusakan hardware kritis pada tanggal 28 April 2026. Dibutuhkan
penggantian segera untuk 3 unit SSD enterprise dan 2 unit RAM ECC.
Estimasi biaya: Rp 85.000.000
Deadline: Maksimal 3 hari kerja
Mohon persetujuan segera mengingat dampak terhadap operasional perusahaan.
Hormat kami,
Tim IT Infrastructure
"""
prompt = f"Klasifikasikan dokumen berikut:\n\n{dokumen}\n\nKlasifikasi:"
hasil = classifier(prompt)
print(hasil.modeldumpjson(indent=2))
Tips dan Best Practices
1. Desain Schema yang Baik
# Gunakan Field untuk dokumentasi dan validasi
class DataPelanggan(BaseModel):
nama: str = Field(
description="Nama lengkap pelanggan",
minlength=2,
maxlength=100
)
usia: int = Field(
ge=0, le=150,
description="Usia dalam tahun"
)
email: str = Field(
description="Alamat email valid"
)
2. Batasi Ukuran Output
# Gunakan maxtokens untuk efisiensi
generator = outlines.generate.json(model, MySchema)
result = generator(prompt, max_tokens=512)
3. Pilih Model yang Tepat
Untuk structured generation, model yang lebih kecil seringkali sudah cukup karena Outlines membantu "mengarahkan" output. Model 3B-7B parameter biasanya sudah memberikan hasil yang baik untuk tugas ekstraksi sederhana.
4. Gunakan Enum untuk Pilihan Terbatas
# Lebih baik menggunakan Enum daripada string bebas
class Sentimen(str, Enum):
POSITIF = "positif"
NEGATIF = "negatif"
NETRAL = "netral"
Daripada
class HasilAnalisis(BaseModel):
sentimen: str # bisa menghasilkan nilai apapun
5. Pertimbangkan Performa
# Cache generator untuk penggunaan berulang
model = outlines.models.transformers("model-name")
generator = outlines.generate.json(model, MySchema)
Generator sudah membangun FSM saat inisialisasi
Panggilan berikutnya lebih cepat karena FSM sudah di-cache
for prompt in prompts:
result = generator(prompt)
Kesimpulan
Outlines mengubah cara kita berinteraksi dengan LLM untuk menghasilkan output terstruktur. Dengan pendekatan constrained decoding, Anda mendapatkan:
- Jaminan 100% output selalu valid sesuai schema
- Tanpa retry, mengurangi biaya dan latensi
- Integrasi fleksibel dengan berbagai model dan format output
- Performa optimal dengan overhead komputasi minimal
Untuk proyek yang membutuhkan output terstruktur dari LLM, seperti pipeline ekstraksi data, klasifikasi dokumen, atau structured data generation, Outlines adalah pilihan yang sangat direkomendasikan dibandingkan pendekatan retry-based tradisional.