LLM Evaluation: .NET'te AI Çıktı Kalitesini Ölçme ve İzleme

LLM'leri production'da güvenle kullanmak için yanıt kalitesini sürekli ölçmek şarttır. Modeller zaman içinde drift gösterebilir, yeni prompt değişiklikleri beklenmedik yan etkilere yol açabilir ya da belirli konu kategorilerinde performans düşebilir. Bu yazıda .NET'te LLM değerlendirme çerçevesi kurmayı göstereceğiz.

Temel Değerlendirme Metrikleri

  • Faithfulness: Yanıt, kaynak belgelerdeki bilgilerle tutarlı mı?
  • Answer Relevance: Yanıt, soruyla ne kadar ilgili?
  • Context Recall: İlgili bağlam doğru şekilde alındı mı?
  • Hallucination Rate: Model bağlamda olmayan bilgi üretti mi?
  • Toxicity: Yanıt zararlı içerik barındırıyor mu?

G-Eval ile LLM-as-Judge

Bir LLM kullanarak başka bir LLM'in yanıtlarını değerlendirin:

public class GEvalService
{
    private readonly ChatClient _judgeClient;

    public async Task<EvaluationScore> EvaluateFaithfulnessAsync(
        string question, string context, string answer)
    {
        var prompt = $"""
            Aşağıdaki soruya verilen yanıtın, sağlanan BAĞLAM ile ne kadar tutarlı olduğunu değerlendir.
            Yanıt yalnızca bağlamda bulunan bilgilere dayanmalıdır.

            SORU: {question}

            BAĞLAM:
            {context}

            YANIT:
            {answer}

            GÖREV:
            1. Yanıttaki her iddiayı bağlamla karşılaştır
            2. Bağlamda desteklenmeyen iddiaları işaretle
            3. 0.0 ile 1.0 arasında bir puan ver (1.0 = tamamen tutarlı)

            JSON olarak yanıtla:
            {{
              "score": 0.0,
              "reasoning": "...",
              "unsupported_claims": ["..."]
            }}
            """;

        var result = await _judgeClient.CompleteChatAsync(
            [
                new SystemChatMessage("Sen tarafsız bir AI değerlendirme uzmanısın."),
                new UserChatMessage(prompt)
            ],
            new ChatCompletionOptions
            {
                ResponseFormat = ChatResponseFormat.JsonObject,
                Temperature = 0,
                ModelId = "gpt-4o"
            });

        return JsonSerializer.Deserialize<EvaluationScore>(result.Value.Content[0].Text)!;
    }

    public async Task<EvaluationScore> EvaluateAnswerRelevanceAsync(
        string question, string answer)
    {
        var prompt = $"""
            Bu soruya verilen yanıtın soruyla ne kadar ilgili olduğunu değerlendir.

            SORU: {question}
            YANIT: {answer}

            0.0-1.0 arası puan ver. JSON: {{"score": 0.0, "reasoning": "..."}}
            """;

        var result = await _judgeClient.CompleteChatAsync(
            [new UserChatMessage(prompt)],
            new ChatCompletionOptions
            {
                ResponseFormat = ChatResponseFormat.JsonObject,
                Temperature = 0
            });

        return JsonSerializer.Deserialize<EvaluationScore>(result.Value.Content[0].Text)!;
    }
}

public record EvaluationScore(
    double Score,
    string Reasoning,
    IReadOnlyList<string>? UnsupportedClaims = null);

RAGAS Benzeri Otomatik Değerlendirme

public class RagEvaluator
{
    private readonly GEvalService _eval;
    private readonly ILogger<RagEvaluator> _logger;

    public async Task<RagEvaluationReport> EvaluateAsync(
        IEnumerable<RagTestCase> testCases)
    {
        var scores = new List<RagEvaluationRecord>();

        foreach (var tc in testCases)
        {
            var faithfulness = await _eval.EvaluateFaithfulnessAsync(
                tc.Question, tc.Context, tc.ActualAnswer);
            var relevance = await _eval.EvaluateAnswerRelevanceAsync(
                tc.Question, tc.ActualAnswer);

            // Ground truth varsa BLEU/ROUGE benzeri string benzerliği
            double? exactMatch = tc.ExpectedAnswer != null
                ? CalculateSemanticSimilarity(tc.ExpectedAnswer, tc.ActualAnswer)
                : null;

            scores.Add(new RagEvaluationRecord(
                Question: tc.Question,
                FaithfulnessScore: faithfulness.Score,
                RelevanceScore: relevance.Score,
                ExactMatchScore: exactMatch,
                HasHallucination: faithfulness.Score < 0.5));

            _logger.LogInformation(
                "Q: {Q} | Faithfulness: {F:F2} | Relevance: {R:F2}",
                tc.Question[..Math.Min(50, tc.Question.Length)],
                faithfulness.Score, relevance.Score);
        }

        return new RagEvaluationReport(
            TotalCases: scores.Count,
            AvgFaithfulness: scores.Average(s => s.FaithfulnessScore),
            AvgRelevance: scores.Average(s => s.RelevanceScore),
            HallucinationRate: scores.Count(s => s.HasHallucination) / (double)scores.Count,
            Records: scores);
    }
}

