Python & AI Tutorials Logo
LangChain & LangGraph

7. Output Terstruktur dengan Pydantic

Di bab-bab sebelumnya, kita telah bekerja dengan output LLM sebagai string teks mentah. Ini berfungsi dengan baik untuk chatbot di mana manusia membaca responsnya, tetapi ketika membangun agen AI di mana program perlu mem-parsing dan menginterpretasikan output LLM, kita membutuhkan data terstruktur yang dapat diprediksi. Di bab ini, kamu akan belajar cara menggunakan skema Pydantic untuk membuat LLM mengembalikan objek Python terstruktur.

7.1) Mengapa Output Terstruktur?

Masalah dengan Output LLM Teks Bebas

Mari kita mulai dengan memahami mengapa respons teks mentah menciptakan masalah dalam aplikasi nyata. Pertimbangkan skenario umum ini:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
# Tanyakan kepada LLM tentang sebuah produk
message = HumanMessage(content="""
Ekstrak informasi produk dari teks ini:
"UltraWidget Pro berharga $299.99 dan saat ini tersedia."
""")
 
response = llm.invoke([message])
print(response.content)

Output:

Nama Produk: UltraWidget Pro
Harga: $299.99
Ketersediaan: Tersedia

Outputnya terlihat bagus. Tetapi sekarang misalkan kamu perlu menggunakan data ini dalam aplikasi Python kamu. Bagaimana cara mengekstrak harga sebagai angka? Bagaimana cara memeriksa ketersediaan secara programatis? Kamu mungkin mencoba parsing string seperti ini:

python
# Pendekatan parsing yang rapuh
text = response.content
price_line = [line for line in text.split('\n') if 'Harga:' in line][0]
price_str = price_line.split('$')[1]
price = float(price_str)  # Rapuh - bagaimana jika formatnya berubah?

Parsing ini tampaknya berhasil. Tetapi sebenarnya tidak. Inilah alasannya:

Mengapa Pendekatan Ini Gagal:

  1. LLM mungkin memformat respons secara berbeda di lain waktu ("Harga: 299.99 USD" atau "Harga Retail: $299.99")

Berikut adalah contoh output berbeda yang dapat terjadi untuk prompt yang sama:

# Contoh 1
"Produknya adalah UltraWidget Pro, berharga $299.99, dan tersedia."
 
# Contoh 2
"Produk: UltraWidget Pro
Biaya: 299.99 dolar
Status: Tersedia"
 
# Contoh 3
"UltraWidget Pro - $299.99 (tersedia)"
 
# Contoh 4
"Saya menemukan UltraWidget Pro. Harganya $299.99 dan saat ini tersedia untuk dibeli."

Ketika respons LLM berubah, kamu membutuhkan logika parsing yang benar-benar berbeda. Ini membuat sulit untuk membangun aplikasi yang andal.

  1. Respons LLM tidak dapat diprediksi: Prompt yang sama dapat menghasilkan format berbeda setiap kali
  2. Parsing string lebih sulit dari yang terlihat: Kamu perlu menangani $, spasi, newline, koma, dan lainnya
  3. Tidak ada keamanan tipe sama sekali: Kamu tidak bisa yakin apakah price adalah float, string, atau None
  4. Penanganan error sulit: Jika LLM mengatakan "Harga tidak tersedia", panggilan float() kamu akan crash
  5. Tidak dapat dipelihara: Ubah prompt sedikit dan kamu menulis ulang semua kode parsing

Ide Inti: Python Membutuhkan Kontrak, Bukan Prosa

Pikirkan tentang ketika kamu berkomunikasi dengan server API di Python. Ketika kamu memanggil REST API tertentu, kamu mengharapkannya mengembalikan respons JSON yang terdefinisi:

python
# Kamu mengharapkan struktur ini
{
    "product_name": "UltraWidget Pro",
    "price": 299.99,
    "in_stock": true
}

Ketika membangun aplikasi AI, kamu membutuhkan prinsip yang sama. Output LLM harus dikembalikan sebagai data dengan struktur yang terdefinisi, bukan sebagai teks bentuk bebas setiap kali.

Prosa vs Kontrak:

  • Prosa: Teks natural bentuk bebas. Bagus untuk dibaca manusia, tetapi sulit untuk diproses program.
  • Kontrak: Data dengan struktur dan tipe yang terdefinisi. Sebuah janji bahwa "field-field ini akan ada dengan tipe-tipe ini."

