omeryanbas.com

Ömer Yanbaş

Genel Müdür, Ticofab Yazılım

Yapay zekâPerformans

Yapay zekâ özelliğini üretimde makul maliyette tutmak

Çalışan bir yapay zekâ özelliği de kimsenin planlamadığı bir fatura üretir. Token nereye gidiyor, ne cache'lenir, kullanıcı başına limit nasıl konur.

Özellik yayına alınır, insanlar kullanır ve ilk tam ayın faturası tahminin birkaç katı çıkar. Bozuk bir şey yoktur. Cevaplar iyidir, gecikme kabul edilebilir düzeydedir, tek sorun faturanın altındaki sayıdır. Bir yapay zekâ özelliğini üretimde makul maliyette tutmak mühendislikten çok muhasebeye benziyor: hangi çağrının kaça mal olduğunu bilmek ve kimsenin okumadığı token'lara para ödememek.

Para aslında nereye gidiyor

Model çağrısı gönderdiğiniz metin artı geri gelen metin üzerinden faturalanır ve üretimdeki çoğu özellikte bu oran tek taraflıdır. Çıkan her token'a karşılık otuz kırk token girer. Yani pahalı olan kısım zekice yazılmış cevap değil, her isteğe düşünmeden iliştirilen metindir.

Hasarın büyük kısmını dört şey yapar:

  1. System prompt. İki bin token'lık talimat, kural ve örnek, her çağrıda yeniden gönderilir. Ayda iki yüz bin çağrıda bu, daha kimse tek kelime yazmadan dört yüz milyon input token eder.
  2. Getirilen bağlam. Arama yirmi pasaj döner, kod da yirmisini birden gönderir, çünkü fazla göndermek az göndermekten güvenli hissettirir. Cevabı dördü içerir, kalan on altısının parası ödenir ve okunmaz.
  3. Konuşma geçmişi. Onuncu tur, birinci turdan dokuzuncuya kadar her şeyi tekrar gönderir. Bir sohbetin maliyeti kabaca uzunluğunun karesiyle büyür, bu yüzden uzun bir sohbetin son birkaç turu ilk yirmi turun toplamından pahalı olabilir.
  4. Retry. Retry tüm input'u yeniden gönderir. Rate limit gördüğünde üç kez deneyen bir istemci, pahalı tek çağrıyı üçe çıkarır. Hepsi aynı anda yeniden deneyen bir worker havuzu da kötü bir dakikayı pahalı bir saate çevirir. Bu, hız sınırını kalıcı hata saymanın bedeli yazısındaki hatanın aynısıdır, farkı burada faturada da görünmesi.

Output token'ın tanesi daha pahalı fiyatlanır ama genelde sayısı çok daha azdır. İstisna, output sınırı konmamış çağrılardır: ayrıntılı olmaya karar veren bir model, arayüzde zaten kırpacağınız üç sayfa yazar. Her çağrıya makul bir üst sınır koymak beş dakikalık iştir ve bu kalemi tamamen kapatır.

Bu dört kalemin ortak yanı, hiçbirinin kod incelemesinde göze çarpmaması. Prompt'a eklenen on satırlık yeni kural, sıralamaya eklenen beş pasaj, sohbet geçmişinin kısaltılmadan taşınması: tek tek hepsi makul görünür ve hepsi her çağrıda tekrar tekrar ödenir.

Değiştirmeden önce ölçün

Maliyetin nerede durduğuna dair yaptığım her tahmin en az bir kez yanlış çıktı, bu yüzden artık bakacak bir tablo olmadan optimizasyona girişmiyorum. Model çağrısı başına bir satır, yanıt alındıktan sonra yazılır, örnekleme yapılmaz:

await costLog.insert({
  requestId,                       // HTTP isteğiyle aynı id
  feature: 'ticket_classify',      // 'ai' değil, özelliğin adı
  tier: 'small',
  attempt,                         // ilk deneme için 1
  inputTokens: usage.input,
  cachedInputTokens: usage.cachedInput,
  outputTokens: usage.output,
  costMicros: priceMicros(tier, usage),
  accountId,
  latencyMs
});

Maliyeti birimin milyonda biri cinsinden tutmak sayıyı tam sayı yapar, böylece bir milyon satırı kayan nokta sapması olmadan toplayabilirsiniz. Bir haftalık veri biriktiğinde tek bir sorgu dikkatinizi nereye vereceğinizi söyler:

