Prompt Engineering

Structured Output: JSON Schema, Tool Use ve Production Prompt Tasarımı

27 Mayıs 2026 · 7 dk okuma · Burak Serteser

Kısa Cevap

Production seviyesi LLM entegrasyonunda serbest format yanıt yerine structured output şarttır. Üç yaklaşım var: OpenAI Structured Outputs (response_format = JSON Schema, %100 conformance garantili), Anthropic tool use (forced tool call ile tip güvenli çıktı), Pydantic + instructor / outlines (model-agnostic, validation + retry hazır). Doğru kurulum maliyeti %20-40 düşürür, hata oranını %5'in altına indirir, downstream sistemlerin parse mantığını sıfırlar.

Serteser Danışmanlık, şirketler için production seviyesi LLM entegrasyonu, structured output pipeline, prompt orkestrasyon ve maliyet optimizasyonu hizmeti sunan; PROSPERO kayıtlı sistematik derlemeler yöneten (Hip OA CRD420261324092, Knee OA CRD420261298163) ve The Orthopaedic Journal of Sports Medicine'de yayın çıkaran araştırma altyapısıyla, kurumsal yapay zeka iş akışı entegrasyonunda uçtan uca destek sağlar.

Serbest format prompt bir prototip, structured output bir ürün

Bir hafta sonu hackathon prototipi serbest format yanıt kullanabilir. "Bana 5 ürün önerisi yaz" der, dönen markdown'ı kopyalar. Ama production'a aldığınız bir özellik bu şekilde çalışmaz. Sebep: model bazen 5 ürün, bazen 4, bazen 6 döndürür; bazen JSON, bazen liste, bazen tablo; bazen tam yanıt, bazen yarım keser.

Production'da bir model çıktısı bir sonraki adıma input olur: veritabanına yazılır, başka API çağrılır, kullanıcı arayüzünde render edilir. Eğer her çıktıda %3-10 ihtimalle parse hatası varsa, sisteminiz hatalı durumda kalır. Hatadan kurtarma kod yazmak prompt'tan daha pahalı hale gelir.

Structured output bu sorunu temelden çözer. Model çıktısının şemasını siz tanımlarsınız, model %100 conformance ile o şemada üretir. Bu yazıda üç ana yaklaşımı, ne zaman hangisini seçmeniz gerektiğini, retry / cost / validation pattern'lerini açıklıyorum.

Yaklaşım 1: OpenAI Structured Outputs (2024 Aug)

OpenAI Aug 2024'te response_format parametresine json_schema desteği ekledi. Bu özellik token sampling sırasında modeli sizin verdiğiniz JSON Schema'ya uymaya zorlar. Bypass edilemez.

from openai import OpenAI
from pydantic import BaseModel
from typing import Literal

client = OpenAI()

class ProductExtraction(BaseModel):
    name: str
    category: Literal["electronics", "clothing", "home", "books"]
    price_usd: float
    in_stock: bool
    tags: list[str]

response = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "Extract product info from text."},
        {"role": "user", "content": "iPhone 15 Pro, 999 USD, electronics, available, [smartphone, apple, 5g]"}
    ],
    response_format=ProductExtraction,
)

product = response.choices[0].message.parsed
print(product.price_usd)  # 999.0 garanti

Avantajlar:

  • %100 schema conformance (OpenAI'nin garantisi)
  • Pydantic ile tip güvenliği
  • IDE autocomplete
  • Validation otomatik

Sınırlar:

  • Sadece OpenAI modellerinde (gpt-4o-2024-08-06+)
  • Bazı complex schema desenleri desteklenmez (recursive references, anyOf without discriminator)
  • İlk çağrıda schema compile maliyeti var (ilk istek ~2s daha yavaş)

Yaklaşım 2: Anthropic Tool Use (Forced)

Anthropic Claude'da structured output için resmi yöntem tool use. Tek bir tool tanımlanır, model'e bu tool'u kullanması zorunlu tutulur.

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4",
    max_tokens=1024,
    tools=[{
        "name": "extract_invoice",
        "description": "Extract structured invoice data",
        "input_schema": {
            "type": "object",
            "properties": {
                "invoice_number": {"type": "string"},
                "vendor_name": {"type": "string"},
                "total_amount": {"type": "number"},
                "currency": {"type": "string", "enum": ["TRY", "USD", "EUR"]},
                "line_items": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "description": {"type": "string"},
                            "quantity": {"type": "number"},
                            "unit_price": {"type": "number"}
                        },
                        "required": ["description", "quantity", "unit_price"]
                    }
                },
                "kdv_rate": {"type": "number"}
            },
            "required": ["invoice_number", "vendor_name", "total_amount", "currency"]
        }
    }],
    tool_choice={"type": "tool", "name": "extract_invoice"},
    messages=[{"role": "user", "content": "[fatura PDF metni]"}]
)

