โ† Semua pembelajaran / Python untuk AI Engineer
Fase 4 ยท Bicara dengan LLM

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 โ€” refusal dan max_tokens bisa 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

  1. SDK memanggil Ekstraksi.model_json_schema()
  2. Skema dikirim sebagai output_config.format
  3. API membatasi keluaran model agar valid terhadap skema itu
  4. 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.formatstrict: True
Yang dibatasiTeks balasan ke penggunaArgumen pemanggilan tool
UntukEkstraksi, klasifikasi, output pipelineAgent yang argumen tool-nya harus tepat
Syarat skemaadditionalProperties: false + requiredSama

Keduanya bisa dipakai bersamaan dalam satu request.

Batasan JSON Schema

DidukungTidak didukung
object, array, string, integer, number, boolean, nullSkema rekursif
enum, const, anyOf, allOf, $ref/$defsminimum, maximum, multipleOf
Format string: date-time, date, email, uri, uuid, ipv4minLength, 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
HalDampak
Skema baru pertama kaliAda biaya kompilasi sekali; hasilnya di-cache 24 jam
stop_reason: refusalOutput bisa tidak sesuai skema
stop_reason: max_tokensJSON terpotong di tengah
Tidak kompatibel denganCitations (error 400), assistant prefill
Kompatibel denganBatches 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:

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.