LLM Maliyet Optimizasyonu: Token Yönetimi ve Akıllı Önbellek

LLM API'leri güçlüdür ama maliyetleri hızla birikebilir. GPT-4o ile çalışan bir üretim uygulaması aylık binlerce dolar harcayabilir. Bu yazıda .NET'te uygulayabileceğiniz somut optimizasyon tekniklerini ele alacağız.

1. Token Sayımı ve Bütçe Takibi

using Microsoft.ML.Tokenizers;

public class TokenBudgetService
{
    private readonly Tokenizer _tokenizer;
    private readonly IDistributedCache _cache;

    public TokenBudgetService()
    {
        // TiktokenTokenizer — GPT modellerinin kullandığı tokenizer
        _tokenizer = TiktokenTokenizer.CreateForModel("gpt-4o");
    }

    public int CountTokens(string text) =>
        _tokenizer.CountTokens(text);

    public int CountChatTokens(IEnumerable<ChatMessage> messages)
    {
        // Her mesaj yaklaşık 4 token overhead taşır
        const int perMessageOverhead = 4;
        const int replyPriming = 3;

        return messages.Sum(m => CountTokens(m.Content ?? "") + perMessageOverhead)
               + replyPriming;
    }

    public async Task<bool> CheckBudgetAsync(string userId, int estimatedTokens)
    {
        var key = $"token_budget:{userId}:{DateTime.UtcNow:yyyyMMdd}";
        var used = int.Parse(await _cache.GetStringAsync(key) ?? "0");
        const int dailyLimit = 100_000;
        return used + estimatedTokens <= dailyLimit;
    }

    public async Task RecordUsageAsync(string userId, int tokensUsed)
    {
        var key = $"token_budget:{userId}:{DateTime.UtcNow:yyyyMMdd}";
        var used = int.Parse(await _cache.GetStringAsync(key) ?? "0");
        await _cache.SetStringAsync(key, (used + tokensUsed).ToString(),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.UtcNow.Date.AddDays(1)
            });
    }
}

2. Semantik Önbellekleme

Aynı veya çok benzer sorular için LLM çağırmak yerine önceki yanıtları döndürün:

public class SemanticCacheService
{
    private readonly ITextEmbeddingGenerationService _embedding;
    private readonly IVectorStoreRecordCollection<string, CachedResponse> _store;
    private const double SimilarityThreshold = 0.95;

    public async Task<string?> GetCachedResponseAsync(string prompt)
    {
        var embedding = await _embedding.GenerateEmbeddingAsync(prompt);

        await foreach (var result in _store.VectorizedSearchAsync(embedding,
            new VectorSearchOptions { Top = 1 }))
        {
            if (result.Score >= SimilarityThreshold)
                return result.Record.Response;
        }

        return null;
    }

    public async Task CacheResponseAsync(string prompt, string response)
    {
        var embedding = await _embedding.GenerateEmbeddingAsync(prompt);
        await _store.UpsertAsync(new CachedResponse
        {
            Id = Guid.NewGuid().ToString(),
            Prompt = prompt,
            Response = response,
            Embedding = embedding,
            CachedAt = DateTime.UtcNow
        });
    }
}

public record CachedResponse
{
    [VectorStoreRecordKey]
    public string Id { get; set; } = "";
    [VectorStoreRecordData]
    public string Prompt { get; set; } = "";
    [VectorStoreRecordData]
    public string Response { get; set; } = "";
    [VectorStoreRecordData]
    public DateTime CachedAt { get; set; }
    [VectorStoreRecordVector(Dimensions: 1536)]
    public ReadOnlyMemory<float> Embedding { get; set; }
}

3. Model Rotasyonu: Göreye Göre Model Seç

public class AdaptiveModelRouter
{
    public string SelectModel(string prompt, TaskComplexity complexity)
    {
        var tokenCount = _tokenizer.CountTokens(prompt);

        return complexity switch
        {
            TaskComplexity.Simple when tokenCount < 500 => "gpt-4o-mini",
            TaskComplexity.Simple => "gpt-4o-mini",
            TaskComplexity.Medium when tokenCount < 2000 => "gpt-4o",
            TaskComplexity.Complex => "gpt-4o",
            _ => "gpt-4o-mini"
        };
    }