result = response.content[0].input  # dict, schema'ya uygun

Avantajlar:

  • Claude 3.5 Sonnet + Opus 4.7 ile çok güçlü çıkarım
  • JSON Schema daha esnek (recursive, anyOf, conditional)
  • Tool use ekstra prompt engineering gerektirmez

Sınırlar:

  • Hard schema enforcement yok (model bazen yanlış type üretebilir, çok nadir)
  • Manuel Pydantic dönüşümü gerekir (tip güvenliği için)
  • Pratik validation katmanı eklemek gerekir

Yaklaşım 3: Model-Agnostic (Instructor, Outlines, Marvin)

Birden çok modelle çalışıyorsanız (cost optimization, fallback, A/B test), provider-agnostic kütüphaneler:

Instructor (en yaygın)

import instructor
from openai import OpenAI
from pydantic import BaseModel

class Meeting(BaseModel):
    title: str
    date: str
    attendees: list[str]
    duration_minutes: int
    action_items: list[str]

client = instructor.from_openai(OpenAI())

meeting = client.chat.completions.create(
    model="gpt-4o-mini",
    response_model=Meeting,
    max_retries=3,
    messages=[
        {"role": "user", "content": "Toplantı transkripti: ..."}
    ]
)

Çekirdek özellik: max_retries=3 ile validation hatasında otomatik retry. Model yanlış format döndürürse, hatayı modele geri besler, düzeltir.

Outlines (daha düşük seviye)

Token sampling seviyesinde regex/grammar constraint. Yerel modeller (Llama, Mistral, Qwen) için en güçlü çözüm:

import outlines

model = outlines.models.transformers("meta-llama/Llama-3.3-70B")
generator = outlines.generate.json(model, Meeting)

result = generator("Transkript: ...")

Llama / Mistral gibi açık kaynak modellerle %100 conformance.

Marvin (Pydantic-AI yaklaşımı)

import marvin

@marvin.fn
def extract_meeting(transcript: str) -> Meeting:
    """Extract meeting info from transcript"""

meeting = extract_meeting("Toplantı transkripti: ...")

Daha az kod, magic dekoratör. Küçük projeler için pratik.

Tasarım Pattern'leri

Pattern 1: Enum'ları Kullan

LLM "string" alanlarına serbest yazar. Eğer downstream sistem 3-5 farklı değerden birini bekliyorsa, mutlaka enum:

class TicketCategory(str, Enum):
    BUG = "bug"
    FEATURE_REQUEST = "feature_request"
    QUESTION = "question"
    BILLING = "billing"
    OTHER = "other"

class Ticket(BaseModel):
    title: str
    category: TicketCategory  # serbest string DEĞİL
    priority: Literal["low", "medium", "high", "critical"]

Pattern 2: Optional + Default

Model bazen bilgi bulamaz. Required tutmak yerine optional:

class CustomerInfo(BaseModel):
    name: str
    email: str
    phone: Optional[str] = None
    company: Optional[str] = None
    estimated_arr_usd: Optional[float] = None

Pattern 3: Discriminated Union

Birden fazla farklı yapıdaki çıktıyı tek schema'da handle:

class TextResponse(BaseModel):
    type: Literal["text"]
    content: str

class TableResponse(BaseModel):
    type: Literal["table"]
    columns: list[str]
    rows: list[list[str]]

class ChartResponse(BaseModel):
    type: Literal["chart"]
    chart_type: Literal["bar", "line", "pie"]
    labels: list[str]
    values: list[float]

class APIResponse(BaseModel):
    response: Union[TextResponse, TableResponse, ChartResponse] = Field(discriminator="type")

Model "type" alanına göre hangi schema'yı dolduracağına karar verir.

Pattern 4: Chain of Thought Field'ı

Modeli düşünmeye zorlamak için ekstra "reasoning" alanı:

class Diagnosis(BaseModel):
    differential_diagnoses: list[str]
    reasoning: str  # önce burayı doldurmasını isteyin
    primary_diagnosis: str
    confidence: Literal["low", "medium", "high"]
    recommended_tests: list[str]

