Tutorial Lengkap Instructor: Output Terstruktur dari LLM dengan Pydantic
Halo temen-temen, di tutorial ini aku mau ngajak kalian kenalan sama salah satu library Python yang menurutku wajib banget dipelajari kalau kalian sering kerja bareng LLM, namanya Instructor. Kalau kalian pernah frustrasi karena output dari model bahasa itu kadang rapi kadang berantakan, kadang JSON-nya valid kadang malah ada teks nyasar di depan atau belakang, nah Instructor ini jawabannya. Aku sendiri udah lama pakai library ini di beberapa project produksi, dan jujur aja ini ngubah cara aku ngoding sama LLM secara total.
Di artikel ini aku bakal bahas dari nol banget, mulai dari kenapa structured output itu penting, cara instalasi, penggunaan dasar, sampai ke fitur-fitur canggih kayak nested model, retry otomatis, streaming, dan dukungan multi-provider. Aku juga bakal kasih banyak contoh kode Python yang beneran bisa kalian jalanin sendiri. Yuk kita mulai.
Kenapa Structured Output Itu Penting
Sebelum masuk ke Instructor, aku mau cerita dulu masalah yang sering banget kita hadapi waktu kerja sama LLM. Bayangin temen-temen lagi bikin aplikasi yang butuh nge-ekstrak data dari teks. Misalnya kalian punya email pelanggan, terus kalian pengen ambil nama, email, dan tingkat urgensi dari email itu. Cara paling naif adalah kalian minta LLM buat balikin JSON.
Masalahnya, LLM itu kan pada dasarnya generator teks. Dia gak punya jaminan bakal ngasih output yang formatnya konsisten. Kadang dia balikin JSON yang valid, kadang dia tambahin kalimat pembuka kayak "Tentu, ini hasilnya:" sebelum JSON-nya. Kadang malah dia bungkus pakai markdown code block. Kadang tipe datanya salah, misalnya kalian minta angka tapi dia kasih string. Ini bikin pusing banget kalau outputnya mau kita proses lebih lanjut di kode.
Pendekatan lama biasanya kita parsing manual pakai regex, atau kita coba json.loads() terus kita bungkus pakai try-except gede-gedean. Tapi ini rapuh banget. Begitu format outputnya berubah dikit, kode kita langsung error. Belum lagi kita harus validasi manual satu-satu, misalnya mastiin field email itu beneran format email, mastiin umur itu angka positif, dan seterusnya.
Nah, di sinilah Instructor masuk. Idenya simpel tapi brilian, temen-temen. Kita definisiin struktur output yang kita mau pakai Pydantic, terus Instructor yang ngurusin biar LLM ngasih output sesuai struktur itu, sekaligus validasinya. Kalau outputnya gak sesuai, Instructor bakal otomatis minta LLM buat benerin. Jadi kita dapet output yang udah terjamin tipe dan validasinya, langsung bisa dipakai di kode. Gak perlu lagi parsing manual yang bikin sakit kepala.
Buat yang belum tau, Pydantic itu library Python buat data validation berbasis type hints. Kita definisiin class dengan field-field beserta tipenya, terus Pydantic yang mastiin data yang masuk sesuai sama definisi itu. Instructor memanfaatkan Pydantic ini sebagai kontrak antara kode kita dan LLM.
Instalasi
Oke, sekarang kita mulai praktik. Instalasinya gampang banget, cukup satu baris pakai pip.
pip install instructor
Instructor secara otomatis udah bawa dependency yang dibutuhin, termasuk Pydantic. Tapi kalian tetep butuh library provider LLM-nya, misalnya OpenAI atau Anthropic. Jadi biasanya aku install sekalian gini.
pip install instructor openai anthropic
Kalau kalian pakai environment virtual (yang mana aku saranin banget), aktifin dulu venv kalian sebelum install. Ini biar dependency project kalian gak campur aduk sama project lain.
python -m venv venv
source venv/bin/activate # kalau di Windows: venv\Scripts\activate
pip install instructor openai
Jangan lupa juga siapin API key kalian. Kalau pakai OpenAI, set environment variable OPENAIAPIKEY. Kalau pakai Anthropic, set ANTHROPICAPIKEY. Aku biasanya taruh di file .env terus load pakai library python-dotenv.
# file .env
OPENAIAPIKEY=sk-xxxxxxxxxxxxxxxx
Penggunaan Dasar
Sekarang kita masuk ke bagian yang seru. Aku bakal tunjukin contoh paling dasar dulu biar temen-temen ngerti konsep intinya. Konsep utama Instructor cuma ada dua, yaitu kita patch client LLM kita, terus kita kasih parameter responsemodel yang isinya class Pydantic.
Mari kita bikin contoh sederhana. Misalnya kita mau ekstrak informasi seseorang dari sebuah kalimat.
import instructor
from openai import OpenAI
from pydantic import BaseModel
Definisiin struktur output yang kita mau
class Orang(BaseModel):
nama: str
umur: int
pekerjaan: str
Patch client OpenAI dengan Instructor
client = instructor.fromopenai(OpenAI())
Panggil LLM dengan responsemodel
orang = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=Orang,
messages=[
{
"role": "user",
"content": "Budi umurnya 28 tahun, dia bekerja sebagai software engineer."
}
],
)
print(orang.nama) # Budi
print(orang.umur) # 28
print(orang.pekerjaan) # software engineer
print(type(orang)) # main.Orang'>
Coba perhatiin temen-temen, yang dikembalikan itu bukan string, bukan dictionary, tapi objek Orang yang beneran. Jadi kita bisa langsung akses orang.nama, orang.umur, dan seterusnya dengan autocomplete di editor, plus type checking. Ini beda banget sama pendekatan lama yang kita harus parsing JSON dulu terus akses pakai dictionary key yang rawan typo.
Bagian pentingnya ada di dua baris ini. Pertama, instructor.fromopenai(OpenAI()) yang nge-patch client OpenAI supaya ngerti parameter responsemodel. Kedua, responsemodel=Orang yang ngasih tau Instructor bentuk output yang kita harapkan. Instructor di belakang layar bakal ngubah class Pydantic itu jadi function schema atau JSON schema, terus dikirim ke LLM sebagai instruksi.
Kita juga bisa nambahin deskripsi ke tiap field pakai Field dari Pydantic. Ini berguna banget buat ngasih petunjuk tambahan ke LLM tentang apa yang kita mau.
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field
class Produk(BaseModel):
nama: str = Field(description="Nama produk yang disebut")
harga: float = Field(description="Harga produk dalam rupiah, tanpa titik atau koma")
kategori: str = Field(description="Kategori produk, misalnya elektronik atau makanan")
client = instructor.fromopenai(OpenAI())
produk = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=Produk,
messages=[
{
"role": "user",
"content": "Aku baru beli laptop gaming seharga 15 juta rupiah."
}
],
)
print(produk.nama) # laptop gaming
print(produk.harga) # 15000000.0
print(produk.kategori) # elektronik
Deskripsi di dalam Field itu langsung dikirim ke LLM sebagai bagian dari schema, jadi model tau persis apa yang harus diisi di tiap field. Ini teknik yang sederhana tapi ngaruh banget ke kualitas output.
Validasi Otomatis
Salah satu kekuatan terbesar Instructor adalah validasi. Karena kita pakai Pydantic, kita bisa manfaatin semua fitur validasi Pydantic. Misalnya kita bisa batesin nilai umur harus positif, atau email harus format email yang valid.
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field, EmailStr
class Kontak(BaseModel):
nama: str
email: EmailStr
umur: int = Field(gt=0, lt=150, description="Umur dalam tahun")
client = instructor.fromopenai(OpenAI())
kontak = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=Kontak,
messages=[
{
"role": "user",
"content": "Sari, 32 tahun, email sari@example.com"
}
],
)
print(kontak) # nama='Sari' email='sari@example.com' umur=32
Kalau LLM ngasih output yang gak valid, misalnya umurnya negatif atau emailnya salah format, Pydantic bakal ngelempar error validasi. Nah yang keren, Instructor bakal nangkep error itu terus otomatis minta LLM buat benerin outputnya dengan ngasih tau apa yang salah. Ini yang bakal kita bahas lebih detail di bagian retry nanti.
Kita juga bisa bikin validator custom pakai decorator fieldvalidator. Misalnya kita mau mastiin nama selalu diawali huruf kapital.
from pydantic import BaseModel, fieldvalidator
class Pengguna(BaseModel):
nama: str
username: str
@field
validator("username")
@classmethod
def usernameharuslowercase(cls, v: str) -> str:
if v != v.lower():
raise ValueError("username harus huruf kecil semua")
return v
Kalau LLM ngasih username dengan huruf besar, validator ini bakal ngelempar error, terus Instructor bakal minta model benerin. Jadi kita punya kontrol penuh atas bentuk dan aturan output kita.
Penggunaan Lanjutan
Nah sekarang kita masuk ke bagian yang lebih menarik, temen-temen. Di sini aku bakal bahas fitur-fitur canggih yang bikin Instructor ini beneran powerful buat aplikasi produksi.
Nested Model
Di dunia nyata, data itu jarang datar. Seringnya data kita punya struktur bertingkat, misalnya satu orang punya banyak alamat, satu pesanan punya banyak item. Instructor handle ini dengan mulus karena Pydantic mendukung nested model.
import instructor
from openai import OpenAI
from pydantic import BaseModel
from typing import List
class Alamat(BaseModel):
jalan: str
kota: str
kodepos: str
class Item(BaseModel):
namabarang: str
jumlah: int
hargasatuan: float
class Pesanan(BaseModel):
namapelanggan: str
alamatkirim: Alamat
items: List[Item]
total: float
client = instructor.fromopenai(OpenAI())
pesanan = client.chat.completions.create(
model="gpt-4o",
responsemodel=Pesanan,
messages=[
{
"role": "user",
"content": (
"Pesanan atas nama Andi, kirim ke Jl. Merdeka No.10, "
"Bandung, kode pos 40111. Beli 2 buku seharga 50000 per buku "
"dan 1 pulpen seharga 15000."
)
}
],
)
print(pesanan.namapelanggan) # Andi
print(pesanan.alamatkirim.kota) # Bandung
print(len(pesanan.items)) # 2
print(pesanan.items[0].namabarang) # buku
print(pesanan.items[0].jumlah) # 2
Lihat kan temen-temen, kita bisa nge-nest model sedalam yang kita mau. Instructor bakal minta LLM ngisi seluruh struktur ini sekaligus. Ini luar biasa berguna buat ekstraksi data kompleks. Aku sering pakai pola ini buat ngeproses invoice, dokumen legal, dan data terstruktur lain yang tadinya cuma teks mentah.
List dan Tipe Opsional
Kadang kita gak tau berapa banyak item yang bakal diekstrak, atau ada field yang mungkin kosong. Untuk itu kita bisa pakai List dan Optional dari typing.
from pydantic import BaseModel
from typing import List, Optional
class Tugas(BaseModel):
judul: str
deadline: Optional[str] = None
prioritas: str
class DaftarTugas(BaseModel):
tugas: List[Tugas]
client = instructor.fromopenai(OpenAI())
hasil = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=DaftarTugas,
messages=[
{
"role": "user",
"content": (
"Besok aku harus selesaiin laporan keuangan (prioritas tinggi), "
"terus balas email klien, dan review kode tim (deadline Jumat)."
)
}
],
)
for t in hasil.tugas:
print(f"{t.judul} - prioritas {t.prioritas} - deadline {t.deadline}")
Dengan Optional, kalau LLM gak nemu informasi buat field tertentu, dia bisa ngisi None tanpa error. Ini bikin ekstraksi kita lebih fleksibel dan robust.
Enum untuk Nilai Terbatas
Kalau kalian punya field yang nilainya cuma boleh dari pilihan tertentu, pakai Enum. Ini mastiin LLM cuma milih dari opsi yang kita kasih.
import instructor
from openai import OpenAI
from pydantic import BaseModel
from enum import Enum
class TingkatUrgensi(str, Enum):
RENDAH = "rendah"
SEDANG = "sedang"
TINGGI = "tinggi"
DARURAT = "darurat"
class TiketSupport(BaseModel):
ringkasan: str
urgensi: TingkatUrgensi
client = instructor.fromopenai(OpenAI())
tiket = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=TiketSupport,
messages=[
{
"role": "user",
"content": "Website kami down total, pelanggan gak bisa checkout sama sekali!"
}
],
)
print(tiket.urgensi) # TingkatUrgensi.DARURAT
Dengan Enum, kita jamin outputnya selalu salah satu dari nilai yang valid. Gak akan ada lagi LLM yang ngasih "agak penting" atau "lumayan mendesak" yang bikin logika kita bingung.
Retry Otomatis
Ini fitur favorit aku, temen-temen. Kadang LLM ngasih output yang gak lolos validasi. Nah Instructor punya mekanisme retry otomatis. Kalau validasi gagal, Instructor bakal kirim ulang request ke LLM dengan tambahan pesan error, biar model tau apa yang salah dan benerin.
import instructor
from openai import OpenAI
from pydantic import BaseModel, fieldvalidator
class Ringkasan(BaseModel):
isi: str
@fieldvalidator("isi")
@classmethod
def maksimalsepuluhkata(cls, v: str) -> str:
jumlahkata = len(v.split())
if jumlahkata > 10:
raise ValueError(
f"Ringkasan terlalu panjang ({jumlahkata} kata), maksimal 10 kata"
)
return v
client = instructor.fromopenai(OpenAI())
ringkasan = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=Ringkasan,
maxretries=3,
messages=[
{
"role": "user",
"content": "Ringkas artikel tentang manfaat olahraga pagi buat kesehatan tubuh."
}
],
)
print(ringkasan.isi)
Parameter maxretries=3 artinya Instructor bakal coba maksimal 3 kali kalau validasi gagal. Setiap kali gagal, pesan error dari Pydantic dikirim balik ke LLM sebagai konteks tambahan. Jadi model punya kesempatan buat belajar dari kesalahannya dan ngasih output yang bener. Menurutku ini brilian karena kita gak perlu nulis logika retry manual.
Buat kontrol yang lebih halus, kita bisa pakai Retrying dari library tenacity.
from tenacity import Retrying, stopafterattempt, waitfixed
hasil = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=Ringkasan,
maxretries=Retrying(
stop=stopafterattempt(5),
wait=waitfixed(2),
),
messages=[...],
)
Dengan tenacity, kita bisa atur strategi retry lebih detail, misalnya kasih jeda antar percobaan atau atur kondisi berhenti yang lebih kompleks.
Streaming
Kalau kalian bikin aplikasi yang butuh nampilin hasil secara real-time, misalnya UI chat, streaming itu penting biar user gak nunggu lama. Instructor mendukung streaming buat partial object dan buat iterable.
Yang pertama, streaming partial. Ini ngasih kita objek yang terisi bertahap seiring LLM ngetik.
import instructor
from openai import OpenAI
from pydantic import BaseModel
class Profil(BaseModel):
nama: str
bio: str
keahlian: str
client = instructor.fromopenai(OpenAI())
stream = client.chat.completions.createpartial(
model="gpt-4o-mini",
responsemodel=Profil,
messages=[
{
"role": "user",
"content": "Buatkan profil untuk seorang data scientist bernama Rina."
}
],
)
for partial in stream:
print(partial)
# objek Profil yang terisi bertahap, field demi field
Yang kedua, streaming iterable. Ini berguna kalau kita mau ekstrak banyak objek dan pengen proses satu per satu begitu masing-masing selesai, tanpa nunggu semuanya kelar.
from typing import Iterable
class Kutipan(BaseModel):
teks: str
penulis: str
client = instructor.fromopenai(OpenAI())
kutipanstream = client.chat.completions.createiterable(
model="gpt-4o-mini",
responsemodel=Kutipan,
messages=[
{
"role": "user",
"content": "Berikan 5 kutipan motivasi terkenal beserta penulisnya."
}
],
)
for kutipan in kutipanstream:
print(f'"{kutipan.teks}" - {kutipan.penulis}')
Streaming ini bikin aplikasi kita terasa jauh lebih responsif. User bisa langsung lihat hasil muncul bertahap, bukan nunggu blank screen sampai semua proses selesai.
Multiple Providers
Nah ini salah satu alasan aku suka banget sama Instructor. Dia gak cuma dukung OpenAI, tapi juga banyak provider lain kayak Anthropic, Google Gemini, Cohere, Mistral, sampai model lokal via Ollama. API-nya konsisten, jadi kalian bisa gonta-ganti provider tanpa ngubah logika utama.
Buat Anthropic Claude, cukup ganti fungsi patch-nya.
import instructor
from anthropic import Anthropic
from pydantic import BaseModel
class Analisa(BaseModel):
sentimen: str
skorkepercayaan: float
alasan: str
client = instructor.fromanthropic(Anthropic())
analisa = client.chat.completions.create(
model="claude-sonnet-4-5",
maxtokens=1024,
responsemodel=Analisa,
messages=[
{
"role": "user",
"content": "Analisa sentimen: 'Pelayanannya ramah banget, aku puas!'"
}
],
)
print(analisa.sentimen) # positif
print(analisa.skorkepercayaan) # 0.95
Perhatiin, satu-satunya yang berubah cuma instructor.fromanthropic(Anthropic()) dan nama modelnya. Untuk Claude kita juga perlu set maxtokens karena itu wajib di API Anthropic. Sisanya, mulai dari responsemodel sampai cara akses hasilnya, sama persis kayak versi OpenAI.
Instructor modern juga punya API terpadu lewat instructor.fromprovider yang bikin kita bisa nentuin provider lewat string. Ini praktis banget kalau providernya mau dikonfigurasi dari environment variable.
import instructor
from pydantic import BaseModel
class Jawaban(BaseModel):
isi: str
provider ditentukan lewat string "provider/model"
client = instructor.fromprovider("openai/gpt-4o-mini")
jawaban = client.chat.completions.create(
responsemodel=Jawaban,
messages=[{"role": "user", "content": "Apa ibukota Indonesia?"}],
)
print(jawaban.isi) # Jakarta
Dengan pola ini, kita bisa gampang banget switch antar provider cuma dengan ganti string, misalnya dari "openai/gpt-4o-mini" ke "anthropic/claude-sonnet-4-5". Buat aplikasi produksi yang butuh fleksibilitas provider, ini penyelamat banget.
Best Practices
Setelah lumayan lama pakai Instructor di berbagai project, aku punya beberapa saran best practice yang mau aku bagi ke temen-temen. Ini hal-hal yang aku pelajari dari pengalaman, kadang dari kesalahan yang aku buat sendiri.
Pertama, kasih deskripsi yang jelas di tiap field. Semakin jelas deskripsi kalian, semakin bagus output LLM. Jangan males nulisField(description=...). Anggep aja kalian lagi ngasih instruksi ke asisten baru yang belum tau konteks apa-apa. Deskripsi yang baik bisa ngurangin kesalahan output secara signifikan.
Kedua, mulai dari model yang sederhana, terus kembangin bertahap. Jangan langsung bikin model raksasa dengan 30 field bertingkat. Mulai dari yang kecil, tes, baru tambahin kompleksitas. Ini bikin debugging jauh lebih gampang kalau ada yang salah.
Ketiga, manfaatin validasi Pydantic sepenuhnya. Jangan cuma pakai tipe dasar. Pakai constraint kayak gt, lt, minlength, maxlength, dan validator custom. Semakin ketat validasi kalian, semakin terjamin kualitas data yang masuk ke sistem. Inget, validasi ini juga otomatis jadi feedback buat LLM lewat mekanisme retry.
Keempat, set maxretries sesuai kebutuhan. Untuk task yang gampang, 2 sampai 3 retry biasanya cukup. Untuk task yang kompleks dengan validasi ketat, mungkin kalian butuh lebih. Tapi hati-hati, retry yang kebanyakan bisa bikin biaya API membengkak dan latensi naik. Jadi seimbangin antara reliabilitas dan biaya.
Kelima, pilih model yang sesuai. Model kecil kayak gpt-4o-mini cukup buat task ekstraksi sederhana dan lebih murah. Tapi buat struktur kompleks dengan banyak nested model dan reasoning, model yang lebih besar biasanya kasih hasil lebih akurat. Jangan boros pakai model mahal buat task yang gampang.
Keenam, tangani exception dengan baik. Meski Instructor punya retry, tetep ada kemungkinan semua percobaan gagal. Bungkus panggilan kalian dengan try-except dan siapin fallback yang masuk akal.
from pydantic import ValidationError
try:
hasil = client.chat.completions.create(
model="gpt-4o-mini",
responsemodel=Kontak,
maxretries=3,
messages=[{"role": "user", "content": teksinput}],
)
except ValidationError as e:
print(f"Gagal validasi setelah semua retry: {e}")
# lakukan fallback, misalnya simpan ke antrian buat diproses manual
Ketujuh, pakai Literal atau Enum buat field kategorikal. Ini ngurangin ruang jawaban LLM dan bikin outputnya lebih konsisten. Daripada berharap LLM ngasih string yang tepat, batesin pilihannya dari awal.
Kedelapan, aktifin logging waktu development. Instructor terintegrasi sama library logging Python. Dengan lihat request dan response mentahnya, kalian bisa lebih ngerti apa yang terjadi di belakang layar dan lebih gampang debug kalau ada masalah.
import logging
logging.basicConfig(level=logging.DEBUG)
Kesimpulan
Oke temen-temen, kita udah lumayan jauh perjalanannya. Aku harap sekarang kalian punya gambaran yang jelas kenapa Instructor ini worth banget buat dipelajari dan dipakai. Mari kita rangkum poin-poin kuncinya.
Instructor menyelesaikan salah satu masalah paling menyebalkan waktu kerja sama LLM, yaitu output yang gak konsisten dan gak terstruktur. Dengan menggabungkan kekuatan Pydantic buat definisi struktur dan validasi, plus mekanisme patch yang mulus ke berbagai client LLM, kita bisa dapet output yang terjamin tipenya, tervalidasi, dan langsung siap dipakai di kode.
Poin-poin penting yang perlu kalian inget. Pertama, konsep intinya cuma dua, yaitu patch client pakai instructor.fromopenai atau sejenisnya, terus kasih responsemodel berisi class Pydantic. Kedua, kalian bisa manfaatin seluruh fitur Pydantic mulai dari nested model, list, optional, enum, sampai validator custom. Ketiga, fitur maxretries bikin sistem kalian jauh lebih robust karena LLM otomatis benerin outputnya kalau gagal validasi. Keempat, streaming lewat createpartial dan create_iterable bikin aplikasi kalian terasa responsif. Kelima, dukungan multi-provider bikin kalian gak terkunci di satu vendor dan gampang gonta-ganti.
Menurutku, Instructor ini salah satu tool yang sekali kalian coba, kalian bakal susah balik ke cara lama. Aku sendiri sekarang hampir gak pernah lagi parsing JSON manual dari LLM. Semua aku serahin ke Instructor dan Pydantic. Kode jadi lebih bersih, lebih aman, dan lebih gampang di-maintain.
Saran aku, langsung praktik aja temen-temen. Ambil satu use case kecil di project kalian, misalnya ekstrak data dari teks, terus coba implementasi pakai Instructor. Aku yakin kalian bakal langsung ngerasain bedanya. Selamat ngoding, dan semoga tutorial ini bermanfaat buat kalian semua. Sampai jumpa di tutorial berikutnya.