Output terstruktur berarti mendefinisikan kontrak: "LLM, saya membutuhkan tepat field-field ini, dengan tepat tipe-tipe ini, dalam tepat format ini."

Di sinilah Pydantic masuk. Pydantic adalah library validasi data Python yang paling populer, dan LangChain menggunakannya untuk menerima output LLM dalam bentuk terstruktur.

Pergeseran Model Mental:

  • Sebelumnya: "LLM, ceritakan tentang produk ini" → Parse teks yang tidak dapat diprediksi
  • Setelahnya: "LLM, respons dalam format yang terdefinisi" → Terima objek Python terstruktur

Pergeseran dari prosa ke kontrak ini fundamental untuk membangun agen AI yang andal. Ketika agen perlu memutuskan tindakan berikutnya berdasarkan respons LLM (misalnya, beli jika tersedia, daftar untuk notifikasi jika tidak), ia harus menerima respons dalam format yang terdefinisi.

Parsing Manual

Output Terstruktur

Sering Rusak

Aman Tipe

Output Teks LLM

Logika String Rapuh

Objek Python Bertipe

Error Runtime

Kode Andal

7.2) Output Terstruktur Pertama Kamu

Di 7.1, kita belajar mengapa LLM harus merespons dengan struktur yang terdefinisi alih-alih teks bentuk bebas. Sekarang mari kita lihat cara benar-benar mengimplementasikan ini.

Ide kunci: Hanya meminta LLM "tolong respons dalam format ini" tidak cukup. Kamu perlu mendefinisikan struktur data yang tepat dalam kode Python dan membuat LangChain meneruskannya ke LLM. Struktur data yang terdefinisi ini disebut skema (schema).

Apa itu Skema?

Skema (schema) adalah blueprint yang mendefinisikan struktur data. Ia menentukan:

  • Field apa yang harus ada
  • Tipe apa yang harus dimiliki setiap field (string, number, boolean, dll.)
  • Constraint apa yang berlaku (opsional vs wajib, rentang valid, dll.)

Di Python, kita mendefinisikan skema menggunakan class BaseModel dari Pydantic. Berikut adalah contoh paling sederhana:

python
from pydantic import BaseModel
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool

Skema ini mengatakan: "Objek ProductInfo harus memiliki tepat tiga field: product_name (string), price (float), dan in_stock (boolean)."

Pola Tiga Langkah: Define, Bind, Invoke

Menggunakan output terstruktur itu sederhana. Ingat saja tiga langkah:

  1. Define: Buat skema dengan class Pydantic
  2. Bind: Hubungkan skema ke LLM menggunakan .with_structured_output()
  3. Invoke: Panggil .invoke() untuk mendapatkan objek bertipe

Ini adalah template standar yang akan kamu gunakan untuk sebagian besar tugas ekstraksi terstruktur:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
 
# Langkah 1: Definisikan skema
class ProductInfo(BaseModel):
    product_name: str = Field(description="Nama lengkap produk")
    price: float = Field(description="Harga dalam USD")
    in_stock: bool = Field(description="Apakah produk tersedia")
 
# Langkah 2: Bind skema ke LLM
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
# Langkah 3: Invoke dan dapatkan objek bertipe
message = HumanMessage(content="""
Ekstrak informasi produk dari teks ini:
"UltraWidget Pro berharga $299.99 dan saat ini tersedia."
""")
 
result = structured_llm.invoke([message])
 
# result sekarang adalah objek ProductInfo, bukan string
print(type(result))  # <class '__main__.ProductInfo'>
print(result.product_name)  # UltraWidget Pro
print(result.price)  # 299.99
print(result.in_stock)  # True

Apa yang Baru Saja Terjadi?

  1. Definisi Skema: Kita mendefinisikan field dan tipe yang kita inginkan
  2. Binding: .with_structured_output(ProductInfo) mengonfigurasi LLM untuk menggunakan output terstruktur
  3. Invocation & Response: Ketika .invoke() dipanggil, LangChain meneruskan JSON Schema ke LLM, dan LLM merespons dengan JSON yang cocok dengan struktur tersebut
  4. Konversi Otomatis: LangChain mengonversi JSON ke objek ProductInfo - tidak perlu kode parsing

Tidak ada parsing. Tidak ada konversi tipe. Tidak ada error.

Gunakan pola 3 langkah ini sebagai template kamu. Ikuti ini setiap kali kamu membutuhkan output terstruktur.

