Yapılandırılmış Çıktı: tool_use, JSON Schema, Validation, Batches
tool_use + JSON Schema'nın en güvenilir yapısal çıktı yöntemi olması, syntax vs semantic hata ayrımı, Pydantic validation, retry-with-error-feedback ve Message Batches API stratejisi.
Öğreneceklerin
- tool_use'un neden en güvenilir yapısal çıktı yöntemi olduğunu (syntax hatasını tamamen ortadan kaldırdığını) açıklamak
- JSON Schema: required vs optional, nullable fields, enum'larda 'other'+detail pattern'ını bilmek
- Semantic validation için Pydantic ve retry-with-error-feedback döngüsünü uygulamak
- Message Batches API'nin %50 ucuzluk + 24 saat pencere + multi-turn YOK kısıtlarını anlatmak
- custom_id ile request/response korelasyonu pratiğini kavramak
- Batch vs sync karar matrisini (blocking vs non-blocking) bilmek
- detected_pattern / calculated_total vs stated_total self-correction stratejisini göstermek
Yapılandırılmış Çıktı: tool_use, JSON Schema, Validation, Batches
Bu ders Domain 4’ün ikinci yarısı. İlk yarıda “iyi prompt nasıl yazılır” öğrendik; burada “iyi prompt’tan güvenilir makine-okunabilir çıktı nasıl alınır” öğreniyoruz. Sınavın Structured Data Extraction senaryosu ve CI/CD pipeline’ları buradan soru çıkarıyor.
Neden bu konu sınavda çıkıyor
Production’da Claude çıktısı iki yere akar: ya insan okur (markdown yeter), ya da aşağı akış kodu (veritabanına yaz, başka API’ye yolla, dashboard’a bas). İkinci yol için “metinden JSON parse et” antipattern’i sınavda sıkça soruluyor. Doğru cevap daima tool_use + JSON Schema + Pydantic üçlüsü.
Temel kavramlar
1. tool_use request/response anatomisi
tool_use blokları, modelin şemaya uygun JSON üretmesi için ayrılmış kanaldır. Normal text üretiminden farkı: parse etmeye gerek yok, API zaten yapılandırılmış obje döndürür.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[{
"name": "extract_invoice",
"description": "Extract invoice fields from OCR text",
"input_schema": {
"type": "object",
"properties": {
"vendor": {"type": "string"},
"total": {"type": "number"},
"currency": {"type": "string", "enum": ["TRY", "USD", "EUR", "GBP"]},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"}
},
"required": ["description", "quantity", "unit_price"]
}
}
},
"required": ["vendor", "total", "currency", "line_items"]
}
}],
messages=[{
"role": "user",
"content": "Extract: 'Acme Corp fatura #1234, toplam 1500 USD, "
"2 kalem: Danışmanlık 10h @ 100, Yazılım 1 lisans @ 500'"
}]
)
# Response'ta text yok; sadece tool_use bloğu var
tool_block = response.content[0]
assert tool_block.type == "tool_use"
invoice = tool_block.input # Bu zaten parse edilmiş dict!
Burada stop_reason == "tool_use" modelin bir tool çağrısı talep ettiğini gösterir — yapısal çıktıyı bu blokta alırsın; ama bu bir “iş bitti” sinyali değildir, agentic loop’ta güvenilir tek completion sinyali stop_reason == "end_turn"’dur. Tek-atımlık extraction’da tool_use bloğu geldiği anda input’u alıp döngüyü sen sonlandırırsın. tool_use geldiğinde metin parse etmeye gerek yoktur ve syntax hatası imkânsızdır — bu yüzden tool_use en güvenilir yapısal çıktı yöntemidir.
tool_choice ile yapısal çıktıyı zorla
tool_choice parametresi modele tool çağırma kararını nasıl vereceğini söyler. Sınav için üç değeri bilmek yeterli:
| Değer | Davranış | Ne zaman kullan |
|---|---|---|
"auto" (default) | Model karar verir; tool çağırabilir veya metin dönebilir | Genel kullanım, multi-tool ajan |
"any" | Model mutlaka BİR tool çağırmalı; hangisini seçeceği serbest | Multi-schema extraction’da “şemalardan biriyle dön” zorlaması |
{"type":"tool","name":"extract_invoice"} | Model belirli tool’u çağırmak zorunda | Tek-schema extraction’da en güvenilir; agentic akışta “önce bu tool çalışsın” garantisi |
2. JSON Schema: required, nullable, enum, additionalProperties
Şema yazarken dört kavramı bilmek şart:
{
"type": "object",
"properties": {
// required: pipeline için zorunlu
"vendor": { "type": "string" },
// nullable: bazen yok, schema'da belirtmek ZORUNLU
// (yoksa "field missing" diye fail eder)
"tax_id": { "type": ["string", "null"] },
// enum + "other" pattern: bilinmeyen değerler için
// detail string ile ne olduğunu sor
"category": {
"type": "string",
"enum": ["consulting", "software", "hardware", "other"]
},
"category_detail": {
"type": "string",
"description": "category='other' ise zorunlu, ne olduğunu açıkla"
},
// additionalProperties: false = model extra alan ekleyemez
},
"required": ["vendor", "category"],
"additionalProperties": false
}
“other” + detail string pattern’ı önemli. Eğer enum’u sıkı tutarsan (“consulting, software, hardware” ve başka bir şey yok), model ya yanlış sınıflandırır ya halüsinasyon yapar. Esnetmek de kötü (her şeyi kabul edersen veri kirli). Çözüm: enum’a “other” ekle, “other” seçildiğinde dolu olan category_detail field’ı zorunlu kıl. Pydantic validator ile bunu kontrol et.
“other” vs “unclear” farkı. İkisi de enum’da bulunmalı ama anlamları farklıdır:
other= model tanımlanabilir bir kategori döndürüyor ama listede yok ("kırtasiye","danışmanlık dışı hizmet"gibi). Bu durumdacategory_detailne olduğunu söyler.unclear= model emin değil, belge belirsiz veya yetersiz.category_detail“model karar veremedi, kaynak X paragrafında çelişkili ifade” gibi bir gerekçe taşır.
Eğer sadece other koyarsan model, aslında belirsiz olduğu durumlarda bile uydurma bir kategori seçer; unclear honest bir çıkış kapısıdır.
required alanlar modeli uydurmaya iter. Bir alan required olarak işaretlendiğinde, model o alanı mutlaka doldurmak zorunda hisseder — kaynak belgede veri yoksa uydurur. Kural: alan her örnekte mevcutsa required yap, mevcut olmayabilecekse ya nullable: ["string","null"] yap ya enum’a unclear/other ekle, ya da required listesinden çıkarıp default ver. required listesi pipeline için zorunlu alanları temsil eder, “her zaman orada olmasını istediğin” alanları değil.
3. Pydantic ile semantic validation
JSON Schema syntax’ı doğrular (alan var mı, tip doğru mu). Semantic doğrulama (mantıklı mı, tutarlı mı) için Pydantic validator yaz:
from pydantic import BaseModel, Field, model_validator
from typing import Literal
class LineItem(BaseModel):
description: str
quantity: float = Field(gt=0)
unit_price: float = Field(ge=0)
class Invoice(BaseModel):
vendor: str
total: float
currency: Literal["TRY", "USD", "EUR", "GBP"]
line_items: list[LineItem]
tax_id: str | None = None
category: Literal["consulting", "software", "hardware", "other"]
category_detail: str | None = None
@model_validator(mode="after")
def check_consistency(self):
# Semantic kural 1: line item toplamı ≈ stated total
calculated = sum(li.quantity * li.unit_price for li in self.line_items)
if abs(calculated - self.total) > 0.02 * self.total:
# %2'den fazla sapma — hata değil, işaret
# (bazen KDV, indirim ayrı kalem)
pass # detected_pattern'da raporlanır
# Semantic kural 2: category=other ise detail zorunlu
if self.category == "other" and not self.category_detail:
raise ValueError(
"category='other' seçildi; category_detail zorunlu. "
"Modele retry ile bildirilecek."
)
return self
İşte detected_pattern alanı burada devreye giriyor: eğer modelin döndüğü line_items toplamı ile total alanı tutmuyorsa, bu modele geri bildirilmez ama çıktıda işaretlenir — çünkü bazen KDV ayrı kalemdir, indirim vardır. False positive analizi için bu izi taşımak lazım. Aşağı akış kodu “stated_total”i ana değer olarak kullanır, ama insan reviewer “detected_pattern: line_items_total_mismatch” notunu görür.
4. Retry-with-error-feedback
Pydantic validation fail ettiğinde aynı mesajı hatayla birlikte geri göndermek modelin düzeltme şansını artırır:
def extract_with_retry(ocr_text: str, max_retries: int = 3) -> Invoice:
messages = [{"role": "user", "content": ocr_text}]
for attempt in range(max_retries):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[invoice_tool_schema],
# Tool seçimini zorla — model free-text dönemez, sadece extract_invoice çağırabilir
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=messages
)
tool_block = response.content[0]
try:
# Pydantic parse — semantic validation dahil
return Invoice.model_validate(tool_block.input)
except ValidationError as e:
# Hatayı modele geri besle
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": (
f"Validation hatası: {e}\n\n"
"Lütfen çıktıyı düzelt. Özellikle:\n"
"- category='other' ise category_detail dolu olmalı\n"
"- quantity > 0, unit_price >= 0 olmalı\n"
"- Tüm required alanlar eksiksiz olmalı"
)
})
if attempt == max_retries - 1:
raise
Bu pattern sınavda doğrudan çıkıyor: “Validation hatası olduğunda ne yapılmalı?” → Hatayı modele geri besle, tool_choice zorla, max 3 retry. Asla parse hatasını sessizce yutma.
Retry’ın İŞE YARAMADIĞI durumlar: bilgi kaynak belgede hiç yok (model uydurmaya zorlanır, her seferinde aynı halüsinasyonu üretir) veya gereken bağlam dış kaynakta (model erişemez). Bu durumlarda retry sayısı 0 olmalı: kaynağa dön, alanı None/optional bırak, veya insan review’a düşür. “Max 3 retry” evrensel değildir — retry, modelin düzeltebileceği hatalar için anlamlıdır.
5. Message Batches API: %50 ucuz, 24 saat pencere, multi-turn yok
Çok sayıda async iş (toplu fatura çıkarma, gecelik ETL, haftalık rapor) için Message Batches API sync API’nin yarı fiyatına çalışır. Ama üç kısıt var:
# Message Batches API kullanımı
batch = client.messages.batches.create(
requests=[
{
"custom_id": "invoice-001", # korelasyon anahtarı
"params": {
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"tools": [invoice_tool_schema],
"messages": [{"role": "user", "content": "..."}]
}
},
{
"custom_id": "invoice-002",
"params": { /* ... */ }
}
# ... max 100,000 request / batch VEYA 256 MB (hangisi önce dolarsa)
]
)
# 24 saat içinde tamamlanır (genelde 1-2 saat)
# Sonuçları custom_id ile eşle
results = client.messages.batches.results(batch.id)
for result in results:
# result bir discriminated union: .result.type ∈ {"succeeded","errored","canceled","expired"}
if result.result.type == "succeeded":
msg = result.result.message # sadece succeeded ise .message var
print(result.custom_id, msg.content)
# "invoice-001" → bu ID'li isteğin cevabı
else:
print(result.custom_id, "FAILED:", result.result.type)
Üç sınav kısıtı:
- %50 ucuz — bütçe optimizasyonu için güçlü argüman
- 24 saat pencere — real-time gerektiren işler için uygun değil
- Multi-turn tool calling YOK — her batch request self-contained’dır: tool_use dönerse aynı request içinde tool_result gönderemezsin — agentic tool loop batch’te çalışmaz. (Önceden hazırlanmış multi-turn conversation’ı tek bir request olarak batch’e koymak ise mümkündür — kısıt, aynı request içinde multi-turn döngü kuramamandır.)
Büyük batch göndermeden önce prompt’u iterate et. 100,000 request’lik batch’i production’a salmadan önce 50–100 örneklem üzerinde prompt + şema + retry stratejisini dene, edge case’leri gözlemle, sonra full batch’e geç. Bir kere gönderdiğinde düzeltme maliyeti yüksektir (24 saat bekle + yeniden gönder).
custom_id korelasyonu: API sonuçları tamamlanma sırasıyla döndürmez, response’lar sıralı değil. Hangi response hangi request’e ait? custom_id ile. Bu ID’leri request oluştururken belirler, sonuçları eşlerken kullanırsın.
6. Batch vs sync karar matrisi
| Kriter | Sync API | Batch API |
|---|---|---|
| Latency | Realtime (2-10s) | 1-24 saat |
| Fiyat | Standart | %50 indirimli |
| Multi-turn tool calling | Var | Yok |
| Max request | Sınırsız (rate-limited) | 100,000 / batch veya 256 MB (hangisi önce dolarsa) |
| Use case | Interactive agent, chat | ETL, nightly, bulk extract |
Sınav tuzağı: “Tüm toplu işler için batch kullan, daha ucuz.” Yanlış. Eğer pipeline’ın geri kalanı sync API’ye bağlıysa ve blocking çağrı yapıyorsa, 24 saat pencere SLA’yı bozar. SLA planlaması: 30 saatlik deadline varsa, 6 saat submission window bırak; batch’i ilk 6 saatte tetikle, kalan sürede sonuçları işle.
7. detected_pattern / calculated_total vs stated_total — self-correction
Bu pattern özellikle sayısal extraction senaryolarında kritik. Model bazen “toplam 1500 USD” yazarken, kalemleri topladığında 1400 çıkar (KDV dahil mi, değil mi?). Bu hatayı iki alan ile yönet:
{
"vendor": "Acme Corp",
"stated_total": 1500,
"currency": "USD",
"line_items": [
{ "description": "Danışmanlık", "quantity": 10, "unit_price": 100 },
{ "description": "Yazılım", "quantity": 1, "unit_price": 500 }
],
"calculated_total": 1500,
"discrepancy_note": null
}
stated_total: modelin metinden okuduğu rakam (insan gözünden)calculated_total: line item’lar üzerinden hesaplanan (kod gözünden)discrepancy_note: ikisi uyuşmuyorsa neden
Aşağı akış kodu hesaplanan değeri (calculated_total) kullanır — çünkü kalem-bazlı vergi/komisyon uygulanabilir. Ama reviewer discrepancy_note’u görür, “bu faturayı manuel kontrol et” der. Bu yaklaşım sınavda “modelin çıktısına nasıl güvenilir?” sorusuna cevap: tek bir alana bağlanma, çapraz doğrulama yap.
Sınav tuzakları
- “Model text içinde JSON üretsin, sonra parse ederim.” → Yanlış. Text parse antipattern; JSON Schema + tool_use kullan, syntax hatası ortadan kalksın.
- “Validation hatası olursa sessizce default değer kullan.” → Yanlış. Hatayı modele geri besle (retry-with-error-feedback); default atamak sessiz veri bozulması.
- “Message Batches API her şey için daha iyi, daha ucaz.” → Yanlış. Multi-turn tool calling desteklemez; realtime senaryolar için sync gerekli. SLA planlaması yap.
- “Enum ne kadar geniş o kadar iyi, model rahat seçsin.” → Yanlış. Geniş enum = kirli veri. “other” + detail pattern’ı optimum.
- “additionalProperties true bırak, model kendi alanlarını eklesin.” → Yanlış. Şema sözleşmesini bozar;
additionalProperties: falsezorunlu. - “stated_total’a güven, modele güveniyoruz.” → Yanlış. Tek alana bağlanma;
calculated_totalile çapraz doğrula, discrepancy durumunda insan review’a düşür.
Hands-on
Üç parçalı bir fatura extraction pipeline’ı — tool_use + Pydantic + batch:
import json
from anthropic import Anthropic
from pydantic import BaseModel, Field, ValidationError, model_validator
client = Anthropic()
class LineItem(BaseModel):
description: str
quantity: float = Field(gt=0)
unit_price: float = Field(ge=0)
class Invoice(BaseModel):
vendor: str
stated_total: float
currency: str
line_items: list[LineItem]
# model_validator atadığı için alanlar tanımlı olmalı; Pydantic v2'de
# tanımsız alanı set etmek AttributeError verir, model_dump'ta görünmez
calculated_total: float | None = None
discrepancy_note: str | None = None
@model_validator(mode="after")
def cross_check(self):
calc = sum(li.quantity * li.unit_price for li in self.line_items)
self.calculated_total = calc
if abs(calc - self.stated_total) > 0.02 * self.stated_total:
self.discrepancy_note = (
f"stated={self.stated_total}, calculated={calc:.2f}, "
f"fark={abs(calc - self.stated_total):.2f}"
)
else:
self.discrepancy_note = None
return self
INVOICE_TOOL = {
"name": "extract_invoice",
"description": "OCR metninden fatura alanlarını çıkar",
"input_schema": {
"type": "object",
"properties": {
"vendor": {"type": "string"},
"stated_total": {"type": "number"},
"currency": {
"type": "string",
"enum": ["TRY", "USD", "EUR", "GBP", "other"]
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"}
},
"required": ["description", "quantity", "unit_price"]
}
}
},
"required": ["vendor", "stated_total", "currency", "line_items"],
"additionalProperties": False
}
}
# Tek fatura — sync, retry-with-error-feedback
def extract_one(ocr: str) -> Invoice:
messages = [{"role": "user", "content": ocr}]
for _ in range(3):
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[INVOICE_TOOL],
messages=messages
)
try:
return Invoice.model_validate(resp.content[0].input)
except ValidationError as e:
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user",
"content": f"Validation hatası, düzelt:\n{e}"})
raise RuntimeError("3 retry'da düzeltmedi")
# Yüzlerce fatura — batch API, custom_id korelasyonu
def extract_bulk(invoices: list[dict]) -> dict[str, Invoice]:
batch = client.messages.batches.create(
requests=[
{
"custom_id": inv["id"], # dış ID'yi korelasyon için taşı
"params": {
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"tools": [INVOICE_TOOL],
"messages": [{
"role": "user",
"content": inv["ocr"]
}]
}
}
for inv in invoices
]
)
# Poll for results (1-24 saat)
out: dict[str, Invoice] = {}
for r in client.messages.batches.results(batch.id):
# Discriminated union: önce succeeded kontrolü, sonra .result.message yolu
if r.result.type == "succeeded":
tool_block = r.result.message.content[0]
out[r.custom_id] = Invoice.model_validate(tool_block.input)
else:
# Hata/iptal/süresi dolmuş kayıtlar — logla, üst katmanda ele alınır
print(f"SKIP {r.custom_id}: {r.result.type}")
return out
Bu iki fonksiyon sınav senaryolarında Structured Data Extraction ve CI/CD Pipeline bölümlerinin cevabı. Sync kısımda stop_reason == "end_turn" güvenilir tek completion sinyali, stop_reason == "tool_use" ise modelin tool çağrısı talep ettiğini gösterir (extraction’da bu blokta input alınır); batch kısımda custom_id korelasyonu; Pydantic kısmında semantic validation — hepsi sınavda sorulan kavramlar.
Özet
- tool_use + JSON Schema en güvenilir yapısal çıktı yöntemidir; syntax hatasını tamamen ortadan kaldırır, parse etmeye gerek bırakmaz.
- JSON Schema:
requiredzorunlu alanları,nullable: ["string","null"]opsiyonelleri,enum+"other"pattern’ı,additionalProperties: falsesözleşmeyi korur. - Pydantic semantic validation syntax’ı aşar:
model_validatorile capraz kontrol (calculated vs stated), iş kuralı enforcement. - Retry-with-error-feedback: validation hatasını modele geri besle, max 3 retry, hatayı yutma.
- Message Batches API: %50 ucuz + 24 saat pencere, ama multi-turn tool calling YOK. Realtime senaryolarda sync kullan.
- custom_id korelasyon anahtarı: response’lar sıralı dönmez, ID ile eşle.
- SLA planlaması: 30 saatlik deadline = 6 saat submission + 24 saat işlem.
- detected_pattern / calculated_total ile self-correction: tek alana bağlanma, çapraz doğrula.