Şubat başında müşteri portalı için bir RAG + chat sistemi yazdık, arkada Claude Sonnet 4.6. Her sorguda aynı 82 bin tokenlık rulebook ve ürün dokümanı seti gidiyordu. Ay sonunda fatura geldi: 2840 dolar. Finans ekibi göstermeden önce iki dakika oturup Claude API prompt caching özelliğini deneyip deneyemeyeceğime baktım. Bir sonraki ay aynı sistem 820 dolara döndü ve asıl ilginç kısım, bu rakama ulaşana kadar beş ayrı yerde yanlış yapmam oldu.
82k Token Sistem Promptu ve Fatura Şoku
Sonnet 4.6 için input fiyatı 1M token başına 3 dolar. Bizim kurduğumuz kurgu şöyleydi: her kullanıcı sorgusunun önüne 8k tokenlık kurumsal rulebook, 74k tokenlık ürün kataloğu ve SKU açıklamaları gidiyordu. Kullanıcı sadece "bu ürünün değişim politikası nedir" yazıyordu ama arkada 82 bin token okutuyorduk.
Günde ortalama 1200 sorgu. Basit matematik: 1200 × 82000 × (3 / 1_000_000) = 295 dolar, sadece input. Output cevapları 300-500 tokenla sınırlıydı, yani output fiyatı marjinaldi. Sorun tamamen statik prefix'ti. Konteksti küçültmek seçenek değildi çünkü rulebook'un tamamını okumadan cevap veremiyorduk, RAG skorlaması da doküman seçimini tutarlı biçimde daraltamıyordu.
Önce Uygulama Tarafında Cache'lemeye Çalıştım
İlk refleksim klasik oldu: Redis'e cevap cache'lemek. Sorunun cevabını hash'leyip 24 saat saklıyordum. İlk haftada cache hit rate %8'de kaldı çünkü kullanıcılar aynı soruyu farklı cümlelerle soruyordu. "İade süreci nasıl işliyor" ile "ürünü geri gönderebilir miyim" semantik olarak aynı ama hash farklı.
İkinci denemede her kullanıcı için session-level konuşma geçmişi tuttum, aynı sessionda sistem promptu göndermiyordum. Bu komple yanlıştı. Anthropic API stateless; her request'te tüm konteksti gönderirsin, istesen de istemesen de her seferinde input token sayılır. Uygulama tarafında ne kadar zekice cache yaparsan yap, API katmanında bedelini ödüyorsun. Bu gerçeği kabul edip Anthropic'in kendi cache mekanizmasına döndüm.
cache_control Ephemeral ile Çözüm
Çözüm tek satırdı aslında. system bloğunun sonuna bir breakpoint koymak:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: [
{
type: "text",
text: rulebook,
},
{
type: "text",
text: productCatalog,
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: userQuery }],
});
Buradaki mantık şu: cache_control nereye konursa, API request'in o noktaya kadarki tüm prefix'ini cache'liyor. Yani aslında ikinci bloğa koyduğum breakpoint'in sağladığı şey, hem rulebook'u hem productCatalog'u birden cache'lemek. İlk çağrıda cache yazılıyor, cache write %25 premium'la faturalanıyor. Sonraki beş dakika içindeki her çağrı o prefix'i cache'den okuyor ve %90 indirimli ödüyorsun. Token muhasebesi şöyle ilerliyor:
console.log({
input: response.usage.input_tokens,
cacheCreation: response.usage.cache_creation_input_tokens,
cacheRead: response.usage.cache_read_input_tokens,
});
İlk request'te cacheCreation: 82340, cacheRead: 0. İkinci request'te tam tersi. Prefix değişmedikçe ve beş dakika geçmedikçe cache okuması devam ediyor. Bizim trafik paterninde bu, günde ortalama 0.91 cache hit rate'e denk geldi. TypeScript tarafında SDK response tiplerini dar tutmak önemli, bunu daha önce TypeScript'in modern JavaScript tarafındaki tip sistemi yazısında anlatmıştım.
Claude Prompt Caching Kurarken Beş Ayrı Şeyi Yanlış Yaptım
İlki TTL süresiydi. Ephemeral cache varsayılan olarak 5 dakika yaşıyor. Gündüz trafiği yoğunken bu harika ama gece yarısı tek tük gelen sorgular cache'i hiç tetiklemiyordu. Bir saatlik cache seçeneğine geçtim: cache_control: { type: "ephemeral", ttl: "1h" }. Cache write maliyeti %100 premium'a çıkıyor ama read tarafı hala %90 indirimli. Gece düşük QPS'de bile cache'de kalınca ortalama fatura daha da aşağı indi. Kararı vermeden önce Anthropic'in resmi prompt caching dokümanından TTL karşılaştırma tablosuna bakmanızı öneririm, kendi QPS profilinize göre hesap değişir.
İkincisi 1024 token minimum breakpoint şartı. Sonnet için prefix 1024 tokenın altındaysa API hata vermiyor, sessizce cache'lemiyor. İlk denememde küçük bir system promptunu (400 token civarı) cache'lemeye çalıştım, cache_read_input_tokens hep 0 geliyordu. "SDK bozuk mu" diye yarım saat docs ve GitHub issues dolandım, meğer sessizce no-op'a düşüyormuş. Haiku için limit 2048, çok daha kötü.
Üçüncüsü breakpoint'in sırası. Cache sadece breakpoint'e kadarki prefix için çalışıyor, sonrası her zaman taze işleniyor. Ben ilk iterasyonda system array'inin ilk bloğuna kullanıcıya özel context koymuştum, breakpoint ikinci bloktaydı. Her farklı kullanıcı girişinde prefix değiştiği için cache sürekli invalid oluyordu. Static olanı başa, dinamik olanı breakpoint'ten sonraya koymak zorundasın. Aksi halde cache write'ı her request'te ödersin, hiç read göremezsin.
Dördüncüsü tool tanımları. Function calling kullanıyorsan ve tools array'in büyükse, onu da cache'lemek gerekiyor. Benim tools listem 3k token civarıydı ve her request'te yeniden okunuyordu. tools array'inin son elemanına da bir breakpoint koyduktan sonra onlar da cache'e girdi. Next.js route handler'larında streaming API cevaplarını aktarırken de aynı pattern işe yarıyor, bunu Next.js advanced patterns yazısındaki edge runtime bölümüyle birleştirip bir kaç ms latency daha kestik.
Beşincisi ölçüm hatasıydı, en can sıkıcısı. Cache çalışıyor mu diye kontrol ederken Anthropic dashboard'una bakıyordum ama dashboard günlük agregeliyor, gerçek zamanlı cache hit/miss görmüyordum. Doğru metrik SDK'nın döndüğü usage objesi. Ben de her response'u loglayıp şu oranı çıkardım:
const hitRate = cacheRead / (cacheRead + cacheCreation + inputTokens);
Hedef 0.85 üstüydü, trafiğin oturduğu ikinci haftada 0.91'e oturdu. Bu sayı altına düştüğünde ya TTL yetersizdir ya prefix invalidation yaşanıyordur ya da cache breakpoint yanlış yerdedir. Üçünü de kontrol et.
Claude Prompt Caching Ne Zaman Kazandırmaz
Her konuşmada 100% kazandıran bir optimizasyon değil. Cache write ilk çağrıda fiyat premium'u getiriyor. Eğer trafiğin 5 dakikada bir request'in altındaysa premium'u geri kazanamazsın, fatura artar. Prompt'un %70'i her request'te değişiyorsa da faydası sınırlı; cache sadece prefix'in sabit kaldığı kısım kadar çalışır. Kısa sistem promptu (1024 altı Sonnet, 2048 altı Haiku) hiç çalışmaz.
Tek metrikle karar vermek istersen kullanım panelinde input/output token oranına bak. 20'den büyükse neredeyse garanti kazandırır, 5'in altındaysa uğraşma. Biz portalda 180 civarındaydık, bu yüzden fatura 3.4 kat düştü. Aynı sayı bir asistan bot için olsa, output token 10 kat daha fazla çıkabilir ve kazanç 1.2 kat kalır.
Şunu yapma: A/B testi kurmadan cache'i production'a açma. İlk iki gün trafiğin yarısında çalıştır, cache_read_input_tokens ortalamasını karşılaştır. Eğer hit rate %60 altında geliyorsa geri alıp prefix invalidation'ı araştır, körleme production'a bırakırsan sessizce cache write premium'unu ödemeye devam edersin ve haberin olmaz.