Lesson 10 · 30 dk okuma

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ğerDavranışNe zaman kullan
"auto" (default)Model karar verir; tool çağırabilir veya metin dönebilirGenel kullanım, multi-tool ajan
"any"Model mutlaka BİR tool çağırmalı; hangisini seçeceği serbestMulti-schema extraction’da “şemalardan biriyle dön” zorlaması
{"type":"tool","name":"extract_invoice"}Model belirli tool’u çağırmak zorundaTek-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 durumda category_detail ne 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ı:

  1. %50 ucuz — bütçe optimizasyonu için güçlü argüman
  2. 24 saat pencere — real-time gerektiren işler için uygun değil
  3. 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

KriterSync APIBatch API
LatencyRealtime (2-10s)1-24 saat
FiyatStandart%50 indirimli
Multi-turn tool callingVarYok
Max requestSınırsız (rate-limited)100,000 / batch veya 256 MB (hangisi önce dolarsa)
Use caseInteractive agent, chatETL, 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: false zorunlu.
  • “stated_total’a güven, modele güveniyoruz.” → Yanlış. Tek alana bağlanma; calculated_total ile ç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: required zorunlu alanları, nullable: ["string","null"] opsiyonelleri, enum + "other" pattern’ı, additionalProperties: false sözleşmeyi korur.
  • Pydantic semantic validation syntax’ı aşar: model_validator ile 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.

Kaynaklar