Sertaç Yıldırım saha notları

Ana sayfa → Teknik

LLM’de Latency: Ortalama Değil, İlk Token

Salı 14:20. Destek ekibinden bir ekran görüntüsü geliyor: müşteri asistana komisyon dökümünü sormuş, 11 saniye boyunca boş bir balon izlemiş, sonra “bozuk mu bu?” yazıp sohbeti kapatmış. Aynı saatte benim panomda ortalama cevap süresi 3,4 saniye yazıyordu. Hedefin altında, yeşil.

Özet
  • LLM gecikmesi tek sayı değil, üç sayı. İlk token’a kadar geçen süre (TTFT), üretim hızı (token/saniye) ve toplam süre. Kullanıcı en çok ilkini hisseder.
  • Ortalama, kısa ve uzun cevabı aynı torbaya atar. Bizde ortalama 3,4 saniyeydi; p95 9,6, p99 14,2 saniye. Şikâyetlerin hepsi kuyruktan geliyordu.
  • Streaming toplam süreyi değil, beklenen süreyi kısaltır. İlk görünen kelimenin p95’i 9,6 saniyeden 3,0 saniyeye indi; toplam süre yerinden kıpırdamadı.
  • İlk kelimenin bir bütçesi olmalı. Retrieval, prompt kurma, model ve filtre — her birine milisaniye yazdık. İki kalem kendi payının iki katından fazlasını yiyordu.
  • En ucuz hızlandırma kısa cevap. Toplam sürenin p95’ini en çok düşüren şey model değiştirmek değil, cevabı kısaltmak oldu: 9,6 → 3,9 saniye.

Sahadan: yeşil pano, boş balon

Asistan Ocak başında canlıya çıkmıştı. Mobil uygulamadaki destek ekranında müşterinin sorusunu alıyor, hesap bilgisine ve politika dokümanlarına bakıyor, bir model sağlayıcısının API’sine gidip cevap üretiyordu. İlk iki hafta kimse gecikmeden şikâyet etmedi, çünkü kimse bakmıyordu. Panoda tek bir sayı vardı: ortalama cevap süresi. Hedefi 4 saniye koymuştum, sayı 3,4’tü.

Ekran görüntüsünden sonra aynı haftanın bütün isteklerini dağılım olarak açtım. Tablo şuydu:

ÖlçüDeğerKimin deneyimi
Ortalama3,4 snKimsenin
p502,6 sn“Para çekme talebim ne zaman geçer?” gibi tek cümlelik sorular
p959,6 snKomisyon dökümü, emir geçmişi, “neden” ile başlayan sorular
p9914,2 snUzun cevap + yavaş başlayan istek, ikisi birden

Hatam basitti: ben sistemin ne kadar sürede cevap verdiğini ölçüyordum, müşterinin ne kadar süre boş ekrana baktığını değil. İkisi ancak cevap bir anda ekrana düşüyorsa aynı şeydir — ve bizde tam olarak öyleydi. Streaming yoktu. Model 560 token’lık bir komisyon açıklamasını bitirene kadar ekranda hiçbir şey değişmiyordu.

Asıl acı veren sayı başka bir yerden çıktı: cevap gelmeden kapatılan sohbet oranı %9. İlk cevabı 8 saniyeden uzun süren sohbetlerde bu oran %31’e çıkıyordu. Yani asistan en uzun, en çok emek verdiği cevapları çoğu zaman kimseye göstermeden çöpe atıyordu.

Kullanıcı ortalamayı yaşamaz. Kendi bekleyişini yaşar — ve şikâyet eden kullanıcının bekleyişi hep p95’tir.

Üç sayı: TTFT, hız, toplam

Klasik bir API’de gecikme tek sayıdır: istek girdi, cevap çıktı. LLM’de cevap parça parça üretilir ve bu yüzden süreyi üçe bölmek gerekiyor:

  1. TTFT (time to first token). İsteğin sunucumuza gelmesinden modelin ilk token’ı üretmesine kadar geçen süre. Retrieval, prompt kurma ve modelin prompt’u okuması bunun içinde. Streaming varsa kullanıcının boş ekrana baktığı süre budur.
  2. Üretim hızı (token/saniye). İlk token’dan sonra modelin saniyede kaç token yazdığı. Büyük ölçüde sağlayıcının ve modelin elinde; bizde ortalama 80 civarındaydı.
  3. Toplam süre. Kabaca TTFT artı çıktı token sayısı / üretim hızı. İkinci terim çoğu zaman birinciden büyük.