    public TaskComplexity ClassifyTask(string prompt)
    {
        // Anahtar kelime tabanlı basit sınıflandırma
        var complexKeywords = new[] { "analiz", "compare", "explain in detail",
            "step by step", "architecture", "design pattern" };

        if (complexKeywords.Any(kw => prompt.Contains(kw, StringComparison.OrdinalIgnoreCase)))
            return TaskComplexity.Complex;

        return prompt.Length > 500 ? TaskComplexity.Medium : TaskComplexity.Simple;
    }
}

public enum TaskComplexity { Simple, Medium, Complex }

4. Sistem Promptunu Optimize Etme

// Kötü: Her istekte tekrar gönderilen uzun sistem promptu
var badPrompt = """
    Sen yardımcı bir asistansın. Kullanıcıların sorularını nazikçe,
    kibar bir dille, Türkçe olarak yanıtla. Teknik terimleri açıkla.
    Asla uygunsuz içerik üretme. Her zaman doğru bilgi ver.
    Bilmediğinde belirt. Kısa ve öz ol ama gerektiğinde detay ver.
    """; // ~60 token, her istekte gönderiliyor

// İyi: Kısa ve öz sistem promptu
var goodPrompt = "Türkçe teknik destek asistanı. Kısa, net yanıtlar ver.";
// ~12 token — aynı davranış, %80 daha az token

5. Prompt Sıkıştırma

public class PromptCompressor
{
    // Konuşma geçmişini özetleyerek token sayısını düşür
    public async Task<string> SummarizeConversationAsync(
        IEnumerable<ChatMessage> history,
        int keepLastN = 3)
    {
        var messages = history.ToList();
        if (messages.Count <= keepLastN + 2) // system + son N
            return BuildContextFromMessages(messages);

        var toSummarize = messages[1..^keepLastN]; // system ve son N hariç
        var summaryPrompt = $"""
            Şu konuşmayı 3 cümleyle özetle, önemli gerçekleri koru:

            {string.Join("\n", toSummarize.Select(m => $"{m.Role}: {m.Content}"))}
            """;

        var summary = await _kernel.InvokePromptAsync(summaryPrompt,
            new KernelArguments(new OpenAIPromptExecutionSettings
            {
                MaxTokens = 200,
                ModelId = "gpt-4o-mini" // Özet için ucuz model
            }));

        // Özet + son N mesajı birleştir
        var compressed = new List<ChatMessage> { messages[0] }; // system
        compressed.Add(new(AuthorRole.System, $"[Önceki konuşma özeti: {summary}]"));
        compressed.AddRange(messages[^keepLastN..]);
        return BuildContextFromMessages(compressed);
    }
}

6. Maliyet İzleme Dashboard

public class LlmCostTracker
{
    private static readonly Dictionary<string, (decimal InputPer1K, decimal OutputPer1K)> Pricing = new()
    {
        ["gpt-4o"]           = (0.0025m, 0.01m),
        ["gpt-4o-mini"]      = (0.00015m, 0.0006m),
        ["text-embedding-3-small"] = (0.00002m, 0m)
    };

    public decimal CalculateCost(string model, int inputTokens, int outputTokens)
    {
        if (!Pricing.TryGetValue(model, out var price)) return 0;
        return (inputTokens / 1000m * price.InputPer1K)
             + (outputTokens / 1000m * price.OutputPer1K);
    }

    public async Task RecordAsync(LlmUsageRecord record)
    {
        record.EstimatedCostUsd = CalculateCost(record.Model, record.InputTokens, record.OutputTokens);
        await _repository.SaveAsync(record);
    }
}

Özet: Maliyet Azaltma Stratejileri

StratejiTipik TasarrufUygulama Kolaylığı
Semantik önbellekleme%30-60Orta
Ucuz modele yönlendirme%50-80Kolay
Sistem promptu kısaltma%10-20Çok kolay
Konuşma geçmişi özeti%20-40Orta
Önceden hesaplama%15-30Zor

Sonuç

LLM maliyetlerini kontrol altında tutmak, ölçeklenen uygulamalar için kritik öneme sahiptir. Semantik önbellekleme ve model rotasyonunu birlikte uygulayarak maliyetlerinizi genellikle %60-70 oranında düşürebilirsiniz. Her stratejiyi A/B testleriyle doğrulayın ve yanıt kalitesini koruyun.