Structured Outputs
Structured output menjamin balasan model sesuai bentuk yang kamu tentukan, sehingga LLM layak dipakai di dalam pipeline.
Intisari
client.messages.parse()+ model Pydantic = objek Python tervalidasi, bukan string yang harus di-parse.- Ini bukan prompt engineering โ formatnya dipaksa di sisi API, bukan sekadar diminta.
- Dua fitur berbeda: format respons (
output_config.format) dan tool ketat (strict: True). - Ada batasan JSON Schema: tidak ada skema rekursif, dan sebagian batasan numerik/string tidak didukung.
- Tetap cek
stop_reasonโrefusaldanmax_tokensbisa menghasilkan output yang tidak sesuai skema.
Masalah yang diselesaikan
# Cara lama โ rapuh
r = client.messages.create(
messages=[{"role": "user", "content": "Ekstrak nama dan email. Balas JSON saja."}],
...
)
teks = r.content[0].text
data = json.loads(teks) # gagal kalau model menambahkan "Tentu, ini hasilnya:"
Kamu kemudian menambal dengan regex, retry, dan stop sequence. Semua itu tidak diperlukan lagi.
from pydantic import BaseModel
class Ekstraksi(BaseModel):
nama: str
email: str
minat: list[str]
minta_demo: bool
response = client.messages.parse(
model="claude-opus-5",
max_tokens=4096,
messages=[{
"role": "user",
"content": "Ekstrak: Budi ([email protected]), tertarik API dan SDK, ingin demo.",
}],
output_format=Ekstraksi,
)
data = response.parsed_output # instance Ekstraksi, SUDAH tervalidasi
print(data.nama, data.minat, data.minta_demo)
Perbedaan pentingnya: ini bukan permintaan, ini batasan. API benar-benar membatasi token yang boleh dihasilkan model agar hasilnya valid terhadap skema. Bukan "tolong balas JSON" yang kadang dipatuhi kadang tidak.
Yang terjadi di balik layar
- SDK memanggil
Ekstraksi.model_json_schema() - Skema dikirim sebagai
output_config.format - API membatasi keluaran model agar valid terhadap skema itu
- SDK mem-parse balasan dan memvalidasinya kembali dengan model Pydantic yang sama
Satu class Python, jaminan bentuk dari ujung ke ujung.
Bentuk skema mentah
response = client.messages.create(
model="claude-opus-5",
max_tokens=4096,
messages=[{"role": "user", "content": "Ekstrak info kontak..."}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"nama": {"type": "string"},
"email": {"type": "string"},
},
"required": ["nama", "email"],
"additionalProperties": False,
},
}
},
)
import json
teks = next(b.text for b in response.content if b.type == "text")
data = json.loads(teks) # dijamin valid
Tool ketat (strict)
tools=[{
"name": "pesan_tiket",
"description": "Pesan tiket perjalanan",
"strict": True, # โ field top-level, BUKAN di tool_choice
"input_schema": {
"type": "object",
"properties": {
"tujuan": {"type": "string"},
"tanggal": {"type": "string", "format": "date"},
"penumpang": {"type": "integer", "enum": [1, 2, 3, 4]},
},
"required": ["tujuan", "tanggal", "penumpang"],
"additionalProperties": False,
},
}]
output_config.format | strict: True | |
|---|---|---|
| Yang dibatasi | Teks balasan ke pengguna | Argumen pemanggilan tool |
| Untuk | Ekstraksi, klasifikasi, output pipeline | Agent yang argumen tool-nya harus tepat |
| Syarat skema | additionalProperties: false + required | Sama |
Keduanya bisa dipakai bersamaan dalam satu request.
Batasan JSON Schema
| Didukung | Tidak didukung |
|---|---|
object, array, string, integer, number, boolean, null | Skema rekursif |
enum, const, anyOf, allOf, $ref/$defs | minimum, maximum, multipleOf |
Format string: date-time, date, email, uri, uuid, ipv4 | minLength, maxLength |
additionalProperties: false (wajib) | Batasan array yang kompleks |
SDK Python dan TypeScript menangani ini otomatis: batasan yang tidak didukung dihapus
dari skema yang dikirim ke API, lalu divalidasi di sisi klien setelah respons diterima.
Jadi Field(ge=1, le=100) tetap ditegakkan โ hanya saja penegakannya terjadi
setelah model selesai, bukan selama ia menulis.
Yang perlu diwaspadai
if response.stop_reason == "refusal":
... # output mungkin TIDAK sesuai skema
elif response.stop_reason == "max_tokens":
... # JSON terpotong โ naikkan max_tokens
else:
data = response.parsed_output
| Hal | Dampak |
|---|---|
| Skema baru pertama kali | Ada biaya kompilasi sekali; hasilnya di-cache 24 jam |
stop_reason: refusal | Output bisa tidak sesuai skema |
stop_reason: max_tokens | JSON terpotong di tengah |
| Tidak kompatibel dengan | Citations (error 400), assistant prefill |
| Kompatibel dengan | Batches API, streaming, token counting, thinking |
Contoh yang berguna
Klasifikasi
from typing import Literal
class Klasifikasi(BaseModel):
kategori: Literal["bug", "fitur", "pertanyaan", "spam"]
keyakinan: float
alasan: str
r = client.messages.parse(
model="claude-opus-5", max_tokens=1024,
output_config={"effort": "low"}, # klasifikasi tidak perlu effort tinggi
messages=[{"role": "user", "content": f"Klasifikasikan tiket ini:\n{teks}"}],
output_format=Klasifikasi,
)
Ekstraksi banyak entitas
class Orang(BaseModel):
nama: str
jabatan: str | None
perusahaan: str | None
class Hasil(BaseModel):
orang: list[Orang]
ringkasan: str
Jawaban RAG dengan sitasi (Fase 7)
class Sitasi(BaseModel):
chunk_id: str
kutipan: str
class JawabanRAG(BaseModel):
jawaban: str
sitasi: list[Sitasi]
cukup_konteks: bool
Field cukup_konteks itu penting. Ia memberi model jalan keluar yang
terstruktur untuk mengatakan "dokumen yang diberikan tidak menjawab pertanyaan ini" โ
alih-alih mengarang jawaban. Kodemu bisa memeriksa field itu dan menampilkan pesan yang jujur.
Kapan tetap butuh prompting
Structured output menjamin bentuk, bukan kualitas isi. Model tetap bisa mengisi field dengan nilai yang salah. Yang tetap kamu perlukan:
- Deskripsi field yang jelas (lewat
Field(description=...)) - System prompt yang menjelaskan tugasnya
- Validasi bisnis di sisi Python setelah hasil diterima
- Eval untuk mengukur akurasi isinya (Fase 7)
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.