Deskripsi Field: Kunci untuk Memandu LLM

Dalam contoh definisi skema di atas, kita menggunakan Field(description="..."). Deskripsi ini bukan hanya dokumentasi. Ini adalah instruksi yang dibaca dan diikuti oleh LLM.

Dalam penggunaan Pydantic biasa, deskripsi Field bersifat opsional:

python
# Pydantic biasa - deskripsi adalah dokumentasi untuk manusia
class User(BaseModel):
    name: str = Field(description="Nama pengguna")  # Berfungsi baik tanpanya

Tetapi ketika bekerja dengan LLM, mereka esensial:

python
# Dengan LLM - deskripsi menentukan perilaku LLM
class CustomerFeedback(BaseModel):
    sentiment: str = Field(
        description="Sentimen keseluruhan: 'positive', 'negative', atau 'neutral'"
    )

LLM membaca deskripsi ini dan menggunakannya untuk memutuskan cara merespons.

Mari kita lihat ini dalam aksi:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
 
class CustomerFeedback(BaseModel):
    sentiment: str = Field(
        description="Sentimen keseluruhan: 'positive', 'negative', atau 'neutral'"
    )
    main_issue: str = Field(
        description="Keluhan atau kekhawatiran utama, jika ada. Gunakan 'none' jika tidak ada masalah yang disebutkan."
    )
    urgency: str = Field(
        description="Seberapa mendesak masalahnya: 'low', 'medium', atau 'high'"
    )
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(CustomerFeedback)
 
message = HumanMessage(content="""
Analisis feedback pelanggan ini:
"Produknya berfungsi dengan baik, tetapi pengiriman memakan waktu 3 minggu. Saya sudah melewatkan deadline proyek saya. Tolong respons segera."
""")
 
result = structured_llm.invoke([message])
print(result.sentiment)    # negative
print(result.main_issue)   # Pengiriman lambat
print(result.urgency)      # high

Bagaimana deskripsi membentuk keputusan LLM:

  • Deskripsi sentiment → LLM belajar nilai valid adalah 'positive', 'negative', 'neutral' → Masalah pengiriman menyebabkan deadline terlewat, jadi memilih 'negative'
  • Deskripsi main_issue → LLM diinstruksikan untuk "menemukan keluhan utama" → Mengidentifikasi "pengiriman lambat" sebagai masalahnya
  • Deskripsi urgency → LLM belajar urgency harus 'low', 'medium', atau 'high' → Melihat "Tolong respons segera" dan memilih 'high'

Apa yang terjadi tanpa deskripsi?

python
sentiment: str  # Tidak ada deskripsi

LLM mungkin mengembalikan "negative", "bad", "unsatisfied", "2/5", "disappointed" dalam format yang tidak dapat diprediksi, membuat sulit bagi kode kamu untuk menangani nilai-nilai tersebut.

Poin kunci: Deskripsi field adalah bagian dari kode kamu yang mengontrol perilaku LLM. Tulis dengan jelas dan spesifik.

Field Kategorikal: Menentukan Nilai yang Diizinkan

Dalam contoh di atas, field sentiment hanya dapat memiliki tiga nilai: 'positive', 'negative', atau 'neutral'. Field yang harus menjadi salah satu dari sekumpulan nilai tertentu disebut field kategorikal.

Untuk field kategorikal, daftarkan semua nilai yang mungkin dalam deskripsi:

python
sentiment: str = Field(
    description="Sentimen: tepat 'positive', 'negative', atau 'neutral' (huruf kecil)"
)

Dengan menentukan "tepat" dan "(huruf kecil)", kita menekankan bahwa LLM harus merespons dengan tepat salah satu dari tiga nilai ini.

Namun, tidak ada jaminan LLM akan selalu merespons dengan salah satu nilai yang ditentukan. Itulah mengapa kamu harus menulis kode defensif.

Kasus di mana LLM mengembalikan nilai yang tidak terduga:

python
result.sentiment = "Positive"    # Huruf kapital
result.sentiment = "NEGATIVE"    # Semua huruf kapital
result.sentiment = "good"        # Kata yang berbeda sama sekali

Menulis kode defensif:

python
allowed = {"positive", "negative", "neutral"}
 
# Konversi ke huruf kecil dan periksa
sentiment = result.sentiment.lower()
 
if sentiment not in allowed:
    sentiment = "neutral"  # Gunakan default untuk nilai yang tidak terduga
 
