← Kembali ke Blog

Dapatkan JSON Bersih dari LLM: Structured Outputs pakai Pydantic (OpenAI + Ollama)

Kamu minta LLM balikin JSON. Yang balik: JSON valid yang dibungkus markdown fence, satu kalimat pembuka, dan kadang key aneh yang nggak pernah kamu minta. Parse gagal, kamu nulis regex, regex-nya pecah pas nemu edge case baru, dan di dalam loop agent, kode yang baca data["result"] mati gara-gara nilainya string None. Semua orang yang bangun agent pernah nyangkut di situ.

Solusinya bukan cuma bikin prompt yang lebih tegas. Ini structured outputs: kasih JSON schema ke model, biar proses generate token-nya dikunci supaya cocok sama schema. Artikel ini nunjukin versi praktisnya, pakai Pydantic buat dua backend, OpenAI API yang hosted dan server Ollama lokal. Satu definisi schema, dua backend, nol parsing manual.

Masalahnya kalau cuma minta JSON

Waktu kamu cuma nulis "return JSON" di prompt, model paling banter pakai JSON mode: JSON yang valid tapi tanpa jaminan schema. Dia bisa ganti nama key, ilangin required field, atau balikin object padahal kamu ngarepin array. JSON mode cuma janji output-nya bisa di-parse. Structured outputs janji cocok sama schema kamu, key per key, type per type.

Structured Outputs (OpenAI) adalah evolusi dari JSON mode. Dia compile JSON Schema kamu ke decoder, dan nutup token mana pun yang bakal ngasilin output yang nggak sesuai. Ollama ngelakuin hal yang sama lewat constrained decoding waktu kamu ngasih schema ke parameter format. Model-nya secara fisik nggak bisa ngeluarin markdown fence atau kalimat nyasar, karena token itu bakal ngerusak grammar-nya.

Prerequisites

  • Python 3.10+ di dalam virtual environment
  • pip install openai pydantic buat API hosted
  • pip install ollama pydantic buat server lokal
  • Docker atau instalasi Ollama yang jalan kalau mau coba lokal (lihat referensi)

Langkah 1: definisiin schema sekali pakai Pydantic

Trik yang bikin dua backend kelakuannya sama: definisiin kontrak kamu sebagai Pydantic model. model_json_schema() ngubah dia jadi JSON Schema yang diterima provider, dan model_validate_json() nge-validasi apa pun yang balik.

from typing import Literal
from pydantic import BaseModel, Field

class CalendarEvent(BaseModel):
    name: str = Field(description="Event name, e.g. 'Science fair'")
    date: str = Field(description="ISO 8601 date")
    participants: list[str] = Field(description="List of people going")
    kind: Literal["meeting", "event", "task"] = "event"

Deskripsi di situ bukan pajangan. Dia ikut kebawa di dalam schema yang dibaca model, jadi dia bertindak kayak instruksi. Literal nyempitin nilai cuma ke opsi yang diizinkan. Model Pydantic yang sama kamu pakai buat validasi, jadi cuma ada satu sumber kebenaran.

Langkah 2: structured output dari OpenAI API

Python SDK OpenAI punya helper parse. Tinggal kasih class Pydantic langsung ke response_format, terus baca object hasil parse dari message.parsed.

from openai import OpenAI

client = OpenAI()
completion = client.chat.completions.parse(
    model="gpt-5.6",
    messages=[
        {"role": "system", "content": "Extract the event information."},
        {"role": "user",
         "content": "Alice and Bob are going to a science fair on Friday."},
    ],
    response_format=CalendarEvent,
)
event = completion.choices[0].message.parsed
print(event.name, event.participants)  # real Python attributes

Nggak ada json.loads, nggak ada try/except di sekitar string yang rapuh. event udah jadi CalendarEvent. Kalau provider nggak sanggup ngikutin schema, dia angkat error yang bisa kamu tangkap, bukan diam-diam ngasih dict setengah rusak.

Model yang sama jalan lewat Responses API pakai client.responses.parse(...) dan text_format=CalendarEvent. Secara fungsi sama aja buat kasus kayak gini.

