Mert Özen Her Satırda Daha İleri
Yapay Zeka LLM Temelleri — #02

LLM'e İlk İstek: Ollama, C# ve TypeScript ile İlk Cevap

Mert Özen 6 Kas 2026 8 dk 2 görüntülenme
LLM'e İlk İstek: Ollama, C# ve TypeScript ile İlk Cevap

Ollama'yı tanıyor, ollama list ve ollama ps ile modellere bakıyor, /api/chat endpoint'ine curl ile ilk isteği atıyoruz. Aynı isteği C#'ta Microsoft.Extensions.AI ve OllamaSharp ile, TypeScript'te ollama paketiyle tekrarlıyor; cevabın içindeki token sayılarını, nanosaniye cinsinden süreleri ve ilk isteğin neden yavaş olduğunu gerçek çıktılarla okuyoruz.

Sürüm notu: Bu yazıdaki örnekler Ollama 0.34 ile yerel modelde (gemma3:4b) denenmiştir; .NET 10 ve Node 22 kullanılmıştır.

Önce Tahmin Et

Modele aynı soruyu art arda iki kez soruyorsun. İlk cevap yaklaşık 3,7 saniyede, ikincisi 1,5 saniyede geliyor. Model ikinci seferde daha az mı düşündü? Cevabı önbellekten mi verdi? Yoksa ilk istekte bambaşka bir iş mi yapıldı?

Cevap, Ollama'nın her cevabın sonuna eklediği birkaç sayıda saklı. Bu yazıda ilk isteği önce curl ile, sonra C# ve TypeScript ile atacak ve o sayıları tek tek okuyacağız. Sorunun cevabını da onlardan çıkaracağız.

Ollama Ne İşe Yarar?

Ollama, open-weight modelleri kendi bilgisayarında çalıştırmanı sağlayan bir araç. İki işi var: modelleri indirip saklamak ve onları bir HTTP API'si üzerinden sunmak. Kurduğunda arka planda bir sunucu başlar ve varsayılan olarak yalnızca 127.0.0.1:11434 adresini dinler. Yani dışarıdan erişilemez; senin kodun bu adrese istek atar, Ollama modeli belleğe yükler, cevabı üretir ve geri döner. Bir veritabanı sunucusuna benzetebilirsin: SQL Server'a nasıl connection string ile bağlanıyorsan, burada da bir URL ve bir model adıyla bağlanıyorsun.

Bilgisayarında hangi modellerin olduğunu ollama list gösterir:

$ ollama list
NAME                       ID              SIZE      MODIFIED
qwen3:4b                   359d7dd4bcda    2.5 GB    7 minutes ago
nomic-embed-text:latest    0a109f422b47    274 MB    8 minutes ago
gemma3:4b                  a2af6cc3eb7f    3.3 GB    5 weeks ago
gemma3:12b                 f4031aab637d    8.1 GB    5 weeks ago

Model adı iki parçadan oluşur: gemma3 modelin adı, iki noktadan sonraki 4b ise tag'i, yani hangi boyutunun kurulu olduğu. Bir model sende yoksa ollama pull gemma3:4b ile indirirsin. Şu an bellekte hangi modellerin yüklü olduğunu ise ollama ps söyler:

$ ollama ps
NAME         ID              SIZE      PROCESSOR    CONTEXT    UNTIL
qwen3:4b     359d7dd4bcda    3.2 GB    100% GPU     4096       4 minutes from now
gemma3:4b    a2af6cc3eb7f    3.7 GB    100% GPU     4096       4 minutes from now

UNTIL sütunu modelin ne kadar süre daha bellekte kalacağını gösteriyor. Ollama bir modeli son istekten sonra varsayılan olarak beş dakika yüklü tutar, sonra boşaltır. CONTEXT sütunundaki 4096'ya dördüncü yazıda ayrıca döneceğiz; çoğu kişinin gözünden kaçan önemli bir sayı.

curl ile İlk İstek

Ollama'nın sohbet endpoint'i /api/chat. Ona bir model adı ve bir mesaj listesi gönderiyorsun:

$ curl -s http://localhost:11434/api/chat -d '{
  "model": "gemma3:4b",
  "messages": [
    { "role": "user", "content": "Recommend one classic science fiction novel in one sentence." }
  ],
  "stream": false,
  "options": { "temperature": 0, "seed": 42 }
}' | jq

messages bir array, çünkü bir sohbet birden fazla mesajdan oluşur; şimdilik içinde user rolüyle tek bir mesaj var. Rolleri beşinci yazıda ayrıntılı göreceğiz. "stream": false cevabın tek parça gelmesini istiyor. options içindeki temperature: 0 ve seed: 42 ise aynı soruya her seferinde aynı cevabı almamızı sağlıyor; böylece curl, C# ve TypeScript çıktılarını yan yana koyabileceğiz. Cevap şu:

{
  "model": "gemma3:4b",
  "created_at": "2026-11-06T20:14:55.44115Z",
  "message": {
    "role": "assistant",
    "content": "H.G. Wells’ *The War of the Worlds* depicts a terrifying Martian invasion of Earth that exposes humanity’s vulnerability and ultimately, its resilience."
  },
  "done": true,
  "done_reason": "stop",
  "total_duration": 1477052125,
  "load_duration": 792250,
  "prompt_eval_count": 20,
  "prompt_eval_cached_count": 15,
  "prompt_eval_duration": 79631000,
  "eval_count": 33,
  "eval_duration": 1395020000
}

Cevabın İçindeki Alanlar

message modelin cevabı; rolü assistant. Bu object'i bir sonraki istekte sohbet geçmişine ekleyeceğiz. done_reason: "stop" modelin cevabı kendi bitirdiğini söylüyor. Bir token sınırına çarpsaydı length görürdük, ilk yazıdaki raw örneğinde olduğu gibi.

Token sayıları iki ayrı alanda. prompt_eval_count modelin okuduğu, yani senin gönderdiğin token sayısı: 20. eval_count modelin ürettiği token sayısı: 33. Bulut sağlayıcıları bu iki sayıyı input ve output token'ları olarak ayrı ayrı fiyatlar, bu yüzden ikisini her zaman ayrı takip etmek gerekir. Gönderdiğimiz cümle 9 kelime ama 20 token tuttu; aradaki farkın bir kısmı Ollama'nın mesajı yerleştirdiği chat template'in özel token'larından geliyor. Bunu üçüncü yazıda ölçeceğiz.

Süreler nanosaniye cinsinden. total_duration isteğin toplam süresi, load_duration modelin belleğe yüklenmesine harcanan süre, prompt_eval_duration prompt'un işlenmesi, eval_duration ise cevabın üretilmesi. Üretim hızını buradan hesaplarsın: 33 token / 1,395 saniye ≈ 23,7 token/saniye.

Açılıştaki sorunun cevabı da burada. Yukarıdaki çıktı ikinci denemeye ait. Model bellekte yokken attığım ilk istekte load_duration 2058644041, yani yaklaşık 2,06 saniyeydi ve total_duration 3,73 saniyeye çıkmıştı. İkinci istekte model zaten yüklü olduğu için load_duration 0,8 milisaniyeye düştü. prompt_eval_cached_count: 15 da başka bir şey anlatıyor: prompt'un 20 token'ının 15'i bir önceki istekten hazırdı, Ollama onları yeniden işlemedi. Yani model daha az düşünmedi; ilk istekte 3,3 GB'lık modeli diskten belleğe taşıma işi vardı. Cevap üretimi iki seferde de yaklaşık 1,3–1,4 saniye sürdü.

C#: IChatClient ile İlk Cevap

C# tarafında Microsoft.Extensions.AI kullanacağız. Bu library, LLM'lerle konuşmak için ortak interface'ler tanımlar; en önemlisi IChatClient. Ollama'ya özel kısmı OllamaSharp paketi sağlıyor: OllamaApiClient class'ı IChatClient'ı implement ediyor. İnternette göreceğin Microsoft.Extensions.AI.Ollama paketi ise deprecated; NuGet sayfası da yerine OllamaSharp'ı öneriyor. Projeyi kuralım:

$ dotnet new console -n Bookstore.FirstRequest
$ cd Bookstore.FirstRequest
$ dotnet add package Microsoft.Extensions.AI --version 10.11.0
$ dotnet add package OllamaSharp --version 5.5.2

