Mert Özen Her Satırda Daha İleri
Yapay Zeka .NET ile Yapay Zeka — #01

.NET'te Yapay Zeka Yığını: Microsoft.Extensions.AI, VectorData, Evaluation ve Agent Framework Nerede Durur?

Mert Özen 8 Kas 2026 11 dk 4 görüntülenme
.NET'te Yapay Zeka Yığını: Microsoft.Extensions.AI, VectorData, Evaluation ve Agent Framework Nerede Durur?

Uygulama Microsoft.Extensions.AI ve bir provider paketi alır, library yalnız Abstractions'a bağlanır; VectorData, DataIngestion, Evaluation ve Agent Framework bu IChatClient katmanının üstünde ya da yanında durur. Paket haritasını çıkarıyor, aichatweb template'ini yerel Ollama ile çalıştırıp katman sınırlarında çıkan üç gerçek hatayı düzeltiyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; Microsoft.Extensions.AI 10.11.0, OllamaSharp 5.5.2, Microsoft.Extensions.AI.Templates 10.11.0-preview.3 ve yerel modelde Ollama 0.34.4 (gemma3:4b, qwen3:4b, nomic-embed-text) kullanılmıştır.

Önce Tahmin Et

Bookstore ekibine "kitap öneren bir asistan ekleyelim" dendi. Biri NuGet'te AI (Artificial Intelligence) diye aratıyor ve önüne Microsoft.Extensions.AI, Microsoft.Extensions.AI.Abstractions, Microsoft.Agents.AI, Microsoft.SemanticKernel, Microsoft.Extensions.VectorData.Abstractions, Microsoft.Extensions.DataIngestion, OllamaSharp ve ModelContextProtocol çıkıyor. Hepsi Microsoft imzalı ya da Microsoft'un dokümanında geçiyor. Hangisini kuracaksın?

İki soru daha. Microsoft'un resmî aichatweb template'ini Ollama seçeneğiyle oluşturup kendi makinendeki modellerle çalıştırırsan ilk mesajda cevap alır mısın? Ve öneri mantığını bir class library'ye taşıyacaksan o library hangi pakete bağımlı olmalı?

Bu yazının sonunda paketlerin hangi katmanda durduğunu bileceksin. Template sorusunun cevabı ise "hayır, üç ayrı hatadan sonra evet" olacak; üçünü de gerçek log'larıyla göreceğiz.

Paket Haritası: Hangi Paket Hangi Problemi Çözer?

LLM (Large Language Model) Temelleri serisinde IChatClient'ı kavram olarak kullandık: bir interface, iki method, arkasında hangi model olduğunu bilmeyen kod. Bu seride aynı interface'i gerçek bir ASP.NET Core uygulamasına taşıyacağız. Önce elimizdeki parçaların haritasını çıkaralım. Sürümleri bu yazının yazıldığı gün NuGet'ten kontrol ettim:

PaketSürümNe sağlar
Microsoft.Extensions.AI.Abstractions10.11.0IChatClient, IEmbeddingGenerator, ChatMessage, ChatOptions, AIFunction gibi ortak tipler
Microsoft.Extensions.AI10.11.0ChatClientBuilder ve hazır middleware'ler: function invocation, caching, logging, OpenTelemetry, chat reducer
OllamaSharp5.5.2Ollama provider'ı; IChatClient ve IEmbeddingGenerator'ı doğrudan uygular
Microsoft.Extensions.AI.OpenAI10.11.0OpenAI SDK'sının (Software Development Kit) client'larını IChatClient'a çeviren AsIChatClient()
Microsoft.Extensions.VectorData.Abstractions10.11.0Vector store abstraction'ı: VectorStore, VectorStoreCollection, kayıt attribute'ları
Microsoft.Extensions.DataIngestion10.11.0-preview.1Doküman okuma, chunking ve vector store'a yazma pipeline'ı (preview)
Microsoft.Extensions.AI.Evaluation.*10.11.0Cevap kalitesini ölçen evaluator'lar ve raporlama; NLP (Natural Language Processing) ve Safety paketleri hâlâ preview
Microsoft.Agents.AI1.28.0Microsoft Agent Framework: AIAgent, session, agent middleware, workflow'lar
ModelContextProtocol2.4.0MCP (Model Context Protocol) için resmî C# SDK: server ve client