Bu formülü kâğıda yazınca p95’in neden 9,6 saniye olduğu kendiliğinden çıktı. Uzun cevapların TTFT’si 2,6 saniye civarındaydı, üstüne 560 token’ı saniyede 80 token’la yazmak 7 saniye ediyordu. Model yavaş değildi. Model çok konuşuyordu.

Olcum: uc sayiyi ayri kaydet
t0 = simdi()                      # istek sunucumuza geldi
ilk_token_ms = None
cikti_token = 0

for parca in model.stream(istek):
    if ilk_token_ms is None:
        ilk_token_ms = simdi() - t0   # TTFT: kullanicinin bos ekrana baktigi sure
    cikti_token += parca.token_sayisi
    istemciye_gonder(parca)

toplam_ms = simdi() - t0
hiz = cikti_token / ((toplam_ms - ilk_token_ms) / 1000)   # token/saniye

# ortalama degil, dagilim: histogram olarak yaz, p95/p99 panoda
metrik.histogram("llm_ttft_ms", ilk_token_ms, etiket={"akis": "destek"})
metrik.histogram("llm_toplam_ms", toplam_ms, etiket={"akis": "destek"})
metrik.histogram("llm_cikti_token", cikti_token, etiket={"akis": "destek"})
metrik.gauge("llm_token_hizi", hiz, etiket={"model": istek.model})

Buna bir dördüncü sayı ekledik ve sonradan en önemlisi o oldu: ilk görünen kelime. Mobil istemcinin kendisi ölçüyor; soruyu gönderdiği andan ekrana ilk harfi bastığı ana kadar. Sunucu tarafındaki TTFT buna ağ süresini ve aşağıda anlatacağım filtreyi eklemiyor. Kullanıcının gördüğü sayı istemcidekidir.

Streaming: beklemenin ilk saniyesini kurtarmak

İlk hafta tek bir şey yaptık: cevabı parça parça ekrana gönderdik. Başka hiçbir şeye dokunmadık. İlk görünen kelimenin p95’i 9,6 saniyeden 3,0 saniyeye indi. Cevap gelmeden kapatılan sohbet oranı %9’dan %4’e düştü. Toplam süre ise aynı kaldı — model hâlâ aynı sürede aynı uzunlukta cevap yazıyordu.

Streaming’in bedava olmadığını ikinci gün öğrendik. Cevabın üstünde bir çıktı filtremiz vardı: yatırım tavsiyesi gibi okunabilecek cümleleri ve cevapta geçen hesap numaralarını yakalıyordu. Filtre cevabın tamamını bekliyordu. Streaming açılınca iki seçenek kaldı: ya filtreyi atlayacaktık (olmaz), ya da cevabı yine sonuna kadar bekleyecektik (streaming’in anlamı kalmaz).

Üçüncü yolu seçtik: cümle cümle filtre. Model yazdıkça metni cümle sınırında biriktiriyoruz, her cümle filtreden geçip öyle ekrana gidiyor. İlk kelimeye bir cümlelik gecikme ekliyor (bizde 250 ms birikme + 150 ms filtre). Karşılığında cevap ilk saniyelerde görünmeye başlıyor.

Streaming ile birlikte gelenler
  • Filtreyi cümle ya da paragraf sınırında çalıştır
  • İlk cümleyi cevabın kendisi yap, girizgâh değil
  • TTFT için ayrı timeout koy, toplam süre için ayrı
  • Yarıda kesilen cevap için ekranda net bir mesaj göster
Streaming ile bozulanlar
  • Cevabın tamamına bakan kontroller
  • “Hata olursa sessizce yeniden dene” — yarım yazılmış cevabı yeniden deneyemezsin
  • Tek sayılık gecikme metriği
  • Cevabı önce üretip sonra yazıyormuş gibi animasyonla göstermek

Cümle cümle filtrenin dürüst bir yan etkisi var: filtre üçüncü cümlede takılırsa, ilk iki cümle ekranda zaten yazılmış oluyor. O durumda cevabı güvenli bir mesajla değiştiriyoruz ve müşteri yazılmış bir metnin silindiğini görüyor. İlk ayda bu cevapların %0,3’ünde oldu. Kabul ettik; filtreyi atlamaktan iyidir, cevabı 10 saniye saklamaktan da.

Streaming cevabı hızlandırmaz. Beklemeyi görünür kılar — ve görünen bekleyiş, görünmeyen bekleyişten her zaman kısa gelir.