# Sekarang sentiment dijamin menjadi salah satu dari nilai yang diizinkan

Kesimpulan kunci:

  1. Tentukan nilai yang diizinkan dalam deskripsi → LLM lebih mungkin merespons dengan benar
  2. Validasi dalam kode → Tangani nilai yang tidak terduga dengan aman

Catatan: Bab 18 menunjukkan pola yang lebih kuat menggunakan enum Python untuk penegakan.

Perbandingan: Parsing Manual vs Output Terstruktur

Mari kita bandingkan tugas yang sama dengan dan tanpa output terstruktur untuk melihat perbedaannya:

Parsing Manual:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
llm = ChatOpenAI(model="gpt-4o-mini")
 
message = HumanMessage(content="""
Ekstrak nama produk, harga, dan ketersediaan dari:
"UltraWidget Pro berharga $299.99 dan saat ini tersedia."
Format: nama | harga | ketersediaan
""")
 
response = llm.invoke([message])
text = response.content
 
# Parsing manual
parts = text.split('|')
product_name = parts[0].strip()
price_str = parts[1].strip().replace('$', '')
price = float(price_str)
availability = parts[2].strip().lower()
in_stock = 'tersedia' in availability or 'available' in availability
 
print(f"Nama: {product_name}")
print(f"Harga: ${price}")
print(f"Tersedia: {in_stock}")

Output Terstruktur:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
 
class ProductInfo(BaseModel):
    product_name: str = Field(description="Nama lengkap produk")
    price: float = Field(description="Harga dalam USD")
    in_stock: bool = Field(description="Apakah produk tersedia")
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
message = HumanMessage(content="""
Ekstrak informasi produk dari:
"UltraWidget Pro berharga $299.99 dan saat ini tersedia."
""")
 
result = structured_llm.invoke([message])
 
print(f"Nama: {result.product_name}")
print(f"Harga: ${result.price}")
print(f"Tersedia: {result.in_stock}")

Perbedaan Kunci:

  1. Tidak Ada Logika Parsing: Versi terstruktur tidak memiliki kode parsing sama sekali
  2. Keamanan Tipe: result.price dijamin menjadi float
  3. Kode Lebih Sederhana: Tidak ada regex, tidak ada pemisahan string, tidak ada konversi tipe manual
  4. Validasi: Pydantic memastikan semua field yang diperlukan ada
  5. Maintainability: Mengubah skema lebih mudah daripada memperbarui logika parsing

7.3) Pertimbangan Desain Skema

Sekarang setelah kamu tahu cara menggunakan output terstruktur, mari kita pelajari cara mendesain skema yang baik. Bagian ini mencakup prinsip desain praktis untuk membedakan field wajib dari field opsional.

Field Wajib

Secara default, semua field dalam model Pydantic adalah wajib. Ini berarti LLM harus mengekstrak atau menyimpulkan nilai untuk setiap field wajib dari prompt pengguna dan menyediakannya dalam respons.

python
from pydantic import BaseModel
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool

Ketika kamu menggunakan skema ini, LLM akan mencoba menemukan nilai untuk ketiga field (product_name, price, in_stock) dalam teks input.

Tetapi apa yang terjadi ketika prompt tidak memiliki informasi untuk field wajib?

Kita mungkin mengharapkan perilaku berikut:

  1. LLM tidak dapat menemukan informasi dalam prompt
  2. LLM menghilangkan field tersebut dari responsnya
  3. LangChain tidak dapat membuat instance ProductInfo yang valid
  4. ValidationError dimunculkan

Namun, ini tidak selalu terjadi.

Alasannya adalah LLM yang berbeda mungkin menangani informasi yang hilang secara berbeda.

Beberapa LLM (seperti model OpenAI) cenderung menghasilkan nilai bahkan ketika informasi yang diperlukan tidak ada dalam prompt. Dalam kasus ini, ValidationError tidak terjadi, tetapi ini dapat menyebabkan masalah yang lebih besar karena aplikasi Python kamu mungkin memproses informasi yang dibuat-buat seolah-olah itu nyata.

Kita akan membahas cara menyelesaikan masalah ini di Bagian 7.4: Ketika Terjadi Kesalahan.

Untuk saat ini, ketahui saja bahwa tidak semua LLM menangani informasi yang hilang dengan cara yang sama.

Field Opsional