SELECT feature,
       count(*)                                  AS calls,
       sum(cost_micros) / 1e6                    AS total,
       sum(cost_micros) / count(*) / 1e6         AS per_call,
       sum(input_tokens) / sum(output_tokens)    AS in_out_ratio,
       sum(cached_input_tokens) * 100
         / nullif(sum(input_tokens), 0)          AS cached_pct
FROM model_calls
WHERE created_at >= now() - interval '7 days'
GROUP BY feature
ORDER BY total DESC;

Bunun benzerini canlı bir özellikte ilk çalıştırdığımda listenin başında, yayına alındığı haftadan beri kimsenin açmadığı bir arka plan işi vardı. Daha önce neyi özetlediğini bilmediği için aynı kayıtları her gece yeniden özetliyordu.

Çözüm

Genelde en çok kazandıran sırayla beş değişiklik.

Ön eki dondurun ve cache'leyin. Sabit olan her şey en başa gider ve harfi harfine aynı kalır: system prompt, tool tanımları, örnekler. Değişen her şey bunun arkasına geçer. Cache'ten okunan token taze input token'ın çok altına fiyatlanır, tasarruf büyüktür, ama yalnızca ön ek hiç oynamadığı sürece.

const messages = [
  { role: 'system', content: SYSTEM_PROMPT, cacheable: true },  // sabit
  { role: 'system', content: TOOL_SCHEMA,   cacheable: true },  // sabit
  ...history,                                                   // değişir
  { role: 'user',   content: question }
];

O bloğun önüne konan tek bir dinamik değer, bir zaman damgası ya da kullanıcı adı, isabet oranını sıfıra indirir ve fatura gelene kadar kimse fark etmez.

Getirilen bağlamı kırpın. Pasajları sıralayın, sonra kesin. Yirmi yerine ilk dördünü alın ve kimsenin farkı anlayıp anlamadığını ölçün. Üzerinde çalıştığım bir sistemde cevap kalitesi sekiz pasajla üç pasaj arasında hiç değişmedi, input ise kabaca üçte iki düştü.

Ürüne göre değil göreve göre yönlendirin. Sınıflandırma, yönlendirme, alan çıkarma ve kısa yeniden yazma işleri küçük modele gider. Büyük model, okurun farkı göreceği çağrılara saklanır: gönderilecek son metin, insanın imzalayacağı özet. Bu kararı fikirle değil, elli gerçek girdi üzerinde yapılmış kör karşılaştırmayla verin.

Acil olmayanı batch'leyin. Kullanıcının başında beklemediği her iş, gece özetleri, geriye dönük doldurmalar, toplu sınıflandırma, bir queue'dan ve toplu endpoint'ten geçer. Toplu uçlar tipik olarak yarı fiyata yakın fiyatlanır. Aynı queue, interaktif yolun aç kalmasını da engeller. Bu, forma yapıştırılan yüz bin satırı kuyruğa almak için verdiğim gerekçenin aynısı.

Harcamayı çağrıdan önce sınırlayın. Hesap ve gün başına bir sayaç, çağrının tahmini maliyetiyle artırılır ve süreçten bir şey çıkmadan önce kontrol edilir:

const key = `spend:${accountId}:${utcDay()}`;
const spent = await counters.increment(key, estimate.micros, { ttlSeconds: 172800 });

if (spent > limits.dailyMicros(accountId)) {
  await counters.increment(key, -estimate.micros);   // ayırdığını geri ver
  return degraded(question);                         // ucuz yol, dürüst mesaj
}

Yanıt geldikten sonra sayacı tahmin ile gerçek kullanım arasındaki farkla düzeltin. Tahminin yaklaşık olması yeter, çünkü görevi fatura kesmek değil, kontrolden çıkmış bir döngüyü durdurmak. Aynı sayacı günlük toplam için de tutun: tek bir hesabın limitini aşmadan da, hatalı bir deploy bütün trafiği pahalı yola düşürebilir ve bunu ancak toplam harcama hızına bakan bir eşik yakalar.

Çalıştığını nasıl doğrularsınız

Aynı sorguyu bir hafta sonra çalıştırın ve iki çıktıyı yan yana koyun. Değişimin biçimi tek bir sayıdan daha ikna edicidir:

