← Kembali ke Blog

Ambil JSON Reliabel dari LLM: Structured Output dan Function Calling

Kamu minta LLM balikin JSON. Sebagian besar request baliknya rapi. Lalu suatu hari balasannya dibungkus markdown fence, field yang satu ganti nama, total malah dikirim sebagai string padahal harusnya number, dan pipeline kamu mati jam dua pagi. Prompting bukan kontrak.

Penyedia LLM sekarang nge-solve separuh masalah "JSON valid" lewat structured output: grammar-constrained decoding yang maksa response cocok dengan schema yang kamu kasih. Kamu nggak perlu parse pake regex sambil berharap lagi. Panduan ini ngebahas cara pakai structured output di OpenAI dan Anthropic, beda tiap penyedia di mana, dan kenapa kamu tetap butuh lapisan validasi di atasnya.

Prerequisites

  • Python 3.10+ dan pip
  • Akun OpenAI dan/atau Anthropic plus API key
  • pip install openai anthropic pydantic

Masalah yang nggak bisa diberesin prompt

Cara klasik: tulis "Return JSON" di system prompt. Sebagian besar waktu berhasil, dan "sebagian besar" itu masalahnya. Model kadang balikin JSON valid tapi nama field salah, ngebungkus response pake code fence, atau nambahin key ekstra yang nggak ada di schema. Seberapa keras pun kamu ngatur kata-kata di prompt, struktur nggak dijamin. Structured output ngubah jaminan itu jadi fitur platform yang dipaksa saat sampling.

Langkah 1: Structured output OpenAI dengan Pydantic

Definisiin bentuknya sekali di kode, kasih ke API, balikannya langsung objek yang ber-tipe.

from openai import OpenAI
from pydantic import BaseModel, Field

client = OpenAI()

class Invoice(BaseModel):
    invoice_number: str
    date: str
    currency: str = Field(description="ISO 4217 code, e.g. USD")
    total_amount: float

def extract_invoice(text: str) -> Invoice:
    response = client.responses.parse(
        model="gpt-5.6",
        input=[
            {"role": "system", "content": "Extract the invoice fields from the text."},
            {"role": "user", "content": text},
        ],
        text_format=Invoice,
    )
    return response.output_parsed

output_parsed udah berupa objek Invoice, jadi nggak ada json.loads atau cek field manual, dan safety refusal ikut ke-flag daripada jadi sampah diam-diam.

Ada dua aturan yang ngebentuk tiap strict schema di OpenAI. Tiap object wajib set additionalProperties: false, dan tiap field wajib ada di required. Artinya nggak ada field yang bener-bener opsional. Kalau nilainya bisa nggak ada, jadikan nullable, contoh str | None daripada string kosong.

Langkah 2: Anthropic, dua cara

Messages API Anthropic punya mode JSON output native di model yang lebih baru: tambah output_config.format dengan raw JSON Schema, dan response dijamin cocok. Buat function calling, yang didukung semua model yang tersedia, jaminan yang sama bisa lewat strict tool use: kamu deklarasiin tool yang input_schema-nya bentuk JSON kamu, lalu pin tool_choice buat maksa tool itu kepanggil.

from anthropic import Anthropic

client = Anthropic()

schema = {
    "type": "object",
    "properties": {
        "invoice_number": {"type": "string"},
        "date": {"type": "string"},
        "currency": {"type": "string"},
        "total_amount": {"type": "number"},
    },
    "required": ["invoice_number", "date", "currency", "total_amount"],
}

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=[
        {
            "name": "record_invoice",
            "description": "Extract structured invoice fields",
            "input_schema": schema,
            "strict": True,
        }
    ],
    tool_choice={"type": "tool", "name": "record_invoice"},
    messages=[{"role": "user", "content": text}],
)

tool_use = next(b for b in response.content if b.type == "tool_use")
invoice = tool_use.input

Dengan tool_choice yang dipaksa ke satu tool, model wajib manggil tool itu, dan input berupa objek yang udah di-parse sesuai schema kamu. Buat ekstraksi sekali jalan, tool_use.id bisa kamu abaikan; kamu cuma echo balik ke blok tool_result kalau percakapannya mau dilanjutin.

Langkah 3: Sandwich validasi

Structured output ngejamin JSON-nya parse dan cocok sama schema. Tapi nggak ngejamin datanya bener. Total bisa parse sebagai number tapi tetap nggak nyambung sama line items, dan customer ID-nya bisa nunjuk ke orang yang nggak ada.

Makanya kamu numpuk cek. Schema nentuin bentuk, business rule ngerafin, dan aplikasi kamu verifikasi ke kondisi nyata. Dengan Pydantic:

from pydantic import BaseModel, field_validator

class Order(BaseModel):
    product_id: str
    quantity: int
    unit_price: float
    total: float

    @field_validator("total")
    @classmethod
    def total_matches(cls, v, info):
        qty = info.data["quantity"]
        price = info.data["unit_price"]
        if abs(v - qty * price) > 0.01:
            raise ValueError("total must equal quantity * unit_price")
        return v

Jangan percaya satu lapis doang. Model ngejamin bentuk, validator kamu nolak angka yang mustahil, dan kode kamu cek kalau produknya beneran ada dan stok cukup.

Tips biaya dan schema

  • Bikin schema seminimal mungkin. Tiap field yang harus dipikirin model itu token dan ruang buat salah. Ekstrak cuma yang kamu pake.
  • Pilih enum daripada string bebas. {"type": "string", "enum": ["low", "medium", "high"]} ngeblok "HIGH" dan "high priority" sebelum nyampe ke database.
  • Pake model termurah yang masih parse dengan andal. Ekstraksi simpel jarang butuh model frontier.
  • Cache schema kamu. Prompt caching berlaku ke schema di system prompt untuk request berulang, dan itu porsi terbesar tagihan di pipeline yang sibuk.

Langkah berikutnya

Rangkai ekstraksi ke dalam agent loop yang di situ satu tool call juga jadi satu langkah workflow, dan validasi tiap tool output dengan cara yang sama. Lalu gabungin structured output sama prompt caching biar bentuk yang dijamin itu juga berhenti bikin biaya.

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis