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ü:
- Daha kısa system prompt yeterli (şema modeli yönlendirir)
- Daha az retry (çıktı zaten temiz)
- 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:
| Metrik | Eşik | Aksiyon |
|---|---|---|
| Schema conformance rate | < %99 | System prompt + schema gözden geçir |
| Average validation retry count | > 0.1 | Field description'ları netleştir |
| Field-level null rate | > %30 | Optional → required veya tersi gözden geçir |
| Latency P95 | > 5s | Smaller schema veya faster model |
| Cost per request | trending up | A/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.