Program.cs dosyasının içeriği:

using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "gemma3:4b");

// temperature ve seed curl örneğiyle aynı, cevaplar karşılaştırılabilsin
ChatOptions options = new() { Temperature = 0, Seed = 42 };

ChatResponse response = await client.GetResponseAsync(
    "Recommend one classic science fiction novel in one sentence.", options);

Console.WriteLine(response.Text);
Console.WriteLine($"Model: {response.ModelId}, finish: {response.FinishReason}");
Console.WriteLine($"Input tokens: {response.Usage?.InputTokenCount}, output tokens: {response.Usage?.OutputTokenCount}");

// Ollama'ya özgü süreler AdditionalProperties içinde geliyor
foreach (KeyValuePair<string, object?> item in response.AdditionalProperties ?? [])
{
    Console.WriteLine($"{item.Key}: {item.Value}");
}

dotnet run çıktısı:

H.G. Wells’ *The War of the Worlds* depicts a terrifying Martian invasion of Earth that exposes humanity’s vulnerability and ultimately, its resilience.
Model: gemma3:4b, finish: stop
Input tokens: 20, output tokens: 33
load_duration: 00:00:00.0009382
total_duration: 00:00:01.5002021
prompt_eval_duration: 00:00:00.1534930
eval_duration: 00:00:01.3439110

Cevap curl'dekiyle harfi harfine aynı. GetResponseAsync'e burada tek bir string verdik; method bunu arka planda user rolünde tek bir ChatMessage'a çeviriyor. ChatOptions içindeki Temperature ve Seed, OllamaSharp tarafından Ollama'nın options alanına taşınıyor.

Cevap object'inde de Ollama'nın alanları ortak isimlere dönüşmüş. prompt_eval_count artık Usage.InputTokenCount, eval_count ise Usage.OutputTokenCount. done_reason da FinishReason oldu. Ortak modelde karşılığı olmayan süreler AdditionalProperties dictionary'sinde TimeSpan olarak duruyor. Bu soyutlamanın faydasını on yedinci yazıda göreceğiz: aynı kod, yalnızca IChatClient'ı oluşturan satırı değiştirerek başka bir sağlayıcıyla çalışabiliyor.

TypeScript: ollama Paketiyle İlk Cevap

TypeScript tarafında Ollama'nın resmî ollama paketini kullanacağız. Node 22.2 TypeScript dosyalarını doğrudan çalıştıramadığı için tsx ekliyoruz; top-level await kullanabilmek için de projeyi ES module yapıyoruz:

$ mkdir bookstore-ts && cd bookstore-ts
$ npm init -y
$ npm pkg set type=module
$ npm install ollama@0.6.4
$ npm install -D tsx typescript @types/node

first-request.ts:

import ollama from "ollama";

const response = await ollama.chat({
  model: "gemma3:4b",
  messages: [
    { role: "user", content: "Recommend one classic science fiction novel in one sentence." },
  ],
  options: { temperature: 0, seed: 42 },
});

console.log(response.message.content);
console.log(`Prompt tokens: ${response.prompt_eval_count}, output tokens: ${response.eval_count}`);

// Süreler nanosaniye cinsinden geliyor
const seconds = response.eval_duration / 1e9;
console.log(`Generation: ${seconds.toFixed(2)} s, ${(response.eval_count / seconds).toFixed(1)} tokens/s`);
console.log(`Load: ${(response.load_duration / 1e6).toFixed(1)} ms, total: ${(response.total_duration / 1e6).toFixed(0)} ms`);

npx tsx first-request.ts çıktısı:

H.G. Wells’ *The War of the Worlds* depicts a terrifying Martian invasion of Earth that exposes humanity’s vulnerability and ultimately, its resilience.
Prompt tokens: 20, output tokens: 33
Generation: 0.75 s, 43.8 tokens/s
Load: 0.8 ms, total: 817 ms

Bu paket bir soyutlama katmanı eklemiyor; response object'i curl'de gördüğün JSON'un birebir tipli hâli. Alan adları da aynı: prompt_eval_count, eval_duration ve diğerleri. Varsayılan adres http://127.0.0.1:11434; farklı bir makineye bağlanmak istersen new Ollama({ host: "..." }) ile kendi instance'ını oluşturursun.

