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

Microsoft Agent Framework'e Giriş: IChatClient'tan AIAgent'a

Mert Özen 8 Kas 2026 9 dk 6 görüntülenme
Microsoft Agent Framework'e Giriş: IChatClient'tan AIAgent'a

AsAIAgent, bir IChatClient'ı ChatClientAgent'a çevirir; agent talimatı, function invocation'ı ve AgentSession ile sohbet geçmişini üstlenir ama modeli akıllandırmaz. Aynı Bookstore asistanını önce düz IChatClient, sonra agent olarak yazıp RunAsync, RunStreamingAsync ve AgentResponse'u gerçek çıktılarla karşılaştırıyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; Microsoft.Agents.AI 1.28.0, Microsoft.Extensions.AI 10.11.0, OllamaSharp 5.5.2 ve Ollama 0.34.4 (qwen3:4b) kullanılmıştır.

Önce Tahmin Et

Bookstore asistanı bu seride bir IChatClient pipeline'ı olarak büyüdü: function invocation, telemetry, cache, rate limiting. Şimdi aynı asistanı Microsoft Agent Framework (MAF) ile bir "agent" olarak yazacağız. Sence ne değişir? Model daha akıllı mı olur? Tool'ları kendi kendine mi keşfeder? Sohbet geçmişini kalıcı olarak mı saklar?

Tahminini bir kenara yaz. Aynı iki soruyu önce düz IChatClient'la, sonra agent olarak soracağız ve farkı satır satır göreceğiz. Cevap çoğu kişinin beklediğinden daha mütevazı. Bu, agent'ın değersiz olduğu anlamına gelmiyor. Neyi çözdüğünü doğru bilmen gerekiyor.

Agent Nedir, Ne Değildir?

MAF dokümantasyonu agent'ı LLM (Large Language Model) kullanarak girdiyi işleyen, tool çağıran ve cevap üreten bir bileşen olarak tanımlıyor. .NET'teki temel tip AIAgent. En sık kullanacağın uygulaması da ChatClientAgent: herhangi bir IChatClient'ı sarıp agent'a çeviren class. Yani MAF, bu seride kurduğumuz Microsoft.Extensions.AI (MEAI) katmanının yerine geçmiyor, onun üstüne oturuyor.

Dokümantasyondaki "agent mı, workflow mu?" tablosu da bu yazının sınırını çiziyor. Görev açık uçlu ya da sohbet biçimindeyse, model tool'ları kendisi seçip planlayacaksa ve tek bir LLM çağrısı (tool'larla birlikte) yetiyorsa agent kullan. Adımlar baştan belliyse, çalışma sırasını sen kontrol etmek istiyorsan ya da birden fazla agent ve fonksiyon koordine olacaksa workflow kullan. Tablonun altında da tek cümlelik bir kural var: bir görevi bir fonksiyonla çözebiliyorsan agent yerine fonksiyonu yaz. "Sipariş 1042'nin durumu nedir?" sorusunu bir form ve bir veritabanı sorgusu da cevaplar. Agent, kullanıcının bunu serbest dille, takip sorularıyla sorduğu durumda anlam kazanır.

Agent pattern'leri, multi-agent workflow'lar ve MCP (Model Context Protocol) ilerideki AI (Artificial Intelligence) Agent'ları ve MCP serisinin konusu. Bu yazı tek bir agent'ın .NET'teki temel API'sine (Application Programming Interface) odaklanıyor. Provider'a özgü agent tipleri de var: örneğin Foundry'de sunucu tarafında yaşayan agent'lar için AsAIAgent'ın kayıtlı bir agent'ı getiren overload'ları. Ama hepsi aynı AIAgent soyutlamasını uyguluyor; bu yazıda yazdığın kod, agent'ın arkasında hangi servis olursa olsun aynı kalır.

Başlangıç Noktası: Düz IChatClient

Önce bildiğimiz yol. Tool class'ı çağrı sayısını da tutuyor:

public sealed class OrderTools
{
    public int Calls { get; private set; }

    [Description("Returns the shipping status of a Bookstore order.")]
    public string GetOrderStatus([Description("The order number")] int orderId)
    {
        Calls++;
        return orderId == 1042 ? "Shipped on 2026-11-06, 2 items, tracking BKS-1042." : "Order not found.";
    }
}

Program.cs iki modu da içeriyor; önce ortak kısım ve chat modu:

using Bookstore.Assistant;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OllamaSharp;

const string Instructions =
    "You are the Bookstore assistant. Use tools for order questions. Answer in one short sentence.";

// Paylaşılan yerel Ollama'da cevap 100 saniyeyi (HttpClient varsayılanı) geçebiliyor
IChatClient ollama = new OllamaApiClient(
    new HttpClient { BaseAddress = new Uri("http://localhost:11434"), Timeout = TimeSpan.FromMinutes(10) },
    "qwen3:4b");
OrderTools tools = new();
AITool[] toolList = [AIFunctionFactory.Create(tools.GetOrderStatus)];

string mode = args.Length > 0 ? args[0] : "agent";

if (mode == "chat")
{
    // Düz IChatClient: geçmiş, system prompt ve tool döngüsü senin sorumluluğunda
    IChatClient chat = ollama.AsBuilder().UseFunctionInvocation().Build();
    List<ChatMessage> history = [new(ChatRole.System, Instructions)];
    ChatOptions options = new() { Temperature = 0, Seed = 42, Tools = toolList };

    foreach (string question in new[] { "Where is my order 1042?", "How many items were in it?" })
    {
        history.Add(new(ChatRole.User, question));
        ChatResponse response = await chat.GetResponseAsync(history, options);
        history.AddMessages(response);
        Console.WriteLine($"> {question}\n{response.Text}");
    }
    Console.WriteLine($"history: {history.Count} messages, tool calls: {tools.Calls}");
}
dotnet run -- chat
> Where is my order 1042?
Your order 1042 is shipped on 2026-11-06 with 2 items and tracking number BKS-1042.
> How many items were in it?
Your order 1042 contained 2 items.
history: 7 messages, tool calls: 1

Çalışıyor. İkinci soru "it" diyor ve model bunun 1042 olduğunu biliyor, çünkü geçmişi biz taşıdık. Listede 7 mesaj var: system, ilk soru, tool isteği, tool sonucu, ilk cevap, ikinci soru, ikinci cevap. Bu kodda üç iş bize ait: system prompt'u listenin başına koymak, function invocation'ı pipeline'a eklemek ve geçmişi her turda güncellemek. Bu serinin "Sohbet Geçmişini Saklamak" yazısında bu listeyi Entity Framework Core (EF Core) ile saklamıştık.

Aynı Asistan, AIAgent Olarak

Programın agent modu:

else
{
    AIAgent agent = ollama.AsAIAgent(
        new ChatClientAgentOptions
        {
            Name = "BookstoreAssistant",
            ChatOptions = new() { Instructions = Instructions, Temperature = 0, Seed = 42, Tools = toolList },
        });

    AgentSession session = await agent.CreateSessionAsync();

    AgentResponse first = await agent.RunAsync("Where is my order 1042?", session);
    Console.WriteLine($"> Where is my order 1042?\n{first.Text}");
    Console.WriteLine($"  messages in response: {first.Messages.Count}, tokens: {first.Usage?.InputTokenCount}/{first.Usage?.OutputTokenCount}");

    Console.WriteLine("> How many items were in it?");
    await foreach (AgentResponseUpdate update in agent.RunStreamingAsync("How many items were in it?", session))
    {
        Console.Write(update.Text);
    }
    Console.WriteLine($"\ntool calls: {tools.Calls}");
}
dotnet run -- agent
> Where is my order 1042?
Your order 1042 is shipped on 2026-11-06 with 2 items and tracking number BKS-1042.
  messages in response: 3, tokens: 613/495
> How many items were in it?
Your order 1042 contained 2 items.
tool calls: 1

Cevaplar aynı. AsAIAgent, IChatClient üzerinde bir extension method ve bir ChatClientAgent döndürüyor. Burada options alan overload'unu kullandık, çünkü sıcaklık ve seed gibi ayarları da vermek istedik. Yalnız talimat ve tool verecek olsaydın ollama.AsAIAgent(instructions: ..., name: ..., tools: ...) overload'u da yeterdi. Talimat artık bir system mesajı değil, ChatOptions.Instructions. Function invocation'ı da biz eklemedik.

ChatClientAgent Neyi Ekliyor, Neyi Eklemiyor?

İki kodu yan yana koyunca agent'ın üstlendiği işler netleşiyor:

  • Function invocation: ChatClientAgent'ın XML (Extensible Markup Language) dokümantasyonu, verilen IChatClient içinde bir FunctionInvokingChatClient yoksa agent'ın kendisinin bir tane eklediğini söylüyor. Biz ollama'yı çıplak verdik, tool yine çalıştı.
  • Geçmiş: history listesi yok. Konuşmayı AgentSession tutuyor. İkinci RunStreamingAsync çağrısına yalnız yeni soruyu verdik.
  • Talimat ve kimlik: system prompt'u her turda biz eklemiyoruz. Agent'ın bir Name'i var; telemetry'de ve hosting'de bu ad kullanılıyor.
  • Tek cevap nesnesi: RunAsync bir AgentResponse döndürüyor. İçinde bu turda üretilen 3 mesaj var: tool isteği, tool sonucu ve son cevap. Metni Text'ten, token sayılarını Usage'tan okuyoruz. Streaming tarafında karşılığı AgentResponseUpdate.

Eklemedikleri de en az bunlar kadar önemli. Model aynı model; qwen3:4b agent olunca daha iyi akıl yürütmüyor. Tool'ları sen veriyorsun, agent bir şey keşfetmiyor. Planlama ya da çok adımlı bir hedef takibi de yok: bu yalnız bir tool döngüsü, düz IChatClient + UseFunctionInvocation ile aynı döngü. Session da varsayılan haliyle bellekte duruyor; uygulama kapanınca konuşma gider. Açılıştaki "daha akıllı mı olur, kalıcı mı saklar?" sorularının cevabı hayır. Agent bir soyutlama katmanı. Asıl değeri bu katmanın üstüne kurulanlarda: middleware, context provider, session saklama, hosting ve workflow'lar. Bunları serinin sonraki iki yazısında göreceğiz.

AgentSession: Çok Turlu Konuşma

CreateSessionAsync'in agent'ın bir method'u olması tesadüf değil. Session'ın içeriği agent'ın tipine göre değişir. ChatClientAgent için session, mesajları tutan bir chat history provider'ın durumunu içeriyor. Stateful bir sağlayıcıda (örneğin sunucu tarafında konuşma tutan bir servis) ise yalnız bir konuşma kimliği taşıyabilir. Çağıran kod bu farkı bilmek zorunda değil. Bu serinin "Sohbet Geçmişini Saklamak" yazısında ConversationId ile stateless/stateful ayrımını elle yönetmiştik; AgentSession aynı ayrımı agent seviyesinde soyutluyor.

Session'ı JSON (JavaScript Object Notation) olarak dışarı almak (SerializeSessionAsync) ve geri yüklemek (DeserializeSessionAsync) de agent'ın method'ları. Bir web uygulamasında her istekte session'ı veritabanından yükleyip iş bitince geri yazarsın. Bunu bir sonraki yazıda, context provider'larla birlikte yapacağız.

RunStreamingAsync ise LLM Temelleri serisinin "LLM Cevabında Streaming" yazısındaki IAsyncEnumerable pattern'inin agent'taki karşılığı. Her update bir metin parçası, bir tool çağrısı ya da bir usage bilgisi taşıyabilir. update.Text yalnız metin parçalarını birleştirir.

Paketler ve Sürüm Hatları

Yalnız bir paket ekledik: Microsoft.Agents.AI 1.28.0. Bu paket MEAI 10.11'e ve Microsoft.Agents.AI.Abstractions'a bağımlı. MAF 1.0'ın GA (General Availability) tarihi 3 Nisan 2026. O günden beri çekirdek paket hızla sürümleniyor: Eylül sonundaki 1.23'ten 6 Kasım'daki 1.28'e altı haftada beş minor sürüm çıktı. Çekirdeğin yanındaki paketler ise farklı olgunluk seviyelerinde. NuGet'teki son sürümler (8 Kasım 2026):

Microsoft.Agents.AI                         1.28.0
Microsoft.Agents.AI.OpenAI                  1.28.0
Microsoft.Agents.AI.Harness                 1.28.0
Microsoft.Agents.AI.Foundry                 1.28.0-preview.261106.1  (latest stable: 1.5.0)
Microsoft.Agents.AI.Hosting                 1.28.0-preview.261106.1
Microsoft.Agents.AI.Hosting.OpenAI          1.28.0-alpha.261106.1

Foundry provider'ının durumu özellikle yanıltıcı: stabil hatta 1.5.0 duruyor, preview hattı çekirdekle birlikte 1.28'e gelmiş. MAF'ın genel bakış sayfasındaki ilk örnek de bu paketi --prerelease ile kuruyor. Bu yazıda bulut sağlayıcılarıyla çalışmadık; kod yerel Ollama ile koşuldu. Ama Foundry ya da hosting paketlerinden birini eklediğinde, projendeki MAF paketlerinin hepsinin aynı sürüm numarasında olduğundan emin ol.

Üretimde Dikkat

Agent tek başına bir şey kazandırmaz, ama telemetry'yi kaybettirebilir. Bu serinin "LLM Çağrılarını Gözlemlemek" yazısındaki UseOpenTelemetry pipeline'ını IChatClient'a ekleyip agent'a o client'ı ver. Agent'ın kendi eklediği function invocation katmanının telemetry'nin dışında kalıp kalmadığını trace'te kontrol et.

Agent nesnesi uzun ömürlüdür, session kısa ömürlüdür. AIAgent'ı uygulama başına bir kez oluştur (DI'da singleton). Session'ı kullanıcı ve konuşma başına oluştur ya da sakladığın yerden yükle. Tool'ların scoped service'lere ihtiyacı varsa (örneğin bir DbContext), bu serinin tool calling yazısındaki yaklaşımla tool'u DI'dan çözümle.

Süreleri ölç. Bu yazıdaki iki soruluk agent koşusu paylaşılan yerel Ollama'da 4 dakika 19 saniye sürdü, aynı soruların chat modu 2 dakika 26 saniye. Fark agent'tan değil, o sırada sunucudaki kuyruktan geliyor. Ama kullanıcı açısından sonuç aynı: timeout'ları, iptal token'larını ve streaming'i buna göre tasarla.

Sık Düşülen Tuzaklar

İlk tuzak, session'ı her istekte yeniden oluşturmak. Bir Web API endpoint'inde her HTTP (Hypertext Transfer Protocol) isteğinde CreateSessionAsync çağırırsan model her seferinde boş bir konuşmayla başlar ve "How many items were in it?" sorusu anlamsızlaşır. Session'ı ya hosting katmanının session store'una bırak ya da kendin serialize edip sakla.

İkincisi, prerelease provider paketlerini stabil çekirdekle karıştırmak. Microsoft.Agents.AI 1.28.0 ile bir provider'ın 1.26 preview'ını aynı projeye koyarsan derleme geçebilir ama çalışma zamanında beklenmedik davranışlar görebilirsin. Sürümleri merkezi paket yönetimiyle (Directory.Packages.props) birlikte sabitle.

Üçüncüsü, HttpClient'ın varsayılan 100 saniyelik timeout'u. Bu yazının ilk denemesinde chat modu, ikinci soruda tam bu yüzden düştü:

Unhandled exception. System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.

Thinking yapan bir model ile meşgul bir sunucu birleşince 100 saniye kolayca dolabilir. Koddaki HttpClient bu yüzden 10 dakikalık timeout ile oluşturuluyor. Production'da ise uzun timeout yerine streaming ve iptal token'ı tercih et.

Son tuzak, her şeyi agent yapmak. Dokümantasyondaki kuralı hatırla: fonksiyonla çözebiliyorsan fonksiyon yaz. Agent'ın her turu bir ya da daha fazla model çağrısı, yani gecikme ve token maliyeti demek.

Küçük Bir Deneme

★☆☆ agent modunda ikinci soruyu yeni bir session ile sor (await agent.CreateSessionAsync()). Model "it" sözcüğünü neye bağlıyor? Tool'u yeniden çağırıyor mu?

★★☆ AgentResponse.Messages içindeki mesajları rol ve content tipleriyle yazdır. Tool isteği ve tool sonucu hangi content tipleriyle geliyor? qwen3:4b'nin reasoning'i bu mesajların neresinde?

★★★ ollama'ya UseOpenTelemetry() ekleyip agent'ı bu client'la oluştur ve trace'i console exporter'la incele. orchestrate_tools ve execute_tool span'leri görünüyor mu? ChatClientAgentOptions.UseProvidedChatClientAsIs değerini true yapınca trace nasıl değişiyor?

Agent'ın çıplak hali bu kadar. Bir sonraki yazıda agent'a asıl karakterini veren parçaları ekleyeceğiz: agent run ve function invocation middleware'leri, kullanıcı tercihini hatırlayan bir context provider ve session'ı JSON olarak saklayıp geri yüklemek.