Tabloyu yukarıdan aşağı okuyunca bir katman yapısı çıkıyor. En altta model var; Ollama'da çalışan gemma3 ya da bulutta bir model. Onun üstünde provider paketi, modelin kendi HTTP (Hypertext Transfer Protocol) API'sini (Application Programming Interface) IChatClient'a çevirir. Onun üstünde MEAI (Microsoft.Extensions.AI) middleware'leri durur. En üstte senin uygulaman ya da Microsoft Agent Framework (MAF) ile yazılmış bir agent. VectorData, DataIngestion ve Evaluation bu dikey hattın yanında, aynı abstraction'ları kullanan yardımcı kolonlar.

01-dotnet-ai-stack-tr.png

Abstraction ve Middleware: Library Hangi Pakete Bağlanır?

Microsoft'un dokümanı ayrımı net yapıyor: abstraction'ı uygulayan library'ler genelde yalnız Microsoft.Extensions.AI.Abstractions'a referans verir; uygulamalar ise Microsoft.Extensions.AI'ı ve bir ya da birkaç provider paketini alır. Bookstore'un öneri mantığını bir class library'ye koyalım ve bu kurala uyalım:

using Microsoft.Extensions.AI;

namespace Bookstore.Recommendations;

// Library yalnız abstraction'ı tanır: hangi provider, hangi model, hangi middleware bilmez
public sealed class BookRecommender(IChatClient chat)
{
    private const string SystemPrompt =
        "You are a bookstore assistant. Recommend exactly one book in one sentence.";

    public async Task<Recommendation> RecommendAsync(string likedBook, CancellationToken ct = default)
    {
        List<ChatMessage> messages =
        [
            new(ChatRole.System, SystemPrompt),
            new(ChatRole.User, $"I liked {likedBook}. What should I read next?"),
        ];

        ChatResponse response = await chat.GetResponseAsync(
            messages, new ChatOptions { Temperature = 0, Seed = 42 }, ct);

        return new Recommendation(response.Text, response.Usage?.InputTokenCount, response.Usage?.OutputTokenCount);
    }
}

public sealed record Recommendation(string Text, long? InputTokens, long? OutputTokens);

Uygulama tarafı ise provider'ı seçer, middleware'leri ekler ve library'ye hazır bir IChatClient verir:

using Bookstore.Recommendations;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using OllamaSharp;

using ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddConsole().SetMinimumLevel(LogLevel.Debug));

// Provider katmanı: OllamaSharp, IChatClient'ı doğrudan uygular
IChatClient provider = new OllamaApiClient(new Uri("http://localhost:11434"), "gemma3:4b");

// Middleware katmanı: Microsoft.Extensions.AI paketinden
IChatClient chat = provider
    .AsBuilder()
    .UseLogging(loggerFactory)
    .Build();

// Pipeline'ın en içindeki provider'ı tanımak için metadata servisini sor
ChatClientMetadata? metadata = chat.GetService<ChatClientMetadata>();
Console.WriteLine($"Provider: {metadata?.ProviderName}, endpoint: {metadata?.ProviderUri}, model: {metadata?.DefaultModelId}");
Console.WriteLine($"Outer client: {chat.GetType().Name}, inner: {chat.GetService<OllamaApiClient>()?.GetType().Name}");

// Uygulama katmanı: library'nin tek bağımlılığı IChatClient
BookRecommender recommender = new(chat);
Recommendation result = await recommender.RecommendAsync("Clean Code");

Console.WriteLine(result.Text);
Console.WriteLine($"Tokens: in={result.InputTokens}, out={result.OutputTokens}");
Provider: ollama, endpoint: http://localhost:11434/, model: gemma3:4b
Outer client: LoggingChatClient, inner: OllamaApiClient
dbug: Microsoft.Extensions.AI.LoggingChatClient[1723383095]
      GetResponseAsync invoked.