İlk kelimenin bütçesi

Streaming’den sonra ilk görünen kelime 3,0 saniyedeydi. Hedef olarak 2 saniye koyduk ve bu 2 saniyeyi kalemlere böldük. Bütçe çıkarınca iki kalemin kendi payının iki katından fazlasını yediği hemen görüldü:

Ilk gorunen kelime butcesi (p95, hedef 2.000 ms)
KALEM                     BUTCE    ONCE      SONRA
-------------------------------------------------------
kimlik + sohbet gecmisi    100 ms    80 ms     80 ms
retrieval (3 kaynak)       450 ms  1.100 ms   420 ms   <-- sirali -> paralel
prompt kurma                50 ms    60 ms     40 ms
model ilk token            800 ms  1.900 ms   800 ms   <-- 6.000 -> 2.200 token
ilk cumle birikmesi        300 ms   250 ms    250 ms
cikti filtresi (1 cumle)   200 ms   150 ms    150 ms
-------------------------------------------------------
not: p95'ler toplanmaz; bu tablo kaba bir hesap,
     asil olcu istemcideki "ilk gorunen kelime" p95'i

Retrieval üç kaynaktan besleniyordu: politika dokümanı araması, hesap özeti servisi ve müşterinin açık destek talepleri. Üçü birbirini beklemiyordu ama kod onları sırayla çağırıyordu. Paralel hâle getirince süre en yavaşının süresine indi: 1.100 ms’den 420 ms’ye. Tek satırlık değişiklik, iki haftadır gözümüzün önündeydi.

Modelin ilk token’ı ise prompt’un boyuyla büyüyordu. Model cevaba başlamadan önce prompt’un tamamını okumak zorunda ve bizim prompt’umuz 6.000 token’dı: sistem talimatının içine bütün sık sorulan sorular listesi yapıştırılmıştı, üstüne son 20 mesajlık sohbet geçmişi. Listeyi çıkarıp yerine aramadan gelen en ilgili 3 maddeyi koyduk, geçmişi son 6 mesaja indirdik. Prompt 2.200 token’a indi, modelin ilk token p95’i 1.900 ms’den 800 ms’ye.

Bütçenin asıl faydası sayıları düşürmek değil, tartışmayı bitirmek oldu. Biri “cevap kalitesi için hesap özetine son 50 işlemi de ekleyelim” dediğinde artık soru “iyi fikir mi” değil, “model ilk token bütçesinden kaç milisaniye yiyor ve onu nereden geri alıyoruz”. Ekleme yapılıyor ama bedeli ölçülerek.

Çıktı uzunluğu: kimsenin bakmadığı düğme

TTFT düzeldikten sonra ilk kelime 1,6 saniyeye indi ama toplam süre hâlâ uzundu. Uzun cevaplarda müşteri metnin akmasını 7–8 saniye izliyordu. Cevaplara tek tek baktım ve modelin ne yaptığını gördüm: soruyu kendi cümleleriyle tekrar ediyor, “harika bir soru” diye başlıyor, sonra cevabı veriyor, sonra da cevabı maddeler hâlinde özetliyordu. Aynı bilgi üç kez.

Üç değişiklik yaptık. Talimata “ilk cümle cevabın kendisi olsun, soruyu tekrar etme” ekledik. Uzun açıklama gerektiren konularda (komisyon dökümü gibi) önce kısa cevap verip “ayrıntısını ister misin?” diye sormasını istedik. Ve çıktıya 350 token’lık bir üst sınır koyduk. Sonuç:

ÖlçüÖnceSonra
İlk görünen kelime, p95 (istemci)9,6 sn1,6 sn
TTFT, p95 (sunucu)2,6 sn1,2 sn
Çıktı uzunluğu, ortalama / p95150 / 560 token110 / 300 token
Toplam süre, p959,6 sn3,9 sn
Toplam süre, ortalama3,4 sn2,1 sn
Cevap gelmeden kapatılan sohbet%9%2

Tablonun ilginç satırı ortalama. 3,4’ten 2,1’e inmiş; iyi ama devrim değil. Aynı dönemde ilk görünen kelime altı kat hızlandı, kapatılan sohbet dörtte birin altına düştü. Başta tek baktığım sayı, değişimin en az görünen sayısıymış.