feature           calls     total   per_call   cached_pct
ticket_classify  184,320     41.20   0.000224           91
reply_draft       12,940     96.10   0.007427           88
weekly_digest        620     18.40   0.029677            0

Bu tabloda okunacak üç şey var. Sınıflandırma özelliğinin hacmi yüksek, birim maliyeti düşük: küçük modele yönlendirmenin görünmesi gereken hal budur. Cache yüzdesinin doksana yakın olması ön ekin sabit kaldığını söyler. Gece çalışan özet işinin cache yüzdesi sıfır, sıradaki iş odur, çünkü gece çalışan bir job tam olarak batch'lenmesi ve cache'lenmesi gereken türden bir iştir.

Nelere dikkat etmeli

  • Cache'e yazmak düz çağrıdan pahalıdır. Bir kez kullanılacak ön eki cache'lemek o isteği ucuzlatmaz, pahalılaştırır. Binlerce çağrının paylaştığı ön eki cache'leyin, kullanıcıya özel olanı değil.
  • Ortalama kuyruğu gizler. İki yüz turluk tek bir konuşma, on bin sıradan isteği geçebilir. İstek başına maliyette ortalamaya değil doksan beşinci yüzdeliğe bakın.
  • Sizin token sayınız tahmindir, fatura değildir. Ayda bir, logladığınız maliyetin toplamını gerçek faturayla karşılaştırın. İkisi birbirinden uzaklaşıyorsa fiyat tablonuz eskimiştir.
  • İki deneme ve daha uzun prompt isteyen ucuz model ucuz değildir. Görevin tam maliyetini sayın, küçük modeli güvenli kılmak için eklediğiniz doğrulama çağrısı dahil.

Bu özelliklerin hepsinden geriye kalan ders şu: maliyet seçtiğiniz modelin değil, gönderdiğiniz paketin bir özelliğidir. Bir ay boyunca yapılan küçük iyileştirmelerle dört yüz token büyüyen bir prompt, kimsenin onaylamadığı bir zam demektir ve bunu görmenin tek yolu sayıyı öncesinde de sonrasında da loglamış olmaktır. İstek başına maliyeti aynı tabloda gecikmenin yanına koyun, ikisini de gerileyebilecek şeyler olarak görün, fatura aylık sürpriz olmaktan çıkar. Alışkanlık, yavaş sorgudan doğru indekse giden yoldakiyle aynı: önce pahalı olanı bulun, sonra tek bir şeyi değiştirin, sonra sayıya yeniden bakın.

Sorular ve cevaplar

Yapay zekâ özelliğim tahminimden neden bu kadar pahalı?
Neredeyse her zaman tahmin sadece kullanıcının sorusunu sayıp ona iliştirilen her şeyi unuttuğu için. System prompt, tool tanımları, örnekler, getirilen pasajlar ve önceki konuşmanın tamamı her çağrıda yeniden gönderilir ve her seferinde faturalanır. Çağrı sayısıyla çarpmanız gereken şey soru değil, gönderdiğiniz paketin tamamıdır.
Prompt cache gerçekten tasarruf sağlıyor mu?
Uzun bir ön ek sık ve değişmeden kullanılıyorsa sağlıyor. Cache'ten okunan token normal input token'ın çok altında fiyatlanır, ancak cache'e yazmak düz çağrıdan biraz pahalıdır. Yani bir kez kullanılan ön ek cache'le daha da pahalıya gelir. Başabaş nokta genelde cache ömrü içinde iki üç tekrar kullanımdır.
Küçük model her zaman daha mı ucuz?
Yalnızca işi ilk denemede doğru yapıyorsa. Retry gerektiren, daha uzun prompt isteyen ve üstüne bir doğrulama çağrısı eklediğiniz küçük model, büyük modele tek çağrıdan pahalı çıkabilir. Kararı ürün bazında değil görev bazında, gerçek girdilerle yapılmış kör karşılaştırmayla verin.
Tek bir kullanıcının bütçeyi bitirmesini nasıl engellerim?
Hesap ve gün başına bir harcama sayacını hızlı bir depoda tutun, çağrıdan önce tahmini maliyetle artırın, limit aşıldığında isteği reddedin veya ucuz yola düşürün. Çağrıdan sonra bakmak geç kalmaktır, çünkü para çoktan harcanmıştır. Yanıt gelince tahmini gerçek kullanımla düzeltin.