public record RagTestCase(string Question, string Context, string ActualAnswer, string? ExpectedAnswer = null);
public record RagEvaluationRecord(
    string Question, double FaithfulnessScore, double RelevanceScore,
    double? ExactMatchScore, bool HasHallucination);

Sürekli İzleme (Production)

public class LlmMonitoringMiddleware
{
    private readonly RequestDelegate _next;
    private readonly LlmEvalSampler _sampler;
    private readonly IMetrics _metrics;

    public async Task InvokeAsync(HttpContext context)
    {
        await _next(context);

        // Her 10 istekte bir değerlendirme örneği al
        if (_sampler.ShouldSample(rate: 0.1))
        {
            var requestLog = context.Items["LlmRequest"] as LlmRequestLog;
            if (requestLog != null)
            {
                _ = Task.Run(async () =>
                {
                    var eval = await _evaluator.EvaluateAsync([new RagTestCase(
                        requestLog.Question,
                        requestLog.Context,
                        requestLog.Answer)]);

                    // Prometheus / OpenTelemetry metriklerine kaydet
                    _metrics.Gauge("llm.faithfulness", eval.AvgFaithfulness);
                    _metrics.Gauge("llm.relevance", eval.AvgRelevance);
                    _metrics.Gauge("llm.hallucination_rate", eval.HallucinationRate);
                });
            }
        }
    }
}

Test Seti Oluşturma

public class TestSetGenerator
{
    private readonly ChatClient _llm;

    // Belgelerden otomatik soru-cevap çiftleri üret
    public async Task<IReadOnlyList<RagTestCase>> GenerateTestSetAsync(
        IEnumerable<string> documents, int questionsPerDoc = 5)
    {
        var testCases = new List<RagTestCase>();

        foreach (var doc in documents)
        {
            var prompt = $"""
                Aşağıdaki belgeden {questionsPerDoc} farklı soru-cevap çifti üret.
                Sorular gerçekçi ve çeşitli olsun (ne, neden, nasıl, kim vb.).

                BELGE:
                {doc}

                JSON array olarak: [{{"question":"...","answer":"..."}}]
                """;

            var result = await _llm.CompleteChatAsync(
                [new UserChatMessage(prompt)],
                new ChatCompletionOptions { ResponseFormat = ChatResponseFormat.JsonObject });

            var pairs = JsonSerializer.Deserialize<List<QaPair>>(
                result.Value.Content[0].Text)!;

            testCases.AddRange(pairs.Select(p =>
                new RagTestCase(p.Question, doc, "", p.Answer)));
        }

        return testCases;
    }
}

public record QaPair(string Question, string Answer);

Regression Testing Pipeline

// dotnet test ile LLM regresyon testleri
public class LlmRegressionTests
{
    private readonly RagEvaluator _evaluator;

    [Theory]
    [MemberData(nameof(GetTestCases))]
    public async Task ResponseQuality_ShouldMeetThresholds(RagTestCase testCase)
    {
        var report = await _evaluator.EvaluateAsync([testCase]);

        Assert.True(report.AvgFaithfulness >= 0.8,
            $"Faithfulness too low: {report.AvgFaithfulness:F2}");
        Assert.True(report.AvgRelevance >= 0.75,
            $"Relevance too low: {report.AvgRelevance:F2}");
        Assert.False(report.HallucinationRate > 0.1,
            "Hallucination rate exceeds threshold");
    }
}

Değerlendirme Sonuçlarını Görselleştirme

Grafana veya Application Insights ile metrikler:

  • Faithfulness Trend: Zaman içinde güvenilirlik değişimi
  • Hallucination Rate: Konu bazlı halüsinasyon oranı
  • P95 Latency: Yanıt süresi dağılımı
  • Token Efficiency: Kalite/maliyet oranı

Sonuç

LLM değerlendirmesi, üretim ortamında AI güvenilirliğinin temeli; eksik kalırsa modeli kör uçar gibi kullanırsınız. G-Eval ve RAGAS benzeri yaklaşımlar, otomatik değerlendirmeyi mümkün kılar. Başlangıç için faithfulness ve answer relevance metriklerini ölçmeye başlayın; zamanla test setinizi büyütün ve CI/CD pipeline'ınıza regresyon testleri ekleyin.