Kamu mungkin memerlukan field yang secara sah dapat ada atau tidak ada, bahkan dalam kasus normal. Misalnya, catatan pengiriman (delivery_note) mungkin atau mungkin tidak diberikan oleh pelanggan, bahkan untuk pesanan yang valid.

Kapan menggunakan Optional:

  • Data itu sendiri mungkin tidak ada (misalnya, ketika ulasan anonim diizinkan, ulasan anonim tidak memiliki nama pengulas)
  • Kamu ingin LLM secara eksplisit menunjukkan informasi yang hilang daripada membuat nilai

Untuk membuat field opsional, gunakan tipe Optional Python dari modul typing:

python
from typing import Optional
 
class ProductReview(BaseModel):
    rating: int
    review_text: str
    reviewer_name: Optional[str] = None  # Ulasan anonim tidak memiliki nama pengulas

Catatan: Pengguna Python 3.10+ dapat menggunakan str | None alih-alih Optional[str].

Ketika field adalah Optional:

  • LLM dapat menghilangkannya dari respons jika informasi tidak ditemukan dalam prompt
  • Field yang dihilangkan diatur ke nilai default (None)

Berikut adalah contoh lengkap:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
from typing import Optional
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool
    discount_percentage: Optional[float] = None
    warranty_years: Optional[int] = None
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
message = HumanMessage(content="""
Ekstrak info produk: "UltraWidget Pro berharga $299.99 dan tersedia."
""")
 
result = structured_llm.invoke([message])
print(result.product_name)  # UltraWidget Pro
print(result.price)  # 299.99
print(result.in_stock)  # True
print(result.discount_percentage)  # None (tidak disebutkan)
print(result.warranty_years)  # None (tidak disebutkan)

Checklist Desain Skema

Sebelum menyelesaikan skema kamu, tanyakan pada diri sendiri:

Pemilihan Field:

  • Apakah field wajib benar-benar esensial? (Apa yang terjadi jika field ini hilang dari prompt?)
  • Bisakah field opsional secara sah tidak ada bahkan dalam kasus normal?

Spesifikasi Field:

  • Apakah setiap field memiliki deskripsi yang jelas?
  • Apakah field kategorikal dibatasi secara eksplisit? (misalnya, "harus tepat 'A', 'B', atau 'C'")

7.4) Ketika Terjadi Kesalahan

Dua masalah dapat terjadi ketika menggunakan output terstruktur:

  1. LLM menghilangkan nilai field wajib → ValidationError terjadi
  2. LLM membuat informasi yang hilang → Tidak ada ValidationError, tetapi kode kamu memproses data yang salah

Bagian ini mencakup cara menangani masing-masing.

Memahami Validation Error

Ketika prompt pengguna tidak memiliki informasi untuk field wajib yang didefinisikan dalam skema, LLM tidak dapat menyediakan nilai untuk field tersebut. Program Python kemudian memunculkan ValidationError:

python
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, ValidationError
 
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool
 
llm = ChatAnthropic(model='claude-sonnet-4-5')
structured_llm = llm.with_structured_output(ProductInfo)
 
# Input tidak memiliki informasi yang diperlukan
message = HumanMessage(content="""
Ekstrak info produk dari: "Widget-nya bagus! Sangat direkomendasikan."
""")
 
try:
    result = structured_llm.invoke([message])
    print(result)
except ValidationError as e:
    print("ValidationError terjadi")

Catatan: Ketika prompt tidak memiliki informasi untuk field wajib, beberapa LLM mungkin membuat nilai dan menyediakannya dalam respons. Dalam kasus ini, ValidationError tidak akan terjadi, tetapi masalah yang lebih besar muncul. Kita akan membahas ini di bagian berikutnya.

Ketika prompt tidak memiliki informasi untuk field wajib dan ValidationError terjadi, ini sebenarnya membantu aplikasi Python kamu. Aplikasi dapat mendeteksi bahwa masalah terjadi dan menangani error dengan cara yang terkontrol. Strategi pemulihan error dibahas di Bab 14 (pemulihan error tingkat agen) dan Bab 17 (logika retry dengan manajemen state).

Masalah yang Lebih Besar: LLM Membuat Informasi yang Hilang

Seperti yang kita bahas di Bagian 7.3, beberapa LLM menunjukkan perilaku yang lebih berbahaya: mereka membuat nilai dan menyediakannya dalam respons ketika informasi hilang dari prompt.