dbug: Microsoft.Extensions.AI.LoggingChatClient[1553703230]
      GetResponseAsync completed.
If you enjoyed “Clean Code,” you might find “The Pragmatic Programmer” a fantastic next read to deepen your understanding of software development principles and best practices.
Tokens: in=41, out=33

Çıktıda üç katman da görünüyor. chat değişkeninin gerçek tipi LoggingChatClient, yani en dıştaki middleware. GetService<T>() çağrısı zincir boyunca içeri doğru iner ve en alttaki OllamaApiClient'ı, onun metadata'sını bulur. BookRecommender ise bunların hiçbirini bilmiyor; logging'i çıkarsan, cache eklesen ya da modeli değiştirsen library'nin tek satırı değişmez.

Paket ağacı da bu ayrımı doğruluyor:

dotnet list Bookstore.Recommendations package --include-transitive
   Top-level Package                           Requested   Resolved
   > Microsoft.Extensions.AI.Abstractions      10.11.0     10.11.0

dotnet list Bookstore.Cli package --include-transitive
   Top-level Package                           Requested   Resolved
   > Microsoft.Extensions.AI                   10.11.0     10.11.0
   > Microsoft.Extensions.Logging.Console      10.0.14     10.0.14
   > OllamaSharp                               5.5.2       5.5.2

Library'nin tek bağımlılığı Abstractions. Bu önemli, çünkü library'ni kullanan ekip başka bir provider, başka bir MEAI sürümü ya da kendi middleware'leriyle çalışabilir. Library OllamaSharp'a bağlansaydı, onu kullanan herkes Ollama paketini de taşımak zorunda kalırdı.

Provider Katmanı: Ollama, OpenAI ve Azure

Provider paketi modelin kendi protokolünü konuşur. Ollama için bu seride OllamaSharp kullanıyoruz; OllamaApiClient hem IChatClient hem IEmbeddingGenerator uygular. Microsoft'un eskiden yayımladığı Microsoft.Extensions.AI.Ollama paketi preview'dan hiç çıkmadı ve NuGet'te deprecated işaretli; Microsoft Learn örnekleri de artık OllamaSharp kullanıyor.

OpenAI ve OpenAI uyumlu servisler için Microsoft.Extensions.AI.OpenAI paketi, resmî OpenAI .NET SDK'sının ChatClient'ını AsIChatClient() ile çevirir. Azure OpenAI için Azure.AI.OpenAI paketindeki AzureOpenAIClient aynı extension'la kullanılır. LLM Temelleri serisinin "Sağlayıcıdan Bağımsız LLM Kodu" yazısında provider'ı configuration'dan seçen kodu yazmıştık; burada tekrar etmiyorum. Bu seride bulut sağlayıcı kodu derlenip gösterilecek ama çalıştırılmayacak: API anahtarı kullanmıyoruz, bütün gerçek çıktılar yerel Ollama'dan.

Veri, Ölçüm ve Agent Katmanları

Haritanın yan kolonlarına geçelim. Her biri bu serinin ya da ileriki serilerin bir yazısına karşılık geliyor.

Microsoft.Extensions.VectorData, vector store'lar için IChatClient'ın yaptığını yapar: bir kayıt modelini attribute'larla tanımlarsın, InMemory, PostgreSQL (pgvector), Qdrant, Redis ya da SQL (Structured Query Language) Server provider'ı arkada değişir. Provider paketlerinin adı da değişti; eskiden Microsoft.SemanticKernel.Connectors.* olanlar bugün NuGet'te CommunityToolkit.VectorData.* adıyla duruyor (ör. CommunityToolkit.VectorData.PgVector 1.0.2). Ayrıntısı bu serinin 9. yazısında.

Microsoft.Extensions.DataIngestion, dokümanları okuyup chunk'lara bölen ve vector store'a yazan pipeline'dır. Bugün hâlâ preview; production'a almadan önce bunu bilmen gerekiyor. RAG (Retrieval-Augmented Generation) pipeline'ının tamamı ilerideki RAG serisinin konusu.

Microsoft.Extensions.AI.Evaluation, "model iyi cevap veriyor mu?" sorusunu test koduna çevirir: relevance, coherence gibi LLM tabanlı evaluator'lar, LLM gerektirmeyen NLP metrikleri, response caching ve dotnet aieval raporu. 10. yazıda xUnit içinde çalıştıracağız.

Microsoft Agent Framework ise en üst katman. Microsoft Learn'deki tanıma göre MAF, Semantic Kernel ve AutoGen'i yapan ekiplerin ikisinin doğrudan devamı olarak yazdığı framework; AutoGen'in basit agent abstraction'larını Semantic Kernel'in session yönetimi, middleware ve telemetry gibi kurumsal özellikleriyle birleştiriyor. Paket bağımlılığı da bunu gösteriyor: Microsoft.Agents.AI 1.28.0, Microsoft.Extensions.AI 10.11.0'a bağımlı. Yani MAF kendi model katmanını getirmiyor, IChatClient'ın üstüne kuruluyor. Dokümandaki şu cümle de seri boyunca aklında olsun: bir görevi fonksiyonla çözebiliyorsan agent yerine fonksiyon yaz. MAF'a 11. yazıda geçeceğiz.

Son parça MCP. ModelContextProtocol paketi, tool ve kaynakları standart bir protokolle dışarı açan server'ları ve onları kullanan client'ları yazmak için resmî C# SDK. Agent pattern'leri ve MCP ilerideki AI Agent'ları ve MCP serisinde.

aichatweb Template'ini Katman Katman Okumak

Haritayı kâğıt üstünde bırakmayalım. Microsoft'un .NET AI template'i bu katmanların neredeyse hepsini tek projede kuruyor. Template paketini kurup Ollama seçeneğiyle, makinemde zaten olan modelleri vererek bir proje oluşturdum (template'in varsayılanı llama3.2 ve all-minilm):

dotnet new install Microsoft.Extensions.AI.Templates::10.11.0-preview.3.26513.7
dotnet new aichatweb -n BookstoreChat --provider ollama --ChatModel gemma3:4b --EmbeddingModel nomic-embed-text

Oluşan Program.cs'in AI ile ilgili kısmı şu:

IChatClient chatClient = new OllamaApiClient(new Uri("http://localhost:11434"),
    "gemma3:4b");
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator = new OllamaApiClient(new Uri("http://localhost:11434"),
    "nomic-embed-text");

var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db");
var vectorStoreConnectionString = $"Data Source={vectorStorePath}";
builder.Services.AddSqliteVectorStore(_ => vectorStoreConnectionString);
builder.Services.AddSqliteCollection<Guid, IngestedChunk>(IngestedChunk.CollectionName, vectorStoreConnectionString);

builder.Services.AddSingleton<DataIngestor>();
builder.Services.AddSingleton<SemanticSearch>();
builder.Services.AddKeyedSingleton("ingestion_directory", new DirectoryInfo(Path.Combine(builder.Environment.WebRootPath, "Data")));
builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging();
builder.Services.AddEmbeddingGenerator(embeddingGenerator);

Katmanları buradan okuyabilirsin. Provider: iki OllamaApiClient, biri chat, biri embedding için. Middleware: AddChatClient(...)'ın döndürdüğü builder'a eklenen UseFunctionInvocation() ve UseLogging(); DI (Dependency Injection) kaydını 3. yazıda ayrıntılı göreceğiz. VectorData: AddSqliteVectorStore ve AddSqliteCollection, preview durumdaki CommunityToolkit.VectorData.SqliteVec paketinden. DataIngestion: DataIngestor içinde IngestionPipeline, SemanticSimilarityChunker ve VectorStoreWriter. Uygulama: Blazor sayfası, iki tool'u (LoadDocuments ve Search) ChatOptions.Tools'a koyup cevabı stream ediyor.

Çalıştırıp ilk soruyu sorduğumda sayfada "An unhandled error has occurred" yazdı. Log'daki ilk hata:

fail: Microsoft.Extensions.AI.LoggingChatClient[1784604714]
      GetStreamingResponseAsync failed.
      System.Text.Json.JsonException: The JSON value could not be converted to System.String. Path: $.properties.filenameFilter.type | LineNumber: 0 | BytePositionInLine: 265.
       ---> System.InvalidOperationException: Cannot get the value of a token type 'StartArray' as a string.

Template .csproj'a OllamaSharp 5.4.12 yazıyor, Microsoft.Extensions.AI ise 10.11.0. MEAI'nin yeni sürümü nullable bir tool parametresinin (string? filenameFilter) JSON (JavaScript Object Notation) şemasında tipi ["string","null"] diye array olarak üretiyor; eski OllamaSharp bunu tek bir string bekleyerek okuyor ve patlıyor. OllamaSharp'ı 5.5.2'ye yükseltince bu hata kayboldu. İkinci hata:

fail: Microsoft.Extensions.AI.LoggingChatClient[1784604714]
      GetStreamingResponseAsync failed.
      OllamaSharp.Models.Exceptions.ModelDoesNotSupportToolsException: registry.ollama.ai/library/gemma3:4b does not support tools

Template'in sohbet akışı tool calling'e dayanıyor, gemma3 ise tool desteklemiyor. Chat modelini tool destekleyen qwen3:4b yaptım. Üçüncü hata ingestion sırasında geldi:

fail: Microsoft.Extensions.DataIngestion.IngestionPipeline[6]
      An error occurred while ingesting document 'Example_Emergency_Survival_Kit.pdf'.
      Microsoft.Extensions.VectorData.VectorStoreException: Call to vector store failed.
       ---> Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 1: 'Dimension mismatch for inserted vector for the "embedding" column. Expected 384 dimensions but received 768.'.

Template, IngestedChunk class'ında vector boyutunu 384 olarak sabitlemiş; yanındaki yorum da "384 is the default vector size for the nomic-embed-text embedding model" diyor. Oysa 384, template'in varsayılan modeli all-minilm'in boyutu; nomic-embed-text 768 boyutlu vector üretiyor ve SQLite tarafındaki kolon bunu reddediyor. VectorDimensions'ı 768 yapıp eski vector-store.db'yi silince iki doküman da Succeeded: 'True' ile yazıldı. Örnek dokümandaki GPS (Global Positioning System) saatini sorduğumda asistan önce "Loading relevant documents", sonra "Searching: GPS watch functions" adımlarını gösterdi ve saat hakkında doğru bir cevap verdi. Sistem prompt'u cevabın sonunda kaynak gösteren bir citation etiketi istiyordu; 4B parametrelik model bu etiketi üretmedi.

Üç hatanın üçü de katman sınırında çıktı: provider ile MEAI'nin sürüm uyumu, uygulamanın istediği yetenek (tool calling) ile modelin sunduğu yetenek, embedding modeli ile vector store şeması. Bu seride bu sınırları tek tek ele alacağız.

Seri Boyunca Kurgu

Template bir başlangıç noktası; seride onun yerine adım adım büyüyen bir ASP.NET Core Web API'si yazacağız: Bookstore.Assistant.Api. Domain sitenin diğer serilerindeki Bookstore'un aynısı: Book, Author, Order, Customer, Inventory. Modeller rollerine göre seçili: varsayılan sohbet için gemma3:4b, "güçlü model" için gemma3:12b, tool calling ve reasoning modelinin davranışını göstermek için qwen3:4b, embedding için nomic-embed-text. Her yazının sürüm notu o yazıda gerçekten hangi modelin koştuğunu söylüyor. Hepsi yerel Ollama'da. Bulut sağlayıcıyla nerede ne değişeceğini her yazıda ayrıca söyleyeceğim.

LLM Temelleri'nde anlatılanları tekrar etmeyeceğim: token ve tokenizer, context window, sampling, prompt yazımı, structured output, tool calling'in mantığı, embedding ve maliyet hesabı orada. Buradaki soru "nasıl çalışır?" değil, "production'da nasıl güvenilir çalışır?".

Üretimde Dikkat

Sürüm uyumunu tek yerden yönet. MEAI ayda bir minor sürüm çıkarıyor, provider paketleri kendi takvimiyle ilerliyor. Template örneğinde olduğu gibi derleme başarılı olsa bile çalışma anında şema uyuşmazlığı çıkabilir. Central Package Management (Directory.Packages.props) ile sürümleri tek dosyada tut, MEAI'yi yükselttiğinde provider'ı da yükselt ve en az bir tool calling isteğini uçtan uca çalıştıran bir smoke test bulundur.

Preview etiketini ciddiye al. DataIngestion, SqliteVec provider'ı, Evaluation'ın NLP ve Safety paketleri ve MAF'ın hosting paketleri bugün preview. Preview paketin API'si minor sürümde kırılabilir. Production'a alacaksan sürümü sabitle, değişiklik notlarını takip et ve kodu küçük bir adapter arkasında tut.

Model yeteneğini açılışta doğrula. "Bu model tool destekliyor mu?", "embedding boyutu kaç?" sorularının cevabı configuration'dan gelir ve yanlış olabilir. Uygulama açılırken tek bir embedding üretip boyutunu şemayla karşılaştırmak ya da tool'lu küçük bir istek atmak, hatayı ilk kullanıcıdan önce yakalar.

Sık Düşülen Tuzaklar

Birincisi, Semantic Kernel'i hâlâ "varsayılan" sanmak. Semantic Kernel NuGet'te güncellenmeye devam ediyor (bugün 1.82.0), ama Microsoft yeni kod için Microsoft.Extensions.AI'ı ve agent'lar için MAF'ı gösteriyor. Eski bir blog yazısındaki Kernel tabanlı kodla yeni proje başlatmak, yarın taşıma işi demek. Tarihçe ve taşıma 14. yazıda.

İkincisi, preview paketi fark etmeden production'a almak. dotnet new aichatweb sana preview DataIngestion ve preview SqliteVec paketleriyle bir proje veriyor. Template prototip için; hangi paketin preview olduğunu .csproj'a bakarak bilmen gerekiyor.

Üçüncüsü, library yazarken concrete provider'a bağlanmak. BookRecommender OllamaApiClient isteseydi test için sahte bir client veremezdin, başka ekip OpenAI'a geçemezdi. Library IChatClient ister; provider'ı uygulama seçer.

Dördüncüsü, template'in yorumlarına ve sabitlerine güvenmek. Vector boyutu örneğinde yorum yanlıştı ve hata ancak ilk ingestion'da çıktı. Model değiştirdiğinde boyut, tool desteği ve context window'u modelin kendi dokümanından kontrol et.

Küçük Bir Deneme

★☆☆ Bookstore.Cli'deki modeli gemma3:12b yap ve aynı soruyu sor. Provider satırı ve token sayıları nasıl değişti? BookRecommender'da tek satır değiştirmen gerekti mi?

★★☆ aichatweb projesinde embedding modelini değiştirmeden VectorDimensions'ı tekrar 384 yap, ama bu sefer vector-store.db'yi silme. Hangi hatayı alıyorsun? Var olan bir collection'ın şemasıyla yeni kodun şeması uyuşmadığında ne olması gerektiğini düşün.

★★★ Bookstore.Recommendations için bir test projesi aç ve IChatClient'ı uygulayan, sabit bir cevap dönen küçük bir sahte client yaz. RecommendAsync'in modele giden mesajları doğru kurduğunu Ollama'ya hiç gitmeden doğrula. 10. yazıda bu fikri büyüteceğiz.

Harita hazır. Bir sonraki yazıda haritanın ortasındaki katmana, MEAI middleware pipeline'ına iniyoruz: ChatClientBuilder zinciri nasıl kuruluyor, sıra neden önemli ve Bookstore için token bütçesi ile kişisel veri maskeleyen kendi DelegatingChatClient'larımızı nasıl yazacağız.