Di level REST mentah, ini ke response_format: {type: "json_schema", json_schema: {name: "...", schema: {...}, strict: true}}. Kamu hampir nggak pernah nulis itu manual; helper SDK-nya yang ngerjain. Ini yang perlu diinget pas orang ngasih contoh curl yang keliatan gede.

Langkah 3: schema yang sama di server Ollama lokal

Jalanin model di mesin sendiri nggak ngubah schema sama sekali. Parameter format di Ollama nerima JSON schema langsung.

from ollama import chat

response = chat(
    model="llama3.1:8b",
    messages=[{"role": "user", "content": "Alice and Bob attend a science fair on Friday."}],
    format=CalendarEvent.model_json_schema(),
    options={"temperature": 0},
)
event = CalendarEvent.model_validate_json(response.message.content)
print(event)

Dua beda yang perlu dicatat. Pertama, kamu ngasih format= pakai schema hasil generate, terus validasi sendiri teks yang balik pakai model_validate_json. Kedua, set temperature ke 0. Docs resmi Ollama nyaranin gitu: suhu rendah bikin model nempel ke schema daripada melenceng. Kalau kamu cuma butuh object valid apa pun, format="json" jadi opsi JSON mode yang lebih ringan, tapi kamu kehilangan kontrak field-nya.

Langkah 4: tugas ekstraksi yang beneran

Structured output paling kerasa gunanya pas kamu ekstrak daftar record dari teks berantakan. Ide yang sama, satu level nesting.

class Pet(BaseModel):
    name: str
    animal: str
    age: int
    color: str | None = None

class PetList(BaseModel):
    pets: list[Pet]

text = "Luna is a 5 year old grey cat who loves yarn. Loki, a 2 year old black cat, only chases tennis balls."
resp = chat(
    model="llama3.1:8b",
    messages=[{"role": "user", "content": text}],
    format=PetList.model_json_schema(),
    options={"temperature": 0},
)
data = PetList.model_validate_json(resp.message.content)
print(data.pets[0].name, data.pets[0].age)

Karena pets di-typed sebagai list[Pet], field yang ilang bakal bikin ValidationError muncul, bukan diam-diam ngasih key bonus. Di dalam agent, itu failure mode yang bener: kamu tangkap, log, ulang pakai prompt yang lebih sempit, daripada nerusin data yang rusak ke downstream.

Kapan pakai structured outputs vs alternatif lain

  • Structured outputs (schema) dan function/tool calling dua-duanya ngasih hasil ber-typed. Pakai function calling pas output-nya emang buat nemicu aksi di sistem kamu. Pakai response_format pas model-nya harus ngasilin data buat user, kayak payload UI atau baris hasil ekstraksi.
  • JSON mode (format="json" atau json_object) tetep ngebantu, tapi cuma jamin JSON bisa di-parse. Anggap ini upgrade murah dari teks mentah, bukan pengganti kontrak schema kalau type-nya penting.
  • JSON cuma lewat prompt cuma cocok buat kasus sekali jalan yang low stakes, di mana balasan malformed nggak fatal. Ini sumber lahirnya regex kamu. Buang begitu output-nya mulai dimakan kode.

Model kecil di hardware terbatas lebih oke dengan schema yang dangkal daripada yang nesting dalam. Jaga kontrak ekstraksi sekitar dua level, terus pindah ke model yang lebih gede kalau list yang nesting mulai balik kosong.

Langkah selanjutnya

  • Baca Structured Outputs guide resmi buat daftar lengkap schema yang didukung dan interaksi sama function calling.
  • Cek docs Ollama structured outputs buat versi JavaScript (Zod) yang setara.
  • Satuangin pola yang sama ke agent LangChain pakai with_structured_output() biar object hasil validasi langsung ngalir ke node berikutnya.

Payout-nya kecil di kode, gede di keandalan. Kamu hapus parser, hapus regex, dan object ber-typed tinggal muncul. Itu kerjaannya.

Referensi

Butuh Bantuan Implementasi?

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

Konsultasi Gratis