Kısa cevabın bir yan etkisi de çıktı: ayrıntı isteyen müşteri “evet” yazınca ikinci bir istek gidiyor, yani uzun cevabın toplam maliyeti biraz artıyor. Ama ayrıntı isteyenlerin oranı %18’de kaldı. Geri kalan %82 kısa cevapla yetindi — o uzun paragrafları kimse okumuyormuş.

Bende işe yaramayanlar

  • Daha küçük, daha hızlı modele geçmek. İlk refleksim buydu. TTFT gerçekten düştü, ama temsilciye aktarılan sohbet oranı iki haftada %11’den %16’ya çıktı. Hızlı ama yanlış cevap, yavaş ama doğru cevaptan pahalı. Geri döndük; hızı prompt ve çıktı uzunluğundan aldık.
  • “Yazıyor…” animasyonu. Streaming’den önce denedik. Müşteriler 1–2 saniye daha fazla bekledi, sonra yine kapattı. Hareket eden üç nokta bilgi değildir; ilk kelime bilgidir.
  • Toplam süreye tek timeout. 15 saniyelik timeout, uzun ama sağlıklı akan bir cevabı ortasından kesiyordu; takılıp hiç başlamayan bir isteği ise 15 saniye bekletiyordu. İkiye böldük: ilk token 5 saniyede gelmezse iptal ve “şu an yoğunuz, seni temsilciye bağlıyorum”; akış başladıysa 30 saniyeye kadar izin.

Ne izlemeli?

NeNeden
İlk görünen kelime p95 (istemcide)Kullanıcının yaşadığı tek sayı; sunucu metrikleri bunu tahmin eder, ölçmez
TTFT p95, akış başınaRetrieval ya da prompt büyüdüğünde ilk burası kıpırdar
Token/saniye, model başınaDüşüyorsa sorun sende değil, sağlayıcıda; ayrı görmek tartışmayı kısaltır
Çıktı token p95Prompt değişikliğinden sonra sessizce uzayan cevaplar burada görünür
Prompt token p95Bütçenin en sinsi kalemi; her “şunu da ekleyelim” buraya yazılır
Cevap gelmeden kapatılan sohbet oranıGecikmenin iş karşılığı; eşik aşılınca ilk bakılacak yer
Yarıda değiştirilen cevap oranıCümle cümle filtrenin bedeli; yükselirse filtre ya da prompt kaymış demektir

Hedefleri koyarken bir kural işimize yaradı: bu sayıların hedefini mühendislik tek başına koymuyor. “İlk kelime p95 2 saniye” bir ürün kararı; destek ekibinin lideriyle birlikte yazdık ve aşıldığında kimin ne yapacağını da yanına ekledik. Bu mantığı SLO ve hata bütçesi yazısında ayrıntılı anlattım.

Kontrol listesi

LLM özelliğin ne kadar hızlı?
  • Panoda ortalama mı var, p95 ve p99 mu?
  • TTFT, token/saniye ve toplam süreyi ayrı ayrı ölçüyor muyum?
  • İlk görünen kelimeyi istemci tarafında ölçen biri var mı?
  • İlk kelime için yazılı bir bütçe var mı? Hangi kalem kendi payını aşıyor?
  • Retrieval çağrıları birbirini gereksiz yere bekliyor mu?
  • Prompt kaç token? Son üç ayda ne kadar büyüdü, kim büyüttü?
  • Çıktı uzunluğunun bir üst sınırı var mı? Cevabın ilk cümlesi cevabın kendisi mi?
  • Streaming açıldığında hangi kontroller cevabın tamamını bekliyor?
  • İlk token ve toplam süre için ayrı timeout var mı?
  • Cevap gelmeden kapatılan sohbet oranını biliyor muyum?

Sonuç

O ekran görüntüsündeki müşteri 11 saniye bekledi ve biz o 11 saniyenin hiçbir parçasını görmedik. Pano yeşildi, çünkü pano sistemin ortalamasına bakıyordu; müşteri ise kendi bekleyişine bakıyordu.

Düzeltmelerin hiçbiri büyük değildi: streaming, paralel çağrı, daha kısa prompt, daha kısa cevap. Büyük olan şey doğru sayıya bakmaya başlamaktı. Doğru sayıyı bulduktan sonra neyin yavaş olduğu kendiliğinden görünür oldu.

Test şu: müşterin ekrana ilk harfin gelmesini kaç saniye bekliyor, ve bu sayıyı bugün söyleyebiliyor musun? Söyleyemiyorsan hızını ölçmüyorsun, sadece ortalamasını alıyorsun.