Bagaimana masalah terjadi:

  1. Prompt tidak memiliki informasi yang diperlukan
  2. LLM menghasilkan nilai yang terlihat masuk akal
  3. ValidationError TIDAK terjadi
  4. Aplikasi Python kamu memproses data yang dibuat-buat seolah-olah itu nyata

Contoh:

python
class ProductInfo(BaseModel):
    product_name: str
    price: float
    in_stock: bool
 
message = HumanMessage(content="""
Ekstrak info produk dari: "Widget-nya bagus!"
""")
 
# Ketika beberapa LLM membuat nilai untuk product_name, price, dan in_stock
result = structured_llm.invoke([message])
# Tidak ada error yang dimunculkan!
print(result.product_name)  # "widget" (diekstrak dari teks)
print(result.price)  # 0.0 (dibuat-buat!)
print(result.in_stock)  # False (dibuat-buat!)
 
# Masalah: Kamu tidak bisa tahu nilai mana yang nyata vs dibuat-buat

Ini lebih buruk daripada ValidationError karena:

  • Aplikasi Python kamu terus mengeksekusi dengan data yang buruk
  • Kamu tidak tahu field mana yang nyata vs dibuat-buat
  • Logika downstream mungkin membuat keputusan yang salah berdasarkan data palsu

Solusi: Gunakan Field Opsional dengan Validasi

Solusinya adalah mendefinisikan semua field wajib sebagai Optional, kemudian gunakan validator untuk memeriksa bahwa semua field wajib memiliki nilai.

python
from typing import Optional
from pydantic import BaseModel, model_validator, ValidationError
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
 
class ProductInfo(BaseModel):
    # Ini sebenarnya wajib, tetapi dideklarasikan sebagai Optional
    # Validasi nyata terjadi di validator di bawah
    product_name: Optional[str] = None
    price: Optional[float] = None
    in_stock: Optional[bool] = None
    
    @model_validator(mode='after')
    def check_required_fields(self):
        """Validasi bahwa semua field esensial ada"""
        if self.product_name is None or self.price is None or self.in_stock is None:
            raise ValueError("Semua field (product_name, price, in_stock) harus disediakan")
        return self
 
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
 
# Tes dengan data tidak lengkap
message = HumanMessage(content="""
Ekstrak info produk dari: "Widget-nya bagus!"
""")
 
try:
    result = structured_llm.invoke([message])
    # Jika kita sampai di sini, semua field dijamin ada
    print(f"Produk: {result.product_name}")
    print(f"Harga: ${result.price}")
except ValidationError as e:
    # Field wajib hilang - ekstraksi gagal
    print(f"Ekstraksi tidak lengkap: {e}")

Apa yang terjadi di sini:

  • @model_validator adalah decorator Pydantic yang menambahkan logika validasi kustom
  • mode='after' berarti validasi berjalan setelah semua field telah di-parse
  • Jika ada field yang None, kita memunculkan ValueError untuk menandakan data tidak lengkap
  • Pydantic secara otomatis membungkus ValueError ini dalam ValidationError

Mengapa ini berhasil:

Ketika prompt tidak memiliki informasi untuk field:

  • LLM tidak membuat nilai dan menghilangkan field tersebut dari respons
  • Dalam kasus ini, field tersebut menjadi None
  • Jika field tersebut sebenarnya wajib, validator Pydantic memunculkan ValueError
  • Pydantic membungkusnya sebagai ValidationError

Dengan cara ini, aplikasi Python kamu menerima error eksplisit untuk ditangani, daripada data yang dibuat-buat.

Kesimpulan kunci: Ketika prompt tidak memiliki informasi untuk field wajib, mendapatkan ValidationError adalah normal dan diharapkan. Bahaya sebenarnya adalah data yang dibuat-buat. Gunakan field Optional dengan validator untuk mencegah LLM membuat informasi yang hilang, sambil secara eksplisit mendeteksi ketika field wajib hilang.


Ringkasan Bab:

Di bab ini, kamu belajar cara mengubah output LLM menjadi objek Python yang andal:

  • Mengapa penting: Parsing teks bebas rapuh; output berbasis skema menyediakan keamanan tipe
  • Cara menggunakannya: Definisikan skema dengan BaseModel Pydantic → Bind dengan .with_structured_output()
  • Prinsip desain: Pilih field wajib vs opsional, pandu LLM dengan deskripsi field
  • Tangani masalah: ValidationError adalah normal; bahaya sebenarnya adalah data yang dibuat-buat