AutoGen İle Çoklu Yapay Zekâ Ajan Sistemi Geliştirme
Merhaba,
Bir önceki AutoGen Nedir? Derinlemesine Teorik İnceleyelim… başlıklı içeriğimizde AutoGen’i teorik boyutta masaya yatırmış ve tam teferruatlı bir incelemede bulunmuştuk. Bu içeriğimizde ise edindiğimiz teorik zemin üzerine artık pratik bir inşada bulunacak ve AutoGen ile çoklu yapay zekâ ajan sistemlerinin (multi-agent systems) nasıl geliştirilebileceğinin derinliklerine temas ediyor olacağız. O halde vakit kaybetmeksizin buyurun başlayalım.
Temel Kurulum ve Yapılandırma
İlk olarak bir Console Application projesi oluşturulmalı ve bu projeye
dotnet add package AutoGen talimatıyla gerekli AutoGen kütüphanesi yüklenmelidir.
Ardından OpenRouter üzerinden bir api key edinilerek, Mistral: Ministral 8B AI modeli eşliğinde aşağıdaki değişkenler tanımlanıp, çalışmaya hali hazır bir altyapı oluşturulmalıdır.
string apiKey = "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"; string model = "mistral/ministral-8b"; string endpoint = "https://openrouter.ai/api/v1";
Evet, artık hazır olduğumuza göre başlayabiliriz.
Temel Seviyede Agent İnşası
AutoGen’de temel seviyede bir agent oluşturmak için manuel yapılandırma maliyetini göze alıp ConversableAgent tercih edilebilir, amma velakin hızlı bir yapılandırmayla en temelde bir agent ayağa kaldırmak için OpenAIChatAgent daha idealdir.
var assistantAgent = new OpenAIChatAgent(
chatClient: new ChatClient(
model: model,
credential: new ApiKeyCredential(apiKey),
options: new OpenAIClientOptions()
{
Endpoint = new Uri(endpoint)
}),
name: "assistant",
systemMessage: "Sen kullanıcının bazı görevleri yapmasına yardımcı olan bir asistansın.")
.RegisterMessageConnector()
.RegisterPrintMessage();
Burada görüldüğü üzere AI modelle etkileşime girebilecek kullanıcıdan mesaj alabilen bir agent oluşturulmuştur. Hatırlarsanız eğer AutoGen’de her agent’ın görev ve yetenekleri yapacağı işin kapsamına göre özelleştirilebildiği için name ve systemMessage kısımlarında bunlar ayarlanmaktadır. Ayrıca tanımlamada yer alan RegisterMessageConnector metodu ile AI modelden gelecek mesajlar AutoGen’in anlayabileceği IMessage formatına dönüştürülmekte ve tam tersi işlem gerçekleştirilmektedir. Bir yandan da RegisterPrintMessage metodu ile de, bu agent’ın aldığı ve gönderdiği mesajlar console’a uygun bir formatta yazdırılmaktadır.
Tabi şimdi bu agent’la haberleşmeyi sağlayacak ve etkileşime girecek bir başka agent daha oluşturmamız gerekmektedir. Misal olarak aşağıdaki gibi UserProxyAgent aracılığıyla kullanıcıyı temsil eden bir agent oluşturulabilir.
var userProxyAgent = new UserProxyAgent(
name: "user",
humanInputMode: HumanInputMode.ALWAYS)
.RegisterPrintMessage();
Evet, artık elimizde biri yardımcı olan biri de kullanıcıyı temsil eden iki adet agent mevcuttur. Artık bu agent’ların kendi aralarındaki haberleşmelerini başlatabiliriz. Bu haberleşme aşağıdaki gibi iki taraftan da başlayabilir;
- Kullanıcı agent’tan, assistant agent’a;
await userProxyAgent.InitiateChatAsync( receiver: assistantAgent, message: Console.ReadLine(), maxRound: 10); - Assistant agent’tan, kullanıcı agent’a;
await assistantAgent.InitiateChatAsync( receiver: userProxyAgent, message: Console.ReadLine(), maxRound: 10);
Yukarıdaki yöntemleri incelersek iletişimin hangi agent’tan başlarsa başlasın InitiateChatAsync metodu aracılığıyla başlatılmakta olduğunu görüyoruz. Bir agent bu metot aracılığıyla receiver parametresine verilen agent’a mesajını gönderecek ve süreci başlatacaktır. Bu haberleşme sürecinde agent’lar arası yanıt döngüsü maxRound parametresiyle belirlenmekte ve böylece gereksiz API çağrıları ve sonsuz iletişimsel döngüler gibi durumlar engellenmektedir.
Bu yaptığımız geliştirmeyi derleyip çalıştırırsak eğer aşağıdaki ekran görüntüsünde olduğu gibi test edebilir ve agent’la sohbet edebiliriz.
Bakın dikkat ederseniz artık sohbeti ayakta tutabilmek için herhangi bir döngüyle vs. iterasyonel çalışma yapmamıza gerek bulunmamaktadır. Çünkü oluşturduğumuz agent’lar sürekli bir şuur misali iletişim kurulabilir bir şekilde ayaktadırlar.
GenerateReplyAsync & SendAsync Metotları İle Haberleşme
Bir agent ile haberleşme sürecinde(ki bu haberleşme kullanıcı/agent arasında ya da agent/agent arasında olabilir) GenerateReplyAsync ve SendAsync metotlarını kullanarak farklı davranışlar sergilenebilmektedir. Şöyle ki;
- GenerateReplyAsync Metodu İle Haberleşme
Bu metot, agent tarafından yalnızca bir yanıt üretilmesi için kullanılmaktadır. Genellikle konuşmanın kontrolü kullanıcıdaysa uygun olmakta ve daha çok test, debugging ya da kontrol senaryolarında tercih edilmektedir.var message = new TextMessage(Role.User, "Merhaba, sen kimsin?"); IMessage reply = await chatAgent.GenerateReplyAsync([message]);
Görüldüğü üzere bu metot girdi olarak bir mesaj dizisi almakta, haliyle bu diziye konuşma geçmişi verilerek bağlamla tutarlı bir cevap elde edilebilmektedir. Böylece çok adımlı, senkronize bir konuşma akışının neticesinde bir yanıt üretilmesi gerekiyorsa bu metot kullanılabilmektedir. Ancak şunu unutmamak gerekmektedir ki, bu metodun döndürdüğü yanıt ne
GroupChat‘e iletilebilir, ne başka bir agent’a gönderilir ne de bir geri bildirim süreci tetikleyebilir. Yani anlayacağınız sohbetin akışı ilerlememektedir. - SendAsync Metodu İle Haberleşme
Bu metot ise mesajı agent’a göndermekte ve alınan cevabı varsa akış mantığına göre sonraki adımlara(başka agent’lara) yollamaktadır. Eğer ki söz konusu birGroupChat‘se, bu metot aracılığıyla tüm süreç tetiklenir ve zincirleme agent iletişimi başlatılabilir.var message = new TextMessage(Role.User, "Merhaba, sen kimsin?"); IMessage reply = await chatAgent.SendAsync(message);
Bu metotlar davranışsal olarak bahsedilen farklara sahip olsalar da manuel kontrol açısından da ciddi fark ortaya koymaktadırlar. Şöyle ki; GenerateReplyAsync metodunda agent’a gönderilen mesaj neticesinde yalnız yanıt alınabilmekte ve sistemin geri kalanına dair hiçbir şey gerçekleşmemektedir. Yani üretilecek bu cevap süreçte varsa başka agent’lar tarafından kontrol edilmemekte veyahut sistemle ilgili herhangi bir event tetiklenmemektedir. Bundan kaynaklı bu metodun kullanıldığı süreçlerde mesajın ne zaman, kime, nasıl gideceğine karar bizlerdedir. Haliyle oldukça sıkı bir manuel kontrol söz konusudur. SendAsync metodu ise sistemin içinde başka agent’larla olan bir akış söz konusuysa otomatik başlatacaktır ve gerektiği taktirde sistem event’lerinden tetikleme gerçekleştirecektir. Ee haliyle bu metotta süreç tarafımızca kontrol edilemediği için manuel kontrol paradigması oldukça zayıflık arz etmektedir.
Ve nihai olarak iki metot arasındaki teknik farklılıkları aşağıdaki tabloda özetleyerek devam edelim;
| Özellik | GenerateReplyAsync | SendAsync |
|---|---|---|
| Yanıt Üretimi | ✔️ | ✔️ |
| Mesaj Gönderimi | ❌ | ✔️ |
| Otomatik Akış Tetikleme | ❌ | ✔️ |
| Manuel Kontrol | ✔️ | ❌ (daha az) |
| Akış Zinciri Tetikleme | ❌ | ✔️ |
Streaming Chat
Agent’larla iletişim süreçlerinde yanıtı tamamen oluşana kadar beklemek yerine oluşturulan parçaları anlık olarak streaming(akış tabanlı) bir şekilde alabilmek mümkündür. Böylece uzun cevaplardan anlık tepki alınabilir ve bir yandan da typing gibi hisler oluşturularak kullanıcı deneyimleri iyileştirilebilir.
var message = new TextMessage(Role.User, "Tarihteki Memlükler devletiyle ilgili sence önemli olan hangi bilgi konuşmaya değer?");
await foreach (var reply in chatAgent.GenerateStreamingReplyAsync([message]))
{
if (reply is TextMessageUpdate update)
Console.WriteLine(update.Content);
}
Middlewares
AutoGen middleware yapısı sayesinde agent’lar arasında geçen mesajları ya da görev akışlarını kesebilmemizi ve böylece sürece dair analizler gerçekleştirebilmemizi, değişiklikler yapabilmemizi ya da yönlendirme olanağı sağlamaktadır. Esasında varlık nedeni Asp.NET Core’da ki bilinen middleware’lerle birebir aynı amaca hizmet etmektedir.
Aşağıda bir agent üzerinden basitçe middleware kullanımı örneklendirilmektedir;
var assistantAgent = new OpenAIChatAgent(
chatClient: new ChatClient(
model: model,
credential: new ApiKeyCredential(apiKey),
options: new OpenAIClientOptions()
{
Endpoint = new Uri(endpoint)
}),
name: "assistant",
systemMessage: "Sen kullanıcının bazı görevleri yapmasına yardımcı olan bir asistansın.")
.RegisterMessageConnector()
//messages : O ana kadar geçen mesajlar - sohbet geçmişi
//options : Yanıt üretim ayarları
//agent : Middleware'in araya girdiği agent'ın kendisi
.RegisterMiddleware(async (messages, options, agent, cancellationToken) =>
{
var lastMessage = messages.LastOrDefault();
if (lastMessage is TextMessage textMessage && textMessage.Content.Contains("Merhaba"))
return new TextMessage(Role.Assistant, "[middleware] Merhabe algılandı.");
//Diğer durumlarda müdahale etme, varsayılan yanıtı üret!
return await agent.GenerateReplyAsync(messages, options, cancellationToken);
})
.RegisterPrintMessage();
var reply = await assistantAgent.SendAsync("Merhaba");
Görüldüğü üzere RegisterMiddleware metodu aracılığıyla agent’a bir middleware tanımlanabilmekte ve üretilecek yanıt sürecinde rahatlıkla araya girilebilmektedir. Burada mühim olan ilgili metodun içerisine verilecek metod parametrelerinin ne işe yaradıklarının bilincinde olmaktır. Özellikle agent parametresinin o an middleware tarafından araya girilen agent olduğunun bilinmesinde fayda vardır.
Function Call
Kimi AI modellerinde, akıllıca işlemler yapabilmesi için sürece harici fonksiyonlar dahil edilebilmekte ve böylece üretilecek yanıtlar manipüle edilerek, AI modelinin yetkinliği arttırılabilmekte ve yetenekleri genişletilebilmektedir.
Peki nasıl function oluşturulur? diye sorduğunuzu duyar gibiyim… AutoGen’de function calling desteğini FunctionDefinition sınıfı üzerinden gerçekleştiriyoruz. Bunu manuel yapmaktansa AutoGen.SourceGenerator kütüphanesini kullanarak source generator’dan istifade edebilir ve otomatik olarak ilgili sınıfı oluşturtturabiliriz. Bunun için Function attribute’unu kullanacağız. Bu attribute ile işaretlenmiş olan metotların otomatik olarak function_calling formatına uygun olan FunctionDefinition dönüşümleri gerçekleştirilecek ve nesneleri elde edilecektir.
public partial class Functions
{
/// <summary>
/// Kullanıcı listesini getirir.
/// </summary>
/// <param name="userId">Eğer null değilse, yalnızca değeriyle eşleşen kullanıcıyı getirir.</param>
[Function]
async Task<string> GetUsersAsync(int? userId)
{
using HttpClient httpClient = new();
var httpResponseMessage = await httpClient.GetAsync($"https://jsonplaceholder.typicode.com/users{(userId.HasValue ? $"/{userId.Value}" : "")}");
var jsonResult = await httpResponseMessage.Content.ReadAsStringAsync();
return jsonResult;
}
}
Yukarıdaki tanımda şunu demek istiyoruz : ‘Bu metodu LLM’e callable bir fonksiyon olarak sunmak istiyorum.’ Ee haliyle bunu reflection’la yapmaktansa source generator’ı devreye sokup gerekli FunctionDefinition nesnesini oluşturacak C# kodunu otomatik ürettiyoruz. Tabi bunun için source generator’ın fonksiyon tanımını üretirken gerekli olan fonksiyon dokümantasyonundan yararlanmasını sağlayabilmek için projede aşağıdaki gibi GenerateDocumentationFile özelliğini true olarak ayarlayıp XML belge desteğini etkinleştirmekte fayda vardır.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> . . . <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> . . . <PackageReference Include="AutoGen.SourceGenerator" Version="0.2.3" /> </ItemGroup> </Project>
Ayrıca şunu da söylemekte fayda vardır ki; source generator’ın kod üretebilmesi için tıpkı yukarıdaki örnekte olduğu gibi public ve partial olan bir class gerekli ve Function attribute’u ile işaretlenmiş metotda public olmalı ve dönüş değeri olarak da Task<T> şeklinde tanımlanmalıdır. Ve unutulmamalıdır ki metot özet kısmındaki(summary) yazılanlar kritik arz etmektedir.
Evet, tüm çalışmalar anlatıldığı şekilde yapıldığı taktirde uygulama derlendiği vakit source generator devreye girecek ve gerekli function tanımı üretilecektir. Bu üretim neticesinde ilgili metodun LLM’e sunulması ve tetiklenebilmesi sürecinde merkezi rol oynayacak olan iki member oluşturulmuş olacaktır. Bunlar FunctionContract property’si ile birlikte Wrapper metodudur.
- FunctionContract
İlgili fonksiyona dair adı, açıklaması, parametreleri vs. gibi bilgileri eşliğinde LLM’e bildirileceği sözleşmeyi yani kontrakt’ı ifade etmektedir. Yukarıdaki metoda karşı source generator aşağıdaki kontraktı üretecektir.public FunctionContract GetUsersAsyncFunctionContract { get => new FunctionContract { Namespace = @"AutoGen_Example", ClassName = @"Functions", Name = @"GetUsersAsync", Description = @"Kullanıcı listesini getirir.", ReturnType = typeof(Task<string>), Parameters = new global::AutoGen.Core.FunctionParameterContract[] { new FunctionParameterContract { Name = @"userId", Description = @"Eğer null değilse, yalnızca değeriyle eşleşen kullanıcıyı getirir.", ParameterType = typeof(int?), IsRequired = true, }, }, }; } - Wrapper
LLM’den gelen JSON formatındaki parametreleri doğru C# koduna dönüştüren metottur. Misal olarak; LLM bizlere
{'userId': 3}döndürdüyse, bunuGetUsersAsyncWrapper(3)şeklinde yorumlayacaktır. Yine benzer mantıkla yukarıdaki metoda karşı source generator aşağıdaki Wrapper çalışmasını gerçekleştirmiştir.private class GetUsersAsyncSchema { [JsonPropertyName(@"userId")] public int? userId {get; set;} }Bakın, görüldüğü üzere önce ilgili metodun parametlerilerini temsil edecek bir schema sınıfı oluşturmuştur.
public Task<string> GetUsersAsyncWrapper(string arguments) { var schema = JsonSerializer.Deserialize<GetUsersAsyncSchema>( arguments, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); return GetUsersAsync(schema.userId); }Ardından LLM’den gelen JSON formatındaki parametreleri bu schema sınıfı aracılığıyla deserialize edip kullanan GetUsersAsyncWrapper isimli wrapper metodunu oluşturmuştur.
Ne kadar kolay değil mi? AutoGen’in, biz geliştiricilerin deneyimini oldukça kolaylaştırması yetmiyormuş gibi bir de source generator ile bu tarz teferruatlı işlemlerin de sorumluluğunu bizlerden törpülemesi taktire şayan👏
Peki hoca! İyi güzel de bu fonksiyonu LLM’den nasıl çağıracağız? sorunuzu da duymuyor değilim. Bunun için aşağıdaki gibi ilgili agent’a RegisterMiddleware üzerinden FunctionCallMiddleware aracılığıyla hedef function(lar) kazandırılabilir ve örnekte olduğu gibi prompt’un mahiyetine göre AI modeli tarafından ilgili function(lar) tetiklenerek üretilecek yanıt şekillendirilip, sürece yeni davranış(lar) kazandırılabilir.
Functions functions = new();
var assistantAgent = new OpenAIChatAgent(
chatClient: new ChatClient(
model: model,
credential: new ApiKeyCredential(apiKey),
options: new OpenAIClientOptions()
{
Endpoint = new Uri(endpoint)
}),
name: "assistant",
systemMessage: "Sen kullanıcının bazı görevleri yapmasına yardımcı olan bir asistansın.")
.RegisterMessageConnector()
.RegisterMiddleware(new FunctionCallMiddleware(
functions: [functions.GetUsersAsyncFunctionContract],
functionMap: new Dictionary<string, Func<string, Task<string>>>()
{
{ nameof(Functions.GetUsersAsync), functions.GetUsersAsyncWrapper}
}
))
.RegisterPrintMessage();
var result = await assistantAgent.GenerateReplyAsync([new TextMessage(Role.User, "bana tüm kullanıcıların bilgilerini getir")]);
Tabi GenerateReplyAsync metodu ile function calling tetikleneceği gibi SendAsync metoduyla da tetiklenecektir.
var result = await assistantAgent.SendAsync("bana tüm kullanıcıların bilgilerini getir");
Velhasıl, prompt neticesinde gelen cevap aşağıdaki görseldeki gibi olacaktır.
Ayrıca function calling özelliğini, AI modelin çıktılarını parça parça elde ettiğimiz (streaming) senaryolardaki RegisterStreamingMiddleware üzerinden de aşağıdaki gibi devreye sokup benzer neticeyi elde edebiliriz.
.RegisterStreamingMiddleware(new FunctionCallMiddleware(
functions: [functions.GetUsersAsyncFunctionContract],
functionMap: new Dictionary<string, Func<string, Task<string>>>()
{
{ nameof(Functions.GetUsersAsync), functions.GetUsersAsyncWrapper}
}))
Function calling özelliğini kullanabilmek için tercih edilen AI modelinin bu davranışı desteklemesi gerekmektedir. Bunun için içerik sürecinde bu başlıktaki testleri
openai/gpt-4.1-nanoAI modeliyle gerçekleştirmiş bulunuyorum.
ChatTool Sınıfı Nedir?
Bu sınıf, AutoGen içerisindeki function’ları temsil eden bir türdür. Bir başka deyişle, AI modelin function calling özelliğiyle çağırabileceği tüm function’lar ChatTool olarak tanımlanmaktadırlar.
Bir kontrakt üzerinden ChatTool nesnesine aşağıdaki gibi ToChatTool metodu aracılığıyla erişilebilmektedir.
var getUsersAsyncFunctionChatTool = functions.GetUsersAsyncFunctionContract.ToChatTool();
FunctionContract‘ı fonksiyonun yapısını JSON şeması misali ifade eden teknik dokümantasyon gibi düşünürsek,ChatToolise bu şemayı LLM ile sohbet sırasında fiziksel olarak kullanılabilir bir nesne haline getiren araç olarak düşünebiliriz.
GetToolCalls Metodu Nedir?
Function calling’e dair bilinmesi gereken son şey ise GetToolCalls metodudur. Bu metot, IMessage türünden olan mesaj nesnelerinde mevcut olan ToolCallMessage‘ların listesini verecektir. Yani, AI modelinin yanıtı sürecinde yer alan tüm tool çağrılarını bizlere sunacaktır. Bunu daha net anlamak için aşağıdaki örnek üzerinden incelemede bulunmakta fayda vardır;
var result = await assistantAgent.GenerateReplyAsync([new TextMessage(Role.User, "bana 1,3 ve 5 id'li kullanıcıların bilgilerini getirir misin?")]); var toolCalls = result.GetToolCalls();
Yukarıdaki isteği AI modeli şu şekilde yorumlayacaktır;
[
{ "name": "GetUsersAsync", "arguments": { "userId": 3 } },
{ "name": "GetUsersAsync", "arguments": { "userId": 5 } },
{ "name": "GetUsersAsync", "arguments": { "userId": 7 } }
]
Haliyle tüm bu tool’ları GetToolCalls metodunu çağırıp aşağıdaki görselde olduğu gibi programatik olarak elde edebiliriz;
Birden Fazla Agent’ı Kendi Aralarında Konuşturma
AutoGen’de birden fazla agent’ı kendi aralarında sıralı bir şekilde yahut belirlenen kurallara göre konuşturabilmekteyiz. Eğer ki sıralı bir şekilde konuşturulacaklarsa RoundRobinGroupChat, yok eğer belirlenen kurallara göre ya da agent’ların rol ve geçmişlerine göre konuşturulacaklarsa da GroupChat tercih edilmelidir.
| GroupChat | RoundRobinGroupChat |
|---|---|
| Kural tabanlıdır. Akıllı yönlendirme yapar. Mesajları sohbete katılımcı agent’lara rol, geçmiş ve içerik bazlı olarak yönlendirir. Böylece her agent kendi iç kurallarına göre gelen mesajı alıp almayacağına karar verir ve cevap verecek agent’lar da iç mantıklarına göre belirlenir. Bu yüzden akış her daim sıraya göre gitmek zorunda değildir.
✅Avantajları;
🚫Dezavantajları;
|
Dönüşümlüdür. Sırayla işleme yapar. Her agent sırayla konuşur, mesajı işler, sonra sıradakine devreder. Haliyle her agent, alakası olsun ya da olmasın çalışacak, bir cevap üretecektir.
✅Avantajları;
🚫Dezavantajları;
|
Şimdi gelin GroupChat‘i belirli senaryo eşliğinde pratikte örneklendirelim;
Evet, şimdi bu senaryoyu aşağıdaki adımlar eşliğinde geliştirme yaparak gerçekleştirebiliriz;
- Adım 1 – (Admin Agent’ın Oluşturulması)
İlk olarak admin agent’ı oluşturarak başlayacağız. Bunun için aşağıdaki gibi bir çalışmanın yapılması yeterli olacaktır.async static Task<IAgent> CreateAdminAgentAsync(string apiKey, string model, string endpoint) { var admin = new OpenAIChatAgent( chatClient: new ChatClient( model: model, credential: new ApiKeyCredential(apiKey), options: new OpenAIClientOptions() { Endpoint = new Uri(endpoint) }), name: "admin", temperature: 0) .RegisterMessageConnector() .RegisterPrintMessage(); return admin; }Burada
temperatureparametresi dikkatinizi çekmiş olabilir. Bu parametre ile AI modelinin üreteceği yanıtların rastgeleliğini(çeşitlilik) kontrol ediyoruz. Nasıl yani? diye sorarsanız; olası kelime veya yanıtlar arasından seçim yaparken ne kadar ‘cesur’ ya da ‘temkinli’ olacağını kastediyoruz. Yani AI modeli daha yaratıcı mı olsun yoksa daha deterministik(tahmin edilebilir) mi? Bu mantık doğrultusunda ilgili parametre 0 ile 2 arasında bir değer almakta ve aşağıdaki tanımlandığı gibi AI modele davranış kazandırmaktadır.Temperature Değeri Davranışı 0 Tamamen deterministik davranış sergiler ve her daim en olası yanıtlar verir. En güvenilir ve net sonuçlar bu değerle üretilir. 0.3 – 0.5 Düşük rastgelelelik söz konusudur. Hala mantıklı ama biraz daha çeşitli cevaplar üretir. 0.7 – 1.0 Dengeli bir yaratıcılık seviyesi hakimdir. Bazen tutarsız cevaplar olabilir. 1.0> Yüksek rastgelelik mevzu bahistir. Kimi zaman aşırı, kimi zaman da saçma sonuçlar olabilir. Dikkat ederseniz sayısal değer yükseldikçe rastgelelik artmaktadır.
Admin agent, görevleri yöneten ve diğer ajanları koordine eden ana kontrol agent’ı olduğu için görevleri açık ve net tanımlamalı, diğer agent’ları doğru yönlendirmeli ve rastgelelikten uzak, tutarlı ve kararlı olmalıdır. İşte bu yüzden temperature değeri 0 ile maksimum 0.3 arasında olmalıdır.
- Adım 2 – Coder Agent’ın Oluşturulması
Coder agent’ı ise aşağıdaki gibi oluşturabiliriz.async static Task<IAgent> CreateCoderAgentAsync(string apiKey, string model, string endpoint) { var coder = new OpenAIChatAgent( chatClient: new ChatClient( model: model, credential: new ApiKeyCredential(apiKey), options: new OpenAIClientOptions() { Endpoint = new Uri(endpoint) }), name: "coder", systemMessage: @" Sen bir .net(dotnet) coder'sın ve gelen task'taki problemi çözmek için dotnet'te C# kodu yazıyorsun. Kod yazmayı bitirdiğinde runner'dan kodu senin için çalıştırmasını isteyeceksin. Dotnet kodu yazarken uyulması gereken kurallar: - Disposable olan nesneleri oluştururken 'using' keyword'ünü kullanma! - Değişken türlerinde 'var' kullan. - Local variable'ların default değerini her zaman ata! - Harici kütüphane kullanmaktan kaçın. Mümkün mertebe .NET Core kütüphanesi kullan. - Kod yazmak için 'top level statement' kullan. - Sonucu her zaman console'a yazdır. - Eğer NuGet paketi yüklenmesi gerekiyorsa, ilgili paketi aşağıdaki biçimde yerleştir. ```nuget nuget_package_name ``` - Kod yanlışsa runner sana hata mesajını verecektir. Hatayı düzelt ve kodu tekrar gönder.") .RegisterMessageConnector() .RegisterPrintMessage(); return coder; }Burada dikkat edilmesi gereken husus bu agent’tın davranışsal kapsamının system message(system prompt) olarak detaylıca belirleniyor olmasıdır. Evet, bu agent çalışmalarında oldukça hayati bir önem arz etmektedir. Agent’lar da system prompt’lar ile görevler ve davranışlar iyice detaylandırılmalıdır.
- Adım 3 – Reviewer Agent’ın Oluşturulması
Reviewer agent ile üretilen kodun belirtilen koşullara uyup uymadığını değerlendirebilmek için aşağıdaki gibi bir function tasarlayabiliriz.Tabi function’dan önce koşulları modellediğimiz bir yapı ortaya koyalım;
public struct CodeReviewResult { public bool HasMultipleCodeBlocks { get; set; } public bool IsTopLevelStatement { get; set; } public bool IsDotnetCodeBlock { get; set; } public bool IsPrintResultToConsole { get; set; } }Bu struct bizlere, reviewer agent’ı tarafından incelenen kodun hangi şartlara uyup uymadığını tip güvenli bir şekilde tutmamızı ve kontrol etmemizi sağlayacak bir tür sağlamaktadır.
public partial class Functions { /// <summary> /// review code block /// </summary> /// <param name="hasMultipleCodeBlocks">Birden fazla csharp kod bloğu varsa true değerindedir.</param> /// <param name="isTopLevelStatement">Eğer kod top level statement ise true değerindedir.</param> /// <param name="isDotnetCodeBlock">Eğer kod bloğu csharp koduysa true değerindedir.</param> /// <param name="isPrintResultToConsole">Eğer kod neticesinde console'a yazılıyorsa true değerindedir.</param> [Function] public async Task<string> ReviewCodeBlockAsync( bool hasMultipleCodeBlocks, bool isTopLevelStatement, bool isDotnetCodeBlock, bool isPrintResultToConsole) { var obj = new CodeReviewResult { HasMultipleCodeBlocks = hasMultipleCodeBlocks, IsTopLevelStatement = isTopLevelStatement, IsDotnetCodeBlock = isDotnetCodeBlock, IsPrintResultToConsole = isPrintResultToConsole, }; return JsonSerializer.Serialize(obj); } }Bu da review edilen kod bloğunu
CodeReviewResulttürüne dönüştürecek fonksiyonun tanımıdır. LLM bu fonksiyon aracılığıyla kodun değerlendirmesini bir JSON objesi olarak bizlere sunacak ve bizler de bu değerlendirmeyi zahiren rahatlıkla görebiliyor olacağız.Ardından reviewer agent aşağıdaki gibi geliştirilebilir;
async static Task<IAgent> CreateReviewerAgentAsync(string apiKey, string model, string endpoint) { var functions = new Functions(); var reviewer = new OpenAIChatAgent( chatClient: new ChatClient( model: model, credential: new ApiKeyCredential(apiKey), options: new OpenAIClientOptions() { Endpoint = new Uri(endpoint) }), name: "reviewer", systemMessage: "Sen coder'ın oluşturduğu kodları inceliyorsun.") .RegisterMessageConnector() .RegisterMiddleware(new FunctionCallMiddleware( functions: [functions.ReviewCodeBlockAsyncFunctionContract], functionMap: new Dictionary<string, Func<string, Task<string>>>() { { nameof(Functions.ReviewCodeBlockAsync), functions.ReviewCodeBlockAsyncWrapper} } )) .RegisterPrintMessage() .RegisterMiddleware(async (messages, options, innerAgent, cancellationToken) => { var maxRetry = 3; var reply = await innerAgent.GenerateReplyAsync(messages, options, cancellationToken); while (maxRetry-- > 0) { if (reply.GetToolCalls() is var toolCalls && toolCalls?.Count == 1 && toolCalls?[0].FunctionName == nameof(Functions.ReviewCodeBlockAsync)) { var replyContent = reply.GetContent(); var reviewResultObject = JsonSerializer.Deserialize<CodeReviewResult>(replyContent); var reviews = new List<string>(); if (reviewResultObject.HasMultipleCodeBlocks) reviews.Add("Kod birden fazla kod bloğuna sahip. Lütfen tek bir kod bloğu gönder."); if (!reviewResultObject.IsTopLevelStatement) reviews.Add("Kod top level statement değil. Lütfen top level statement kullan."); if (!reviewResultObject.IsDotnetCodeBlock) reviews.Add("Kod csharp kod bloğu değil. Lütfen csharp kod bloğu kullan."); if (!reviewResultObject.IsPrintResultToConsole) reviews.Add("Kod sonucu console'a yazdırmıyor. Lütfen sonucu console'a yazdır."); if (reviews.Count > 0) { StringBuilder stringBuilder = new(); stringBuilder.AppendLine("Kod reviewer açısından bazı kurallara uyulmamıştır. Bunlar düzeltilmelidir;"); foreach (var review in reviews) stringBuilder.AppendLine($"- {review}"); return new TextMessage(Role.Assistant, stringBuilder.ToString(), from: "reviewer"); } else return new TextMessage(Role.Assistant, "Geçerli kod, runner kodu çalıştırabilir."); } else { var originalContent = reply.GetContent(); var prompt = $@"Lütfen içeriği ReviewCodeBlockAsync fonksiyon parametrelerine dönüştür. ## Original Content {originalContent} "; reply = await innerAgent.SendAsync(prompt, messages, cancellationToken); } } throw new Exception("Kod bloğunu inceleme başarısız oldu!"); }); return reviewer; }Burada da özellikle 28 ile 72. satır aralığına dikkat ederseniz, yapılan istekte ReviewCodeBlockAsync adında tool çağrısı varsa eğer bu tool’un neticesi JSON olarak elde edilmekte ve üretilen kod tarafımızca belirlenen koşullar doğrultusunda değerlendirilmektedir. Tabi bu değerlendirme sürecinde de takınılan kurallar varsa onlar metinsel olarak bir koleksiyona not alınmakta ve 49 ile 58. satır aralığında bu notlar mesaj olarak gönderilip sistem bilgilendirilmektedir. Yok eğer tool hiç çağrılmamışsa o taktirde de 62 ile 71. satır aralığında olduğu gibi ilgili tool’un çağrıması gerektiğine dair atıfta bulunulmaktadır.
Dikkat! Burada
FunctionCallMiddleware‘inRegisterPrintMessage‘dan önce tanımlanmasına özen gösteriniz. Aksi taktirde agent’a tanımlanan hiçbir tool diğer middleware’ler tarafından algılanmayacaktır. - Adım 4 – Runner Agent’ın Oluşturulması
Runner, coder’ın üreteceği kodları çalıştıracağı için öncelikle üretilen kodu metinsel konseptinden yakalayabilmesi gerekmektedir. Şöyle ki; coder üreteceği kodu```csharp ```içerisine koyacağı için buradan saf bir şekilde bu kodun metinsel değerlerden temizlenerek elde edilmesi gerekecektir. Haliyle bunun için AutoGen.DotnetInteractive kütüphanesinin extension’larına ihtiyaç duyacağız. Ayrıca üretilen C# kodunu çalıştırabilmek için bu kütüphanenin bizlere sunduğu C# Kernel’dan istifade edeceğiz.
Öncelikle C# Kernel’ı oluşturarak ilerleyelim;
var kernel = new CompositeKernel() { new CSharpKernel().UseKernelHelpers() }.UseDefaultMagicCommands(); kernel.DefaultKernelName = "csharp";Burada kullandığımız
new CSharpKernel().UseKernelHelpers()metodu varsayılan olarak C#’ı destekleyen bir kernel yapılandırmaktadır. Bu kernel eşliğinde aşağıdaki gibi agent’ı geliştirebiliriz;async static Task<IAgent> CreateRunnerAgentAsync(string apiKey, string model, string endpoint) { var kernel = new CompositeKernel() { new CSharpKernel().UseKernelHelpers() }.UseDefaultMagicCommands(); kernel.DefaultKernelName = "csharp"; var runner = new DefaultReplyAgent( name: "runner", defaultReply: "Geçersiz kod!") .RegisterMiddleware(async (messages, option, agent, _) => { if (!messages.Any()) return new TextMessage(Role.Assistant, "Geçersiz kod! Coder lütfen kodu yaz."); else { var coderMessage = messages.Last(message => message.From == "coder"); if (coderMessage.ExtractCodeBlock("```csharp", "```") is string code) { var codeResult = await kernel.RunSubmitCodeCommandAsync(code, "csharp"); codeResult = $""" [RUNNER_RESULT] {codeResult} """; return new TextMessage(Role.Assistant, codeResult) { From = "runner" }; } else return new TextMessage(Role.Assistant, "Geçersiz kod! Coder lütfen kodu yaz."); } }) .RegisterPrintMessage(); return runner; }Runner agent’ının kodlarını da incelerseniz, eğer gelen bir mesaj varsa bu demek oluyor ki üretilen bir kod vardır. Haliyle bu kod 19. satırda
ExtractCodeBlockmetodu ile yukarıdaki satırlarda bahsedildiği gibi salt bir şekilde metinsel değerin içerisinden alınmakta ve 21. satırda daRunSubmitCodeCommandAsyncmetodu ile C# kernel’ında çalıştırılarak üretilen sonuç elde edilmektedir. - Adım 5 – Agent’ların Oluşturulması ve Birbirlerine Rol ve Görev Tanımlarında Bulunarak, Sürecin Başlatılması
OluşturulanGroupChatiçinde agent’lar arası işbirliğinin sağlanması ve görev paylaşımının netleşmesi için her agent’ın diğer agent’lara kim olduğunu ve ne yapacağını söylemesi gerekmektedir. Böylece bir yandan da her agent’ın kendi görevini anladığı da ifade edilmiş olunacaktır. Bunun için aşağıdaki gibiSendIntroductionmetodundan istifade edilmektedir.public static async Task RunAsync(string apiKey, string model, string endpoint) { var admin = await CreateAdminAgentAsync(apiKey, model, endpoint); var coder = await CreateCoderAgentAsync(apiKey, model, endpoint); var reviewer = await CreateReviewerAgentAsync(apiKey, model, endpoint); var runner = await CreateRunnerAgentAsync(apiKey, model, endpoint); var groupChat = new GroupChat( admin: admin, members: [coder, reviewer, runner]); admin.SendIntroduction("Gelen görevin çözüm sürecini takip edeceğim. Çözüldüğü zaman da süreci sonlandıracağım.", groupChat); coder.SendIntroduction("Gelen görevi çözmek için dotnet kodu yazacağım.", groupChat); reviewer.SendIntroduction("Coder tarafından üretilen dotnet kodunu kurallar doğrultusunda gözden geçireceğim. Onaylanmadığım zaman coder tekrardan kod yazdıracağım ve onaylayana kadar denetleyeceğim.", groupChat); runner.SendIntroduction("Reviewer incelemeye tamamladıktan sonra dotnet kodunu çalıştıracağım.", groupChat); var result = groupChat.SendAsync([new TextMessage(Role.User, "Bana fibonacci sayı diziminde 35. haneyi verir misin?")], maxRound: 15); await foreach (var message in result) { if (message.From == "runner") Console.WriteLine(message.GetContent()); } }Bir yandan 8 ile 10. satır aralığında üretilen agent’lardan bir
GroupChatoluşturulduğuna dikkatinizi çekerim. 17. satırda ise buGroupChat‘e verilen mesaj eşliğinde iletişim süreci başlatılmakta ve runner agent’tan elde edilen netice console’a yazdırılmaktadır. - Adım 6 – Test
Uygulamayı bu vaziyette derleyip çalıştırdığımızda aşağıdaki gibi verilen mesaja karşın agent’lar arası istişare süreci cereyan edecek ve netice elde edilecektir.
İşte bu kadar 🙂 Görüldüğü üzere verdiğimiz görev gereğince admin nezaretinde coder bir kod üretmekte, reviewer bu kodu belirlediğimiz koşullara göre değerlendirmekte ve runner’da nihai olarak çalıştırıp netice elde edilmektedir.
GroupChat’i Kontrol Etmek İçin Graph Kullanımı
Bazen, GroupChat‘te çözmek istenen göreve bağlı olarak sıradaki agent’ın nasıl seçileceği konusunda daha fazla kontrol eklemek isteyebiliriz. Misal olarak, yukarıdaki örnekte admin’in doğrudan reviewer ile ve benzer şekilde coder’ın da runner ile iletişim kurması gerekmemektedir. Bunu aşağıdaki diyagramda gösterilen şekilde daha da idealize edebiliriz.
Evet, GroupChat‘te belirli bir graph akışını takip edilebilir kılıp, agent’lara bir ön bilgi kazandırabilir ve konuşmayı daha verimli ve sağlam hale getirebiliriz. Bunun için yukarıdaki grafiği temsil edebilecek kodu aşağıda ele alabiliriz.
public static async Task UseGraphToControlDynamicGroupChatAsync(string apiKey, string model, string endpoint)
{
var admin = await CreateAdminAgentAsync(apiKey, model, endpoint);
var coder = await CreateCoderAgentAsync(apiKey, model, endpoint);
var reviewer = await CreateReviewerAgentAsync(apiKey, model, endpoint);
var runner = await CreateRunnerAgentAsync(apiKey, model, endpoint);
var adminToCoderTransition = Transition.Create(admin, coder);
var coderToReviewerTransition = Transition.Create(coder, reviewer);
var reviewerToRunnerTransition = Transition.Create(
from: reviewer,
to: runner,
canTransitionAsync: async (from, to, messages) =>
{
var lastMessage = messages.LastOrDefault();
if (lastMessage is not TextMessage textMessage)
return false;
return textMessage.Content.Contains("Geçerli kod, runner kodu çalıştırabilir.");
});
var reviewerToCoderTransition = Transition.Create(
from: reviewer,
to: coder,
canTransitionAsync: async (from, to, messages) =>
{
var lastMessage = messages.LastOrDefault();
if (lastMessage is not TextMessage textMessage)
return false;
return textMessage.Content.Contains("Kod birden fazla kod bloğuna sahip. Lütfen tek bir kod bloğu gönder.");
});
var runnerToCoderTransition = Transition.Create(
from: runner,
to: coder,
canTransitionAsync: async (from, to, messages) =>
{
var lastMessage = messages.LastOrDefault();
if (lastMessage is not TextMessage textMessage)
return false;
return textMessage.Content.Contains("Geçersiz kod! Coder lütfen kodu yaz.");
});
var runnerToAdminTransition = Transition.Create(runner, admin);
var groupChat = new GroupChat(
admin: admin,
workflow: new Graph(
[
adminToCoderTransition,
coderToReviewerTransition,
reviewerToRunnerTransition,
reviewerToCoderTransition,
runnerToCoderTransition,
runnerToAdminTransition
]),
members:
[
admin,
coder,
runner,
reviewer,
]);
admin.SendIntroduction("Gelen görevin çözüm sürecini takip edeceğim. İlk çözüldüğü zaman da süreci sonlandıracağım ve grupchat'i kapatacağım.", groupChat);
coder.SendIntroduction("Gelen görevi çözmek için dotnet kodu yazacağım.", groupChat);
reviewer.SendIntroduction("Coder tarafından üretilen dotnet kodunu kurallar doğrultusunda gözden geçireceğim. Onaylanmadığım zaman coder tekrardan kod yazdıracağım ve onaylayana kadar denetleyeceğim.", groupChat);
runner.SendIntroduction("Reviewer incelemeye tamamladıktan sonra dotnet kodunu çalıştıracağım.", groupChat);
var result = groupChat.SendAsync([new TextMessage(Role.User, "Bana fibonacci sayı diziminde 35. haneyi verir misin?", from: admin.Name)], maxRound: 15);
await foreach (var message in result)
{
if (message.From == "runner")
Console.WriteLine(message.GetContent());
await Task.Delay(1000);
}
}
Yukarıdaki çalışmaya göz atarsanız eğer Transition.Create() metodu ile agent’lar arasındaki geçişe dair hiyerarşi belirlenebilmekte ve dikkat ederseniz kimi durumlarda birden fazla kırılımın geçerli olabileceği çok ihtimalli süreçlerde yönlendirilebilecek şekilde tanımlanabilmektedirler.
Yukarıda verilen diyagramın koda yansıyışını okursak eğer; 8. satırdaki adminToCoderTransition transition ile admin’den coder’a, 10. satırdaki coderToReviewerTransition transition ile de coder’dan reviewer’a geçişi ifade etmektedir. Bu aşamada reviewer iki ihtimale sahiptir; ya kodu onaylayacak ve runner’a geçiş yapılacak ya da onaylamayacak ve coder’a tekrardan kod yazdırılacaktır. İşte bu durumda 12. satırdaki reviewerToRunnerTransition transition’ı kodun onaylandığı durumu temsil etmekte ve reviewer’dan runner’a geçişi ifade etmektedir. 24. satırdaki reviewerToCoderTransition transition’ı ise olası koşulsuzluklardan kaynaklı kodun tekrardan coder tarafından yazılması gerektiği durumu ifade etmektedir. Benzer mantıkla runner’da kodu çalıştırdığı taktirde ya başarılı olacak ve admin’e bildirecek ya da hata alacak ve coder’a geri dönecektir. Haliyle 36. satırdaki runnerToCoderTransition transition’ı ile koddaki olası hataya istinaden runner’dan coder’a geçiş ifade etmekteyken, 48. satırda ise runnerToAdminTransition transition’ı ile de sürecin nihai olarak başarıyla tamamlanacağını temsil etmektedir.
Graph, koşullu transition’ları/geçişleri desteklemektedir. Bir transition’ı koşullu hale getirmek için yukarıdaki örnekte olduğu gibi canTransitionAsync parametresine gerekli lambda fonksiyonu geçebilir ve transition’ın yapılıp yapılamayacağını belirten bir koşul çalışması gerçekleştirebilirsiniz.
Eğer ki tanımlanmış transtion’lardan birden fazla geçerli olan söz konusu olursa, bu taktirde GroupChat‘in yönetici agent’ı olan admin, konuşmanın bağlamına göre hangi duruma geçileceğine karar verecektir.
Ayrıca burada teknik olarak dikkat edilmesi gereken bir detay daha vardır ki; o da, graph’in kullanıldığı çalışmalarda admin’in 63. satırda olduğu gibi members’lara eklenmesi ve GroupChat‘i başlatacak olan SendAsync metoduna verilen TextMessage nesnesinde de ‘from’ olarak belirtilmesi gerekmektedir.
Nihai olarak;
Bu içeriğimizde AutoGen’i pratiksel olarak geniş açıdan deneyimlemiş ve temel agent yapılandırmalarından tutun, agent’ların aralarındaki haberleşme süreçlerindeki davranışlarına kadar detaylıca incelemiş bulunuyoruz. Bir yandan da agent’lar arasındaki iletişim anında kullanılabilecek streaming, middleware, function calling vs. gibi özelliklere temas etmiş ve güzel bir örnek senaryo üzerinden neyin ne olduğunu ve nerelerde ne şekilde kullanılabileceğini tecrübe etmiş bulunuyoruz. Özellikle function contract ve wrapper yapılanmalarını ve LLM ile herhangi bir function’ın nasıl call edilebileceğini detaylarıyla izah etmiş ve son olarak da tasarlanmış bir GroupChat’te Graph ile iletişim sürecini daha detaylı bir şekilde nasıl yapılandırabileceğimizi incelemiş bulunuyoruz.
Evet, bu içeriğimizde her ne kadar Semantic Kernel ile AutoGen’i birlikte kullanmaya değinmemiş olsak da sonraki yazılarımda bu iki teknolojiyi harmanlamayı ve ayrıca MCP (Model Context Protocol)‘yi de işin içine katarak süreci daha da yetenekli hale getirmeyi tasarlamıyor değilim 🙂 O halde takip kalın 😉
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar dilerim…
Not : Örnek çalışmaya aşağıdaki GitHub adresinden erişebilirsiniz.
https://github.com/gncyyldz/AutoGen_Example

Hocam yazan parmaklarınız dert görmesin hemen test ediyorum 🙂
Hocam burada hangi LLM’in Middleware – fonksiyon desteği olduğunu nereden takip ediyorsunuz. Chat kısımlarını free modellerde test edebiliyorum ama iş RegisterMiddleware aşamasına geldiğinde bir türlü modeller 404 Service Request failed geçemiyor burayı