Aynı Cevap, Üç Farklı Hız

Üç yoldan da aynı metni aldık, çünkü üçü de aynı endpoint'e aynı ayarlarla gitti. Ama üretim hızına bak: curl'de saniyede 23,7, C#'ta yaklaşık 24,6, TypeScript'te 43,8 token. Kod aynı işi yapıyor; fark makinenin o anki yükünden geliyor. Bu yazıdaki denemeler sırasında aynı Ollama sunucusunu kullanan başka işler de vardı. Yerelde ölçtüğün süreler bu yüzden oynak olur. Tek bir ölçüme bakıp karar verme; on altıncı yazıda tokens/s ölçümünü daha düzgün yapacağız.

Hangi katmanı ne zaman kullanacağına gelince: curl, bir modeli ve bir ayarı hızlıca denemek için ideal. Uygulama kodunda ise C#'ta IChatClient, TypeScript'te ollama paketi işini görür. IChatClient sağlayıcı değiştirmeyi kolaylaştırır ama Ollama'ya özgü her alanı ortak isme taşıyamaz. Ham API'ye ihtiyaç duyduğun anda response.RawRepresentation ile OllamaSharp'ın kendi object'ine inebilirsin.

Sık Düşülen Tuzaklar

İlk tuzak, stream alanını unutmak. Ollama'da streaming varsayılan olarak açık. Aynı curl isteğini "stream": false olmadan gönderirsen tek bir JSON yerine her satırda bir JSON object'i gelir:

{"model":"gemma3:4b","created_at":"2026-11-06T20:17:05.643538Z","message":{"role":"assistant","content":"H"},"done":false}
{"model":"gemma3:4b","created_at":"2026-11-06T20:17:05.669653Z","message":{"role":"assistant","content":"."},"done":false}
{"model":"gemma3:4b","created_at":"2026-11-06T20:17:05.69386Z","message":{"role":"assistant","content":"G"},"done":false}

Bu NDJSON akışını JSON.parse ile tek seferde okumaya çalışırsan hata alırsın. Streaming'i yedinci yazıda bilerek kullanacağız.

İkincisi, model adını eksik yazmak. Tag vermezsen Ollama latest tag'ini arar. Bende gemma3:latest kurulu olmadığı için "model": "gemma3" ile atılan istek HTTP 404 ile döndü:

{"error":"model 'gemma3' not found"}

Üçüncüsü, süreleri milisaniye sanmak. Ham API'de bütün süreler nanosaniye; 1477052125 bir buçuk saniye eder, on yedi gün değil. C# tarafında OllamaSharp bunları TimeSpan'e çevirdiği için bu tuzak orada yok.

Dördüncüsü, ilk isteğin süresini modelin hızı sanmak. İlk istek model yükleme süresini de içerir. Bir benchmark yapacaksan önce bir ısınma isteği at, ölçümü sonrakilerden al.

Küçük Bir Deneme

★☆☆ curl isteğindeki soruyu değiştir ve prompt_eval_count ile eval_count değerlerine bak. Cevabı "one sentence" yerine "three sentences" isteyince eval_count ve eval_duration nasıl değişiyor?

★★☆ ollama ps ile modelin yüklü olduğunu gör, beş dakikadan fazla bekle ve tekrar bak. Model boşaldıktan sonra C# programını çalıştırıp load_duration değerinin ne kadar büyüdüğünü ölç.

★★★ C# örneğini gemma3:12b ile çalıştır. Aynı soru için cevabı, token sayılarını ve tokens/s değerini 4b ile karşılaştır; aradaki farkı on altıncı yazıdaki ölçümle kıyaslamak için not al.

Bir sonraki yazıda bu yazıda gördüğümüz prompt_eval_count ve eval_count sayılarının peşine düşeceğiz: token nedir, tokenizer metni nasıl böler, aynı cümlenin Türkçesi neden İngilizcesinden daha çok token tutar ve kod ile sayılar kaç token'a mal olur, hepsini gerçek ölçümlerle göreceğiz.