LLM'lerde Token ve Tokenizer: Türkçe Neden Daha Çok Token Tutar?
Token nedir, BPE ve SentencePiece metni nasıl böler? Ollama'nın prompt_eval_count sayacıyla aynı cümlenin Türkçesini ve İngilizcesini gemma3:4b ve qwen3:4b tokenizer'larında ölçüyor; sayıların, GUID'lerin, JSON'un ve kodun kaç token tuttuğunu, chat template'in görünmeyen token'larını ve token'ın maliyet, limit ve hıza etkisini gerçek sayılarla görüyoruz.
Sürüm notu: Bu yazıdaki örnekler Ollama 0.34 ile yerel modellerde (gemma3:4b, qwen3:4b) denenmiştir; .NET 10 ve Node 22 kullanılmıştır.
Önce Tahmin Et
Bookstore'un müşterilerine gönderdiği bir bildirim var: "Your order has been shipped and will arrive tomorrow." Aynı cümlenin Türkçesi: "Siparişiniz kargoya verildi ve yarın elinizde olacak." Türkçe cümlede 7, İngilizcede 9 kelime var. Model hangisini daha ucuza okur? Peki "okuyamadıklarımızdan" gibi tek bir kelime kaç token eder: bir mi, beş mi, on mu?
Tahminini bir kenara yaz. Bu yazıda token'ları gerçekten sayacağız; önce Türkçe ile İngilizceyi, sonra kodu, sayıları ve kimlikleri. Cevabın büyük kısmı kullandığın modelin tokenizer'ına bağlı, bunu da iki farklı modelle göreceğiz.
Token Nedir?
Model metni harf harf de kelime kelime de okumaz; token token okur. Token, modelin sözlüğündeki bir parçadır. Sık geçen kelimeler genellikle tek bir token'dır, nadir kelimeler ise birkaç parçaya bölünür. Bu sözlüğü oluşturan ve metni parçalara ayıran bileşene tokenizer denir.
Bugünkü tokenizer'ların çoğu subword yaklaşımını kullanır. En bilineni Byte-Pair Encoding (BPE): karakterlerden başlar, metinde en sık yan yana gelen iki parçayı birleştirir ve bunu sözlük istenen büyüklüğe ulaşana kadar tekrarlar (Sennrich ve arkadaşları, 2015). SentencePiece ise boşluğu da sıradan bir sembol gibi ele alıp ham metin üzerinde eğitilen, dilden bağımsız bir tokenizer (Kudo ve Richardson, 2018). Gemma 3 SentencePiece kullanıyor; teknik raporuna göre sözlüğü 262 bin token'lık, rakamları tek tek bölüyor ve sözlükte olmayan karakterler için byte seviyesine iniyor.
Bir benzetme: tokenizer metni hecelere ayıran bir dizgici gibi çalışır. Ama heceleri dilbilgisine göre değil, eğitim verisinde ne sıklıkta gördüğüne göre belirler. Eğitim verisinde çok İngilizce varsa İngilizce kelimeler büyük ve az sayıda parçaya ayrılır, az görülen diller küçük ve çok sayıda parçaya.
Token Saymak: Ollama'nın Sayaçları
Ollama'nın 0.34 sürümünde ayrı bir tokenize endpoint'i yok; /api/tokenize adresine istek atınca 404 dönüyor. Ama her cevapta prompt_eval_count var. Prompt'u raw: true ile chat template'ine sokmadan gönderip tek bir token ürettirirsek, bu sayı neredeyse doğrudan metnin token sayısı olur:
import ollama from "ollama";
// raw: true chat template'ini atlar; sayıya yalnızca modelin başa eklediği token'lar karışır
export async function countTokens(model: string, text: string): Promise<number> {
const res = await ollama.generate({
model,
prompt: text,
raw: true,
options: { temperature: 0, num_predict: 1 },
});
return res.prompt_eval_count;
}
Neden "neredeyse"? Gemma 3 her metnin başına bir başlangıç (BOS) token'ı ekliyor. "Hello" kelimesini bu fonksiyonla saydığımda Gemma 3 için 2, Qwen 3 için 1 sonucu aldım. Yani aşağıdaki Gemma sayılarının her birinde bu bir token'lık pay da var. Karşılaştırma yaparken önemli olan, aynı modelde aynı yöntemle saymak.
Türkçe ve İngilizce: Aynı Cümle, Farklı Fatura
Bu bölümdeki kod, konusu bizzat Türkçe olduğu için Türkçe metin içeriyor. Dört çeviri çiftini iki farklı modelin tokenizer'ıyla sayıyoruz:
import { countTokens } from "./count-tokens.js";
const pairs: [string, string][] = [
["Your order has been shipped and will arrive tomorrow.",
"Siparişiniz kargoya verildi ve yarın elinizde olacak."],
["Can you recommend a short science fiction novel for a long train journey?",
"Uzun bir tren yolculuğu için kısa bir bilim kurgu romanı önerebilir misiniz?"],
["We could not find any books matching your search.",
"Aramanızla eşleşen hiçbir kitap bulamadık."],
["from the ones we could not read",
"okuyamadıklarımızdan"],
];
for (const model of ["gemma3:4b", "qwen3:4b"]) {
console.log(model);
for (const [en, tr] of pairs) {
const enTokens = await countTokens(model, en);
const trTokens = await countTokens(model, tr);
const enWords = en.split(" ").length;
const trWords = tr.split(" ").length;
console.log(` EN tokens=${enTokens} words=${enWords} | TR tokens=${trTokens} words=${trWords}`);
}
}
gemma3:4b
EN tokens=11 words=9 | TR tokens=16 words=7
EN tokens=15 words=13 | TR tokens=23 words=12
EN tokens=11 words=9 | TR tokens=13 words=5
EN tokens=8 words=7 | TR tokens=7 words=1
qwen3:4b
EN tokens=10 words=9 | TR tokens=21 words=7
EN tokens=14 words=13 | TR tokens=25 words=12
EN tokens=10 words=9 | TR tokens=16 words=5
EN tokens=7 words=7 | TR tokens=8 words=1
Açılıştaki sorunun cevabı: Gemma 3'te kargo cümlesinin İngilizcesi 11, Türkçesi 16 token. Türkçe cümle iki kelime daha kısa olduğu hâlde yaklaşık yüzde 45 daha pahalı. İkinci çiftte fark yüzde 53, üçüncüde yüzde 18. "okuyamadıklarımızdan" tek başına 7 token tutuyor, yani İngilizcedeki yedi kelimelik karşılığıyla neredeyse aynı.
Bu tabloyu kelime başına okumak yanıltıcı olur. Türkçe kelime başına 2–3 token harcıyor, İngilizce 1,2 civarında. Ama Türkçe eklerle her kelimeye daha çok anlam yüklüyor, bu yüzden cümle başına fark kelime başına fark kadar büyük değil. Asıl sürpriz ikinci modelde: Qwen 3'ün tokenizer'ında aynı Türkçe cümleler İngilizcelerinin 1,6 ile 2,1 katı arasında tutuyor (21'e 10, 25'e 14, 16'ya 10). Gemma 3'ün teknik raporu yeni tokenizer'ın İngilizce dışındaki diller için daha dengeli olduğunu söylüyor; ölçüm de bunu destekliyor.
Literatür de aynı yerde. Petrov ve arkadaşlarının NeurIPS 2023 çalışması, aynı metnin çevirileri arasında token uzunluğunun 15 kata kadar değişebildiğini gösteriyor. Türkçe üzerine yapılan 2025 tarihli bir çalışma da (Bayram ve arkadaşları) tokenizer'ın geçerli Türkçe parçaları ne kadar iyi yakaladığının, modelin Türkçe MMLU sonuçlarıyla ilişkili olduğunu bildiriyor. Senin için pratik sonuç: Türkçe bir uygulamanın token bütçesini İngilizce örneklerden tahmin etme, kullanacağın modelde ölç.
Kod, Sayılar ve Kimlikler
LLM'e gönderdiğin şey her zaman düzgün bir cümle değildir. Bookstore asistanı sipariş numaraları, JSON, kod parçaları ve kimlikler de görecek. Bunları da sayalım:
import { countTokens } from "./count-tokens.js";
const samples = [
"1234567890",
"1,234,567.89",
"2026-11-06",
"3f2504e0-4f89-11d3-9a0c-0305e82c3301",
'{"orderId": 1042, "status": "shipped"}',
"var total = order.Items.Sum(i => i.Price * i.Quantity);",
"return total;",
" return total;",
];
for (const text of samples) {
const tokens = await countTokens("gemma3:4b", text);
console.log(`${String(tokens).padStart(3)} tokens ${String(text.length).padStart(3)} chars ${text}`);
}
12 tokens 10 chars 1234567890
14 tokens 12 chars 1,234,567.89
12 tokens 10 chars 2026-11-06
38 tokens 36 chars 3f2504e0-4f89-11d3-9a0c-0305e82c3301
18 tokens 38 chars {"orderId": 1042, "status": "shipped"}
20 tokens 55 chars var total = order.Items.Sum(i => i.Price * i.Quantity);
4 tokens 13 chars return total;
5 tokens 21 chars return total;
Sayılar karakter başına bir token'dan daha pahalı: Gemma 3 rakamları tek tek böldüğü için on haneli bir sayı 12 token tutuyor. İlk yazıdaki döngüde 1949 yılının dört ayrı token olarak gelmesinin sebebi de bu. En pahalısı GUID: 36 karakter için 38 token. Rastgele görünen her şey (GUID, hash, token, base64) tokenizer'ın sözlüğünde karşılık bulamadığı için neredeyse karakter karakter bölünür. Bir prompt'a yüzlerce sipariş kimliği koymadan önce bunu hatırla.
Kod ise sanıldığı kadar pahalı değil. C# satırı 55 karakterde 20 token tutuyor, çünkü return, total, = gibi parçalar eğitim verisinde çok sık geçiyor. Girinti de ucuz: başa eklenen sekiz boşluk yalnızca bir token ekledi. JSON'da da benzer bir tablo var; anahtar adları ve tırnaklar kısa parçalar hâlinde gidiyor.
Chat Template'in Görünmeyen Token'ları
Şimdiye kadar raw: true ile saydık. Gerçek bir sohbet isteğinde ise Ollama mesajını chat template'ine yerleştirir ve o template'in de token'ları vardır. C# tarafında Usage üzerinden bakalım:
using Microsoft.Extensions.AI;
using OllamaSharp;
IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "gemma3:4b");
ChatOptions options = new() { Temperature = 0, Seed = 42 };
string[] prompts =
[
"Hello",
"Your order has been shipped and will arrive tomorrow.",
"Summarize the plot of Dune in exactly three sentences.",
];
foreach (string prompt in prompts)
{
ChatResponse response = await client.GetResponseAsync(prompt, options);
// Üretim süresi Ollama'ya özgü, AdditionalProperties içinde TimeSpan olarak geliyor
TimeSpan evalDuration = (TimeSpan)response.AdditionalProperties!["eval_duration"]!;
long output = response.Usage!.OutputTokenCount!.Value;
Console.WriteLine($"input={response.Usage.InputTokenCount} output={output} " +
$"generation={evalDuration.TotalMilliseconds:F0} ms \"{prompt}\"");
}
input=11 output=48 generation=2117 ms "Hello"
input=20 output=37 generation=1658 ms "Your order has been shipped and will arrive tomorrow."
input=21 output=145 generation=3350 ms "Summarize the plot of Dune in exactly three sentences."
"Hello" ham hâliyle 2 token'dı, sohbet isteğinde 11 oldu. Kargo cümlesi de 11'den 20'ye çıktı. Her iki durumda da fark 9 token: kimin konuştuğunu ve sıranın modelde olduğunu belirten template parçaları. Tek bir mesajda 9 token önemsiz görünür; ama on turluk bir sohbette her mesaj bu payı yeniden öder, system prompt da her istekte baştan gönderilir.
Output tarafı daha da öğretici. "Hello" demek modele 48 token'lık bir karşılama yazdırdı. Üç cümlelik Dune özeti 145 token tuttu ve en uzun süren istek oldu. Gecikmeyi asıl belirleyen, modelin ne kadar yazdığıdır. Süreler bu makinede aynı anda çalışan başka işler yüzünden oynuyor: ilk iki istek saniyede 22–23 token hızla üretildi, Dune isteği ise saniyede 43 token hızla.
Token Neden Önemli?
Üç sebebi var: maliyet, limit ve hız. Bulut sağlayıcıları input ve output token'larını ayrı fiyatlar, genellikle milyon token başına. Output token'ları çoğunlukla input'tan daha pahalıdır ve tekrar eden prompt parçaları için önbellek indirimi uygulayan sağlayıcılar var. Fiyatlar sık değiştiği için burada rakam vermiyorum; maliyet hesabını on altıncı yazıda yapacağız. Ama formül hep aynı: aynı içerik Türkçede yüzde 45 daha çok token tutuyorsa, faturası da o kadar büyür.
Limit tarafında modelin bir istekte görebileceği token sayısı sınırlıdır; buna context window (bağlam penceresi) denir ve bir sonraki yazının konusu. Sınır karakterle değil token'la ölçülür. Dolayısıyla Türkçe bir belge, aynı uzunluktaki İngilizce bir belgeden daha erken sınıra dayanır.
Hız tarafında iki ayrı iş var. Prompt'u işlemek toplu yapılan, hızlı bir iştir: bir sonraki yazıdaki uzun prompt denemesinde Ollama'nın log'una göre 2051 token 2,3 saniyede, yani saniyede yaklaşık 880 token hızla işlendi. Cevap üretmek ise token token ilerler: aynı isteğin 18 token'lık cevabı saniyede yaklaşık 21 token hızla üretildi. Uzun bir cevap, uzun bir prompt'tan çok daha fazla bekletir.
Sık Düşülen Tuzaklar
İlk tuzak, "4 karakter yaklaşık 1 token eder" kuralına güvenmek. Bu kural İngilizce düz metin için kaba bir tahmindir. Türkçe metinde, sayılarda ve kimliklerde tutmaz; yukarıdaki GUID karakter başına birden fazla token tuttu.
İkincisi, bir modelin token sayısını başka bir model için kullanmak. Tokenizer modele özgüdür. Aynı Türkçe cümle Gemma 3'te 16, Qwen 3'te 21 token tuttu. Model değiştirdiğinde bütçe hesabını da yeniden yap.
Üçüncüsü, yalnızca kullanıcının mesajını saymak. Gerçek input'a chat template token'ları, system prompt, sohbet geçmişi ve ileride göreceğimiz tool tanımları da girer. Faturayı prompt_eval_count ya da Usage.InputTokenCount belirler, senin metin kutundaki karakter sayısı değil.
Dördüncüsü, önbelleğin token sayısını düşürdüğünü sanmak. İkinci yazıda 20 token'ın 15'i önbellekten geldiğinde bile prompt_eval_count 20 olarak kaldı. Önbellek süreyi kısaltır; Ollama yine prompt'un tamamını raporlar. Bulut sağlayıcılarında önbellekteki token'lar ayrıca ve indirimli raporlanabilir, sağlayıcının dokümanına bak.
Küçük Bir Deneme
★☆☆ tr-vs-en.ts dosyasına Bookstore'dan kendi cümlelerini ekle: bir iade politikası, bir kitap tanıtımı. Türkçe ile İngilizce arasındaki farkın cümleden cümleye ne kadar değiştiğine bak.
★★☆ code-tokens.ts ile aynı JSON'u bir kez girintili, bir kez tek satır olarak say. Sonra anahtar adlarını kısalt (orderId yerine id) ve kazancı ölç. Bir LLM'e büyük JSON gönderen bir uygulamada bu fark ne kadar eder?
★★★ C# örneğine aynı soruyu Türkçe soran bir prompt ekle ve modelden Türkçe cevap iste. Input ile birlikte output token'larını ve üretim süresini İngilizce cevapla karşılaştır.
Bir sonraki yazıda bu token'ların sığması gereken alana, context window'a bakacağız. Modelin aslında hiçbir şey hatırlamadığını, sohbet geçmişini her seferinde senin gönderdiğini ve Ollama'nın varsayılan 4096 token'lık penceresinin uzun bir prompt'u hiçbir hata vermeden nasıl sessizce kestiğini gerçek bir denemeyle göreceğiz.