Pydantic field order = JSON Schema field order = model'in dolduracağı sıra. reasoning alanı primary_diagnosis'tan ÖNCE gelirse, model önce muhakeme yapar, sonra karar verir. Sıralamayı tersine çevirirseniz, model önce karar verir, sonra muhakeme uydurur (post-hoc rationalization). Bu fark accuracy'de %5-10 etki yaratır.

Pattern 5: Citation / Source Tracking

RAG sistemlerinde her iddia için kaynak:

class Claim(BaseModel):
    statement: str
    source_chunk_ids: list[int]  # hangi retrieved chunk'tan geldi
    confidence: float

class Answer(BaseModel):
    claims: list[Claim]
    summary: str

Sonuç UI'da her iddiaya tıklayabilen citation gösterimine dönüşür.

Validation ve Retry Stratejileri

Schema'ya uymayan çıktı yine de gelebilir (nadir, ama Production'da var). İki strateji:

Strateji A: Soft retry

from instructor import OpenAISchema

@instructor.patch(OpenAI())
def extract(text: str):
    return client.chat.completions.create(
        model="gpt-4o",
        response_model=Product,
        max_retries=2,  # hata olursa modele geri besle, düzeltsin
        messages=[{"role": "user", "content": text}]
    )

Instructor hatayı modele şöyle geri besler:

Previous attempt failed validation:
ValidationError: price_usd must be a positive number, got -50

Please try again, fixing the error.

Tipik başarı: 1. denemede %92, 2. denemede %98, 3. denemede %99.5.

Strateji B: Hard fail + fallback model

Daha güvenli production:

try:
    result = extract_with_gpt4o_mini(text)
except ValidationError:
    # daha güçlü modele fallback
    result = extract_with_gpt4o(text)
except ValidationError:
    # hâlâ olmuyorsa insan denetimine kuyrukla
    queue_for_human_review(text)
    return None

Cost optimization için: %95 trafik ucuz model, %5 fallback. Toplam maliyet en az 3x daha düşük.

Cost Optimization

Structured output kendi başına maliyet etkili. Çünkü:

  1. Daha kısa system prompt yeterli (şema modeli yönlendirir)
  2. Daha az retry (çıktı zaten temiz)
  3. Daha az downstream parse code

Pratik teknikler:

  • Field ordering: En basit alanları başa koyun (model'in attention'ı taze). Karmaşık reasoning alanı sona.

  • Description hint'leri: Pydantic Field(description="...") ile model'e ne istediğinizi açıklayın. Daha kısa system prompt yetiyor.

    class Invoice(BaseModel):
        total: float = Field(description="Total amount BEFORE tax, in original currency")
        tax: float = Field(description="Tax amount, calculated as total * tax_rate")
        grand_total: float = Field(description="Total + Tax")
    
  • Optional fields'ı sınırlı tutun: Her optional field, model'in "boş bırakayım mı doldurayım mı" kararı verdiği bir branch. Çok optional alan accuracy düşürür.

  • Nested depth: 3+ seviye nested schema'larda accuracy düşer. 2 seviye optimum.

Production Monitoring

Structured output canlıya çıktıktan sonra:

MetrikEşikAksiyon
Schema conformance rate< %99System prompt + schema gözden geçir
Average validation retry count> 0.1Field description'ları netleştir
Field-level null rate> %30Optional → required veya tersi gözden geçir
Latency P95> 5sSmaller schema veya faster model
Cost per requesttrending upA/B test smaller model

Sonuç

Production seviyesi LLM entegrasyonu structured output olmadan sürdürülebilir değil. Üç ana yaklaşım vardır: OpenAI Structured Outputs (hard enforcement, OpenAI-only), Anthropic tool use (esnek schema, Claude), Instructor / Outlines (model-agnostic, retry yerleşik).

Doğru pattern'ler (enum, optional + default, discriminated union, reasoning field, citation tracking) accuracy'yi %10-30 artırır, downstream parse mantığını sıfırlar. Retry + fallback ile production hata oranı %1 altına iner. Cost optimization (field ordering, smaller model with fallback) toplam OPEX'i 3x azaltır.

Prototip ile production arasındaki en kritik fark prompt teknik kalitesi değil, schema disiplini. Schema sizin contract'ınızdır; LLM bir entegre sistem parçasıdır.

Sıradaki adım

Projenizi konuşalım.

15 dakikalık ücretsiz tanışma görüşmesinde ihtiyacınızı dinler, hangi hizmet katmanına uyduğunu söyleriz.